Multi-line opening tags break sibling tag detection
What happened?
Multi-line opening tags break sibling tag detection. When an opening tag spans multiple lines, subsequent sibling tags get incorrectly nested as children instead of remaining at the same level.
Expected: Both documents below should produce identical ASTs since they are semantically equivalent per the Markdoc tag specification, which explicitly allows newlines within tags.
Actual:
- Single-line: Both fields at depth 3 (siblings) ✓
- Multi-line: Field "a" at depth 1, field "b" at depth 4 (nested inside "a") ✗
To reproduce
Two semantically identical documents—the only difference is line wrapping in the opening tag:
Single-line opening tag (parses correctly):
{% field id="a" attr="x" %}{% /field %}
{% field id="b" %}{% /field %}Multi-line opening tag (parses incorrectly):
{% field id="a"
attr="x" %}{% /field %}
{% field id="b" %}{% /field %}Test script:
import Markdoc from '@markdoc/markdoc';
const singleLine = `{% field id="a" attr="x" %}{% /field %}
{% field id="b" %}{% /field %}`;
const multiLine = `{% field id="a"
attr="x" %}{% /field %}
{% field id="b" %}{% /field %}`;
function getFieldDepths(doc) {
const ast = Markdoc.parse(doc);
let fields = [];
function walk(node, depth = 0) {
if (node.type === 'tag' && node.tag === 'field') {
fields.push({ id: node.attributes?.id, depth });
}
for (const child of node.children || []) walk(child, depth + 1);
}
walk(ast);
return fields;
}
console.log('Single-line:', getFieldDepths(singleLine));
// → [ { id: 'a', depth: 3 }, { id: 'b', depth: 3 } ] ✓ Same depth
console.log('Multi-line:', getFieldDepths(multiLine));
// → [ { id: 'a', depth: 1 }, { id: 'b', depth: 4 } ] ✗ Different depths!Version
0.5.4
Additional context
Root Cause
In src/tokenizer/plugins/annotations.ts, the block tag rule:
Detects that the opening tag spans lines 0-1:
{% field id="a" attr="x" %}{% /field %}Advances
state.linepast those linesContent after
%}on line 1 (the{% /field %}closing tag) is never tokenized
The closing tag is lost, so the block-level tag_open has no matching tag_close, and all subsequent content becomes its children.
Token-level Evidence
const Tokenizer = Markdoc.Tokenizer;
const tokenizer = new Tokenizer();
// Single-line: both tags inline, properly paired
tokenizer.tokenize(singleLine);
// → paragraph containing inline: tag_open, tag_close, tag_open, tag_close
// Multi-line: closing tag for "a" is missing!
tokenizer.tokenize(multiLine);
// → tag_open (field a, block-level)
// → paragraph containing inline: tag_open (field b), tag_close (field b)
// → NO tag_close for field a!Workaround
Keep opening tags on a single line. Do not wrap attributes across multiple lines within {% ... %} delimiters.
Source: markdoc/markdoc