diff --git a/doc/playground.css b/doc/playground.css index 9666ae4bfe..038c314947 100644 --- a/doc/playground.css +++ b/doc/playground.css @@ -268,7 +268,7 @@ main { } .tree-null { - color: var(--color-text-light); + color: #7e22ce; font-style: italic; } diff --git a/doc/playground.js b/doc/playground.js index f593b96afe..e617bdb53f 100644 --- a/doc/playground.js +++ b/doc/playground.js @@ -6,6 +6,9 @@ const editorDiv = document.getElementById("editor"); const loading = document.getElementById("loading"); const toast = document.getElementById("toast"); +const encoder = new TextEncoder(); +const decoder = new TextDecoder(); + // Load Prism WASM and Monaco, show error if either fails let instance, monaco; try { @@ -115,7 +118,7 @@ end // URL-safe base64 encode/decode (RFC 4648 §5) function encodeSource(str) { - const bytes = new TextEncoder().encode(str); + const bytes = encoder.encode(str); let binary = ""; for (let i = 0; i < bytes.length; i++) binary += String.fromCharCode(bytes[i]); return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, ""); @@ -123,7 +126,7 @@ function encodeSource(str) { function decodeSource(str) { const padded = str.replace(/-/g, "+").replace(/_/g, "/") + "==".slice(0, (4 - str.length % 4) % 4); - return new TextDecoder().decode(Uint8Array.from(atob(padded), ch => ch.codePointAt(0))); + return decoder.decode(Uint8Array.from(atob(padded), ch => ch.codePointAt(0))); } // Read initial source from URL hash or use default @@ -241,22 +244,30 @@ document.getElementById("expand-all").addEventListener("click", () => { output.querySelectorAll(".tree-toggle").forEach(toggle => setToggleState(toggle, false)); }); -// Convert byte offset to line:column using the source string -function offsetToLineCol(source, offset) { +// Convert byte offset to line:column using the utf8 bytes +function offsetToLineCol(utf8Bytes, offset) { let line = 1, col = 0; - for (let i = 0; i < offset && i < source.length; i++) { - if (source[i] === "\n") { line++; col = 0; } + for (let i = 0; i < offset && i < utf8Bytes.length; i++) { + // Check for newline + if (utf8Bytes[i] === 10) { line++; col = 0; } else { col++; } } return { line, col }; } -function formatLoc(source, loc) { +function formatLoc(utf8Bytes, loc, includeSlice) { if (!loc || loc.startOffset === undefined) return null; - const start = offsetToLineCol(source, loc.startOffset); - const end = offsetToLineCol(source, loc.startOffset + loc.length); - return { start, end, text: `${start.line}:${start.col}-${end.line}:${end.col}` }; + const start = offsetToLineCol(utf8Bytes, loc.startOffset); + const end = offsetToLineCol(utf8Bytes, loc.startOffset + loc.length); + + let text = `${start.line}:${start.col}-${end.line}:${end.col}`; + + if (includeSlice) { + const slice = decoder.decode(utf8Bytes.slice(loc.startOffset, loc.startOffset + loc.length)); + text = `${text} = ${JSON.stringify(slice)}` + } + return { start, end, text }; } function locDataAttrs(loc) { @@ -284,6 +295,10 @@ function isNode(value) { return value && typeof value === "object" && !Array.isArray(value) && value.location && value.constructor && value.constructor.name !== "Object"; } +function isString(value) { + return value && typeof value === "object" && Object.hasOwn(value, "encoding") +} + // Get the node type name from the class name function nodeType(node) { return node.constructor?.name || "Unknown"; @@ -333,7 +348,7 @@ function hasChildNodes(fields, node) { const CONNECTOR = { last: "└── ", mid: "├── ", lastPad: " ", midPad: "│ " }; // Build the AST tree as interactive HTML -function renderNode(node, source, prefix, isLast, isRoot) { +function renderNode(node, utf8Bytes, prefix, isLast, isRoot) { if (!isNode(node)) return ""; const type = nodeType(node); @@ -346,11 +361,11 @@ function renderNode(node, source, prefix, isLast, isRoot) { if (!isRoot) html += ``; if (foldable) html += ``; - const loc = formatLoc(source, node.location); + const loc = formatLoc(utf8Bytes, node.location, false); const locAttrs = locDataAttrs(loc); html += `@ ${escapedType}`; - if (loc) html += ` (${loc.text})`; + if (loc) html += ` (location: ${loc.text})`; html += ``; html += `
`; @@ -375,7 +390,7 @@ function renderNode(node, source, prefix, isLast, isRoot) { html += `
${escapeHtml(field)}: (${value.length} item${value.length === 1 ? "" : "s"})
`; value.forEach((item, i) => { if (isNode(item)) { - html += renderNode(item, source, fieldChildPrefix, i === value.length - 1); + html += renderNode(item, utf8Bytes, fieldChildPrefix, i === value.length - 1); } else { const itemConnector = i === value.length - 1 ? CONNECTOR.last : CONNECTOR.mid; html += `
${escapeHtml(JSON.stringify(item))}
`; @@ -384,14 +399,14 @@ function renderNode(node, source, prefix, isLast, isRoot) { } } else if (isNode(value)) { html += `
${escapeHtml(field)}:
`; - html += renderNode(value, source, fieldChildPrefix, true); + html += renderNode(value, utf8Bytes, fieldChildPrefix, true); } else if (typeof value === "object" && value.startOffset !== undefined) { - const fieldLoc = formatLoc(source, value); + const fieldLoc = formatLoc(utf8Bytes, value, true); if (fieldLoc) { html += `
${escapeHtml(field)}: ${fieldLoc.text}
`; } - } else if (typeof value === "string") { - html += `
${escapeHtml(field)}: ${escapeHtml(JSON.stringify(value))}
`; + } else if (isString(value)) { + html += `
${escapeHtml(field)}: ${escapeHtml(JSON.stringify(value.value))}
`; } else { html += `
${escapeHtml(field)}: ${escapeHtml(String(value))}
`; } @@ -407,8 +422,8 @@ function escapeHtml(str) { } // Render a single diagnostic line -function renderDiagnostic(source, item, kind) { - const loc = formatLoc(source, item.location); +function renderDiagnostic(utf8Bytes, item, kind) { + const loc = formatLoc(utf8Bytes, item.location, false); const cssClass = kind === "Error" ? "error-text" : "warning-text"; return `
${kind}: ${escapeHtml(item.message)}${loc ? ` (${loc.text})` : ""}
`; } @@ -453,9 +468,10 @@ function render() { output.setAttribute("aria-labelledby", currentTab === "ast" ? "tab-ast" : "tab-diagnostics"); + const utf8Bytes = encoder.encode(lastSource); switch (currentTab) { case "ast": - const tree = renderNode(lastResult.value, lastSource, "", true, true); + const tree = renderNode(lastResult.value, utf8Bytes, "", true, true); output.innerHTML = tree ? `
${tree}
` : `
${escapeHtml(lastResult.error || "Failed to parse.")}
`; @@ -468,8 +484,8 @@ function render() { output.innerHTML = `
No errors or warnings.
`; } else { let html = ""; - for (const err of errors) html += renderDiagnostic(lastSource, err, "Error"); - for (const warn of warnings) html += renderDiagnostic(lastSource, warn, "Warning"); + for (const err of errors) html += renderDiagnostic(utf8Bytes, err, "Error"); + for (const warn of warnings) html += renderDiagnostic(utf8Bytes, warn, "Warning"); output.innerHTML = html; } break;