Files
Outpost/Vendor/swift-markdown-engine/CHANGELOG.md
T
Puranjay Savar Mattas 6e1ffb96fd chore: vendor swift-markdown-engine as plain tracked source
Convert from git submodule to plain vendored copy. No push access to
upstream nodes-app/swift-markdown-engine meant our local fix commit
(05c1720) was stranded and unreachable from any remote on fresh
clones/CI. Vendoring as plain files folds it into normal repo history
instead.
2026-08-20 13:26:13 +01:00

378 lines
21 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Changelog
All notable changes to swift-markdown-engine are documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased]
## [0.12.0] - 2026-08-10
### Added
- `onPersistScrollOffset` / `restoreScrollOffset` on `NativeTextViewWrapper`
scroll memory an embedder can keep somewhere that outlives the editor. The
engine's own per-document offsets live on the coordinator, so an embedder that
routes to a different screen and back lost them: nothing recorded the offset on
the way out (there was no `dismantleNSView` at all), and the restore was gated
on a document switch, which a remount is not — `makeCoordinator` seeds
`documentId`, so the first update pass never looks like one. Teardown now hands
the offset over, and the restore is latched instead of gated, retrying for a
bounded few passes because the first pass after a remount still carries the
embedder's empty buffer. Both closures are asked at call time, so the
embedder's own retention rules can see changes made on the way out. Passing
neither leaves behavior unchanged.
- `NSAttributedString.Key.markdownBlockBackground` — a background painted
across the whole line box by `MarkdownTextLayoutFragment` instead of the
glyph box AppKit's `.backgroundColor` covers. Embedder extensions can use it
wherever a fill should read as a block.
### Changed
- **Inline parse cost is linear in the spans per region, not quadratic.** Every
pass after the first consulted the claimed ranges by scanning the whole array
— once per character in `scanEscapes` and `collectDelimiterRuns`, once per
candidate in `scanLinkFamily` — and `buildTree` decided containment by testing
each span against every other. The passes walk the string left to right and
claimed ranges never partially overlap, so a cursor over the sorted ranges answers both
questions in amortised constant time, and sorting spans by start ascending /
length descending turns containment into a single ordered walk. A paragraph of
240 code spans parses in 0.5ms rather than 33ms; 6x the spans now costs 6x the
parse instead of ~30x. Affects every claimed-span construct — code, escapes,
links, images, wiki links, inline LaTeX, emphasis, and extension spans. No
parse result changes.
- `==highlight==` fills the line box. AppKit paints `.backgroundColor` over
ascent + descent only, so the marker fell short of the line height by the
leading plus `paragraph.lineHeightExtraSpacing`, and a highlight that wrapped
came out as a stack of bands. `HighlightExtension` returns
`.markdownBlockBackground` now, so the block is continuous at any font size.
Table cells rasterize their own text and keep the glyph-box fill.
### Fixed
- Bare URLs and emails survive rich copy as real links. The editor styler
linkifies them with `NSDataDetector`, but the HTML renderer emitted them as
plain text, so the pasteboard's HTML/RTF/web-archive flavors carried no anchor
at all, and whether a copied URL arrived clickable was left to the receiving
app — Apple Mail runs its own detection and linkifies anyway, a consumer that
takes the rich flavor verbatim pastes dead text. `MarkdownHTMLRenderer` now
wraps detector matches in `<a href>` (emails as `mailto:`) using the same
system detector as the styler; the RTF and web-archive flavors are derived
from that HTML, so all three inherit the link. Explicit `[title](url)` links
were already correct; a URL-shaped run inside a link's own title stays plain
so anchors never nest, and code spans remain excluded, matching the styler.
Table cells are unaffected: they render no inline markup on the copy path.
- Markdown link labels may hold inline code and escaped punctuation —
``[`App`](/tmp/App.swift:56)`` stayed literal. Code spans and escapes are
claimed before links so they stay opaque, and the link pass rejected every
candidate overlapping a claimed span, including one lying entirely inside the
label. Spans contained in the label are permitted now, links act as
containers when the tree is built, and partial overlaps or spans crossing the
label boundary are still rejected.
- Initially narrow tables reflow when the editor width shrinks instead of
retaining stale image geometry until an unrelated full restyle.
## [0.11.0] - 2026-07-31
### Added
- **Ordered lists render their position.** An item's number is computed from its
place in the run and painted over the source digits, so typing, deleting,
merging and pasting renumber live. The `.md` file is never rewritten — the
source stays valid CommonMark whatever it says. The whole source marker is
hidden as one unit and the slot is kerned to the display width, so the dot
travels with the digits at any digit count; the raw digits are revealed while
they are edited.
- `MarkdownEditorConfiguration.cursorFollowsSpanInk` (opt-in, off by default):
the caret and the I-beam take the ink of the extension span they sit in. It
matters for an extension that INVERTS its content — dark ink on a light block
— where both cursors are otherwise drawn in the block's own color and
disappear inside it. `InvertedIBeamCursor` recolors the live `NSCursor.iBeam`
image, which keeps the system shape and the user's pointer size.
### Fixed
- Find-in-document no longer erases other backgrounds. Clearing its highlights
removed `.backgroundColor` across the whole document, which blanked extension
spans, code fences and table cells until some unrelated restyle repainted
them. Find now marks its own backgrounds and restyles only the paragraphs it
touched.
### Performance
- **Large notes open ~14× faster.** Measured on a 346k-char / 5,241-block note
(Release): first open 19.5 s → 1.35 s, warm open 1.1 s → 590 ms, switching
away 440 ms → 115 ms. Styling is built on a detached string and transferred
with one `setAttributedString` — per-key `addAttribute` on live TextKit-2
storage left weak tombstones in Foundation's attribute-intern table, which
turned quadratic. The restyle apply uses a paragraph overlap index above 32
paragraphs, the redundant second full-document parse and the re-entrant
full-document layout during rebuild are gone, and the SwiftMath render cache
persists to disk (717 ms → 35 ms on relaunch, byte-identical geometry).
- **Editing long ordered lists.** One Return in an 800-item loose list:
944 ms → 67 ms (was quadratic in list length). Typing in a 1,600-item ordered
list: 89 ms → 41 ms per key. Resolving the caret ink across 1,600 spans:
0.134 ms → 0.002 ms per keystroke.
### Known limitations
- A list item's continuation line is a paragraph and ends the run, so a
multi-line item switches numbering off below it.
- A loose list keeps stale numbers after a pure digit edit (a digit edit is not
classified as structural).
- Ordered task items (`1. [ ] x`) consume a number but render none.
## [0.10.1] - 2026-07-22
### Added
- Custom SF Symbols for task checkboxes: `MarkdownEditorConfiguration` accepts
custom unchecked and checked symbols for `- [ ]` / `- [x]` task-list items
(opt-in; the defaults are unchanged).
### Fixed
- List markers no longer disappear while a selection covers them, and selecting
a list item now reveals its raw marker syntax like other inline constructs.
- An unclosed ``` fence no longer swallows the rest of the document: typing an
opening fence above existing content left every block below it (tables,
block LaTeX, thematic breaks, links) unrendered until the closing fence was
typed. A fence now forms a code block only once its closing fence exists.
## [0.10.0] - 2026-07-15
### Added
- **Extension seam**: opt-in constructs beyond pure markdown. A
`MarkdownExtension` contributes an inline form (`==highlight==`), a fenced
block form (`::: … :::`), or both — plus content attributes and an HTML
wrapper for the clean-copy path; register instances via
`MarkdownEditorConfiguration.extensions`. Extensions never emit ranges — the
parser derives all geometry, so a misbehaving extension can at worst restyle
its own construct. Marker/fence hiding, caret reveal, incremental restyle,
table cells, and rich copy are handled generically. Registered extensions can
change at runtime; all parse caches key on the registry.
- `HighlightExtension` (`==text==`) and `StrikethroughExtension` (`~~text~~`),
the former built-ins repackaged as extensions, and `ContainerExtension`
(`::: … :::`), the first fenced block extension.
### Changed
- **Breaking**: `==highlight==` and `~~strikethrough~~` are no longer part of
the core grammar. Unregistered, the syntax stays literal text. To keep the
previous behavior:
`configuration.extensions = [HighlightExtension(), StrikethroughExtension()]`.
The formatting actions (context menu, `applyHighlightRequest` /
`applyStrikethroughRequest`) still insert/remove the markers either way;
construct detection (toggle-off, selection state) requires the extension.
### Fixed
- A pre-existing incremental-parse gap surfaced by the seam review:
backspace-joining two paragraphs could leave transiently wrong styling
(extra spacing or a stray emphasis/code span across the join) until the next
edit re-parsed the region.
## [0.9.0] - 2026-07-13
### Added
- `MarkdownEditorConfiguration.rawSourceMode`: present the document as raw
Markdown source — no syntax hiding, no markdown styling, and no wiki-link
display transform (`[[Name|UUID]]` shows verbatim). The editor keeps base
font/paragraph styling and stays fully editable; smart Markdown input
handling (list continuation, `$$`/`![[` auto-wrap, ⇧⇥ outdent) is disabled
while raw. Runtime switching is supported and rebuilds the document
immediately; the current document's undo stack is dropped on a switch
because undo actions recorded against the other mode's display text would
replay at stale ranges. Default `false` — existing embedders are unaffected.
- Find & replace: two optional bus notifications, `replaceCurrent` (replace the
focused match and advance) and `replaceAll` (replace every match in one undo
step, back-to-front so ranges stay valid). Both edit the engine's displayed
text with proper `shouldChangeText`/`didChangeText` undo registration and
report the remaining count via `findResults`. Purely additive — embedders that
don't set the bus names are unaffected.
- Clean clipboard: ⌘C copies the selection as rich text (RTF + `com.apple.webarchive`)
built from the AST rather than the raw storage form, and paste converts HTML to
Markdown. Wiki-link `[[Name|UUID]]` side-channels no longer leak into copied text.
### Fixed
- Find/jump scroll now works without a reading column. The TextKit 2
fragment-enumeration scroll path (with `.ensuresLayout`) runs universally
instead of only when `readingWidth` was set; the unreliable
`NSTextView.scrollRangeToVisible` (which routes through the absent TextKit 1
layout manager for off-screen content) is now only the last-resort fallback.
- Inline syntax markers (`**`, `*`, `~~`, `==`) now use `mutedText` foreground
color while the caret is inside the corresponding span, matching the existing
behavior of inline code backticks and link/wiki-link brackets. This makes
highlight `==` markers visually distinct from body text in edit state.
- Web links `[text](url)` now share the wiki-link "edit zone": clicking the outer
~30% of the link's first/last visible character places the caret just outside the
markers (before `[` / after `)`) and reveals the source for editing instead of
navigating, matching `[[…]]` behavior. Previously the edit zone only resolved
`.wikiLink` tokens, so a web link dropped the caret between its brackets and did
not reveal. Middle-of-link clicks still navigate; read-only links stay navigable.
- Auto-linking (`NSDataDetector`) no longer linkifies a URL that sits inside a
markdown or wiki link's own range. A link's `(url)` previously got its own
competing `.link` attribute on top of the link — making the raw URL independently
navigable and offsetting the click edit zone. Bare URLs outside links still
autolink; URLs inside code were already excluded.
- Wiki links: UUID-robust labels/embeds and keyboard navigation in the inline
autocomplete list.
### Contributors
- Find/jump scroll fix and find & replace by @ChristineTham
- Inline syntax-marker color fix by @sospartan
- rawSourceMode, clean clipboard, web-link edit zone, and wiki-link robustness by @luca-chen198
## [0.8.0] - 2026-06-28
### Added
- `MarkdownEditorBus.findQuery` / `findResults`: query-based in-document find. The host posts a
search string (+ current index) and the engine matches against its OWN displayed text,
highlighting in display coordinates and posting the match count back. This is correct where the
displayed text differs from the source — e.g. node links rendered shorter than `[[Name|UUID]]`,
LaTeX, or images — which the legacy `findScrollToRange` (host-computed source-coordinate ranges)
highlighted at the wrong offset. Opt-in; `findScrollToRange` is unchanged for existing embedders.
- `NativeTextView.isCursorExcluded: ((CGPoint) -> Bool)?` — embedder-supplied
predicate that suppresses the edit-mode I-beam cursor when the mouse is inside
a defined exclusion zone (e.g. a formatting toolbar). When the closure returns
`true`, `mouseMoved:` skips calling `super.mouseMoved` to avoid NSTextView's
built-in I-beam cursor, setting the arrow cursor instead. Exposed through
`NativeTextViewWrapper.isCursorExcluded`.
- `NativeTextViewWrapper.onBuildContextMenu: ((NSMenu, NSRange) -> NSMenu)?` —
embedder hook to build the editor's right-click menu. The engine hands over the
default `NSMenu` + the current selection; the embedder returns the menu to show
(driving the `didMarkdown*` actions through the bus). Keeps the engine UI-free.
- `==highlight==` inline markup: double-equals markers around text apply a
background color (configurable via `MarkdownEditorTheme.highlightColor`,
default `.systemOrange.withAlphaComponent(0.4)`). Content is recursively parsed so nested emphasis,
code, etc. work inside highlights.
- `MarkdownEditorBus.applyHighlightRequest` /
`selectionHighlightDidChange`: bus notification names for driving a
highlight toolbar button from host UI.
- `MarkdownEditorBus` extended with nine new notification types for
formatting toolbar integration: `applyStrikethroughRequest`,
`applyInlineCodeRequest`, `applyBlockquoteRequest`,
`applyUnorderedListRequest`, `applyOrderedListRequest`,
`applyLinkRequest`, `applyCodeBlockRequest`,
`applyHorizontalRuleRequest`, `applyImageRequest`. Embedders wire
these into `NotificationCenter` to trigger formatting from
external UI (toolbars, menus) without reaching into the editor's
view hierarchy.
- New formatting actions on the coordinator (callable directly or
via the bus above): `didMarkdownStrikethrough`, `didMarkdownInlineCode`,
`didMarkdownBlockquote`, `didMarkdownLink`, `didMarkdownCodeBlock`,
`didMarkdownHorizontalRule`, `didMarkdownImage`.
- Word-boundary detection in inline formatting: when the cursor is
placed inside an English word with no active text selection, bold,
italic, strikethrough, and inline-code actions now auto-select the
containing word before wrapping. If no word character is adjacent
to the cursor, empty markers are inserted as before. The cursor's
relative offset within the word is preserved after wrapping
(e.g. `wo|rd` → `**wo|rd**`).
- Headless test suite for formatting actions (`FormattingActionTests`
— 21 tests covering bold, strikethrough, inline code, blockquote,
link, code block, horizontal rule, and image insertion).
### Changed
- The engine no longer ships a built-in right-click "Format" context menu — menus
are now embedder-supplied via `onBuildContextMenu` (above). The system rich-text
"Font" submenu (Bold/Italic/Show Colors…) is stripped from the default menu, since
those font traits don't apply to Markdown.
### Fixed
- Blockquote removal no longer doubles trailing newlines when the
original line already carries one.
## [0.7.1] - 2026-06-20
### Added
- `MarkdownEditorConfiguration.heightBehavior` (`.scrolls` default / `.fitsContent`):
in `.fitsContent` the editor grows to its content height and reports it to
SwiftUI, so an enclosing `ScrollView` scrolls the page instead of a nested
internal scroller. Opt-in, off by default — no change for existing embedders. (#75)
- `BlockquoteStyle` configuration struct with `extraLineHeight` to control line
spacing inside blockquotes, following the `ListStyle.extraLineHeight` /
`ParagraphStyle.lineHeightExtraSpacing` pattern. Defaults to `0` (no extra
spacing), preserving existing rendering. (#76)
### Fixed
- Mouse-wheel / trackball scrolling no longer clamps back at the bottom past a
stale-small content-height measurement. (#71)
- Inspector clip mask and caret reveal at the document end. (#73)
- Scroll position is remembered per document across switches, and Writing Tools
results stay styled and visible after accept. (#70)
- Empty-file placeholder no longer clips to one line after a view rebuild. (#69)
### Added
- Scroll-away header: `NativeTextViewWrapper` gains `header: AnyView?`,
`headerCollapsedHeight: CGFloat`, and `headerExpanded: Bool`. The engine
hosts the supplied SwiftUI view above the document body, scrolling with
it; collapsing animates the reserved band down to `headerCollapsedHeight`
(the top row stays visible, lower rows clip away). The hosted content
refreshes on every SwiftUI update and stays fully interactive. Composes
with `readingWidth`. See the README's *Scrolling Header* section.
### Changed
- The scroll view's `documentView` is now always an engine-internal
container view (hosting the text view, the optional scroll-away header,
and the reading column's breakout overlays) rather than sometimes the
`NSTextView` itself. Embedders that reached into
`scrollView.documentView` expecting an `NSTextView` must adapt — the
document view's class was never API.
- **Breaking**: The editor's enclosing scroll view no longer applies a
hard-coded `top: 55.4` content inset. The default is now `0` on every
edge, matching the most common embedding case where the editor fills
its container exactly. Embedders that previously relied on the engine
reserving header space (e.g. for a translucent toolbar) must opt in
explicitly:
```swift
var config = MarkdownEditorConfiguration.default
config.safeAreaInsets = SafeAreaInsets(top: 55.4)
```
### Added
- `SafeAreaInsets` struct exposing `top` / `leading` / `trailing` / `bottom`
inset knobs for the editor's enclosing scroll view, configurable via
`MarkdownEditorConfiguration.safeAreaInsets`.
- `MarkdownASTStyler` now stamps `.spellingState: 0` on fenced code blocks
and inline `` `code` `` spans, completing the engine's existing
spell-check suppression convention (links, wiki-links, LaTeX, and tables
already carry the same attribute). The system spell-checker no longer
underlines tokens inside code regions even when continuous spell
checking is enabled.
### Fixed
- Undo is now kept per `documentId`, so Cmd+Z keeps working after switching
files. The single reused `NSTextView` previously wiped its undo manager on
every document switch; the editor now vends a per-document `UndoManager`
(via the new `undoManager(for:)` delegate method) whose undo/redo stack
survives switching away and back. (#77)
- A document's surviving undo stack is dropped when its text is reloaded
*changed* while it was switched away (e.g. renaming a node rewrites the
`[[label]]` in every file that links it), so Cmd+Z can no longer replay
stale ranges against the rewritten content. (#78)
- `NativeTextViewWrapper` keeps links clickable and text selectable
when `isEditable: false`; `isSelectable` is no longer coupled to
`isEditable`. (#31)
- `NativeTextViewWrapper` now applies its initial styling pass even when
the bound text starts at its final value (e.g. supplied as a SwiftUI
`@State` initializer). Previously the editor would render the raw
Markdown source until the user clicked into the document, because the
coordinator's `lastSyncedText` already matched the bound text at first
`updateNSView`. The early-return now also requires `didInitialFormatting`
to be true, which only flips after the first styling pass completes.
### Added
- Initial public API surface:
- `NativeTextViewWrapper` — SwiftUI bridge for the AppKit-backed editor
- `MarkdownEditorConfiguration` — every spacing / sizing / behavior knob
- `MarkdownEditorTheme` — color palette, defaults to system colors
- `MarkdownEditorServices` — container for the four service protocols
- Service protocols: `WikiLinkResolver`, `EmbeddedImageProvider`,
`SyntaxHighlighter`, `LatexRenderer`
- No-op default implementations: `NoOpWikiLinkResolver`,
`NoOpEmbeddedImageProvider`, `PlainTextSyntaxHighlighter`,
`NoOpLatexRenderer`
- `WikiLinkService` — bidirectional storage / display roundtrip helper
- `PasteboardImageReader` — pasteboard image inspection helpers
- Selection / replacement value types: `WikiLinkSelection`,
`InlineSelectionState`, `InlineReplacementRequest`, `CodeBlockSelection`
- `CodeBlockButton` — drop-in copy button overlay
- DocC documentation catalog with landing page and topic groups
- Triple-slash documentation comments on the full public API surface
[Unreleased]: https://github.com/nodes-app/swift-markdown-engine/compare/0.7.1...HEAD
[0.7.1]: https://github.com/nodes-app/swift-markdown-engine/compare/0.7.0...0.7.1