Skip to content

Directives (3/4): draw a self-contained call as a glyph - #158

Open
wildthink wants to merge 1 commit into
nodes-app:mainfrom
wildthink:feat/directives-glyphs
Open

Directives (3/4): draw a self-contained call as a glyph#158
wildthink wants to merge 1 commit into
nodes-app:mainfrom
wildthink:feat/directives-glyphs

Conversation

@wildthink

Copy link
Copy Markdown
Contributor

Phase 3 of the directive seam, on top of #120 and #155. Styling only — the parser is untouched.

The gap this closes

A self-contained call (@pagebreak) has no body. It parses, claims its span, and then renders as its own literal source, so @pagebreak looks exactly like the text @pagebreak. #155's README says as much:

Self-contained calls parse and claim their span … but they currently render as their literal source, and no self-contained directive ships yet.

A directive now returns a DirectivePresentation — an SF Symbol, replacement text, or an NSImage — and the styler draws it in place of the source.

How it draws

The mechanism is the one inline LaTeX already uses, not a new one. The characters stay in the storage: the first carries the image plus enough kern to occupy the glyph's width, the rest collapse to zero width via clear colour and the shrunk marker font. MarkdownTextLayoutFragment draws it.

That is what keeps "markers shrink, they don't disappear" true here. Selection, find, copy, and undo all still see the real characters, and the caret entering the call reveals the source muted — the same flip every other construct performs.

Failure is visible rather than silent: .literal, or a symbol name the system doesn't know, leaves the source on screen rather than collapsing it to a gap the user can neither see nor fix.

Rasterised glyphs are cached in an NSCache keyed by everything that determines the pixels. Presentation re-runs for every visible directive on every keystroke, and rasterising text each time is the one part of this path expensive enough to matter.

Scope

file change
MarkdownDirective.swift DirectivePresentation + the presentation requirement, defaulting to .literal
MarkdownASTStyler+Directives.swift the glyph pass
DirectiveScanner.swift comment-only — it already emitted the geometry this needs
BuiltinDirectives.swift header only; its text still said presentation is "not yet consulted by the styler"

Autocomplete is deliberately not here — it's Phase 4, and after your note on #120 about shipping a directive that parses but doesn't render, I'd rather not repeat the shape in reverse.

One test changes meaning

DirectiveStylingTests.selfContainedStaysVisible asserted that a self-contained call renders as literal text with nothing collapsing it. That was Phase 1 pinning its own limit, and it is precisely what this PR changes — so it becomes a collapse/reveal pair rather than being quietly deleted, and the suite is no longer named "Phase 1 styling". Flagging it explicitly since a green diff would otherwise hide a deliberate behaviour change.

Reference directives

FontDirective and ColorDirective are both containers and draw no glyph, so the engine still ships no self-contained directive. Demo/ gains @icon, @flag, @emoji, and @pagebreak as embedder-side examples, 30–60 lines each — curated data and print semantics are app concerns, not engine primitives. @flag computes its glyph from the code's regional-indicator scalars and carries no dataset.

Verification

Phase 4 (name and argument-value autocomplete) is ready behind this and I'll open it once this lands. #154 is still mine to take whenever you'd like it.

🤖 Generated with Claude Code

Phase 3 of the directive seam. A self-contained call has no body, so until now
it parsed, claimed its span, and then rendered as its own literal source —
`@pagebreak` looked exactly like the text `@pagebreak`. This gives it
something to draw.

A directive returns a `DirectivePresentation` — an SF Symbol, replacement
text, or an NSImage — and the styler collapses the source behind it. The
mechanism is the one inline LaTeX already uses, not a new one: the characters
stay in the storage, the first carries the image plus enough kern to occupy
its width, the rest collapse to zero width via clear colour and the shrunk
marker font. `MarkdownTextLayoutFragment` draws it.

That is what keeps "markers shrink, they don't disappear" true here. Selection,
find, copy, and undo all still see the real characters, and the caret entering
the call reveals the source muted — the same flip every other construct does.

Failure is visible rather than silent: `.literal`, or a symbol name the system
doesn't know, leaves the source on screen instead of collapsing it to a gap the
user can neither see nor fix.

Rasterised glyphs are cached in an NSCache keyed by everything that determines
the pixels — presentation re-runs for every visible directive on every
keystroke, and rasterising text each time is the one part of this path
expensive enough to matter.

The parser is untouched: this is styling only, and the 4000-input corpus
fingerprint is unchanged. `DirectiveScanner`'s diff is comment-only — it
already emitted the geometry this needs, and its comments pointed forward to
this change.

DirectiveStylingTests asserted that a self-contained call renders as literal
text with nothing collapsing it. That was Phase 1 stating its own limit, and
it is exactly what this changes, so it becomes a collapse/reveal pair rather
than being deleted.

`FontDirective` and `ColorDirective` are both containers and draw no glyph, so
the engine still ships no self-contained directive. Demo/ gains `@icon`,
`@flag`, `@emoji`, and `@pagebreak` as embedder-side examples — curated data
and print semantics are app concerns. `@flag` computes its glyph from
regional-indicator scalars and carries no dataset.

463 tests pass, demo builds.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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.

2 participants