aboutsummaryrefslogtreecommitdiff
path: root/README.md
diff options
context:
space:
mode:
Diffstat (limited to 'README.md')
-rw-r--r--README.md168
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
+```