JsonFormatter
Formats, minifies and sorts JSON, keeps numbers exactly, names the line and column of an error, and repairs trailing commas and single quotes.
Demo
JSON formatter
Result
{
"price": 1.10,
"id": 12345678901234567890,
"big": 1e400,
"name": "caf\u00e9",
"items": [],
"meta": {
"z": -0,
"a": true
}
}Valid JSON, 147 characters.
With on_change
Line 1, column 2
A key needs double quotes, not single quotes.
{'name': 'it\'s', "tags": ["a", …
^Change the text to see on_change.
Get it
It needs theme.qwe, as every component does; or take the whole kit, quillo-ui.qwe. Each component brings its own styles to the pages that show it.
$ curl -O https://quillolang.org/components/ui/theme.qwe$ curl -O https://quillolang.org/components/ui/json_formatter.qwemain.qwe
import ui/theme.qwe use theme_cssuse ui/json_formatter.qweroute GET / = <html><head><style>{theme_css}</style></head><body class="ui-root">...</body></html>Example
The code of the preview above.
demos/json_formatter.qwe
component JsonFormatterDemoLive() { state seen = null <div.demo-stack> <JsonFormatter id="json-formatter-demo-live" title="With on_change" name="config" value={`{'name': 'it\\'s', "tags": ["a", "b",],}`} on_change={(r) => { seen = r }}/> <p.demo-note>{seen == null ? "Change the text to see on_change." : (seen.valid ? `on_change: valid, ${len(seen.output)} characters.` : `on_change: not valid, at line ${seen.error == null ? "?" : seen.error.line}.`)}</p> </div>}view JsonFormatterDemo() = <div.demo-stack> <JsonFormatter id="json-formatter-demo" value={`{"price": 1.10, "id": 12345678901234567890, "big": 1e400, "name": "caf\\u00e9", "items": [], "meta": {"z": -0, "a": true}}`}/> <JsonFormatterDemoLive/> </div>Source
ui/json_formatter.qwe
// JsonFormatter: formats, minifies and sorts JSON, keeping every number and string// exactly as written. `1.10`, `1e400` and `12345678901234567890` stay those characters,// which a trip through a JSON parser would change, so it has its own parser, and that// parser can also say what is wrong: on an error it names the line and column, shows// the text around them with a caret, and says the likely cause. Where the trouble is// trailing commas or single quotes it offers a repair, done on the tokens, so text// inside strings is never touched. A key given twice is kept and reported.//// <JsonFormatter value={`{"b": 1.10, "a": [1, 2,],}`}/>// <JsonFormatter value={body} mode="minify" sort={true} on_change={(r) => { ok = r.valid }}/>//// value: the JSON it starts with. mode: "format" (the default) or "minify". sort: sorts// keys, at every depth, by code point (false). indent: "2", "4" or "tab" ("2"). title:// its heading. id: a prefix for its ids, unique on the page. name: the field's name// ("json"), so a form around it sends the text. live_limit: past this many characters it// waits for the Format button rather than working on each keystroke (200,000), and// past 5,000,000 it does not try, and says so. on_change(result): inside one of your// components, given { valid, output, error } after each change, error being// { line, column, message } or null. class: more classes for its outer element.//// json_formatter_check(text) reads text: { ok, tree, error, repairs, duplicates },// error being { at, line, column, message, excerpt, caret }.// json_formatter_pretty(text, indent, sort) and json_formatter_minify(text, sort) give// the text, or null when it is not JSON. json_formatter_repair(text) gives { text,// commas, quotes }. They work in a program too. Nesting past 500 levels is refused.import ui/theme.qwe use ui_clslet json_formatter_css = css`.ui-json-fmt { display: grid; gap: 12px; min-width: 0; padding: 20px 22px; border: 1px solid var(--ui-border); border-radius: var(--ui-radius); background: var(--ui-surface); color: var(--ui-fg); font: 14px/1.45 var(--ui-font); }.ui-json-fmt .ui-json-fmt-title { margin: 0; font-size: 16px; font-weight: 650; }.ui-json-fmt-field { display: grid; gap: 5px; min-width: 0; }.ui-json-fmt-field > label, .ui-json-fmt-label { font-size: 13px; font-weight: 500; }.ui-json-fmt-field textarea { box-sizing: border-box; width: 100%; padding: 8px 10px; border: 1px solid var(--ui-border); border-radius: var(--ui-radius-sm); background: var(--ui-surface); color: var(--ui-fg); font: 13px/1.5 var(--ui-mono); resize: vertical; }.ui-json-fmt-field textarea:focus-visible, .ui-json-fmt-bar select:focus-visible { outline: none; border-color: var(--ui-accent); box-shadow: var(--ui-focus); }.ui-json-fmt-field textarea[aria-invalid] { border-color: var(--ui-danger); }.ui-json-fmt-bar { display: flex; flex-wrap: wrap; align-items: center; gap: 8px 12px; }.ui-json-fmt-seg { display: inline-flex; gap: 0; }.ui-json-fmt-btn { height: 32px; padding: 0 12px; border: 1px solid var(--ui-border); border-radius: var(--ui-radius-sm); background: var(--ui-surface); color: var(--ui-fg); font: 500 13px/1 var(--ui-font); cursor: pointer; }.ui-json-fmt-seg .ui-json-fmt-btn:first-child { border-radius: var(--ui-radius-sm) 0 0 var(--ui-radius-sm); }.ui-json-fmt-seg .ui-json-fmt-btn:last-child { margin-left: -1px; border-radius: 0 var(--ui-radius-sm) var(--ui-radius-sm) 0; }.ui-json-fmt-btn:hover { background: var(--ui-subtle); }.ui-json-fmt-btn:focus-visible { outline: none; box-shadow: var(--ui-focus); position: relative; }.ui-json-fmt-btn[aria-pressed="true"] { border-color: var(--ui-accent); background: var(--ui-accent-soft); }.ui-json-fmt-check { display: inline-flex; align-items: center; gap: 6px; font-size: 13px; cursor: pointer; }.ui-json-fmt-check input { margin: 0; accent-color: var(--ui-accent); }.ui-json-fmt-check input:focus-visible { outline: none; box-shadow: var(--ui-focus); }.ui-json-fmt-bar select { height: 32px; padding: 0 6px; border: 1px solid var(--ui-border); border-radius: var(--ui-radius-sm); background: var(--ui-surface); color: var(--ui-fg); font: 13px var(--ui-font); }.ui-json-fmt-indent { display: inline-flex; align-items: center; gap: 6px; font-size: 13px; }.ui-json-fmt-error { display: grid; gap: 6px; padding: 10px 12px; border-left: 4px solid var(--ui-danger); border-radius: var(--ui-radius-sm); background: var(--ui-danger-soft); }.ui-json-fmt-where { margin: 0; font-weight: 600; color: var(--ui-danger); }.ui-json-fmt-why { margin: 0; overflow-wrap: anywhere; }.ui-json-fmt-excerpt { margin: 0; padding: 6px 8px; overflow-x: auto; border-radius: 4px; background: var(--ui-surface); font: 12px/1.5 var(--ui-mono); white-space: pre; }.ui-json-fmt-excerpt:focus-visible { outline: none; box-shadow: var(--ui-focus); }.ui-json-fmt-actions { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; }.ui-json-fmt-output { max-height: 360px; margin: 0; padding: 10px 12px; overflow: auto; border: 1px solid var(--ui-border); border-radius: var(--ui-radius-sm); background: var(--ui-subtle); font: 13px/1.5 var(--ui-mono); white-space: pre-wrap; overflow-wrap: anywhere; }.ui-json-fmt-output:focus-visible { outline: none; box-shadow: var(--ui-focus); }.ui-json-fmt-note { margin: 0; color: var(--ui-muted); font-size: 13px; }.ui-json-fmt-warn { margin: 0; color: var(--ui-warning); font-size: 13px; overflow-wrap: anywhere; }`let json_formatter_most = 5000000let json_formatter_depth = 500// One token: a string (open or closed) in either quote, something number-like, a// bracket or punctuation, a word, a comment, or any single other character. What is// not matched is JSON's own white space, as the last class leaves out only those four.let json_formatter_token_re = "\"(?:[^\"\\\\\\n\\r]|\\\\.)*\"?|'(?:[^'\\\\\\n\\r]|\\\\.)*'?|[-+]?[0-9.][0-9A-Za-z.+-]*|[-+][A-Za-z]*|[{}\\[\\],:]|[A-Za-z_$][\\w$]*|//[^\\n]*|/\\*|[^ \\t\\r\\n]"let json_formatter_string_re = "^\"(?:[^\"\\\\\\x00-\\x1f]|\\\\[\"\\\\/bfnrt]|\\\\u[0-9a-fA-F]{4})*\"$"let json_formatter_number_re = "^-?(?:0|[1-9][0-9]*)(?:\\.[0-9]+)?(?:[eE][-+]?[0-9]+)?$"let json_formatter_hints = { True: "true", False: "false", TRUE: "true", FALSE: "false", None: "null", NULL: "null", Null: "null", nil: "null", undefined: "null" }// The tokens: { k (kind), t (text), at (place in characters) }. Fast, as the regular// expression does the walking, so the Quillo around it stays small.fn json_formatter_tokens(text: str) -> list = re.find_all(text, json_formatter_token_re) | map((m) => { let t = m.text let c = t[0] let k = c == "\"" ? "str" : (c == "'" ? "sq" : (t == "{" || t == "}" || t == "[" || t == "]" || t == "," || t == ":" ? t : (re.test(c, "[-+0-9.]") ? "num" : (re.test(c, "[A-Za-z_$]") ? "word" : "other")))) { k, t, at: m.start } })// What is wrong with a double-quoted string token, as { at (offset in it), message },// or null.fn json_formatter_string_problem(t: str) { if re.test(t, json_formatter_string_re) { return null } let closed = len(t) >= 2 && re.test(t, "^\"(?:[^\"\\\\]|\\\\.)*\"$") if !closed { return { at: 0, message: "This string is never closed: its ending \" is missing on this line." } } let tab = re.find(t, "[\\x00-\\x1f]") if tab != null { return { at: tab.start, message: "A string cannot hold a raw tab or control character: write it as \\t or \\u00XX." } } let esc = re.find(t, "\\\\(?:[^\"\\\\/bfnrtu]|u[0-9a-fA-F]{0,3}(?:[^0-9a-fA-F]|$))") if esc != null { return { at: esc.start, message: `${slice(esc.text, 0, 2)} is not an escape JSON knows.` } } { at: 0, message: "This string is not valid." }}// What is wrong with a token that should be a value, or null.fn json_formatter_scalar_problem(tok: map) -> str? { let t = tok.t match tok.k { "num" => re.test(t, json_formatter_number_re) ? null : json_formatter_number_problem(t) "word" => { if t == "true" || t == "false" || t == "null" { return null } let hint = get(json_formatter_hints, t) if hint != null { return `${t} is not JSON: did you mean ${hint}?` } if t == "NaN" || t == "Infinity" { return `${t} is not JSON: there is no such number.` } `${t} is not a value: a string needs double quotes.` } "sq" => "A string needs double quotes, not single quotes." "other" => json_formatter_other_problem(t) _ => null }}fn json_formatter_number_problem(t: str) -> str { if starts_with(t, "+") { return `${t} is not a number in JSON: a number has no plus sign.` } if re.test(t, "^-?0[0-9]") { return `${t} is not a number in JSON: it has a zero in front.` } if re.test(t, "^-?0[xX]") { return `${t} is not a number in JSON: there is no hex.` } if re.test(t, "^-?\\.") { return `${t} is not a number in JSON: it needs a digit before the point.` } if re.test(t, "\\.$|\\.[eE]") { return `${t} is not a number in JSON: it needs a digit after the point.` } if re.test(t, "^-?[0-9.]+[eE][-+]?$") { return `${t} is not a number in JSON: its exponent has no digits.` } `${t} is not a number in JSON.`}fn json_formatter_other_problem(t: str) -> str { if starts_with(t, "//") || t == "/*" { return "JSON has no comments." } if t == "\u{FEFF}" { return "A byte order mark is here: take it out." } if t == " " { return "A no-break space is here, which JSON does not count as space." } if t == "“" || t == "”" || t == "‘" || t == "’" { return "This is a curly quote: JSON needs a straight \"." } `${t} is not expected here.`}fn json_formatter_found(tok) -> str = tok == null ? "the end of the text" : (len(tok.t) > 24 ? `${slice(tok.t, 0, 24)}…` : tok.t)// One value from token i: { n (node), i (the next token) } or { e: { at, message } }.// A node is { t: text }, { a: [nodes] } or { o: [[key text, node, place]] }. Items gather// in chunks of 256 and are joined at the end: `push` in a loop copies the list in the// browser, which a list of 100,000 numbers would feel on every keystroke.fn json_formatter_value(toks: list, i: int, depth: int, end: int) -> map { let tok = i < len(toks) ? toks[i] : null if tok == null { return { e: { at: end, message: "The text ends where a value should be." } } } if depth > json_formatter_depth { return { e: { at: tok.at, message: `This is nested deeper than ${json_formatter_depth} levels.` } } } match tok.k { "{" => json_formatter_object(toks, i, depth, end) "[" => json_formatter_array(toks, i, depth, end) "str" => { let p = json_formatter_string_problem(tok.t) p != null ? { e: { at: tok.at + p.at, message: p.message } } : { n: { t: tok.t }, i: i + 1 } } "num" | "word" | "sq" | "other" => { let p = json_formatter_scalar_problem(tok) p != null ? { e: { at: tok.at, message: p } } : { n: { t: tok.t }, i: i + 1 } } _ => { e: { at: tok.at, message: tok.t == "," ? "A comma here has no value before it." : `Expected a value, found ${tok.t}.` } } }}fn json_formatter_array(toks: list, start: int, depth: int, end: int) { let n = len(toks) let items = [] let chunks = [] let i = start + 1 if i < n && toks[i].k == "]" { return { n: { a: [] }, i: i + 1 } } while true { let r = json_formatter_value(toks, i, depth + 1, end) if r.e != null { return r } items = push(items, r.n) if len(items) == 256 { chunks = push(chunks, items) items = [] } i = r.i let tok = i < n ? toks[i] : null if tok == null { return { e: { at: end, message: `The text ends inside the list opened at character ${toks[start].at + 1}: a ] is missing.` } } } if tok.k == "]" { return { n: { a: flatten(push(chunks, items)) }, i: i + 1 } } if tok.k != "," { return { e: { at: tok.at, message: `Expected a comma or ] after a list item, found ${json_formatter_found(tok)}.` } } } i += 1 if i < n && toks[i].k == "]" { return { e: { at: tok.at, message: "Trailing comma: nothing follows it before the ]." } } } }}fn json_formatter_object(toks: list, start: int, depth: int, end: int) { let n = len(toks) let pairs = [] let chunks = [] let i = start + 1 if i < n && toks[i].k == "}" { return { n: { o: [] }, i: i + 1 } } while true { let key = i < n ? toks[i] : null if key == null { return { e: { at: end, message: `The text ends inside the object opened at character ${toks[start].at + 1}: a } is missing.` } } } if key.k == "sq" { return { e: { at: key.at, message: "A key needs double quotes, not single quotes." } } } if key.k != "str" { return { e: { at: key.at, message: key.k == "word" ? `The key ${key.t} needs double quotes.` : `Expected a key in double quotes, found ${json_formatter_found(key)}.` } } } let bad = json_formatter_string_problem(key.t) if bad != null { return { e: { at: key.at + bad.at, message: bad.message } } } let colon = i + 1 < n ? toks[i + 1] : null if colon == null || colon.k != ":" { return { e: { at: colon == null ? end : colon.at, message: `Expected a colon after the key ${json_formatter_found(key)}, found ${json_formatter_found(colon)}.` } } } let r = json_formatter_value(toks, i + 2, depth + 1, end) if r.e != null { return r } pairs = push(pairs, [key.t, r.n, key.at]) if len(pairs) == 256 { chunks = push(chunks, pairs) pairs = [] } i = r.i let tok = i < n ? toks[i] : null if tok == null { return { e: { at: end, message: `The text ends inside the object opened at character ${toks[start].at + 1}: a } is missing.` } } } if tok.k == "}" { return { n: { o: flatten(push(chunks, pairs)) }, i: i + 1 } } if tok.k != "," { return { e: { at: tok.at, message: `Expected a comma or } after a value, found ${json_formatter_found(tok)}.` } } } i += 1 if i < n && toks[i].k == "}" { return { e: { at: tok.at, message: "Trailing comma: nothing follows it before the }." } } } }}// A place as a line and column from 1, and the text around it with a caret under it.// Wide lines are cut around the place, and a tab shows as a space to keep the caret true.fn json_formatter_place(text: str, at: int) -> map { let before = split(slice(text, 0, at), "\n") let line = len(before) let column = len(before[-1]) + 1 let whole = split(text, "\n")[line - 1] ?? "" let shown = replace(replace(whole, "\t", " "), "\r", "") let from = max([0, column - 31]) let to = min([len(shown), column + 30]) let cut = `${from > 0 ? "…" : ""}${slice(shown, from, to)}${to < len(shown) ? "…" : ""}` let pad = column - 1 - from + (from > 0 ? 1 : 0) { at, line, column, excerpt: cut, caret: `${repeat(" ", pad)}^` }}// Tokens to repair: which single-quoted strings and trailing commas there are.fn json_formatter_fixable(toks: list) -> map { let commas = 0 let quotes = 0 let n = len(toks) let i = 0 while i < n { let k = toks[i].k if k == "sq" { quotes += 1 } if k == "," && i + 1 < n && (toks[i + 1].k == "}" || toks[i + 1].k == "]") { commas += 1 } i += 1 } { commas, quotes }}// A single-quoted token as a double-quoted one: \' is ' and a bare " is \".fn json_formatter_requote(t: str) -> str { let closed = len(t) >= 2 && ends_with(t, "'") let inner = slice(t, 1, closed ? len(t) - 1 : len(t)) let body = re.replace(inner, "\\\\.|\"", (m) => m.text == "\\'" ? "'" : (m.text == "\"" ? "\\\"" : m.text)) `"${body}${closed ? "\"" : ""}`}// Takes out trailing commas and turns single quotes to double, by tokens, so that a// comma or a quote inside a string stays. { text, commas, quotes }.fn json_formatter_repair(text: str) -> map { let toks = json_formatter_tokens(text) let n = len(toks) let done = [] let pieces = [] let pos = 0 let commas = 0 let quotes = 0 let i = 0 while i < n { let tok = toks[i] let next = i + 1 < n ? toks[i + 1].k : "" if tok.k == "sq" { pieces = push(pieces, slice(text, pos, tok.at)) pieces = push(pieces, json_formatter_requote(tok.t)) pos = tok.at + len(tok.t) quotes += 1 } else if tok.k == "," && (next == "}" || next == "]") { pieces = push(pieces, slice(text, pos, tok.at)) pos = tok.at + 1 commas += 1 } if len(pieces) >= 512 { done = push(done, join(pieces, "")) pieces = [] } i += 1 } pieces = push(pieces, slice(text, pos, len(text))) { text: `${join(done, "")}${join(pieces, "")}`, commas, quotes }}// Keys given twice, by the path of the object, the first few.fn json_formatter_duplicates(node: map, path: str, out: list) -> list { if len(out) >= 5 { return out } let found = out if node.o != null { let seen = {} for p in node.o { if has(seen, p[0]) && len(found) < 5 { found = push(found, `${p[0]} is given twice in ${path == "" ? "the top object" : path}.`) } seen = set(seen, p[0], true) found = json_formatter_duplicates(p[1], `${path}.${slice(p[0], 1, len(p[0]) - 1)}`, found) } } if node.a != null { for x in node.a { found = json_formatter_duplicates(x, `${path}[]`, found) } } found}// Reads text. See the top of the file.fn json_formatter_check(text: str) -> map { let body = starts_with(text, "\u{FEFF}") ? slice(text, 1, len(text)) : text let toks = json_formatter_tokens(body) let fix = json_formatter_fixable(toks) let repairs = { commas: fix.commas, quotes: fix.quotes } let failed = (at: int, message: str) => { let p = json_formatter_place(body, at) { ok: false, tree: null, repairs, duplicates: [], error: { ...p, message } } } if len(toks) == 0 { return { ok: false, tree: null, repairs, duplicates: [], error: null, empty: true } } let r = json_formatter_value(toks, 0, 0, len(body)) if r.e != null { return failed(r.e.at, r.e.message) } if r.i < len(toks) { return failed(toks[r.i].at, `Only one value is allowed, and more follows it: ${json_formatter_found(toks[r.i])}.`) } { ok: true, tree: r.n, repairs, error: null, duplicates: json_formatter_duplicates(r.n, "", []) }}// The text a key sorts by: what it says, escapes read, without its quotes.fn json_formatter_key(k: str) -> str { if !contains(k, "\\") { return slice(k, 1, len(k) - 1) } let v = json.parse(k) type_of(v) == "str" ? v : k}fn json_formatter_pairs(node: map, sort: bool) -> list = sort ? sort_by(node.o, (p) => json_formatter_key(p[0])) : node.o// A tree as text: pad is one level's indent, or null for none at all.fn json_formatter_write(node: map, pad: str?, sort: bool, here: str) -> str { if node.t != null { return node.t } let inner = pad == null ? "" : here + pad let comma = pad == null ? "," : `,\n${inner}` let open_with = pad == null ? "" : `\n${inner}` let close_with = pad == null ? "" : `\n${here}` if node.a != null { if len(node.a) == 0 { return "[]" } return `[${open_with}${join(node.a | map((x) => json_formatter_write(x, pad, sort, inner)), comma)}${close_with}]` } if len(node.o) == 0 { return "{}" } let colon = pad == null ? ":" : ": " `{${open_with}${join(json_formatter_pairs(node, sort) | map((p) => `${p[0]}${colon}${json_formatter_write(p[1], pad, sort, inner)}`), comma)}${close_with}}`}fn json_formatter_pad(indent: str) -> str = indent == "tab" ? "\t" : (indent == "4" ? " " : " ")fn json_formatter_pretty(text: str, indent: str = "2", sort: bool = false) -> str? { let c = json_formatter_check(text) c.ok ? json_formatter_write(c.tree, json_formatter_pad(indent), sort, "") : null}fn json_formatter_minify(text: str, sort: bool = false) -> str? { let c = json_formatter_check(text) c.ok ? json_formatter_write(c.tree, null, sort, "") : null}fn json_formatter_repair_words(r: map) -> str { let parts = [r.commas > 0 ? `${r.commas} trailing ${r.commas == 1 ? "comma" : "commas"}` : null, r.quotes > 0 ? `${r.quotes} single-quoted ${r.quotes == 1 ? "string" : "strings"}` : null] | filter((p) => p != null) `Repair ${join(parts, " and ")}`}component JsonFormatter(value: str = "", mode: str = "format", sort: bool = false, indent: str = "2", title: str = "JSON formatter", id: str = "json-formatter", name: str = "json", live_limit: int = 200000, on_change = null, class: str? = null) { style global json_formatter_css state text = value state how = mode state sorted = sort state pad_kind = indent state pressed = value state copied = "" state box = null let size = len(text) let too_big = size > json_formatter_most let live = size <= live_limit let source = live ? text : pressed let waiting = !live && pressed != text let c = too_big ? { ok: false, tree: null, repairs: { commas: 0, quotes: 0 }, duplicates: [], error: null, big: true } : json_formatter_check(source) let out = c.ok ? json_formatter_write(c.tree, how == "minify" ? null : json_formatter_pad(pad_kind), sorted, "") : "" let fixes = c.repairs.commas + c.repairs.quotes let tell = () => { if on_change != null { let g = json_formatter_check(text) let e = g.error on_change({ valid: g.ok, output: g.ok ? json_formatter_write(g.tree, how == "minify" ? null : json_formatter_pad(pad_kind), sorted, "") : null, error: e == null ? null : { line: e.line, column: e.column, message: e.message } }) } } let show_place = () => { if box != null && c.error != null { // The field counts UTF-16 units; a character past U+FFFF is two of them. let wide = len(re.find_all(slice(source, 0, c.error.at), "[\u{10000}-\u{10FFFF}]")) let at = c.error.at + wide box.focus() box.setSelectionRange(at, at + 1) } } let copy = () => { js("navigator.clipboard.writeText", out).then((_ = null) => { copied = "Copied" }, (_ = null) => { copied = "Could not copy: select the text instead" }) } <section class={ui_cls(["ui-json-fmt", class])} aria-labelledby={`${id}-title`}> <h3.ui-json-fmt-title id={`${id}-title`}>{title}</h3> <div.ui-json-fmt-field> <label for={`${id}-input`}>JSON</label> <textarea id={`${id}-input`} name={name} rows="8" spellcheck="false" autocomplete="off" value={text} aria-invalid={c.error != null ? "true" : null} @mount={(el) => { box = el }} @input={(e) => { text = e.value; copied = ""; tell() }}></textarea> </div> <div.ui-json-fmt-bar> <div.ui-json-fmt-seg role="group" aria-label="Layout"> <button.ui-json-fmt-btn type="button" data-mode="format" aria-pressed={how == "format" ? "true" : "false"} @click={() => { how = "format"; pressed = text; tell() }}>Format</button> <button.ui-json-fmt-btn type="button" data-mode="minify" aria-pressed={how == "minify" ? "true" : "false"} @click={() => { how = "minify"; pressed = text; tell() }}>Minify</button> </div> <label.ui-json-fmt-check><input type="checkbox" name={`${name}_sort`} checked={sorted} @change={(e) => { sorted = e.checked; tell() }}/> Sort keys</label> <label.ui-json-fmt-indent>Indent <select name={`${name}_indent`} disabled={how == "minify"} @change={(e) => { pad_kind = e.value; tell() }}> <option value="2" selected={pad_kind == "2"}>2 spaces</option> <option value="4" selected={pad_kind == "4"}>4 spaces</option> <option value="tab" selected={pad_kind == "tab"}>Tabs</option> </select> </label> </div> {too_big && <p.ui-json-fmt-warn role="alert">{`This is ${format_number(size, "en-US", 0)} characters, over the ${format_number(json_formatter_most, "en-US", 0)} it will read.`}</p>} {waiting && <p.ui-json-fmt-note>{`Large text: it is not read as you type. Press Format or Minify.`}</p>} {c.empty == true && <p.ui-json-fmt-note>Paste or type JSON to start.</p>} {c.error != null && <div.ui-json-fmt-error role="alert"> <p.ui-json-fmt-where>{`Line ${c.error.line}, column ${c.error.column}`}</p> <p.ui-json-fmt-why>{c.error.message}</p> <pre.ui-json-fmt-excerpt tabindex="0" aria-label="The text around it">{`${c.error.excerpt}\n${c.error.caret}`}</pre> <div.ui-json-fmt-actions> {fixes > 0 && <button.ui-json-fmt-btn type="button" data-repair @click={() => { let r = json_formatter_repair(text); text = r.text; pressed = r.text; tell() }}>{json_formatter_repair_words(c.repairs)}</button>} <button.ui-json-fmt-btn type="button" data-show @click={show_place}>Show in the text</button> </div> </div>} {c.ok && <div.ui-json-fmt-field> <span.ui-json-fmt-label id={`${id}-output-label`}>Result</span> <pre.ui-json-fmt-output tabindex="0" role="region" aria-labelledby={`${id}-output-label`}>{out}</pre> <div.ui-json-fmt-actions> <button.ui-json-fmt-btn type="button" data-copy @click={copy}>Copy</button> <span.ui-json-fmt-note aria-live="polite">{copied != "" ? copied : `Valid JSON, ${format_number(len(out), "en-US", 0)} characters.`}</span> </div> </div>} {for d in c.duplicates => <p.ui-json-fmt-warn key={d}>{d}</p>} </section>}Related
HttpRequestBuilderBuilds an HTTP request as curl, fetch and HTTPie with correct shell escaping, warns on a body on GET and a missing content type, and hides secrets until asked.3 views · 5 downloadsJwtInspectorDecodes a JSON Web Token locally, reads its dates relative to now, warns on alg none and a missing exp, and says the signature was not verified.3 views · 7 downloadsUrlInspectorSplits a URL into parts with a query table to edit, encodes on rebuild, flags tracking and double encoding, and shows punycode.3 views · 5 downloadsCronBuilderBuilds a five-field cron line from dropdowns or reads one typed, says it in English, lists the next runs in a time zone, and rejects a day that never comes.2 views · 6 downloadsFormBuilderAdd, reorder and set up fields beside a live preview; names cannot repeat, and the form goes out as JSON that also loads back.3 views · 6 downloadsRegexTesterTests a pattern live: matches marked, groups listed, a replace preview, the pattern explained, errors at their place, and a guard against patterns that hang.3 views · 10 downloads
Thread
Thread 0 posts
On JsonFormatter: questions, tips, corrections, and code that uses it.
No posts yet. The first one opens the thread.