From d23f1bc214f354c0aabb23949e9382b1d04e4e9d Mon Sep 17 00:00:00 2001 From: Will Maier Date: Sun, 31 Aug 2025 07:38:56 -0700 Subject: [PATCH 1/3] Add failing test of "``` sql" code fence behavior --- .../fenced-code-space-after-backticks.md | 13 ++ .../fenced-code-space-after-backticks.md.json | 157 ++++++++++++++++++ 2 files changed, 170 insertions(+) create mode 100644 test/input/fenced-code-space-after-backticks.md create mode 100644 test/output/fenced-code-space-after-backticks.md.json diff --git a/test/input/fenced-code-space-after-backticks.md b/test/input/fenced-code-space-after-backticks.md new file mode 100644 index 000000000..404a36df6 --- /dev/null +++ b/test/input/fenced-code-space-after-backticks.md @@ -0,0 +1,13 @@ +# Fenced code with space after backticks + +This should work according to CommonMark but currently fails: + +``` sql +SELECT 1 + 2 AS result; +``` + +This works without the space: + +```sql +SELECT 3 + 4 AS result; +``` \ No newline at end of file diff --git a/test/output/fenced-code-space-after-backticks.md.json b/test/output/fenced-code-space-after-backticks.md.json new file mode 100644 index 000000000..c86a03c0c --- /dev/null +++ b/test/output/fenced-code-space-after-backticks.md.json @@ -0,0 +1,157 @@ +{ + "data": {}, + "title": "Fenced code with space after backticks", + "style": null, + "code": [ + { + "id": "5472e671", + "node": { + "body": { + "type": "Program", + "start": 0, + "end": 75, + "body": [ + { + "type": "ExpressionStatement", + "start": 0, + "end": 75, + "expression": { + "type": "CallExpression", + "start": 0, + "end": 74, + "callee": { + "type": "Identifier", + "start": 0, + "end": 7, + "name": "display" + }, + "arguments": [ + { + "type": "CallExpression", + "start": 8, + "end": 73, + "callee": { + "type": "MemberExpression", + "start": 8, + "end": 20, + "object": { + "type": "Identifier", + "start": 8, + "end": 14, + "name": "Inputs" + }, + "property": { + "type": "Identifier", + "start": 15, + "end": 20, + "name": "table" + }, + "computed": false, + "optional": false + }, + "arguments": [ + { + "type": "AwaitExpression", + "start": 21, + "end": 55, + "argument": { + "type": "TaggedTemplateExpression", + "start": 27, + "end": 55, + "tag": { + "type": "Identifier", + "start": 27, + "end": 30, + "name": "sql" + }, + "quasi": { + "type": "TemplateLiteral", + "start": 30, + "end": 55, + "expressions": [], + "quasis": [ + { + "type": "TemplateElement", + "start": 31, + "end": 54, + "value": { + "raw": "SELECT 3 + 4 AS result;", + "cooked": "SELECT 3 + 4 AS result;" + }, + "tail": true + } + ] + } + } + }, + { + "type": "ObjectExpression", + "start": 57, + "end": 72, + "properties": [ + { + "type": "Property", + "start": 58, + "end": 71, + "method": false, + "shorthand": false, + "computed": false, + "key": { + "type": "Identifier", + "start": 58, + "end": 64, + "name": "select" + }, + "value": { + "type": "Literal", + "start": 66, + "end": 71, + "value": false, + "raw": "false" + }, + "kind": "init" + } + ] + } + ], + "optional": false + } + ], + "optional": false + } + } + ], + "sourceType": "module" + }, + "declarations": [], + "references": [ + { + "type": "Identifier", + "start": 0, + "end": 7, + "name": "display" + }, + { + "type": "Identifier", + "start": 8, + "end": 14, + "name": "Inputs" + }, + { + "type": "Identifier", + "start": 27, + "end": 30, + "name": "sql" + } + ], + "files": [], + "imports": [], + "expression": false, + "async": true, + "input": "display(Inputs.table(await sql`SELECT 3 + 4 AS result;`, {select: false}));" + }, + "mode": "block" + } + ], + "path": "fenced-code-space-after-backticks.md" +} \ No newline at end of file From af940ce4af51c68066cec7004ad85240fe696da9 Mon Sep 17 00:00:00 2001 From: Will Maier Date: Sun, 31 Aug 2025 07:49:05 -0700 Subject: [PATCH 2/3] Make test fail as expected --- test/output/fenced-code-space-after-backticks.html | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 test/output/fenced-code-space-after-backticks.html diff --git a/test/output/fenced-code-space-after-backticks.html b/test/output/fenced-code-space-after-backticks.html new file mode 100644 index 000000000..0b47a2e7c --- /dev/null +++ b/test/output/fenced-code-space-after-backticks.html @@ -0,0 +1,5 @@ +

Fenced code with space after backticks

+

This should work according to CommonMark but currently fails:

+
+

This works without the space:

