diff options
| author | Ruben Beltran del Rio <jj@r.bdr.sh> | 2026-06-16 12:08:21 +0200 |
|---|---|---|
| committer | Ruben Beltran del Rio <jj@r.bdr.sh> | 2026-06-16 12:22:47 +0200 |
| commit | 501fdce29e4d2c46c4708ad7f44056878e0db3fa (patch) | |
| tree | db923b77037db9f5b37600a6e34c3bb2e8a971be /README.md | |
Initial extraction from Norganize1.0.0
Diffstat (limited to 'README.md')
| -rw-r--r-- | README.md | 168 |
1 files changed, 168 insertions, 0 deletions
diff --git a/README.md b/README.md new file mode 100644 index 0000000..04b711f --- /dev/null +++ b/README.md @@ -0,0 +1,168 @@ +# NorgKit + +NorgKit is a Swift parser and library for `.norg` / [Neorg](https://github.com/nvim-neorg/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. + +## Usage + +`NorgParser.parse` outputs a flat list of blocks you can iterate over. + +```swift +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` + +```swift +// 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: + +```swift +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) + +```swift +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: + +```bash +just benchmark +``` + +## Development + +A `Justfile` is provided to run common tasks: + +```sh +just build # swift build +just test # swift test +just lint # swiftlint +just format # swift-format + swiftlint --fix +``` |