Skip to content

Prevent Liquid evaluation in markdown code blocks - #60

Merged
jesseditson merged 2 commits into
mainfrom
claude/liquid-parsing-code-blocks-t0kilb
Sep 15, 2026
Merged

jesseditson merged 2 commits into
mainfrom
claude/liquid-parsing-code-blocks-t0kilb

Conversation

@jesseditson

Copy link
Copy Markdown
Collaborator

Summary

This change prevents Liquid template syntax from being evaluated when it appears inside markdown code blocks (inline code spans, fenced code blocks, and indented code blocks). This ensures that code examples containing {{ }} or {% %} are rendered literally rather than being processed as Liquid templates.

Key Changes

  • Replaced comrak::markdown_to_html function call with a custom markdown_to_html implementation that:

    • Parses the markdown document into an AST
    • Traverses code nodes (inline code, code blocks, and fenced code blocks)
    • Neutralizes Liquid delimiters by replacing opening braces with a sentinel character (\0)
    • Renders the modified AST to HTML
    • Replaces the sentinel with { in the final HTML output
  • Added neutralize_liquid helper function that:

    • Detects {{ and {% patterns in code literals
    • Replaces only the opening brace of these delimiters with the sentinel
    • Returns None if no delimiters are found (optimization)
  • Updated imports to use lower-level comrak APIs (parse_document, format_html, Arena, NodeValue) instead of the high-level markdown_to_html function

  • Added comprehensive test coverage with 6 new tests validating:

    • Liquid syntax in inline code spans is neutralized
    • Liquid syntax in fenced code blocks is neutralized
    • Liquid syntax in indented code blocks is neutralized
    • Plain braces (not opening delimiters) are left untouched
    • Consecutive braces don't leave delimiters behind
    • Handwritten HTML with Liquid is unaffected (only markdown code is processed)
    • Code without Liquid remains unchanged
  • Updated documentation in authoring.md to clarify that literal {{ in markdown code doesn't need escaping

Implementation Details

  • Uses \0 (NUL character) as a sentinel because comrak replaces all NUL bytes with U+FFFD during parsing, making it impossible for the sentinel to appear in the AST from source input
  • The sentinel becomes { after HTML rendering, which displays as { in browsers
  • Only processes markdown's own code nodes; hand-written HTML passes through untouched, preserving the ability to use {% raw %} for opt-out
  • Maintains a cache for markdown rendering performance

https://claude.ai/code/session_01YXKzyT7wjxop7Me8egDDqZ

Liquid written inside a markdown code span or block is a sample, not a
statement, but a markdown field becomes html before the page renders and
`crate::tags::output` renders liquid it finds in a value — so the sample
was evaluated. Worse, comrak escapes code, so a filter argument's quotes
arrive as `"` and the value fails to parse at all, failing the build:

    {{sample_menus | where: "slug", "lunch" | map: "url" | first }}

Comrak's html renderer escapes code itself, so it can't be asked for a
literal `{`. Markdown is now parsed to an ast first, every `{` that opens
a liquid delimiter in a `Code` or `CodeBlock` node is replaced with a NUL
sentinel the renderer passes through, and the sentinel becomes `{`
once the html exists. NUL is sound as a sentinel because comrak replaces
every NUL in its input with U+FFFD, as CommonMark requires, so one can't
reach the ast from the source.

Only markdown's own code nodes are treated this way: html the author
wrote by hand still renders liquid, `<code>` included.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YXKzyT7wjxop7Me8egDDqZ
The gotcha still described the old architecture, where archival rendered
a finished page a second time if the output still held liquid — which is
what made a bare `{% raw %}` useless. `crate::tags::output` replaced that
with rendering a value in place, one level deep, so a template's raw tag
now behaves as it does anywhere else.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YXKzyT7wjxop7Me8egDDqZ
@jesseditson
jesseditson merged commit 698f040 into main Sep 15, 2026
14 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants