Match AST subclasses by ancestry and add a block flag to AST::Code - #72
Merged
Conversation
Normalizer rules, tag dispatch, and render_default matched the exact class of a node, so a consumer subclass silently lost all base-class behavior — a block code subclass inside Bold was not hoisted and broke the surrounding markers. All three lookups now match by ancestry: the most specific class wins, an exact registration always overrides an inherited one, and Tag::PASSTHROUGH opts a subclass out of an inherited tag. Frozen libraries and rule sets flatten the resolution at freeze time (descendant classes are known through AST::Node.inherited), so the shared no-customization path answers subclass lookups with a single hash hit. Benchmarks show the end-to-end cost stays within noise. docs/extending.md documents the normalizer extension points and the ancestry matching. Also fixes a stale Normalizer.shared_for call that crashed bench/corpus_bench.rb.
The renderer decides inline vs block per Code node by looking for a newline in the text. Sources that clearly mean "code block" lost that intent for single-line content: [code]x[/code] and <pre>x</pre> came out as `x`, and the language was dropped because inline code has no place for it. AST::Code now takes block: true when the source construct is a code block by definition. Such a node always renders as a fenced block (or <pre><code> in html_mode), and the normalizer hoists it out of inline containers even when it is single-line. Without the flag the newline check applies unchanged. The flag is set by: - BBCode [code] and [pre] ([tt] stays inline) - HTML <pre> (<code> and <tt> keep the newline check) - s9e/TextFormatter CODE - MediaWiki space-indented blocks and line-level <pre> blocks Visible change: single-line [code], [pre], <pre> and CODE content now renders as a fence with its language instead of inline code.
gschlager
force-pushed
the
ast-ancestry-and-block-code
branch
from
July 27, 2026 14:31
5656bf8 to
58c3d17
Compare
gschlager
marked this pull request as ready for review
July 27, 2026 14:44
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.
This builds on #70 (it is based on that branch — merge #70 first, GitHub will retarget this one to main). These are the two deeper design changes from the same adapter review, split out so they can be reviewed on their own.
render_defaultmatched the exact class of a node, so a consumer subclass (a common way to bind a custom tag to only some nodes) silently lost all base-class behavior — a block code subclass inside bold was not hoisted and broke the surrounding markers. All three lookups now match by ancestry: the most specific class wins, an exact registration always overrides an inherited one, andTag::PASSTHROUGHopts a subclass out of an inherited tag. Frozen libraries and rule sets flatten the resolution at freeze time (descendant classes are known throughAST::Node.inherited), so the shared no-customization path answers subclass lookups with a single hash hit. I benchmarked withbench/corpus_bench.rb: end-to-end cost stays within the noise floor, and flattened subclasses dispatch as fast as the base classes.AST::Codehas ablock:flag. The renderer used to decide inline vs block only from the content (a newline means block), so a single-line code block lost its fence and its language —[code=ruby]x[/code]became`x`. Parsers setblock: truewhere the source construct is a block by definition: BBCode[code]/[pre], HTML<pre>, s9eCODE, and MediaWiki indented and<pre>blocks. Inline constructs ([tt],<code>,<tt>) keep the heuristic, and the normalizer hoists a forced-block code out of inline containers even when it is single-line.Behavior changes: AST subclasses now inherit rules and tags from their base class — register on the subclass to get different behavior. And single-line
[code],[pre],<pre>, and s9eCODEcontent renders as a fenced block with its language instead of inline code.