aboutsummaryrefslogtreecommitdiff

NorgKit

NorgKit is a Swift parser and library for .norg / Neorg documents.

  • Depends only on Foundation, so usable outside of iOS/ipadOS/macOS apps.
  • Concurrency safe and sendable.
  • Types are Hashable and Codable for caching and serialization.
  • Reasonably Fast.

Installation

Add the package with Swift Package Manager:

.package(url: "https://git.r.bdr.sh/norg-ui", from: "1.0.0"),

…or the xcode package manager, and depend on the NorgKit product from your target.

Usage

NorgParser.parse outputs a flat list of blocks you can iterate over.

import NorgKit

let document = NorgParser.parse("""
* (x) Ship NorgKit
Some *bold* and /italic/ prose, plus a {https://example.com}[link].

@code swift
let answer = 42
@end
""")

for block in document.blocks {
    switch block {
    case let .heading(level, status, content, line):
        print("h\(level) @\(line): \(content.plainText) [\(status?.rawValue ?? "no status")]")
    case let .paragraph(content, _):
        print(content.plainText)
    case let .codeBlock(language, code, _):
        print("```\(language ?? "")\n\(code)\n```")
    default:
        break
    }
}

Hierarchical parsing

When you need the document's structure as a tree (eg. for folding), you can call either .parseTree(), or .tree() on a NorgDocument

// Parse straight to a tree...
let nodes = NorgParser.parseTree(source)   // [NorgNode]

// ...or fold an existing flat parse.
let document = NorgParser.parse(source)
let tree = document.tree()

func render(_ node: NorgNode) {
    emit(node.block)            // a NorgBlock
    node.children.forEach(render)
}
tree.forEach(render)

Inline-only parsing

NorgInlineParser.parse parses a single line of inline markup into [InlineSpan], useful when you only need intra-paragraph styling:

let spans = NorgInlineParser.parse("a `verbatim` and *bold* run")

Scanning and updating tasks

Besides the parser, NorgKit contains TaskScanner, which extracts tasks across a file and can rewrite status markers in place. It's faster than a parse; fast enough to run on a whole vault! (Eg. to look for all open tasks)

let url = URL(filePath: "/vault/today.norg")
let tasks = TaskScanner.scan(content: contents, fileURL: url)

// Toggle the task on line 3 to done, preserving indentation.
if let updated = TaskScanner.updatedContent(contents, line: 3, to: .done) {
    try updated.write(to: url, atomically: true, encoding: .utf8)
}

Types Exported

  • NorgDocument: a parsed document. Has blocks: [NorgBlock] and a tree() method that folds them into [NorgNode].
  • NorgBlock: a block-level element, including its line, and an attached status that denotes its task status if any, and can be copied to a new status (or have it cleared, with nil) via settingStatus(_:). Its inline text is available as content and the nested blocks of a range-able block as body. Can be one of:
    • heading
    • paragraph
    • unorderedListItem
    • orderedListItem
    • quote
    • codeBlock
    • definition ($ Term / ranged $$ … $$), with a title and body,
    • footnote (^ Name / ranged ^^ … ^^), with a title and body,
    • tableCell (: Address / ranged :: … ::), with a title and body,
    • rangedTag,
    • horizontalRule (___),
    • weakDelimiter (---),
    • strongDelimiter (===).
  • NorgNode: a node in the hierarchical view from tree() / parseTree. Wraps a block: NorgBlock and its nested children: [NorgNode].
  • InlineSpan — a run of text with styles: InlineStyle and an optional link: InlineLink?. Array<InlineSpan>.plainText concatenates the text, ignoring styling.
  • InlineLink — a linkable: a kind (.link for {location}, .anchor for [name]) and an optional target. The target is nil for a bare anchor declaration whose location is defined elsewhere in the document.
  • InlineStyle — an OptionSet of inline modifiers:
    • .bold
    • .italic
    • .underline
    • .strikethrough
    • .verbatim
    • .superscript
    • .subscript
    • .spoiler
    • .math
  • TaskStatus — a Norg TODO status, one of:
    • .undone
    • .done
    • .needsInput
    • .urgent
    • .recurring
    • .pending
    • .onHold
    • .cancelled
  • NorgTask — a task located in a file, with its fileURL, line, status, and text.

Reasonably Fast

(All measures p90)

Benchmarked on an M1 Pro. A document with around 40 blocks parses in ~150µs. While a larger document with roughly 1,600 blocks (about 2,500 lines) does so in ~2ms.

Folding costs ~100 microseconds, so the benchmarks above apply whether you fold or not.

Scanning tasks is faster and meant to be run over whole vaults. The same ~2,500-line document (around 800 tasks) takes ~420µs.

You can run the benchmarks by using:

just benchmark

Development

A Justfile is provided to run common tasks:

just build   # swift build
just test    # swift test
just lint    # swiftlint
just format  # swift-format + swiftlint --fix