Skip to content

fix(post): parse nested tags with context-aware lexer - #5800

Open
stevenjoezhang wants to merge 4 commits into
masterfrom
fix/context-aware-post-lexer
Open

fix(post): parse nested tags with context-aware lexer#5800
stevenjoezhang wants to merge 4 commits into
masterfrom
fix/context-aware-post-lexer

Conversation

@stevenjoezhang

@stevenjoezhang stevenjoezhang commented Jul 24, 2026

Copy link
Copy Markdown
Member

What does it do?

This replaces Hexo's separate post-render scanners with one context-aware lexer and processor for:

  • Markdown fenced code
  • Markdown inline code
  • HTML comments
  • Nunjucks variables, comments, and block tags
  • Opaque {% raw %} blocks

The lexer tracks these contexts in a single pass, so delimiters belonging to one syntax are not interpreted as another.

Why is it needed?

The previous post-render scanner matched a block tag with the first apparent end<tag>. It could not correctly handle a tag nested inside another tag with the same name:

{% folding outer %}
  {% tabs install %}
    <!-- tab `JavaScript` -->
    {% folding, inner %}
    {% endfolding %}
    <!-- endtab -->
  {% endtabs %}
{% endfolding %}

The inner endfolding could be mistaken for the end of the outer block. Markdown processing would then split the outer tag body before the theme plugin received it.

This breaks comment-based protocols used by themes such as NexT and Volantis. HTML comments like <!-- tab ... --> are functional input to those plugins and must remain available during tag rendering.

The new lexer pairs block tags by their exact Nunjucks token name and maintains a nesting stack. Tag names are read using Nunjucks delimiter rules, including syntax such as {% folding, ... %}.

Fixes #5799 without regressing the functional HTML-comment handling required by #5433.

Implementation

Shared lexer state

post_render_lexer performs a single context-aware scan:

  • Nunjucks strings and regular expressions hide Markdown and HTML delimiters.
  • Markdown code and HTML comments hide Nunjucks delimiters.
  • {% raw %} contents are opaque; only valid nested raw and endraw boundaries are recognized.
  • Incomplete tags inside raw content cannot consume the real endraw.
  • Same-name nested block tags are paired with a stack.
  • Custom paired tag names beginning with end are supported.
  • Whitespace-control delimiters and Nunjucks symbol delimiters are recognized.

Unified post-render processing

post_render_processor consumes the lexer ranges to:

  • Protect complete Nunjucks blocks before Markdown rendering.
  • Preserve HTML comments used as tag-plugin protocols.
  • Keep Nunjucks-looking text inside those comments literal.
  • Protect inline and fenced code from Nunjucks evaluation.
  • Highlight fenced code using metadata from the same lexer pass.
  • Restore comments and Nunjucks blocks before tag rendering.
  • Restore code placeholders after tag rendering.

The old before_post_render/backtick_code_block filter is removed, so the document is no longer independently parsed for fenced code a second time.

One observable ordering change is intentional: custom before_post_render filters now receive the original Markdown, before Hexo performs built-in fenced-code highlighting.

No new runtime dependency is introduced.

Tests

Added unit and integration coverage for:

  • The reported Volantis folding and tabs structure.
  • Direct same-name nesting with {% folding, ... %} syntax.
  • Comment protocols containing literal Nunjucks expressions.
  • HTML comments inside and outside Markdown code.
  • Nunjucks strings containing Markdown or HTML delimiters.
  • Inline code inside Nunjucks tokens.
  • Raw blocks containing backticks, comments, and incomplete tags.
  • Nested and unterminated raw blocks.
  • Paired tag names beginning with end.
  • Whitespace-control delimiters and Nunjucks regular expressions.
  • Highlighted and unhighlighted fenced code.

Validation performed:

  • 194 passing, 1 pending in the focused post-render test suite.
  • TypeScript build passed.
  • ESLint passed for lib and test.
  • 20,000 generated mixed-syntax documents completed without crashes or invalid ranges.
  • Differential compatibility checks against origin/master.
  • The original Volantis reproduction retains both tab panes, all timeline nodes, both folding blocks, and all fenced code blocks.

Performance

Local end-to-end post.render() benchmarks were run with Node.js 24.15.0 after 15 warm-up iterations.

Corpus Size master median This PR median Difference
Ordinary Markdown 67.7 KB 13.78 ms 14.08 ms +0.31 ms (+2.2%)
Template-dense Markdown 3.6 KB 0.790 ms 0.870 ms +0.081 ms (+10.2%)
Fenced-code-dense Markdown 67.7 KB 18.59 ms 19.99 ms +1.40 ms (+7.5%)

The ordinary no-Nunjucks/no-fence path exits before lexing. Even in the deliberately fence-heavy corpus, the absolute median overhead remained approximately 1.4 ms.

Raw-block scanning is linear: a stress input containing 8,000 unmatched raw openers was scanned in approximately 2.1 ms locally.

Pull request tasks

  • Add test cases for the changes.
  • Pass local build and lint checks.
  • Pass the CI test matrix.

@github-actions

Copy link
Copy Markdown

How to test

git clone -b fix/context-aware-post-lexer https://github.com/hexojs/hexo.git
cd hexo
npm install
npm test

@stevenjoezhang
stevenjoezhang requested a review from a team July 24, 2026 04:22
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.

建议回滚PR #5616 #5717 #5722

1 participant