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*"}],
+ 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}")
+ );
+ });
+});