aboutsummaryrefslogtreecommitdiff
path: root/Sources/NorgKit/Models
diff options
context:
space:
mode:
Diffstat (limited to 'Sources/NorgKit/Models')
-rw-r--r--Sources/NorgKit/Models/DetachedModifier.swift61
-rw-r--r--Sources/NorgKit/Models/InlineLink.swift15
-rw-r--r--Sources/NorgKit/Models/InlineSpan.swift12
-rw-r--r--Sources/NorgKit/Models/InlineStyle.swift28
-rw-r--r--Sources/NorgKit/Models/NorgBlock.swift108
-rw-r--r--Sources/NorgKit/Models/NorgDocument.swift16
-rw-r--r--Sources/NorgKit/Models/NorgNode.swift10
-rw-r--r--Sources/NorgKit/Models/NorgTask.swift20
-rw-r--r--Sources/NorgKit/Models/TaskStatus.swift33
9 files changed, 303 insertions, 0 deletions
diff --git a/Sources/NorgKit/Models/DetachedModifier.swift b/Sources/NorgKit/Models/DetachedModifier.swift
new file mode 100644
index 0000000..1f28c36
--- /dev/null
+++ b/Sources/NorgKit/Models/DetachedModifier.swift
@@ -0,0 +1,61 @@
+/// A detached modifier with its marker, nestling level, task status (if set),
+/// and content.
+struct DetachedModifier {
+ let marker: Character
+ let level: Int
+ let status: TaskStatus?
+ let statusIndex: String.Index?
+ let content: Substring
+
+ /// Finds and parses a detached modifier if present. The accepting markers
+ /// is used to reduce the markers being parsed.
+ static func parse(_ line: Substring, accepting markers: ASCIIByteSet) -> DetachedModifier? {
+
+ let utf8 = line.utf8
+ let end = utf8.endIndex
+ var i = utf8.startIndex
+
+ while i < end, ASCIIHelper.isWhitespace(utf8[i]) { i = utf8.index(after: i) }
+ guard i < end else { return nil }
+
+ let markerByte = utf8[i]
+ guard markers.contains(markerByte) else { return nil }
+
+ var level = 0
+ var run = i
+ while run < end, utf8[run] == markerByte {
+ level += 1
+ run = utf8.index(after: run)
+ }
+
+ guard run < end, ASCIIHelper.isWhitespace(utf8[run]) else { return nil }
+
+ var rest = run
+ while rest < end, ASCIIHelper.isWhitespace(utf8[rest]) { rest = utf8.index(after: rest) }
+
+ var status: TaskStatus?
+ var statusIndex: String.Index?
+ var contentStart = rest
+ if rest < end, utf8[rest] == UInt8(ascii: "(") {
+ let markerPosition = utf8.index(after: rest)
+ if markerPosition < end {
+ let closePosition = utf8.index(after: markerPosition)
+ let statusByte = utf8[markerPosition]
+ if closePosition < end, utf8[closePosition] == UInt8(ascii: ")"),
+ let parsed = TaskStatus(markerByte: statusByte) {
+ status = parsed
+ statusIndex = markerPosition
+ contentStart = utf8.index(after: closePosition)
+ }
+ }
+ }
+
+ return DetachedModifier(
+ marker: Character(Unicode.Scalar(markerByte)),
+ level: level,
+ status: status,
+ statusIndex: statusIndex,
+ content: TextHelper.whitespaceTrimmed(line[contentStart...])
+ )
+ }
+}
diff --git a/Sources/NorgKit/Models/InlineLink.swift b/Sources/NorgKit/Models/InlineLink.swift
new file mode 100644
index 0000000..b2f6a15
--- /dev/null
+++ b/Sources/NorgKit/Models/InlineLink.swift
@@ -0,0 +1,15 @@
+/// A link or anchor.
+public struct InlineLink: Equatable, Hashable, Sendable, Codable {
+ public enum Kind: Equatable, Hashable, Sendable, Codable {
+ case link
+ case anchor
+ }
+
+ public var kind: Kind
+ public var target: String?
+
+ public init(kind: Kind, target: String?) {
+ self.kind = kind
+ self.target = target
+ }
+}
diff --git a/Sources/NorgKit/Models/InlineSpan.swift b/Sources/NorgKit/Models/InlineSpan.swift
new file mode 100644
index 0000000..bebe60b
--- /dev/null
+++ b/Sources/NorgKit/Models/InlineSpan.swift
@@ -0,0 +1,12 @@
+/// A contiguous run of text sharing the same style and link.
+public struct InlineSpan: Equatable, Hashable, Sendable, Codable {
+ public var text: String
+ public var styles: InlineStyle
+ public var link: InlineLink?
+
+ public init(text: String, styles: InlineStyle = [], link: InlineLink? = nil) {
+ self.text = text
+ self.styles = styles
+ self.link = link
+ }
+}
diff --git a/Sources/NorgKit/Models/InlineStyle.swift b/Sources/NorgKit/Models/InlineStyle.swift
new file mode 100644
index 0000000..807de7b
--- /dev/null
+++ b/Sources/NorgKit/Models/InlineStyle.swift
@@ -0,0 +1,28 @@
+/// Composable inline styles that may be applied to a single run of text.
+public struct InlineStyle: OptionSet, Hashable, Sendable, Codable {
+ public let rawValue: Int
+
+ public init(rawValue: Int) {
+ self.rawValue = rawValue
+ }
+
+ public static let bold = InlineStyle(rawValue: 1 << 0)
+ public static let italic = InlineStyle(rawValue: 1 << 1)
+ public static let underline = InlineStyle(rawValue: 1 << 2)
+ public static let strikethrough = InlineStyle(rawValue: 1 << 3)
+ public static let verbatim = InlineStyle(rawValue: 1 << 4)
+ public static let superscript = InlineStyle(rawValue: 1 << 5)
+ public static let `subscript` = InlineStyle(rawValue: 1 << 6)
+ public static let spoiler = InlineStyle(rawValue: 1 << 7)
+ public static let math = InlineStyle(rawValue: 1 << 8)
+
+ public init(from decoder: Decoder) throws {
+ let container = try decoder.singleValueContainer()
+ self.init(rawValue: try container.decode(Int.self))
+ }
+
+ public func encode(to encoder: Encoder) throws {
+ var container = encoder.singleValueContainer()
+ try container.encode(rawValue)
+ }
+}
diff --git a/Sources/NorgKit/Models/NorgBlock.swift b/Sources/NorgKit/Models/NorgBlock.swift
new file mode 100644
index 0000000..32e4eeb
--- /dev/null
+++ b/Sources/NorgKit/Models/NorgBlock.swift
@@ -0,0 +1,108 @@
+/// A block-level element of a Norg document, including its line position in
+/// the source.
+public enum NorgBlock: Equatable, Hashable, Sendable, Codable {
+ case heading(level: Int, status: TaskStatus?, content: [InlineSpan], line: Int)
+ case paragraph(content: [InlineSpan], line: Int)
+ case unorderedListItem(level: Int, status: TaskStatus?, content: [InlineSpan], line: Int)
+ case orderedListItem(level: Int, status: TaskStatus?, content: [InlineSpan], line: Int)
+ case quote(level: Int, status: TaskStatus?, content: [InlineSpan], line: Int)
+ case codeBlock(language: String?, code: String, line: Int)
+ case definition(title: [InlineSpan], status: TaskStatus?, body: [NorgBlock], line: Int)
+ case footnote(title: [InlineSpan], status: TaskStatus?, body: [NorgBlock], line: Int)
+ case tableCell(title: [InlineSpan], status: TaskStatus?, body: [NorgBlock], line: Int)
+ case rangedTag(name: [String], parameters: [String], content: String, line: Int)
+ case horizontalRule(line: Int)
+ case weakDelimiter(line: Int)
+ case strongDelimiter(line: Int)
+
+ /// The source line on which the block begins.
+ public var line: Int {
+ switch self {
+ case .heading(_, _, _, let line),
+ .paragraph(_, let line),
+ .unorderedListItem(_, _, _, let line),
+ .orderedListItem(_, _, _, let line),
+ .quote(_, _, _, let line),
+ .codeBlock(_, _, let line),
+ .definition(_, _, _, let line),
+ .footnote(_, _, _, let line),
+ .tableCell(_, _, _, let line),
+ .rangedTag(_, _, _, let line),
+ .horizontalRule(let line),
+ .weakDelimiter(let line),
+ .strongDelimiter(let line):
+ return line
+ }
+ }
+
+ /// The task status attached to the block, if any.
+ public var status: TaskStatus? {
+ switch self {
+ case .heading(_, let status, _, _),
+ .unorderedListItem(_, let status, _, _),
+ .orderedListItem(_, let status, _, _),
+ .quote(_, let status, _, _),
+ .definition(_, let status, _, _),
+ .footnote(_, let status, _, _),
+ .tableCell(_, let status, _, _):
+ return status
+ default:
+ return nil
+ }
+ }
+
+ /// The block's inline styled content (eg. text of a heading/paragraph, or
+ /// title of a definition/footnote).
+ public var content: [InlineSpan]? {
+ switch self {
+ case .heading(_, _, let content, _),
+ .paragraph(let content, _),
+ .unorderedListItem(_, _, let content, _),
+ .orderedListItem(_, _, let content, _),
+ .quote(_, _, let content, _):
+ return content
+ case .definition(let title, _, _, _),
+ .footnote(let title, _, _, _),
+ .tableCell(let title, _, _, _):
+ return title
+ default:
+ return nil
+ }
+ }
+
+ /// The nested blocks owned by a range-able block (definition, footnote, or
+ /// table cell).
+ public var body: [NorgBlock]? {
+ switch self {
+ case .definition(_, _, let body, _),
+ .footnote(_, _, let body, _),
+ .tableCell(_, _, let body, _):
+ return body
+ default:
+ return nil
+ }
+ }
+
+ /// Returns a copy of the block with its task status replaced (pass `nil` to
+ /// clear it). Blocks that cannot carry a status are returned unchanged.
+ public func settingStatus(_ status: TaskStatus?) -> NorgBlock {
+ switch self {
+ case .heading(let level, _, let content, let line):
+ return .heading(level: level, status: status, content: content, line: line)
+ case .unorderedListItem(let level, _, let content, let line):
+ return .unorderedListItem(level: level, status: status, content: content, line: line)
+ case .orderedListItem(let level, _, let content, let line):
+ return .orderedListItem(level: level, status: status, content: content, line: line)
+ case .quote(let level, _, let content, let line):
+ return .quote(level: level, status: status, content: content, line: line)
+ case .definition(let title, _, let body, let line):
+ return .definition(title: title, status: status, body: body, line: line)
+ case .footnote(let title, _, let body, let line):
+ return .footnote(title: title, status: status, body: body, line: line)
+ case .tableCell(let title, _, let body, let line):
+ return .tableCell(title: title, status: status, body: body, line: line)
+ default:
+ return self
+ }
+ }
+}
diff --git a/Sources/NorgKit/Models/NorgDocument.swift b/Sources/NorgKit/Models/NorgDocument.swift
new file mode 100644
index 0000000..73a3ed6
--- /dev/null
+++ b/Sources/NorgKit/Models/NorgDocument.swift
@@ -0,0 +1,16 @@
+/// A fully parsed Norg document.
+public struct NorgDocument: Equatable, Hashable, Sendable, Codable {
+
+ /// The parsed blocks, as a flat list.
+ public var blocks: [NorgBlock]
+
+ /// Given an array of parsed blocks, create a document.
+ public init(blocks: [NorgBlock] = []) {
+ self.blocks = blocks
+ }
+
+ /// Converts the flat document into a tree.
+ public func tree() -> [NorgNode] {
+ TreeFolder(blocks).fold()
+ }
+}
diff --git a/Sources/NorgKit/Models/NorgNode.swift b/Sources/NorgKit/Models/NorgNode.swift
new file mode 100644
index 0000000..99ae1f1
--- /dev/null
+++ b/Sources/NorgKit/Models/NorgNode.swift
@@ -0,0 +1,10 @@
+/// A node in a tree view of a Norg document. Includes its block and children.
+public struct NorgNode: Equatable, Hashable, Sendable, Codable {
+ public var block: NorgBlock
+ public var children: [NorgNode]
+
+ public init(block: NorgBlock, children: [NorgNode] = []) {
+ self.block = block
+ self.children = children
+ }
+}
diff --git a/Sources/NorgKit/Models/NorgTask.swift b/Sources/NorgKit/Models/NorgTask.swift
new file mode 100644
index 0000000..7eb5c53
--- /dev/null
+++ b/Sources/NorgKit/Models/NorgTask.swift
@@ -0,0 +1,20 @@
+import Foundation
+
+/// A single norg task / TODO.
+public struct NorgTask: Identifiable, Equatable, Hashable, Sendable, Codable {
+
+ public let fileURL: URL
+ /// This is zero-indexed
+ public let line: Int
+ public var status: TaskStatus
+ public let text: String
+
+ public init(fileURL: URL, line: Int, status: TaskStatus, text: String) {
+ self.fileURL = fileURL
+ self.line = line
+ self.status = status
+ self.text = text
+ }
+
+ public var id: String { "\(fileURL.absoluteString):\(line)" }
+}
diff --git a/Sources/NorgKit/Models/TaskStatus.swift b/Sources/NorgKit/Models/TaskStatus.swift
new file mode 100644
index 0000000..74badf3
--- /dev/null
+++ b/Sources/NorgKit/Models/TaskStatus.swift
@@ -0,0 +1,33 @@
+/// A Norg TODO status.
+public enum TaskStatus: String, CaseIterable, Identifiable, Sendable, Hashable, Codable {
+ case undone = " "
+ case done = "x"
+ case needsInput = "?"
+ case urgent = "!"
+ case recurring = "+"
+ case pending = "-"
+ case onHold = "="
+ case cancelled = "_"
+
+ /// Identifier, corresponds to its character.
+ public var id: String { rawValue }
+
+ /// Creates a TaskStatus based on a UTF-8 character.
+ public init?(marker: Character) {
+ self.init(rawValue: String(marker))
+ }
+
+ init?(markerByte byte: UInt8) {
+ switch byte {
+ case UInt8(ascii: " "): self = .undone
+ case UInt8(ascii: "x"): self = .done
+ case UInt8(ascii: "?"): self = .needsInput
+ case UInt8(ascii: "!"): self = .urgent
+ case UInt8(ascii: "+"): self = .recurring
+ case UInt8(ascii: "-"): self = .pending
+ case UInt8(ascii: "="): self = .onHold
+ case UInt8(ascii: "_"): self = .cancelled
+ default: return nil
+ }
+ }
+}