Integration guide / Swift

From parser ranges to a live editor.

Cindermark reads Markdown. Your app owns the text, draws the styles, and decides how editing should feel. This is the narrow bridge between those responsibilities.

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.swift
.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.

First parse
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.

Incremental update
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.