+
From 67cff34ea17f5382ba9cdda87417a1724ddd567b Mon Sep 17 00:00:00 2001 From: Will Maier Date: Sun, 31 Aug 2025 07:56:14 -0700 Subject: [PATCH 3/3] Fix fenced code block parsing to allow whitespace after opening backticks MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The CommonMark specification (0.29 ยง4.5) allows whitespace between the opening code fence and the info string. Previously, parseInfo() would treat leading whitespace as part of parsing state, causing "``` sql" to be parsed as an empty tag instead of "sql". This fix skips leading whitespace before parsing the tag name, ensuring both "```sql" and "``` sql" are correctly recognized as SQL code blocks, per the CommonMark spec which states that the first word of the info string typically specifies the language for syntax highlighting. Changes: - Modified parseInfo() in src/info.ts to skip leading whitespace before tag parsing - Updated test expectations to verify both formats work correctly - Both code blocks now properly render as Observable SQL blocks with hash IDs --- src/info.ts | 11 +- .../fenced-code-space-after-backticks.html | 4 +- .../fenced-code-space-after-backticks.md.json | 149 ++++++++++++++++++ 3 files changed, 160 insertions(+), 4 deletions(-) diff --git a/src/info.ts b/src/info.ts index e2a340a92..f3e0b417e 100644 --- a/src/info.ts +++ b/src/info.ts @@ -33,14 +33,21 @@ export function parseInfo(input: string): Info { let attributeName: string | undefined; let attributeNameStart: number | undefined; let attributeValueStart: number | undefined; + let tagStart = 0; const attributes = {}; - for (let i = 0, n = input.length; i <= n; ++i) { + + // Skip leading whitespace + while (tagStart < input.length && isSpaceCode(input.charCodeAt(tagStart))) { + tagStart++; + } + + for (let i = tagStart, n = input.length; i <= n; ++i) { const code = input.charCodeAt(i); // note: inclusive upper bound; code may be NaN! switch (state) { case STATE_TAG_NAME: { if (isSpaceCode(code) || isNaN(code)) { state = STATE_BEFORE_ATTRIBUTE_NAME; - tag = lower(input, 0, i); + tag = lower(input, tagStart, i); } break; } diff --git a/test/output/fenced-code-space-after-backticks.html b/test/output/fenced-code-space-after-backticks.html index 0b47a2e7c..0d39e5ef4 100644 --- a/test/output/fenced-code-space-after-backticks.html +++ b/test/output/fenced-code-space-after-backticks.html @@ -1,5 +1,5 @@

Fenced code with space after backticks

This should work according to CommonMark but currently fails:

-
+

This works without the space:

-
+
diff --git a/test/output/fenced-code-space-after-backticks.md.json b/test/output/fenced-code-space-after-backticks.md.json index c86a03c0c..06f8f2ed9 100644 --- a/test/output/fenced-code-space-after-backticks.md.json +++ b/test/output/fenced-code-space-after-backticks.md.json @@ -3,6 +3,155 @@ "title": "Fenced code with space after backticks", "style": null, "code": [ + { + "id": "853e4a25", + "node": { + "body": { + "type": "Program", + "start": 0, + "end": 75, + "body": [ + { + "type": "ExpressionStatement", + "start": 0, + "end": 75, + "expression": { + "type": "CallExpression", + "start": 0, + "end": 74, + "callee": { + "type": "Identifier", + "start": 0, + "end": 7, + "name": "display" + }, + "arguments": [ + { + "type": "CallExpression", + "start": 8, + "end": 73, + "callee": { + "type": "MemberExpression", + "start": 8, + "end": 20, + "object": { + "type": "Identifier", + "start": 8, + "end": 14, + "name": "Inputs" + }, + "property": { + "type": "Identifier", + "start": 15, + "end": 20, + "name": "table" + }, + "computed": false, + "optional": false + }, + "arguments": [ + { + "type": "AwaitExpression", + "start": 21, + "end": 55, + "argument": { + "type": "TaggedTemplateExpression", + "start": 27, + "end": 55, + "tag": { + "type": "Identifier", + "start": 27, + "end": 30, + "name": "sql" + }, + "quasi": { + "type": "TemplateLiteral", + "start": 30, + "end": 55, + "expressions": [], + "quasis": [ + { + "type": "TemplateElement", + "start": 31, + "end": 54, + "value": { + "raw": "SELECT 1 + 2 AS result;", + "cooked": "SELECT 1 + 2 AS result;" + }, + "tail": true + } + ] + } + } + }, + { + "type": "ObjectExpression", + "start": 57, + "end": 72, + "properties": [ + { + "type": "Property", + "start": 58, + "end": 71, + "method": false, + "shorthand": false, + "computed": false, + "key": { + "type": "Identifier", + "start": 58, + "end": 64, + "name": "select" + }, + "value": { + "type": "Literal", + "start": 66, + "end": 71, + "value": false, + "raw": "false" + }, + "kind": "init" + } + ] + } + ], + "optional": false + } + ], + "optional": false + } + } + ], + "sourceType": "module" + }, + "declarations": [], + "references": [ + { + "type": "Identifier", + "start": 0, + "end": 7, + "name": "display" + }, + { + "type": "Identifier", + "start": 8, + "end": 14, + "name": "Inputs" + }, + { + "type": "Identifier", + "start": 27, + "end": 30, + "name": "sql" + } + ], + "files": [], + "imports": [], + "expression": false, + "async": true, + "input": "display(Inputs.table(await sql`SELECT 1 + 2 AS result;`, {select: false}));" + }, + "mode": "block" + }, { "id": "5472e671", "node": {