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-value-null { color: #be123c; font-style: italic; }
|
||||||
.json-empty { padding: .25rem .65rem; color: #475569; 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 { 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-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 (max-width: 42rem) { .json-entry { grid-template-columns: 1fr; gap: .05rem; } }
|
||||||
@media (prefers-color-scheme: dark) {
|
@media (prefers-color-scheme: dark) {
|
||||||
.json-view { border-color: #475569; background: #111827; color: #e5e7eb; }
|
.json-view { border-color: #475569; background: #111827; color: #e5e7eb; }
|
||||||
@@ -69,6 +71,7 @@
|
|||||||
.json-value-number { color: #c4b5fd; }
|
.json-value-number { color: #c4b5fd; }
|
||||||
.json-value-boolean { color: #fcd34d; }
|
.json-value-boolean { color: #fcd34d; }
|
||||||
.json-value-null { color: #fda4af; }
|
.json-value-null { color: #fda4af; }
|
||||||
|
.json-raw-value { color: #cbd5e1; }
|
||||||
}
|
}
|
||||||
#admin-toolbar { display: flex; flex-wrap: wrap; gap: .5rem; margin-block: 1rem; }
|
#admin-toolbar { display: flex; flex-wrap: wrap; gap: .5rem; margin-block: 1rem; }
|
||||||
#player-search { display: flex; flex-wrap: wrap; align-items: end; gap: .5rem; }
|
#player-search { display: flex; flex-wrap: wrap; align-items: end; gap: .5rem; }
|
||||||
@@ -656,6 +659,24 @@
|
|||||||
parent.append(element);
|
parent.append(element);
|
||||||
return 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) {
|
function renderJsonTree(container, value) {
|
||||||
container.className = 'json-view';
|
container.className = 'json-view';
|
||||||
container.setAttribute('aria-label', 'Utfällbart JSON-svar');
|
container.setAttribute('aria-label', 'Utfällbart JSON-svar');
|
||||||
@@ -677,7 +698,19 @@
|
|||||||
const groupSummary = item => Array.isArray(item)
|
const groupSummary = item => Array.isArray(item)
|
||||||
? `Lista · ${item.length} ${item.length === 1 ? 'post' : 'poster'}`
|
? `Lista · ${item.length} ${item.length === 1 ? 'post' : 'poster'}`
|
||||||
: `Objekt · ${Object.keys(item).length} fält`;
|
: `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 addLeaf = (parent, label, item) => {
|
||||||
|
const readable = typeof item === 'string' ? decodeEscapedSequences(item) : item;
|
||||||
if (typeof item === 'string' && item.length > 180) {
|
if (typeof item === 'string' && item.length > 180) {
|
||||||
const details = document.createElement('details');
|
const details = document.createElement('details');
|
||||||
details.className = 'json-long-value';
|
details.className = 'json-long-value';
|
||||||
@@ -685,13 +718,15 @@
|
|||||||
summary.append(document.createTextNode(`${label ? `${label} · ` : ''}Text · ${item.length} tecken · `));
|
summary.append(document.createTextNode(`${label ? `${label} · ` : ''}Text · ${item.length} tecken · `));
|
||||||
const preview = document.createElement('span');
|
const preview = document.createElement('span');
|
||||||
preview.className = 'json-value-string';
|
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);
|
summary.append(preview);
|
||||||
const fullText = document.createElement('div');
|
const fullText = document.createElement('div');
|
||||||
fullText.className = 'json-long-text';
|
fullText.className = 'json-long-text';
|
||||||
fullText.textContent = item;
|
fullText.textContent = readable;
|
||||||
details.append(summary, fullText);
|
details.append(summary, fullText);
|
||||||
parent.append(details);
|
parent.append(details);
|
||||||
|
if (readable !== item) addRawText(parent, item);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const row = document.createElement('div');
|
const row = document.createElement('div');
|
||||||
@@ -705,9 +740,10 @@
|
|||||||
const valueNode = document.createElement('span');
|
const valueNode = document.createElement('span');
|
||||||
const type = item === null ? 'null' : typeof item;
|
const type = item === null ? 'null' : typeof item;
|
||||||
valueNode.className = `json-value-${type === 'boolean' ? 'boolean' : type}`;
|
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);
|
row.append(valueNode);
|
||||||
parent.append(row);
|
parent.append(row);
|
||||||
|
if (typeof item === 'string' && readable !== item) addRawText(parent, item);
|
||||||
};
|
};
|
||||||
const populate = (details, item, depth) => {
|
const populate = (details, item, depth) => {
|
||||||
if (details.dataset.loaded === 'true') return;
|
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 'adminPlayerTrace(accountId: \$accountId, limit: \$limit, offset: \$offset)' index.html
|
||||||
grep -q 'function renderDecisionTrace' index.html
|
grep -q 'function renderDecisionTrace' index.html
|
||||||
grep -q 'function renderJsonTree' 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 "'Öppna alla block'" index.html
|
||||||
grep -q "'Fäll ihop nästlade block'" index.html
|
grep -q "'Fäll ihop nästlade block'" index.html
|
||||||
grep -q "nested.className = 'json-node'" 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.match(page, /JSON\.stringify\(npc\.profile, null, '\\t'\)/);
|
||||||
assert.doesNotMatch(page, /jsnview|cdn\.jsdelivr/);
|
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