docs(playground): add key folding option

This commit is contained in:
Johann Schopplich
2025-12-01 10:31:04 +01:00
parent ef02ebe4c8
commit 0974a58527
2 changed files with 94 additions and 12 deletions

View File

@@ -1,5 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
import type { Delimiter } from '../../../../packages/toon/src' import type { Delimiter, EncodeOptions } from '../../../../packages/toon/src'
import { useClipboard, useDebounceFn } from '@vueuse/core' import { useClipboard, useDebounceFn } from '@vueuse/core'
import { unzlibSync, zlibSync } from 'fflate' import { unzlibSync, zlibSync } from 'fflate'
import { base64ToUint8Array, stringToUint8Array, uint8ArrayToBase64, uint8ArrayToString } from 'uint8array-extras' import { base64ToUint8Array, stringToUint8Array, uint8ArrayToBase64, uint8ArrayToString } from 'uint8array-extras'
@@ -7,10 +7,8 @@ import { computed, onMounted, ref, shallowRef, watch } from 'vue'
import { DEFAULT_DELIMITER, encode } from '../../../../packages/toon/src' import { DEFAULT_DELIMITER, encode } from '../../../../packages/toon/src'
import VPInput from './VPInput.vue' import VPInput from './VPInput.vue'
interface PlaygroundState { interface PlaygroundState extends Required<Pick<EncodeOptions, 'delimiter' | 'indent' | 'keyFolding' | 'flattenDepth'>> {
json: string json: string
delimiter: Delimiter
indent: number
} }
const PRESETS = { const PRESETS = {
@@ -73,10 +71,15 @@ const DELIMITER_OPTIONS: { value: Delimiter, label: string }[] = [
{ value: '|', label: 'Pipe (|)' }, { value: '|', label: 'Pipe (|)' },
] ]
const DEFAULT_JSON = JSON.stringify(PRESETS.hikes, undefined, 2) const DEFAULT_JSON = JSON.stringify(PRESETS.hikes, undefined, 2)
const SHARE_URL_LIMIT = 8 * 1024
const jsonInput = ref(DEFAULT_JSON) const jsonInput = ref(DEFAULT_JSON)
const delimiter = ref<Delimiter>(DEFAULT_DELIMITER) const delimiter = ref<Delimiter>(DEFAULT_DELIMITER)
const indent = ref(2) const indent = ref(2)
const keyFolding = ref<'off' | 'safe'>('safe')
const flattenDepth = ref(2)
const canShareState = ref(true)
const hasCopiedUrl = ref(false) const hasCopiedUrl = ref(false)
const tokenizer = shallowRef<typeof import('gpt-tokenizer') | undefined>() const tokenizer = shallowRef<typeof import('gpt-tokenizer') | undefined>()
@@ -88,14 +91,16 @@ const encodingResult = computed(() => {
output: encode(parsedInput, { output: encode(parsedInput, {
indent: indent.value, indent: indent.value,
delimiter: delimiter.value, delimiter: delimiter.value,
keyFolding: keyFolding.value,
flattenDepth: flattenDepth.value,
}), }),
error: undefined, error: undefined,
} }
} }
catch (e) { catch (error) {
return { return {
output: '', output: '',
error: e instanceof Error ? e.message : 'Invalid JSON', error: error instanceof Error ? error.message : 'Invalid JSON',
} }
} }
}) })
@@ -123,20 +128,29 @@ const tokenSavings = computed(() => {
const { copy, copied } = useClipboard({ source: toonOutput }) const { copy, copied } = useClipboard({ source: toonOutput })
async function copyShareUrl() { async function copyShareUrl() {
if (!canShareState.value)
return
await navigator.clipboard.writeText(window.location.href) await navigator.clipboard.writeText(window.location.href)
hasCopiedUrl.value = true hasCopiedUrl.value = true
setTimeout(() => (hasCopiedUrl.value = false), 2000) setTimeout(() => (hasCopiedUrl.value = false), 2000)
} }
const updateUrl = useDebounceFn(() => { const updateUrl = useDebounceFn(() => {
if (typeof window === 'undefined')
return
const hash = encodeState() const hash = encodeState()
const baseUrl = `${window.location.origin}${window.location.pathname}${window.location.search}`
const targetUrl = `${baseUrl}#${hash}`
if (targetUrl.length > SHARE_URL_LIMIT) {
canShareState.value = false
return
}
canShareState.value = true
window.history.replaceState(null, '', `#${hash}`) window.history.replaceState(null, '', `#${hash}`)
}, 300) }, 300)
watch([jsonInput, delimiter, indent], () => { watch([jsonInput, delimiter, indent, keyFolding, flattenDepth], () => {
updateUrl() updateUrl()
}) })
@@ -152,6 +166,8 @@ onMounted(() => {
jsonInput.value = state.json jsonInput.value = state.json
delimiter.value = state.delimiter delimiter.value = state.delimiter
indent.value = state.indent indent.value = state.indent
keyFolding.value = state.keyFolding ?? 'safe'
flattenDepth.value = state.flattenDepth ?? 2
} }
}) })
@@ -160,6 +176,8 @@ function encodeState() {
json: jsonInput.value, json: jsonInput.value,
delimiter: delimiter.value, delimiter: delimiter.value,
indent: indent.value, indent: indent.value,
keyFolding: keyFolding.value,
flattenDepth: flattenDepth.value,
} }
const compressedData = zlibSync(stringToUint8Array(JSON.stringify(state))) const compressedData = zlibSync(stringToUint8Array(JSON.stringify(state)))
@@ -215,6 +233,28 @@ async function loadTokenizer() {
> >
</VPInput> </VPInput>
<VPInput id="keyFolding" label="Key Folding">
<select id="keyFolding" v-model="keyFolding">
<option value="off">
Off
</option>
<option value="safe">
Safe
</option>
</select>
</VPInput>
<VPInput id="flattenDepth" label="Flatten Depth">
<input
id="flattenDepth"
v-model.number="flattenDepth"
type="number"
min="1"
max="10"
:disabled="keyFolding === 'off'"
>
</VPInput>
<VPInput id="preset" label="Preset"> <VPInput id="preset" label="Preset">
<select id="preset" @change="(e) => loadPreset((e.target as HTMLSelectElement).value as keyof typeof PRESETS)"> <select id="preset" @change="(e) => loadPreset((e.target as HTMLSelectElement).value as keyof typeof PRESETS)">
<option value="" disabled selected> <option value="" disabled selected>
@@ -238,11 +278,25 @@ async function loadTokenizer() {
<button <button
class="share-button" class="share-button"
:class="[hasCopiedUrl && 'copied']" :class="[hasCopiedUrl && 'copied']"
:aria-label="hasCopiedUrl ? 'Link copied!' : 'Copy shareable URL'" :aria-label="
!canShareState
? 'State too large to share via URL'
: hasCopiedUrl
? 'Link copied!'
: 'Copy shareable URL'
"
:title="!canShareState ? 'State too large to share via URL' : undefined"
:disabled="!canShareState"
:aria-disabled="!canShareState"
@click="copyShareUrl" @click="copyShareUrl"
> >
<span class="vpi-link" :class="[hasCopiedUrl && 'check']" aria-hidden="true" /> <span class="vpi-link" :class="[hasCopiedUrl && 'check']" aria-hidden="true" />
{{ hasCopiedUrl ? 'Copied!' : 'Share' }} <template v-if="!canShareState">
Too large to share
</template>
<template v-else>
{{ hasCopiedUrl ? 'Copied!' : 'Share' }}
</template>
</button> </button>
</div> </div>
@@ -362,6 +416,12 @@ async function loadTokenizer() {
border: 1px solid var(--vp-c-divider); border: 1px solid var(--vp-c-divider);
} }
@media (max-width: 768px) {
.options-bar {
gap: 8px;
}
}
.vpi-link { .vpi-link {
--icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' stroke='currentColor' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' viewBox='0 0 24 24'%3E%3Cpath d='M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71'/%3E%3Cpath d='M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71'/%3E%3C/svg%3E"); --icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' stroke='currentColor' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' viewBox='0 0 24 24'%3E%3Cpath d='M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71'/%3E%3Cpath d='M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71'/%3E%3C/svg%3E");
display: inline-block; display: inline-block;
@@ -409,6 +469,13 @@ async function loadTokenizer() {
color: var(--vp-c-green-1); color: var(--vp-c-green-1);
} }
.share-button:disabled {
color: var(--vp-c-text-3);
border-color: var(--vp-c-divider);
background: var(--vp-c-bg-soft);
cursor: not-allowed;
}
.editor-container { .editor-container {
display: grid; display: grid;
grid-template-columns: 1fr 1fr; grid-template-columns: 1fr 1fr;

View File

@@ -47,6 +47,21 @@ defineProps<{
border-color: var(--vp-c-brand-1); border-color: var(--vp-c-brand-1);
} }
.input-wrapper :deep(select:disabled),
.input-wrapper :deep(input:disabled) {
color: var(--vp-c-text-3);
background-color: var(--vp-c-bg-soft);
border-color: var(--vp-c-divider);
cursor: not-allowed;
}
.input-wrapper :deep(select:disabled):hover,
.input-wrapper :deep(input:disabled):hover,
.input-wrapper :deep(select:disabled):focus,
.input-wrapper :deep(input:disabled):focus {
border-color: var(--vp-c-divider);
}
.input-wrapper :deep(input[type="number"]) { .input-wrapper :deep(input[type="number"]) {
width: 70px; width: 70px;
} }