Let each thematic-break marker carry its own look - #160
Open
Nicolas-Py wants to merge 1 commit into
Open
Conversation
Nicolas-Py
force-pushed
the
feat/star-thematic-break
branch
from
August 18, 2026 07:56
f6eee03 to
78945f1
Compare
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
force-pushed
the
feat/star-thematic-break
branch
from
August 18, 2026 08:27
78945f1 to
6f9b98f
Compare
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.
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.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.mdsays 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 byisThematicBreakand then discarded. This keeps it.The extension seam cannot express it, for three independent reasons:
BlockParserclassifies 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);BlockSyntaxis 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.Ctxalready 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
.thematicBreakattribute keeps itsBoolvalue. All three readers type-test it asas? BooloffAny(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
drawThematicBreakssplits intothematicBreakDecorations(at:)plus a thin draw loop, mirroringblockBackgroundFills(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