diff --git a/src/client/main.js b/src/client/main.js index 67f3d55cc..76e8ca9d4 100644 --- a/src/client/main.js +++ b/src/client/main.js @@ -33,7 +33,7 @@ export function define(cell) { if (loading) root._nodes.push(loading); const pending = () => reset(root, loading); const rejected = (error) => reject(root, error); - const v = main.variable({_node: root, pending, rejected}, {shadow: {}}); // _node for visibility promise + const v = main.variable({_node: root.parentNode, pending, rejected}, {shadow: {}}); // _node for visibility promise if (inputs.includes("display") || inputs.includes("view")) { let displayVersion = -1; // the variable._version of currently-displayed values const display = inline ? displayInline : displayBlock; @@ -141,7 +141,7 @@ export function findRoots(root) { } function isRoot(node) { - return node.nodeType === 8 && /^:[0-9a-f]{8}:$/.test(node.data); + return node.nodeType === 8 && /^:[0-9a-f]{8}(?:-\d+)?:$/.test(node.data); } function isLoading(node) { diff --git a/src/html.ts b/src/html.ts index 0ca522f19..cf5720ab9 100644 --- a/src/html.ts +++ b/src/html.ts @@ -169,6 +169,8 @@ export function rewriteHtml( ? hljs.highlight(child.textContent!, {language}).value : isElement(child) ? child.outerHTML + : isComment(child) + ? `` : ""; } code.innerHTML = html; @@ -244,7 +246,7 @@ export function isElement(node: Node): node is Element { } function isRoot(node: Node): node is Comment { - return isComment(node) && /^:[0-9a-f]{8}:$/.test(node.data); + return isComment(node) && /^:[0-9a-f]{8}(?:-\d+)?:$/.test(node.data); } function isLoading(node: Node): node is Element { diff --git a/src/markdown.ts b/src/markdown.ts index e5d992e45..7e5d43f63 100644 --- a/src/markdown.ts +++ b/src/markdown.ts @@ -3,7 +3,6 @@ import {createHash} from "node:crypto"; import he from "he"; import MarkdownIt from "markdown-it"; import type {RuleCore} from "markdown-it/lib/parser_core.js"; -import type {RuleInline} from "markdown-it/lib/parser_inline.js"; import type {RenderRule} from "markdown-it/lib/renderer.js"; import MarkdownItAnchor from "markdown-it-anchor"; import type {Config} from "./config.js"; @@ -15,6 +14,7 @@ import {parseInfo} from "./info.js"; import type {JavaScriptNode} from "./javascript/parse.js"; import {parseJavaScript} from "./javascript/parse.js"; import {isAssetPath, relativePath} from "./path.js"; +import {parsePlaceholder} from "./placeholder.js"; import {transpileSql} from "./sql.js"; import {transpileTag} from "./tag.js"; import {InvalidThemeError} from "./theme.js"; @@ -36,10 +36,8 @@ export interface MarkdownPage { code: MarkdownCode[]; } -interface ParseContext { +export interface ParseContext { code: MarkdownCode[]; - startLine: number; - currentLine: number; path: string; } @@ -103,7 +101,6 @@ function makeFenceRenderer(baseRenderer: RenderRule): RenderRule { source = isFalse(attributes.run) ? undefined : getLiveSource(token.content, tag, attributes); if (source != null) { const id = uniqueCodeId(context, source); - // TODO const sourceLine = context.startLine + context.currentLine; const node = parseJavaScript(source, {path}); context.code.push({id, node}); html += `
${ @@ -123,161 +120,31 @@ function makeFenceRenderer(baseRenderer: RenderRule): RenderRule { }; } -const CODE_DOLLAR = 36; -const CODE_BRACEL = 123; -const CODE_BRACER = 125; -const CODE_BACKSLASH = 92; -const CODE_QUOTE = 34; -const CODE_SINGLE_QUOTE = 39; -const CODE_BACKTICK = 96; - -function parsePlaceholder(content: string, replacer: (i: number, j: number) => void) { - let afterDollar = false; - for (let j = 0, n = content.length; j < n; ++j) { - const cj = content.charCodeAt(j); - if (cj === CODE_BACKSLASH) { - ++j; // skip next character - continue; - } - if (cj === CODE_DOLLAR) { - afterDollar = true; - continue; - } - if (afterDollar) { - if (cj === CODE_BRACEL) { - let quote = 0; // TODO detect comments, too - let braces = 0; - let k = j + 1; - inner: for (; k < n; ++k) { - const ck = content.charCodeAt(k); - if (ck === CODE_BACKSLASH) { - ++k; - continue; - } - if (quote) { - if (ck === quote) quote = 0; - continue; - } - switch (ck) { - case CODE_QUOTE: - case CODE_SINGLE_QUOTE: - case CODE_BACKTICK: - quote = ck; - break; - case CODE_BRACEL: - ++braces; - break; - case CODE_BRACER: - if (--braces < 0) { - replacer(j - 1, k + 1); - break inner; - } - break; - } - } - j = k; +const transformPlaceholders: RuleCore = (state) => { + const context: ParseContext = state.env; + const outputs: string[] = []; + for (const {type, value} of parsePlaceholder(state.src)) { + if (type === "code") { + const id = uniqueCodeId(context, value); + try { + const node = parseJavaScript(value, {path: context.path, inline: true}); + context.code.push({id, node}); + outputs.push(``); + } catch (error) { + if (!(error instanceof SyntaxError)) throw error; + outputs.push( + `SyntaxError: ${he.escape( + error.message + )}` + ); } - afterDollar = false; - } - } -} - -function transformPlaceholderBlock(token) { - const input = token.content; - if (/^\s*]/.test(input)) return [token]; // ignore "}], + placeholders("") + ); + assert.deepStrictEqual( + [{type: "content", value: ""}], + placeholders("") + ); + assert.deepStrictEqual( + [{type: "content", value: ""}], + placeholders("") + ); + assert.deepStrictEqual( + [{type: "content", value: "${1 + 2}"}], + placeholders("${1 + 2}") + ); + }); + it("ignores placeholders within code", () => { + assert.deepStrictEqual([{type: "content", value: "`${1 + 2}`"}], placeholders("`${1 + 2}`")); + assert.deepStrictEqual([{type: "content", value: "``${1 + 2}``"}], placeholders("``${1 + 2}``")); + assert.deepStrictEqual([{type: "content", value: "``${`1 + 2`}``"}], placeholders("``${`1 + 2`}``")); + }); + it("ignores placeholders within comment", () => { + assert.deepStrictEqual([{type: "content", value: ""}], placeholders("")); + assert.deepStrictEqual([{type: "content", value: ""}], placeholders("")); + assert.deepStrictEqual( + [ + {type: "content", value: "\n"}, + {type: "code", value: "1"} + ], + placeholders("\n${1}") + ); + }); + it("ignores unterminated placeholders", () => { + assert.deepStrictEqual([{type: "content", value: "${1 + 2"}], placeholders("${1 + 2")); + assert.deepStrictEqual( + [{type: "content", value: "Hello, ${{foo: [1, 2]}"}], + placeholders("Hello, ${{foo: [1, 2]}") + ); + assert.deepStrictEqual( + [{type: "content", value: "Hello, ${`unterminated}"}], + placeholders("Hello, ${`unterminated}") + ); + }); +});