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
HashableandCodablefor caching and serialization. - Reasonably Fast.
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. Hasblocks: [NorgBlock]and atree()method that folds them into[NorgNode].NorgBlock: a block-level element, including itsline, and an attachedstatusthat denotes its task status if any, and can be copied to a new status (or have it cleared, withnil) viasettingStatus(_:). Its inline text is available ascontentand the nested blocks of a range-able block asbody. Can be one of:headingparagraphunorderedListItemorderedListItemquotecodeBlockdefinition($ Term/ ranged$$ … $$), with atitleandbody,footnote(^ Name/ ranged^^ … ^^), with atitleandbody,tableCell(: Address/ ranged:: … ::), with atitleandbody,rangedTag,horizontalRule(___),weakDelimiter(---),strongDelimiter(===).
NorgNode: a node in the hierarchical view fromtree()/parseTree. Wraps ablock: NorgBlockand its nestedchildren: [NorgNode].InlineSpan— a run of text withstyles: InlineStyleand an optionallink: InlineLink?.Array<InlineSpan>.plainTextconcatenates the text, ignoring styling.InlineLink— a linkable: akind(.linkfor{location},.anchorfor[name]) and an optionaltarget. Thetargetisnilfor a bare anchor declaration whose location is defined elsewhere in the document.InlineStyle— anOptionSetof 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 itsfileURL,line,status, andtext.
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