Skip to content

Custom heading typeface and heading text color - #121

Draft
dylandeheer wants to merge 1 commit into
nodes-app:mainfrom
GetWeeve:feature/heading-font-and-color
Draft

Custom heading typeface and heading text color#121
dylandeheer wants to merge 1 commit into
nodes-app:mainfrom
GetWeeve:feature/heading-font-and-color

Conversation

@dylandeheer

@dylandeheer dylandeheer commented Aug 3, 2026

Copy link
Copy Markdown

Motivation

An embedder that sets serif display headings over a sans body can't get there today: the theme routes every color through MarkdownEditorTheme except heading text (headings inherit bodyText from the view-level base attributes), and HeadingStyle covers every heading metric (per-level multipliers, top spacing) except the typeface — headings always derive from the single base fontName with the bold trait added. Those are the only two gaps we hit embedding the engine; this PR closes both without changing any default.

What's added

Two optional knobs, both nil by default:

  • HeadingStyle.fontName: String? — PostScript face for heading text, resolved at the per-level multiplied size. The name is honored exactly, so the chosen weight survives (pick a -Bold / -Semibold face for heavier headings); an unresolvable name falls back to the stock bold base font, mirroring TaskCheckboxStyle's symbol fallback. Emphasis inside a heading still composes on top of the configured face — bold/italic add traits while the family and per-level size are kept (# **n*o*des** stays consistent).
  • MarkdownEditorTheme.headingText: NSColor? — foreground for heading text only. The # glyphs stay on the existing headingMarker, and inline constructs inside a heading (links, inline code, extension spans) keep their own ink — the styler paints the heading range before the marker loop and the inline descent, so the existing later-range-wins layering does the rest.

Behavior preservation

With both knobs nil the styler emits value-identical [StyledRange] output to 0.11.0 — same font resolution path, same emission order, no extra ranges. Covered by a test comparing explicit-nil configs against .default attribute-by-attribute.

Tests

Tests/MarkdownEngineTests/HeadingFontAndColorTests.swift (8 tests, in the existing AST-styler test style):

  • custom face applied verbatim at the multiplied size per level; body text untouched
  • emphasis inside a custom-face heading keeps family/size and adds traits
  • unresolvable face falls back to the stock heading font
  • headingText colors content only; # markers keep headingMarker; body keeps inheriting bodyText; links keep link ink; emphasis keeps the heading ink
  • nil knobs: stock font, no foreground range, value-identical output to .default

swift build and swift test are green locally (the four ScrollingHeaderController animation-timing expectations fail identically on the pristine 0.11.0 tag in my sandbox, so they're environmental, not from this change).

Notes

  • Branched from the 0.11.0 tag so we can pin the exact revision while this is reviewed — happy to rebase onto main (only the CHANGELOG insertion point should touch).
  • Opened as a draft in the spirit of CONTRIBUTING's "issue first for non-trivial features" — treating this as the design conversation. Happy to adjust API names/placement or wire a Demo toggle if you'd like one.
  • CHANGELOG entry added under [Unreleased]; DocC comments on both new public fields.

Made with Cursor

Follow-ups

This PR turned out to be the first of a family: embedding the engine against a full design spec surfaced a few more places where a knob (or a bugfix) was needed. Each is opened as its own focused draft, stacked on this branch (their diffs over this PR are single-feature; every default preserves today's rendering exactly):

The theme can recolor every element except heading text (it inherits
bodyText), and the heading config covers every metric except the face —
so an embedder that wants serif display headings over a sans body has
to fork. HeadingStyle.fontName picks a PostScript face for headings,
honored exactly so the chosen weight survives; an unresolvable name
falls back to the stock bold base font, like TaskCheckboxStyle's symbol
fallback. MarkdownEditorTheme.headingText colors heading text while the
`#` glyphs stay on headingMarker and inline constructs inside a heading
keep their own ink. Emphasis inside a heading still composes on top of
the configured face (traits added, family and per-level size kept).
Both knobs default to nil, which keeps styling identical to before.

Co-authored-by: Cursor <cursoragent@cursor.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.

1 participant