1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
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
```
|