Decode escaped text in admin JSON views
Build and publish frontend / publish (push) Successful in 53s
Build and publish frontend / publish (push) Successful in 53s
This commit is contained in:
+40
-4
@@ -55,8 +55,10 @@
|
||||
.json-value-null { color: #be123c; font-style: italic; }
|
||||
.json-empty { padding: .25rem .65rem; color: #475569; font-style: italic; }
|
||||
.json-long-value { border-inline-start: 2px solid #94a3b8; margin-inline-start: .5rem; }
|
||||
.json-long-value > summary { cursor: pointer; padding: .3rem .45rem; overflow-wrap: anywhere; }
|
||||
.json-long-value > summary, .json-raw-value > summary { cursor: pointer; padding: .3rem .45rem; overflow-wrap: anywhere; }
|
||||
.json-long-text { margin: .2rem .5rem .5rem; padding: .5rem; max-height: 24rem; overflow: auto; background: #e2e8f0; white-space: pre-wrap; overflow-wrap: anywhere; }
|
||||
.json-raw-value { margin-inline-start: .5rem; color: #475569; font-size: .9em; }
|
||||
.json-raw-value > .json-long-text { border-inline-start: 2px solid #94a3b8; }
|
||||
@media (max-width: 42rem) { .json-entry { grid-template-columns: 1fr; gap: .05rem; } }
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.json-view { border-color: #475569; background: #111827; color: #e5e7eb; }
|
||||
@@ -69,6 +71,7 @@
|
||||
.json-value-number { color: #c4b5fd; }
|
||||
.json-value-boolean { color: #fcd34d; }
|
||||
.json-value-null { color: #fda4af; }
|
||||
.json-raw-value { color: #cbd5e1; }
|
||||
}
|
||||
#admin-toolbar { display: flex; flex-wrap: wrap; gap: .5rem; margin-block: 1rem; }
|
||||
#player-search { display: flex; flex-wrap: wrap; align-items: end; gap: .5rem; }
|
||||
@@ -656,6 +659,24 @@
|
||||
parent.append(element);
|
||||
return element;
|
||||
}
|
||||
function decodeEscapedSequences(text) {
|
||||
const source = String(text).replace(/\\(?:\r\n|\n|\r)[ \t]*/g, '');
|
||||
const escapes = {
|
||||
'0': '\0', a: '\x07', b: '\b', t: '\t', n: '\n', v: '\x0b',
|
||||
f: '\f', r: '\r', e: '\x1b', ' ': ' ', '"': '"', "'": "'",
|
||||
'/': '/', '\\': '\\', N: '\u0085', _: '\u00a0', L: '\u2028', P: '\u2029',
|
||||
};
|
||||
return source.replace(/\\(U[0-9a-fA-F]{8}|u[0-9a-fA-F]{4}|x[0-9a-fA-F]{2}|[\s\S])/g, (sequence, escape) => {
|
||||
if (Object.hasOwn(escapes, escape)) return escapes[escape];
|
||||
if (/^x[0-9a-fA-F]{2}$/.test(escape)) return String.fromCharCode(Number.parseInt(escape.slice(1), 16));
|
||||
if (/^u[0-9a-fA-F]{4}$/.test(escape)) return String.fromCharCode(Number.parseInt(escape.slice(1), 16));
|
||||
if (/^U[0-9a-fA-F]{8}$/.test(escape)) {
|
||||
const point = Number.parseInt(escape.slice(1), 16);
|
||||
if (point <= 0x10ffff && !(point >= 0xd800 && point <= 0xdfff)) return String.fromCodePoint(point);
|
||||
}
|
||||
return sequence;
|
||||
});
|
||||
}
|
||||
function renderJsonTree(container, value) {
|
||||
container.className = 'json-view';
|
||||
container.setAttribute('aria-label', 'Utfällbart JSON-svar');
|
||||
@@ -677,7 +698,19 @@
|
||||
const groupSummary = item => Array.isArray(item)
|
||||
? `Lista · ${item.length} ${item.length === 1 ? 'post' : 'poster'}`
|
||||
: `Objekt · ${Object.keys(item).length} fält`;
|
||||
const addRawText = (parent, value) => {
|
||||
const raw = document.createElement('details');
|
||||
raw.className = 'json-raw-value';
|
||||
const rawSummary = document.createElement('summary');
|
||||
rawSummary.textContent = 'Visa lagrad text med escape-tecken';
|
||||
const rawText = document.createElement('div');
|
||||
rawText.className = 'json-long-text';
|
||||
rawText.textContent = value;
|
||||
raw.append(rawSummary, rawText);
|
||||
parent.append(raw);
|
||||
};
|
||||
const addLeaf = (parent, label, item) => {
|
||||
const readable = typeof item === 'string' ? decodeEscapedSequences(item) : item;
|
||||
if (typeof item === 'string' && item.length > 180) {
|
||||
const details = document.createElement('details');
|
||||
details.className = 'json-long-value';
|
||||
@@ -685,13 +718,15 @@
|
||||
summary.append(document.createTextNode(`${label ? `${label} · ` : ''}Text · ${item.length} tecken · `));
|
||||
const preview = document.createElement('span');
|
||||
preview.className = 'json-value-string';
|
||||
preview.textContent = `${item.slice(0, 100)}${item.length > 100 ? '…' : ''}`;
|
||||
const previewText = readable.slice(0, 100);
|
||||
preview.textContent = `${previewText}${readable.length > 100 ? '…' : ''}`;
|
||||
summary.append(preview);
|
||||
const fullText = document.createElement('div');
|
||||
fullText.className = 'json-long-text';
|
||||
fullText.textContent = item;
|
||||
fullText.textContent = readable;
|
||||
details.append(summary, fullText);
|
||||
parent.append(details);
|
||||
if (readable !== item) addRawText(parent, item);
|
||||
return;
|
||||
}
|
||||
const row = document.createElement('div');
|
||||
@@ -705,9 +740,10 @@
|
||||
const valueNode = document.createElement('span');
|
||||
const type = item === null ? 'null' : typeof item;
|
||||
valueNode.className = `json-value-${type === 'boolean' ? 'boolean' : type}`;
|
||||
valueNode.textContent = typeof item === 'string' ? JSON.stringify(item) : String(item);
|
||||
valueNode.textContent = typeof readable === 'string' ? readable : String(readable);
|
||||
row.append(valueNode);
|
||||
parent.append(row);
|
||||
if (typeof item === 'string' && readable !== item) addRawText(parent, item);
|
||||
};
|
||||
const populate = (details, item, depth) => {
|
||||
if (details.dataset.loaded === 'true') return;
|
||||
|
||||
@@ -100,6 +100,9 @@ grep -q "query AdminPlayerTrace" index.html
|
||||
grep -q 'adminPlayerTrace(accountId: \$accountId, limit: \$limit, offset: \$offset)' index.html
|
||||
grep -q 'function renderDecisionTrace' index.html
|
||||
grep -q 'function renderJsonTree' index.html
|
||||
grep -q 'function decodeEscapedSequences' index.html
|
||||
grep -q 'decodeEscapedSequences(item)' index.html
|
||||
grep -q "Visa lagrad text med escape-tecken" index.html
|
||||
grep -q "'Öppna alla block'" index.html
|
||||
grep -q "'Fäll ihop nästlade block'" index.html
|
||||
grep -q "nested.className = 'json-node'" index.html
|
||||
|
||||
@@ -64,3 +64,18 @@ test('admin JSON responses are readable as accessible collapsible trees', () =>
|
||||
assert.match(page, /JSON\.stringify\(npc\.profile, null, '\\t'\)/);
|
||||
assert.doesNotMatch(page, /jsnview|cdn\.jsdelivr/);
|
||||
});
|
||||
|
||||
test('admin JSON text decodes common JSON and YAML escapes while retaining the stored form', () => {
|
||||
const decoder = page.match(/function decodeEscapedSequences\(text\) \{[\s\S]*?\n \}/);
|
||||
assert.ok(decoder, 'escape decoder should be present');
|
||||
assert.match(page, /Visa lagrad text med escape-tecken/);
|
||||
assert.match(page, /decodeEscapedSequences\(item\)/);
|
||||
const decode = new Function(`${decoder[0]}; return decodeEscapedSequences;`)();
|
||||
|
||||
assert.equal(decode(String.raw`Rad 1\nRad 2`), 'Rad 1\nRad 2');
|
||||
assert.equal(decode(String.raw`Kolumn 1\tKolumn 2`), 'Kolumn 1\tKolumn 2');
|
||||
assert.equal(decode(String.raw`citat: \'hej\'`), "citat: 'hej'");
|
||||
assert.equal(decode(String.raw`\u00E5\x21`), 'å!');
|
||||
assert.equal(decode(String.raw`\U0001F642`), '🙂');
|
||||
assert.equal(decode(String.raw`okänt: \q`), String.raw`okänt: \q`);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user