Skip to content

Let each thematic-break marker carry its own look - #160

Open
Nicolas-Py wants to merge 1 commit into
mainfrom
feat/star-thematic-break
Open

Let each thematic-break marker carry its own look#160
Nicolas-Py wants to merge 1 commit into
mainfrom
feat/star-thematic-break

Conversation

@Nicolas-Py

Copy link
Copy Markdown
Member

What

MarkdownEditorConfiguration.thematicBreak (ThematicBreakStyle) maps each of the three thematic-break markers to an optional mark string. Nil — the default for all three — draws today's full-width rule, so every existing embedder is byte-identical to before. A non-nil mark is drawn centred in the text container in the body font.

config.thematicBreak.asteriskMark = "* * *"   // novel-style scene break on ***
// --- and ___ stay rules

Presentation only: the source text is untouched, the caret still reveals the raw ***, and the construct still exports as <hr>. A document written this way reads correctly in any other editor — which is the whole point of not inventing syntax for it.

Why this isn't a new grammar case

CONTRIBUTING.md says new constructs are extensions, not core grammar. This is neither a new construct nor new grammar: *** is already a CommonMark thematic break, already classified, already styled, already drawn. The marker character was always parsed by isThematicBreak and then discarded. This keeps it.

The extension seam cannot express it, for three independent reasons: BlockParser classifies thematic breaks at line 312, long before extension block entries are consulted at 345 (and "built-in constructs always classify first" is a stated invariant); BlockSyntax is a fence with an opening and closing line, while a break is one line with no content; and the protocol has no drawing hook at all. The directive seam is inline-only, so it cannot claim a bare block line either. That leaves the documented exception for overlay-rendered constructs.

Three things this deliberately does NOT do

No marker payload on BlockKind/BlockNode. Ctx already carries both the string and the configuration, and the marker is by construction the line's first non-whitespace character. Threading a payload would have forced edits through the parser, the AST, the tokenizer and two test files to move information its only consumer can already see. All 344 pre-existing tests compile unmodified as a result.

The .thematicBreak attribute keeps its Bool value. All three readers type-test it as as? Bool off Any (MarkdownTextLayoutFragment :192, :494, :526). A changed value type compiles clean, returns nil at every one, and silently blanks every rule in the document — with no compiler diagnostic and no test to catch it. The mark rides on a second key instead, keeping every attribute value ObjC-bridgeable like the other fifteen.

The grammar is untouched. A spaced * * * remains a list item, as before. That is a real deviation from CommonMark (which calls it a thematic break) and worth fixing, but it changes how existing documents render, so it belongs in its own change.

Testing

drawThematicBreaks splits into thematicBreakDecorations(at:) plus a thin draw loop, mirroring blockBackgroundFills(at:), so the geometry is assertable headlessly.

12 new tests in ThematicBreakMarkTests: marker resolution per slot, indented breaks, longer runs, caret suppression, trailing-newline bounds, attribute value types, and geometry (rule spans the container; mark is narrower and centred within 0.01pt). This is net-new coverage — nothing previously read what the styler emits for a thematic break.

459 tests in 66 suites pass on this branch. Verified against a real embedder (Nodes) building and running its own suite of 558 against this working tree.

Generated with Claude Code

@Nicolas-Py
Nicolas-Py force-pushed the feat/star-thematic-break branch from f6eee03 to 78945f1 Compare August 18, 2026 07:56
CommonMark gives `---`, `***` and `___` one meaning and one rendering, so an
embedder who wants a novel-style star divider on `***` has had to invent
syntax for it -- and any syntax they invent stops being a thematic break
everywhere else. The marker character was already parsed and then discarded;
keeping it costs nothing and buys the distinction for free.

`MarkdownEditorConfiguration.thematicBreak` maps each of the three markers to
an optional mark string. Nil, the default for all three, draws today's
full-width rule, so every existing embedder is unaffected. A non-nil mark is
drawn centred in the text container in the body font. Presentation only: the
source text is untouched, the caret still reveals the raw `***`, and the
construct still exports as `<hr>`.

Three things this deliberately does NOT do. The marker is recovered in the
styler from the line's first non-whitespace character rather than carried as
an associated value on `BlockKind`/`BlockNode` -- `Ctx` already holds both the
string and the configuration, and the payload would have forced edits through
the parser, the AST, the tokenizer and two test files to move information its
only consumer can already see. The `.thematicBreak` attribute keeps its `Bool`
value and the mark rides on a second key, because all three readers type-test
it as `as? Bool` off `Any`: a changed value type compiles clean, returns nil
everywhere, and silently blanks every rule in the document with no test to
catch it. And the grammar is untouched, so a spaced `* * *` remains a list
item as before -- correcting that is a separate change with its own blast
radius.

`drawThematicBreaks` splits into `thematicBreakDecorations(at:)` plus a thin
draw loop, mirroring `blockBackgroundFills(at:)`, so the geometry is
assertable headlessly. That is new coverage: nothing previously read what the
styler emits for a thematic break.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@Nicolas-Py
Nicolas-Py force-pushed the feat/star-thematic-break branch from 78945f1 to 6f9b98f Compare August 18, 2026 08:27
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