1. Install a tagged release
Add Cindermark with Swift Package Manager. Version 0.3.0 provides the published Apple binary for iOS 16+ and macOS 13+. A consuming app does not need a Rust toolchain.
.package(url: "https://github.com/renedeanda/cindermark", from: "0.3.0")2. Keep the source as the source of truth
Create one parser for the current document and make an initial editable parse. The returned block and inline ranges use UTF-16 coordinates, the same unit that NSRange and NSTextStorage use. Style the characters in your text storage; keep the original Markdown string for saving and export.
import Foundation
import Cindermark
let parser = CindermarkParser()
let source = "# Today\n\nA **small** idea."
let result = parser.parseEditable(text: source)
for block in result.blocks {
let range = NSRange(
location: Int(block.utf16Start),
length: Int(block.utf16End - block.utf16Start)
)
// Apply the block's style within this TextKit range.
// block.inlineSpans provides smaller ranges for emphasis and links.
}TextKit attributes are your application’s design choice. The parser reports that a span is emphasized or that a block is a callout; it does not supply fonts, colors, spacing, selection behavior, or the callout view. That is why the browser playground is a demonstration of the parser with an Ember-inspired treatment, not a copy of TextKit.
3. Send each edit in UTF-16 coordinates
Capture the range in the old text, the replacement, and the complete new text. The edit start and both lengths are UTF-16 counts. Swift’s String.count counts characters and is not a substitute: an emoji can occupy more than one UTF-16 code unit.
func applyEdit(
parser: CindermarkParser,
oldText: String,
oldRange: NSRange,
replacement: String
) -> (text: String, update: FfiIncrementalStyleResult) {
// oldRange must be a valid range in oldText's UTF-16 coordinates.
let next = (oldText as NSString).replacingCharacters(
in: oldRange, with: replacement
)
let update = parser.parseEditableIncrementalStyleOnly(
text: next,
editUtf16Start: UInt32(oldRange.location),
editOldUtf16Len: UInt32(oldRange.length),
editNewUtf16Len: UInt32((replacement as NSString).length)
)
return (next, update)
}
// update.blocks contains the current document's blocks.
// update.dirtyStart..<update.dirtyEnd identifies changed block indexes.The API returns the current block list plus a half-open dirty block index range. Reapply styling around those blocks and clear obsolete attributes when text or blocks disappear. Structural changes such as an open code fence can legitimately make a larger region dirty; the parser chooses correctness over a small reported range. Keep edit calls sequential on the same parser instance, and use resetState() or a new instance when opening another note.
4. Make the editing feel native
Do not replace the whole text view after every parse. Let TextKit own the caret and selection, update styling in place, and avoid changing attributes that are unrelated to the edit. Test typing through a heading marker, a nested checklist, a table, an emoji, and a long note. Check deletion and paste as carefully as insertion.
Cindermark can parse a callout or table, but your editor decides whether it shows source marks near the caret, how a checkbox is tapped, and how a cell is edited. These choices are the visible product. The playground lets you inspect the same Markdown as a styled note and as parser ranges.
Check the source contract
The code above follows the current parser README and the 0.3 Swift FFI method names. The compatibility profile documents source ranges and fallback behavior. The docs.rs crate page may retain older README installation text from the immutable 0.3.0 package; use the tagged 0.3.0 quickstart here or the current repository README for this release.