Skip to content

fix(core,builder): one owner for fenced-code detection and reserved directive names - #105

Merged
leoafarias merged 3 commits into
mainfrom
fix/parser-fence-and-directive-names
Aug 13, 2026
Merged

fix(core,builder): one owner for fenced-code detection and reserved directive names#105
leoafarias merged 3 commits into
mainfrom
fix/parser-fence-and-directive-names

Conversation

@leoafarias

Copy link
Copy Markdown
Member

Intent

Two parse-phase rules each had multiple owners that disagreed. This gives each one a single owner, with no intended change to authoring semantics.

Seam 1 — fenced code

Three owners had their own fence regex:

Owner Rule before
MarkdownParser (slide split) backticks only — ```dart {.hero} did not open a fence, tildes never matched
TagTokenizer (directives) backtick or tilde, but only at column 0; closer had to match the opener's exact length
SlideSerializer (escape) backtick or tilde, but a ~~~ line could close a ``` fence

All three now resolve fences through fencedCodeLines in core. The rule follows what SuperDeck already renders via package:markdown (codeFencePattern), with two deliberate departures recorded in the source:

  • Closers keep their info string. demo/slides.md closes a fence with ```{.code}. Treating that as "not a closer" leaves the fence open, swallows the next ---, and merges two slides.
  • Any leading whitespace opens a fence (vs. package:markdown's 0–3 spaces). Over-hiding is safe — a more deeply indented fence is an indented code block to the renderer either way — while under-hiding would split a slide mid-code-sample.

Seam 2 — reserved directive names

BlockParser rejected a raw 'column' string while SlideSerializer._reservedTags duplicated the model discriminators as literals. Dropping column from the serialize copy would emit @column, which parse then rejects. Both now use reservedDirectiveNames, derived from SectionBlock.key / ContentBlock.key / WidgetBlock.key plus the rejected column alias.

Impacted packages

  • superdeck_core — new fencedCodeLines (the only fence function exported); TagTokenizer now recognizes indented fences and longer-than-opener closers
  • superdeck_builderMarkdownParser, SlideSerializer, BlockParser, new directive_names.dart

Verification

Check Result
melos run analyze (dart + DCM, all packages) SUCCESS
melos run test --no-select (all packages) SUCCESS
packages/core tests 688 passed
packages/builder tests 139 passed
dart format 0 changed
demo/ superdeck_cli build 34 slides, and superdeck.json / superdeck_full.json byte-identical to main

That last row is the behavior-preservation evidence: the real deck compiles to the same artifact, and only the build_status.json timestamp moved (reverted).

Tests drive the shipped APIs rather than a reimplemented scanner, and fence_agreement_test pins MarkdownParser and TagTokenizer to the same snippets so the two can't drift apart again.

Slide-split, directive tokenization, and serialize-escape each had their
own fence regex and disagreed:

- MarkdownParser matched backticks only, so ```dart {.hero} did not open
  a fence and tilde fences never matched at all.
- TagTokenizer matched backtick or tilde but required column 0.
- SlideSerializer let a ~~~ line close a ``` fence.

Add fencedCodeRanges/isInsideFencedCode in core and route all three
through it. The rule follows what SuperDeck already renders via
package:markdown: 3+ backticks or tildes, backtick openers reject
backticks in the info string, a closer matches the opening character at
>= the opening length, and an unclosed fence runs to EOF.

Closers keep their info string. demo/slides.md closes a fence with
```{.code}; treating that as "not a closer" swallowed the following ---
and merged two slides.

Tests drive the shipped APIs (MarkdownParser, TagTokenizer,
SlideSerializer) rather than a reimplemented scanner, and
fence_agreement_test pins split and tokenize to the same snippets.
The authoring reserved set lived twice: BlockParser rejected a raw
'column' string, and SlideSerializer._reservedTags duplicated the model
discriminators as string literals. Dropping 'column' from the serialize
copy would emit @column, which parse then rejects.

Add directive_names.dart deriving the set from SectionBlock.key,
ContentBlock.key, and WidgetBlock.key plus the rejected column alias, and
use it from both parse and serialize. Semantics are unchanged; only the
owner of the fact moved.

It lives in builder because directive grammar is a parse concern and
column is not a block type in the core model.

The serializer test now loops over reservedDirectiveNames, so a name
added to the set cannot silently lose its @widget escaping.
Both builder consumers are line-based and were each rebuilding character
offsets from their own line list:

    offset += lines[i].length + 1;

That is one rule with two owners, and it only worked because
MarkdownParser fed fencedCodeRanges the joined lines rather than the
original content; passing the original would have desynced silently on
CRLF input.

Derive both views from one span walker and add fencedCodeLines, which
returns the line indices (addressing text.split('\n')) that sit inside a
fence. Slide splitting and serialize-escape now test membership directly.

fencedCodeLines is the only fence function exported from the barrel; the
offset-based range API has one in-package consumer, TagTokenizer. Neither
was in a released version, so narrowing the export breaks nothing.

Also record why the rule keeps a closer's info string and accepts any
leading whitespace, since both depart from package:markdown deliberately.
@github-actions

Copy link
Copy Markdown

Visit the preview URL for this PR (updated for commit d96be79):

https://superdeck-dev--pr105-fix-parser-fence-and-mkf69akj.web.app

(expires Sat, 12 Sep 2026 16:01:41 GMT)

🔥 via Firebase Hosting GitHub Action 🌎

Sign: bd68fc230762285849207e7e120aaf87cd4ca2f9

@leoafarias
leoafarias merged commit 73ebc73 into main Aug 13, 2026
5 checks passed
@leoafarias
leoafarias deleted the fix/parser-fence-and-directive-names branch August 13, 2026 17:08
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.

1 participant