docs: add JSON baseline format selector to playground

This commit is contained in:
Johann Schopplich
2026-01-08 17:16:32 +01:00
parent f15619de6c
commit 69111cca5a

View File

@@ -7,8 +7,11 @@ 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'
type JsonFormat = 'pretty-2' | 'pretty-4' | 'pretty-tab' | 'compact'
interface PlaygroundState extends Required<Pick<EncodeOptions, 'delimiter' | 'indent' | 'keyFolding' | 'flattenDepth'>> { interface PlaygroundState extends Required<Pick<EncodeOptions, 'delimiter' | 'indent' | 'keyFolding' | 'flattenDepth'>> {
json: string json: string
jsonFormat: JsonFormat
} }
const PRESETS = { const PRESETS = {
@@ -70,20 +73,37 @@ const DELIMITER_OPTIONS: { value: Delimiter, label: string }[] = [
{ value: '\t', label: 'Tab (\\t)' }, { value: '\t', label: 'Tab (\\t)' },
{ value: '|', label: 'Pipe (|)' }, { value: '|', label: 'Pipe (|)' },
] ]
const JSON_FORMAT_OPTIONS: { value: JsonFormat, label: string, indent: string | number | undefined }[] = [
{ value: 'pretty-2', label: 'Pretty (2 spaces)', indent: 2 },
{ value: 'pretty-4', label: 'Pretty (4 spaces)', indent: 4 },
{ value: 'pretty-tab', label: 'Pretty (tabs)', indent: '\t' },
{ value: 'compact', label: 'Compact', indent: undefined },
]
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 SHARE_URL_LIMIT = 8 * 1024
// Input state
const jsonInput = ref(DEFAULT_JSON) const jsonInput = ref(DEFAULT_JSON)
const jsonFormat = ref<JsonFormat>('pretty-2')
const currentFormatIndent = computed(() =>
JSON_FORMAT_OPTIONS.find(opt => opt.value === jsonFormat.value)?.indent,
)
const formattedJson = computed(() => {
try {
return formatJson(JSON.parse(jsonInput.value))
}
catch {
return jsonInput.value
}
})
// Encoder options
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 keyFolding = ref<'off' | 'safe'>('safe')
const flattenDepth = ref(2) const flattenDepth = ref(2)
const canShareState = ref(true) // Encoding output
const hasCopiedUrl = ref(false)
const tokenizer = shallowRef<typeof import('gpt-tokenizer') | undefined>()
const encodingResult = computed(() => { const encodingResult = computed(() => {
try { try {
const parsedInput = JSON.parse(jsonInput.value) const parsedInput = JSON.parse(jsonInput.value)
@@ -104,12 +124,13 @@ const encodingResult = computed(() => {
} }
} }
}) })
const toonOutput = computed(() => encodingResult.value.output) const toonOutput = computed(() => encodingResult.value.output)
const error = computed(() => encodingResult.value.error) const error = computed(() => encodingResult.value.error)
// Token analysis
const tokenizer = shallowRef<typeof import('gpt-tokenizer') | undefined>()
const jsonTokens = computed(() => const jsonTokens = computed(() =>
tokenizer.value?.encode(jsonInput.value).length, tokenizer.value?.encode(formattedJson.value).length,
) )
const toonTokens = computed(() => const toonTokens = computed(() =>
tokenizer.value && toonOutput.value ? tokenizer.value.encode(toonOutput.value).length : undefined, tokenizer.value && toonOutput.value ? tokenizer.value.encode(toonOutput.value).length : undefined,
@@ -125,17 +146,11 @@ const tokenSavings = computed(() => {
return { diff, percent, sign, isSavings: diff > 0 } return { diff, percent, sign, isSavings: diff > 0 }
}) })
// UI state
const canShareState = ref(true)
const hasCopiedUrl = ref(false)
const { copy, copied } = useClipboard({ source: toonOutput }) const { copy, copied } = useClipboard({ source: toonOutput })
async function copyShareUrl() {
if (!canShareState.value)
return
await navigator.clipboard.writeText(window.location.href)
hasCopiedUrl.value = true
setTimeout(() => (hasCopiedUrl.value = false), 2000)
}
const updateUrl = useDebounceFn(() => { const updateUrl = useDebounceFn(() => {
const hash = encodeState() const hash = encodeState()
const baseUrl = `${window.location.origin}${window.location.pathname}${window.location.search}` const baseUrl = `${window.location.origin}${window.location.pathname}${window.location.search}`
@@ -150,10 +165,17 @@ const updateUrl = useDebounceFn(() => {
window.history.replaceState(null, '', `#${hash}`) window.history.replaceState(null, '', `#${hash}`)
}, 300) }, 300)
watch([jsonInput, delimiter, indent, keyFolding, flattenDepth], () => { watch([jsonInput, delimiter, indent, keyFolding, flattenDepth, jsonFormat], () => {
updateUrl() updateUrl()
}) })
watch(jsonFormat, () => {
try {
jsonInput.value = formatJson(JSON.parse(jsonInput.value))
}
catch {}
})
onMounted(() => { onMounted(() => {
loadTokenizer() loadTokenizer()
@@ -168,9 +190,14 @@ onMounted(() => {
indent.value = state.indent indent.value = state.indent
keyFolding.value = state.keyFolding ?? 'safe' keyFolding.value = state.keyFolding ?? 'safe'
flattenDepth.value = state.flattenDepth ?? 2 flattenDepth.value = state.flattenDepth ?? 2
jsonFormat.value = state.jsonFormat ?? 'pretty-2'
} }
}) })
function formatJson(value: unknown) {
return JSON.stringify(value, undefined, currentFormatIndent.value)
}
function encodeState() { function encodeState() {
const state: PlaygroundState = { const state: PlaygroundState = {
json: jsonInput.value, json: jsonInput.value,
@@ -178,6 +205,7 @@ function encodeState() {
indent: indent.value, indent: indent.value,
keyFolding: keyFolding.value, keyFolding: keyFolding.value,
flattenDepth: flattenDepth.value, flattenDepth: flattenDepth.value,
jsonFormat: jsonFormat.value,
} }
const compressedData = zlibSync(stringToUint8Array(JSON.stringify(state))) const compressedData = zlibSync(stringToUint8Array(JSON.stringify(state)))
@@ -196,7 +224,16 @@ function decodeState(hash: string) {
} }
function loadPreset(name: keyof typeof PRESETS) { function loadPreset(name: keyof typeof PRESETS) {
jsonInput.value = JSON.stringify(PRESETS[name], undefined, 2) jsonInput.value = formatJson(PRESETS[name])
}
async function copyShareUrl() {
if (!canShareState.value)
return
await navigator.clipboard.writeText(window.location.href)
hasCopiedUrl.value = true
setTimeout(() => (hasCopiedUrl.value = false), 2000)
} }
async function loadTokenizer() { async function loadTokenizer() {
@@ -258,7 +295,7 @@ async function loadTokenizer() {
<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>
Load example... Load example
</option> </option>
<option value="hikes"> <option value="hikes">
Hikes (mixed structure) Hikes (mixed structure)
@@ -275,6 +312,14 @@ async function loadTokenizer() {
</select> </select>
</VPInput> </VPInput>
<VPInput id="jsonFormat" label="JSON Baseline">
<select id="jsonFormat" v-model="jsonFormat">
<option v-for="opt in JSON_FORMAT_OPTIONS" :key="opt.value" :value="opt.value">
{{ opt.label }}
</option>
</select>
</VPInput>
<button <button
class="share-button" class="share-button"
:class="[hasCopiedUrl && 'copied']" :class="[hasCopiedUrl && 'copied']"
@@ -307,8 +352,8 @@ async function loadTokenizer() {
<div class="pane-header"> <div class="pane-header">
<span class="pane-title">JSON Input</span> <span class="pane-title">JSON Input</span>
<span class="pane-stats"> <span class="pane-stats">
<span>{{ jsonTokens ?? '...' }} tokens</span> <span class="stat-primary" title="Token count using selected JSON baseline format">{{ jsonTokens ?? '' }} tokens</span>
<span>{{ jsonInput.length }} chars</span> <span class="stat-secondary">{{ formattedJson.length }} chars</span>
</span> </span>
</div> </div>
<textarea <textarea
@@ -319,7 +364,7 @@ async function loadTokenizer() {
aria-label="JSON input" aria-label="JSON input"
:aria-describedby="error ? 'json-error' : undefined" :aria-describedby="error ? 'json-error' : undefined"
:aria-invalid="!!error" :aria-invalid="!!error"
placeholder="Enter JSON here..." placeholder="Enter JSON here"
/> />
</div> </div>
@@ -333,8 +378,8 @@ async function loadTokenizer() {
</span> </span>
</span> </span>
<span class="pane-stats"> <span class="pane-stats">
<span>{{ toonTokens ?? '...' }} tokens</span> <span class="stat-primary">{{ toonTokens ?? '' }} tokens</span>
<span>{{ toonOutput.length }} chars</span> <span class="stat-secondary">{{ toonOutput.length }} chars</span>
</span> </span>
</div> </div>
<div class="editor-output"> <div class="editor-output">
@@ -541,6 +586,15 @@ async function loadTokenizer() {
letter-spacing: normal; letter-spacing: normal;
} }
.stat-primary {
font-weight: 600;
color: var(--vp-c-text-1);
}
.stat-secondary {
color: var(--vp-c-text-3);
}
.savings-badge { .savings-badge {
display: inline-flex; display: inline-flex;
padding: 2px 6px; padding: 2px 6px;