SwiftMarkdownEngine: a TextKit 2 Markdown editor for macOS apps
A native AppKit Markdown editor for macOS, built on TextKit 2 and bridged to SwiftUI.
At a glance
- What is it?
- SwiftMarkdownEngine is an Apache-2.0 licensed Swift package that gives a macOS app a native Markdown editor with live styling, wiki links, image embeds, LaTeX, syntax highlighted code blocks and extensions, built on AppKit and TextKit 2 and bridged into SwiftUI. The app supplies the pieces it cannot know about through four service protocols, and the package ships in three products so you only take the dependencies you need.
- Who is it for?
- Adopt SwiftMarkdownEngine if you are building a macOS app that needs a Markdown editor and you want a native TextKit 2 implementation rather than a web view, especially if your documents contain wiki links, LaTeX or fenced code, since the package handles the styling and the tricky rendering details.
- Can I use it commercially?
- Yes. Apache-2.0 is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 1 day ago.
- What is it written in?
- Mainly Swift, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 22, 2026, and from our analysis. They are not legal advice.
Editorial analysis
A Markdown editor that renders while you type
SwiftMarkdownEngine is a native AppKit Markdown editor for macOS, built on TextKit 2 and bridged to SwiftUI. It is the editor inside Nodes, a macOS notes app.
That provenance is the most important fact in the repository, because it explains both the strengths and the limits. This is not a general purpose Markdown rendering library with a large user base, it is the editor component extracted from a shipping application, so the features it has are the ones that app needed and the features it lacks are the ones that app did not. The README is explicit about that relationship rather than hiding it.
The platform requirements are stated in the badges: Swift 5.9 or newer, macOS 14 or newer, and the licence is Apache 2.0. The last push to `main` was on 2026-10-12, and the recent releases are 0.14.0 from 2026-10-04, 0.13.0 from 2026-09-20 and 0.12.0 from 2026-08-10, so the project is moving quickly.
The feature list is the real content. Live Markdown styling covers bold, italic, headings, lists, blockquotes, GFM tables, code, links, task checkboxes and horizontal rules, and the rendering happens as you type rather than in a separate preview. Beyond that there is wiki-style linking, image embeds, LaTeX, code blocks with syntax highlighting and copy buttons, a reading column, a scroll-away header, a Writing Tools integration, and two extension mechanisms.
The TextKit 2 foundation is what makes the rest plausible. TextKit 2 is Apple's newer text engine with a layout model designed for long documents, and the README calls it out as providing accurate, modern text rendering. Building a live-styled Markdown editor on the older TextKit 1 would mean fighting a layout system that was never designed to have attributes change underneath a cursor.
The target user is a macOS developer who needs an editor in their own app, in their own document format, with their own data model behind the links.
Four service protocols and the no-op defaults
The engine does not know about your data. It talks to your app through four service protocols, each with a no-op default, so you implement only what you actually need.
The table in the README is the whole API surface. `WikiLinkResolver` resolves a `[[Name]]` to a stable opaque id, and the suggested implementation is your own data model. `EmbeddedImageProvider` looks up an `NSImage` for `![[Name]]`, and the suggested implementation is your asset store. `SyntaxHighlighter` highlights code blocks for a given language, with `HighlighterSwiftBridge` given as the ready-made bridge built on HighlighterSwift. `LatexRenderer` renders a LaTeX string to an `NSImage`, with `SwiftMathBridge` as the ready-made bridge built on SwiftMath.
The point of the no-op defaults is worth spelling out. A host application that has no wiki links, no images and no LaTeX can pass nothing at all and get an editor that works, rather than a configuration object full of optional nils. The defaults are also what makes the package's three products possible, since a bridge that pulls in a highlighting library cannot live in the zero-dependency product.
Wiring them up looks like this:
struct MyResolver: WikiLinkResolver {
func resolve(displayName: String, range: NSRange) -> WikiLinkResolution? {
myIndex[displayName].map { WikiLinkResolution(id: $0, exists: true) }
}
}
configuration.services = MarkdownEditorServices(
wikiLinks: MyResolver()
// images, syntaxHighlighter, latex omitted → no-op defaults
)The `WikiLinkResolution` type is where the interesting part of that contract sits. It carries an id and an `exists` flag, which means the resolver can tell the editor that a link points at something that is not there. That is what lets an editor render a broken link differently from a good one, and it is a decision the host application has to make because only it knows what exists.
Wiki links, embeds and LaTeX: what the host supplies
Three of the constructs are unusual, and all three are designed around the idea that the host application owns the data.
Wiki-style linking uses two-form storage and display roundtripping, `[[Name|<id>]]` and `[[Name]]`. The document stores both the human readable name and a stable identifier, and the display form is what the reader sees. That design is a direct consequence of the `WikiLinkResolver` protocol: the editor cannot resolve `[[Name]]` on its own, because only the host knows the identifier behind a name and whether that name still exists.
Image embeds come in two flavours. The Obsidian-style `![[Name]]` form means the embedder supplies the bytes, and the standard Markdown `` form is also supported. Again the split is between what the editor can do, which is the layout and the styling, and what the app must do, which is finding the image.
LaTeX is handled the same way, with both block `$$ ... $$` and inline `$...$` forms, and the embedder supplies the renderer. The engine asks for an `NSImage` and draws it, so any LaTeX to image pipeline will work, and the `MarkdownEngineLatex` product ships one built on SwiftMath for people who do not want to write that bridge.
Code blocks follow the same pattern, with embedder-supplied syntax highlighting and overlayable copy buttons. The copy button being overlayable matters for a live editor, because a copy button that scrolls with the text is in a different place every time the user moves, and one that floats over the block stays where the user's eye is.
What this all adds up to is a deliberate division of labour. The package owns the hard part of drawing styled Markdown text in a live TextKit 2 document, and it asks the host for the four things it genuinely cannot know. The cost of that design is that the package is less useful the more your document format diverges from plain Markdown, since every construct beyond the CommonMark core needs a bridge.
Three products, and picking the one you need
The package ships three library products, and the README is direct about the rule: add only what you need.
`MarkdownEngine` is the editor alone, with zero external dependencies. `MarkdownEngineCodeBlocks` gives the full visual code-block experience, meaning the background fill, the monospace font and the syntax highlighting, without writing your own bridge, and it pulls in HighlighterSwift transitively. `MarkdownEngineLatex` gives LaTeX formula rendering without writing your own bridge, and it pulls in SwiftMath transitively.
The install is a Swift Package Manager dependency:
dependencies: [
.package(url: "https://github.com/nodes-app/swift-markdown-engine", from: "0.1.0")
],
targets: [
.target(
name: "YourApp",
dependencies: [
.product(name: "MarkdownEngine", package: "swift-markdown-engine"),
]
)
]Or in Xcode, File and then Add Package Dependencies, and paste the repository URL. The version range starts at 0.1.0, so the package is available on the same dependency resolution path as anything else in your app.
The quick start is deliberately tiny:
import SwiftUI
import MarkdownEngine
struct EditorScreen: View {
@State private var text: String = "# Hello, *world*"
var body: some View {
NativeTextViewWrapper(text: $text)
}
}One wrapper view over a string binding, and you have a Markdown editor. The wrapper is the bridge from SwiftUI to the AppKit and TextKit 2 view underneath.
There is one instruction that is easy to skip and expensive to ignore. If you display multiple editors, pass a stable, unique `documentId` such as `"your-doc-id"`, so that undo history and pending replacements stay scoped to each editor instance. Without it, two editors in the same window share undo state, which is the kind of bug that appears as a cursor jumping in a different document.
The code block bridge is the recommended path, and the README explains why in unusually specific terms. Rolling your own `SyntaxHighlighter` has subtle footguns that the bridge already handles: line-height metrics across light and dark themes, appearance-change observation, layout-pass timing, font name extraction from the theme, and CSS-theme-derived background colors. The bridge also auto-switches between `atom-one-light` and `atom-one-dark` with the system appearance, with different theme names or a pinned single theme configurable through its init parameters.
Reading column, scroll-away header and Writing Tools
Three features are about the reading and writing experience rather than the Markdown syntax, and they are where the AppKit heritage shows.
The reading column is an opt-in fixed-width centred column, with wide tables breaking out to the full window width, controlled by the `readingWidth` setting. That is a typographic decision with a specific escape hatch: prose wants a measure of around 60 to 80 characters, and a table does not, so the table breaks out rather than forcing the whole document to a narrow column.
The scroll-away header lets you host your own SwiftUI view above the document. It scrolls with the content and collapses to a pinned top row, which is the behaviour a document title bar or a breadcrumb wants when the user scrolls down into a long note and back up.
The Writing Tools integration is listed as available on macOS 15.1 and later, which places the package in the current Apple platform story rather than the legacy one.
There are three smaller items that show the level of detail this package reaches. Comfortable bottom overscroll, so the caret never pins to the viewport edge while typing. Drag-select autoscroll boost, for long documents where dragging past the edge should scroll faster. And spelling and grammar with code, LaTeX and wiki-link suppression, so that a fenced code block does not produce a page of red squiggles and a wiki link is not spell checked as prose.
That last one is the pattern worth noting. Each of these is a small thing that only becomes a problem once real documents meet the editor, and each is a symptom of the package having been extracted from an app that people type in every day.
Two extension points: delimiter pairs and typed directives
The package offers two ways to add syntax beyond Markdown, and they are different in kind rather than being two versions of the same idea.
Extensions are opt-in constructs defined by a delimiter pair, and the examples given are `==highlight==` and `~~strikethrough~~`. You define the opening and closing markers and the engine treats the text between them as a distinct run with your own styling. This is the right shape for anything that wraps a span of text.
Directives are opt-in constructs defined by a name and typed arguments, for what a delimiter pair can't express, with the example `@font(size: 18){text}`. The braces carry a body, the parentheses carry parameters, and the parameters are typed rather than free text.
The distinction is the interesting part, because it is a syntax design question rather than an API question. A delimiter pair is cheap to define and cheap to parse, and it can nest badly, because the engine has to decide which closing marker belongs to which opening one when the same marker appears inside the span. A directive with typed arguments costs more to define and needs a real parser, and in exchange it can carry structured information, so the host can act on the arguments rather than only style the result.
The README links both to their own documentation sections, `MarkdownExtension` and `MarkdownDirective`, and each protocol and its no-op default is documented in DocC.
The repository structure backs this up. There is a `Sources/` tree, a `Tests/` directory, a `Demo/MarkdownEngineDemo.xcodeproj` with a `Demo/MarkdownEngineDemo/` target, an `ARCHITECTURE.md` at the root, a `CHANGELOG.md` and a `CONTRIBUTING.md`. There is also a `.spi.yml`, which is the Swift Package Index configuration, and the documentation site at the package page is where the DocC reference lives.
Where SwiftMarkdownEngine is the wrong choice
There are three cases where this package does not fit, and the first is the most common reason to look at it and then stop.
The first is cross platform. The platform requirement is macOS 14 or newer and the implementation is AppKit with TextKit 2, so a SwiftUI app that also ships on iOS or Windows cannot use it as is. The alternative in that case is a web view with a JavaScript Markdown editor, which is portable but gives up the native feel, the system Writing Tools integration, and the live TextKit rendering.
The second is a document format that is mostly not Markdown. The package is a Markdown editor with extensions, not a general document renderer. If your documents are structured data with a schema, a WYSIWYG or a code editor with a custom model fits better, because a Markdown editor will fight your format at every construct that is not Markdown.
The third is a static or one-off render. If you only need to display Markdown once, at view time, and never edit it, a renderer that produces attributed text is enough and this package is more machinery than the job needs.
There is also a project maturity point, and it deserves a plain statement rather than a hedge. The last push was on 2026-10-12 and three releases landed between August and October 2026, so the code is moving. But the version numbering starts at 0.1.0, the API is not marked stable anywhere in the README, and the package is a component of one application rather than a library with many consumers. The features are exactly the ones that application needed, the documentation is good on those features and thin elsewhere, and the choices it has made, a single `documentId` for scoping undo, a resolver protocol that must return an `exists` flag, a bridge with a specific set of footguns already handled, are the choices of a project with one real user.
The alternative approach for most teams is to write a small attributed-string renderer over your own document model, which is a day of work and gives you exactly the constructs you need. The difference in approach is that SwiftMarkdownEngine gives you a complete, carefully solved live editor with a well designed extension API, at the cost of a dependency whose surface is shaped by somebody else's application.
Editorial conclusion
Adopt SwiftMarkdownEngine if you are building a macOS app that needs a Markdown editor and you want a native TextKit 2 implementation rather than a web view, especially if your documents contain wiki links, LaTeX or fenced code, since the package handles the styling and the tricky rendering details. Do not adopt it for a cross platform app, since the platform requirement is macOS 14 with Swift 5.9 or newer, and do not adopt it expecting a complete Markdown implementation, because the syntax highlighter, the LaTeX renderer and the image bytes all come from your side. Verify three things first: that the `MarkdownEngine` product alone covers your needs, since the other two pull in HighlighterSwift and SwiftMath, that the wiki link round trip through `[[Name|<id>]]` and `[[Name]]` matches your document format, and that the rendering of the constructs you use in practice looks right on the macOS versions you target, since the package is the editor inside one shipping app rather than a general purpose library with a wide user base.
Frequently asked questions
What is SwiftMarkdownEngine and what does it give a macOS app?
It is a native AppKit Markdown editor for macOS, built on TextKit 2 and bridged into SwiftUI, with live styling, wiki-style links, image embeds, LaTeX, syntax highlighted code blocks, a reading column, a scroll-away header and extensions. It is the editor inside the Nodes macOS notes app.
How do I add SwiftMarkdownEngine to a Swift project?
Add the package with `.package(url: "https://github.com/nodes-app/swift-markdown-engine", from: "0.1.0")` and depend on the product you need, for example `.product(name: "MarkdownEngine", package: "swift-markdown-engine")`. In Xcode you can use File, Add Package Dependencies and paste the repository URL.
What are the four service protocols in SwiftMarkdownEngine?
WikiLinkResolver, which resolves a `[[Name]]` to a stable id, EmbeddedImageProvider, which looks up an NSImage for `![[Name]]`, SyntaxHighlighter, which highlights code blocks for a language, and LatexRenderer, which renders a LaTeX string to an NSImage. Each has a no-op default, so you implement only what you need.
What is the difference between the MarkdownEngine, MarkdownEngineCodeBlocks and MarkdownEngineLatex products?
MarkdownEngine is the editor with zero external dependencies. MarkdownEngineCodeBlocks adds the full visual code-block experience with syntax highlighting and pulls in HighlighterSwift. MarkdownEngineLatex adds LaTeX rendering and pulls in SwiftMath. The README advises adding only what you need.
How do I display several editors with SwiftMarkdownEngine?
Pass a stable, unique `documentId` such as "your-doc-id" to each editor, so that undo history and pending replacements stay scoped to each editor instance instead of being shared between them.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/nodes-app-swift-markdown-engine)Was this useful?