Skip to content

Fix double-processing of highlighted code causing span tags to render as text (#1661) - #1668

Merged
lsegal merged 2 commits into
lsegal:mainfrom
dduugg:fix/double-highlight-span-escape-1661
Apr 8, 2026
Merged

Fix double-processing of highlighted code causing span tags to render as text (#1661)#1668
lsegal merged 2 commits into
lsegal:mainfrom
dduugg:fix/double-highlight-span-escape-1661

Conversation

@dduugg

@dduugg dduugg commented Apr 8, 2026

Copy link
Copy Markdown
Contributor

Problem

When {yard:include_tags} (or similar tag embedding) resolves, it returns HTML containing already-highlighted <pre class="example code"><code>HIGHLIGHTED</code></pre> blocks. These blocks are then re-processed by parse_codeblocks, which calls html_syntax_highlight_ruby_ripper on the already-highlighted HTML.

Ripper fails to parse HTML as Ruby and falls into the rescue clause:

rescue Parser::ParserSyntaxError
  source =~ /^<span\s+class=/ ? source : h(source)
end

The ^ anchor means "start of line". For highlighted code with leading whitespace (e.g., indented def foo produces <span class='kw'>def</span> ...), the first characters are spaces, not <span. The regex fails, h(source) is called, and all span tags get HTML-escaped into visible text — <span class='kw'> appears literally on the page.

Fixes #1661.

Changes

lib/yard/templates/helpers/html_syntax_highlight_helper.rb

  • Extract the already-highlighted detection pattern to a named constant ALREADY_HIGHLIGHTED_RE = /<span[\s>]/ so both fix sites reference the same definition
  • Change the rescue check from /^<span\s+class=/ to ALREADY_HIGHLIGHTED_RE: removes the line-start anchor so spans are detected anywhere in the source; also handles style= spans (e.g., from Commonmarker highlighting)

lib/yard/templates/helpers/html_helper.rb

  • Add a guard in parse_codeblocks using ALREADY_HIGHLIGHTED_RE to skip re-highlighting when the code block content already contains <span> tags. This prevents the double-processing entirely and also prevents CGI.unescapeHTML from corrupting &lt;< inside span contents when they're returned by the rescue path.
  • Add a comment documenting why the guard skips CGI.unescapeHTML and noting the known edge case: code blocks in :html markup that contain a literal <span> in the Ruby source being documented will have highlighting suppressed (uncommon; old behavior was to corrupt the output anyway)

spec/templates/helpers/html_syntax_highlight_helper_spec.rb

  • Add require for spec/templates/spec_helper.rb so the spec works when run in isolation (previously html_equals_string was only available when the full suite loaded other template specs first)
  • Add two unit tests that directly exercise the rescue clause in html_syntax_highlight_ruby_ripper with pre-highlighted content — one without leading whitespace, one with — so the regex fix has targeted coverage independent of the parse_codeblocks guard

spec/templates/helpers/html_helper_spec.rb

  • Add two regression tests covering the double-processing scenarios
  • These tests exercise the parse_codeblocks guard (not Ripper itself), so drop the if HAVE_RIPPER guard that was preventing them from running on non-Ripper CI configurations

Test plan

  • bundle exec rspec spec/templates/helpers/ — all pass
  • bundle exec rspec spec/templates/helpers/html_syntax_highlight_helper_spec.rb — passes in isolation (was broken before)
  • bundle exec rspec — full suite, 0 failures

…der as text (lsegal#1661)

When {yard:include_tags} embeds already-highlighted <pre class="example code">
blocks into a page, parse_codeblocks re-processes them. The rescue clause in
html_syntax_highlight_ruby_ripper checked /^<span\s+class=/ to detect pre-highlighted
content, but the ^ anchor only matches spans at the start of a line. Highlighted code
with leading whitespace (e.g., indented `def foo`) starts with spaces, so the check
failed and h(source) HTML-escaped all the span tags into visible text.

Fix 1: Change the rescue check from /^<span\s+class=/ to /<span[\s>]/ so it detects
spans anywhere in the source and also handles style= spans (e.g., from Commonmarker).

Fix 2: Guard parse_codeblocks against re-processing already-highlighted blocks by
skipping html_syntax_highlight when the code block content already contains <span>
tags. This also prevents CGI.unescapeHTML from corrupting escaped entities inside spans.

Also add require in html_syntax_highlight_helper_spec.rb so the spec loads correctly
when run in isolation (html_equals_string was only available via other specs in full suite).
@lsegal

lsegal commented Apr 8, 2026

Copy link
Copy Markdown
Owner

This is really good stuff!

- Extract /<span[\s>]/ to ALREADY_HIGHLIGHTED_RE constant in
  HtmlSyntaxHighlightHelper, used by both the rescue clause and the
  parse_codeblocks guard so the pattern stays in sync
- Add explanatory comment on the parse_codeblocks guard documenting
  the CGI.unescapeHTML motivation and the known false-positive edge case
  for :html markup containing literal <span> tags in source
- Add two unit tests directly targeting the rescue clause in
  html_syntax_highlight_ruby_ripper — one without and one with leading
  whitespace — so Fix 1 (the regex change) has its own coverage
  independent of the parse_codeblocks guard
- Drop `if HAVE_RIPPER` from the two html_helper_spec regression tests;
  they exercise the parse_codeblocks guard (Fix 2), not Ripper itself,
  so the guard is misleading and prevents coverage on non-Ripper runs
@dduugg
dduugg marked this pull request as ready for review April 8, 2026 20:59
@dduugg

dduugg commented Apr 8, 2026

Copy link
Copy Markdown
Contributor Author

@lsegal Thanks! It's ready for review.

@lsegal
lsegal merged commit e691c82 into lsegal:main Apr 8, 2026
24 checks passed
@lsegal

lsegal commented Apr 8, 2026

Copy link
Copy Markdown
Owner

Thanks for the PR!

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.

Code examples broken in current documentation

2 participants