Parse markdown adjacent to a raw HTML block line - #660
Open
gordonwoodhull wants to merge 1 commit into
Open
Conversation
…ent-markdown-unparsed-0qnjuwuy)
Since 0.29.0 a line of prose immediately after a raw HTML block line was
emitted verbatim — backticks, asterisks and every other inline construct
survived into the page as literal text:
<details>
<summary>Example custom instructions</summary>
This example demonstrates how a `quarto.instructions.md` file shapes …
Quarto 1 parses that, as does pandoc 3.8.1, and so did q2 0.28.0. A blank line
was where the parser recovered, so to an author it read like a content typo
rather than an engine bug.
The lift added in bd-block-html-wrapped-in-p-w8qebxig turned a paragraph
opening with a block-level HTML tag into a `RawBlock` by re-reading its source
text verbatim. The extent was right — a paragraph and a CommonMark type-6 HTML
block both run to the next blank line — but re-reading the source also switched
off inline parsing inside it. That conflated two pandoc extensions: dropping
`native_divs` costs the `<p>` wrapper and the `Div` node, which was intended;
dropping `markdown_in_html_blocks` costs inline parsing, which was not. Measured
on a tight `<div>`, q2's output was byte-identical to
`pandoc -f markdown-native_divs-markdown_in_html_blocks`, and its AST was one
verbatim `RawBlock` where `-native_divs` alone gives `RawBlock`/`Plain`/
`RawBlock`.
`process_paragraph` already held the fully-parsed inlines and discarded them.
It now partitions that vector instead: runs of block-level raw HTML coalesce
into a `RawBlock`, the text between them becomes a `Plain`, and the parts are
returned as `IntermediateSection`, which every block container already splices.
Each inline is classified on its own — no tag matching, no lookahead — so this
stays inside the no-backtracking constraint in `dev-docs/syntax-notes.md`. Two
details earn their comments: the tag test runs on trimmed text, because
tree-sitter folds preceding whitespace into a non-first `RawInline`; and each
part gets its own span, since cloning the paragraph's onto all of them gives
siblings overlapping ranges and misleads the incremental writer.
`<pre>`, `<script>`, `<style>` and `<textarea>` are exempt and stay verbatim.
They are CommonMark's raw-text elements, pandoc does not parse markdown inside
them, and all four are in `BLOCK_TAGS` — without the exemption a split would
parse markdown inside a `<script>`.
Block-level raw HTML is now written bare rather than inside a ```{=html} fence,
which reverses the normalisation the lift originally shipped with. The fence is
kept for anything else, gated on whether the text would re-read as the same
single `RawBlock`: a pure run of block tags, or a raw-text element. Without that
gate an author's own ```{=html} block loses its fence and its contents are
parsed on the next read — a backtick becoming `Code`, a `#` line becoming a
`Header`.
Writing the AST back out does not reproduce the `Plain`. Blocks are always
separated by a blank line, so a lifted interior re-reads as a `Paragraph` and
gains a `<p>`; for `<div>` that matches Quarto 1, and for a phrasing-only
element such as `<summary>` it is invalid, though only after a write/read cycle
and never on first render (bd-8md6k9dv). Preserving the `Plain` would mean
writing it tight against the neighbouring tag, which requires knowing the two
blocks came from one paragraph. The AST does not record that. A rule that
inferred it from block types alone — as pandoc's writer does, licensed by a
reader invariant we do not have — merged a `Plain` with a following *unrelated*
`<script>` so its JavaScript was parsed as markdown, let a closing tag fall back
inside a paragraph, and collapsed filter-produced blocks into one. The writer
does not guess.
Interiors are always `Plain`, never `Paragraph`. Pandoc chooses between them by
tracking which element is still open, which needs the balanced matching
`syntax-notes.md` rejects. Every resulting difference is a `<p>` wrapper — the
`native_divs` gap qmd already accepts — and against Quarto 1 this matches on
every markdown-parsing question. Documented in `dev-docs/syntax-notes.md` and
`crates/pampa/README.md`.
`incremental_writer_tests::roundtrip_comment_in_blockquote` no longer uses
`assert_roundtrip`, which demands full structural equality after a rewrite.
That guarantee genuinely no longer holds, so it asserts what does: tags in block
position, prose parsed, shape stable from the first rewrite.
Snapshots: 2 modified, both the same class — `html-comment-17` and
`html-comment-44`, where a comment followed by text on the same line is now
`RawBlock` + `Plain` instead of one verbatim `RawBlock`. Rendering was checked
before accepting: neither the old nor the new output carries the `<p>` Quarto 1
emits there, so the comment simply moves onto its own line, closer to Q1.
Not fixed, filed as bd-495qnexy: a block-level tag mid-paragraph still does not
interrupt the paragraph, so a trailing `</details>` lands inside a `<p>`.
Pre-existing and byte-identical on 0.28.0, 0.29.0 and main.
Tests: workspace 13697 passed, 0 failed, 199 skipped — baseline 13676, and +21
is exactly the tests added here. Clippy, cargo fmt and cargo xtask lint clean;
cargo xtask verify passes all 14 steps including the WASM leg. Both repro
fixtures are green and q2 matches Quarto 1 on all four rows of the regression
repro; verified end-to-end through `q2 render` with the output inspected.
Contributor
✅ Snyk checks have passed. No issues have been found so far.
💻 Catch issues earlier using the plugins for VS Code, JetBrains IDEs, Visual Studio, and Eclipse. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Since 0.29.0, a line of prose immediately following a raw HTML block line is emitted verbatim. Backticks, asterisks and every other inline construct survive into the rendered page as literal text:
Quarto 1 parses that, as does pandoc 3.8.1, and so did q2 0.28.0. A blank line is where the parser recovers, which makes it read like a content typo rather than an engine bug.
Cause
The lift added in bd-block-html-wrapped-in-p-w8qebxig turns a paragraph opening with a block-level HTML tag into a
RawBlockby re-reading its source text verbatim. The extent is right — a markdown paragraph and a CommonMark type-6 HTML block both run to the next blank line — but re-reading the source also switches off inline parsing inside it.That conflates two pandoc extensions. Dropping
native_divscosts the<p>wrapper and theDivnode, which was intended. Droppingmarkdown_in_html_blockscosts inline parsing, which was not. On a tight<div>, q2's output was byte-identical topandoc -f markdown-native_divs-markdown_in_html_blocks, and its AST was a single verbatimRawBlockwhere-native_divsalone givesRawBlock/Plain/RawBlock.Reader
process_paragraphalready holds the fully-parsed inlines and was discarding them. It now partitions that vector: runs of block-level raw HTML coalesce into aRawBlock, text between them becomes aPlain, and the parts return asIntermediateSection, which every block container already splices. Classification is per-inline — no tag matching, no lookahead — so it stays inside the no-backtracking constraint indev-docs/syntax-notes.md.Two details are load-bearing. The tag test runs on trimmed text, because tree-sitter folds preceding whitespace into a non-first
RawInline. And each part gets its own span; cloning the paragraph's onto all of them gives siblings overlapping ranges and misleads the incremental writer.<pre>,<script>,<style>and<textarea>are exempt and stay verbatim — CommonMark's raw-text elements, which pandoc also leaves unparsed. All four are inBLOCK_TAGS, so without the exemption a split would parse markdown inside a<script>.Writer
Block-level raw HTML is written bare rather than inside a
```{=html}fence, reversing the normalisation the lift originally shipped with. The fence is kept for everything else, gated on whether the text would re-read as the same singleRawBlock— a pure run of block tags, or a raw-text element. Without that gate, an author's own```{=html}block loses its fence and its contents are parsed on the next read: a backtick becomesCode, a#line becomes aHeader.Blocks are always separated by a blank line. Pandoc's writer is tight for
RawBlock→PlainandPlain→RawBlock, but that is safe only because pandoc's reader guaranteesPlainhere means "inside an HTML block that is still open". q2 has no such invariant —Plainis emitted for every split interior, and filters emit them freely — so the writer cannot tell whether aPlainand an adjacentRawBlockcame from one paragraph or two, and the AST does not record the answer.Known divergences
Paragraphafter an AST → qmd → read cycle, gaining a<p>. For<div>this matches Quarto 1; for a phrasing-only element such as<summary>it is invalid, though only after a write/read cycle and never on first render. bd-8md6k9dv.Plain, neverParagraph. Pandoc chooses by tracking which element is open, which needs balanced matching. Every resulting difference is a<p>wrapper — thenative_divsgap qmd already accepts. Against Quarto 1 this matches on every markdown-parsing question.</details>lands inside a<p>. Pre-existing and byte-identical on 0.28.0, 0.29.0 and main. bd-495qnexy.Notes for review
incremental_writer_tests::roundtrip_comment_in_blockquoteno longer usesassert_roundtrip, which demands full structural equality after a rewrite. That guarantee no longer holds; the test now asserts tags in block position, prose parsed, and a fixed point from the first rewrite.Two snapshots change, both the same class —
html-comment-17andhtml-comment-44, where a comment followed by same-line text becomesRawBlock+Plaininstead of one verbatimRawBlock. Neither the old nor the new output carries the<p>Quarto 1 emits there; the comment simply moves onto its own line.Workspace 13740 passed / 0 failed / 199 skipped (baseline 13719 at
origin/main; +21 are the tests added here). Clippy,cargo fmtandcargo xtask lintclean;cargo xtask verifypasses all 14 steps including the WASM leg. Both repro fixtures inq2-positron-docs/llms-info/repros/are green simultaneously, and q2 matches Quarto 1 on all four rows of the regression repro.