diff options
Diffstat (limited to 'Sources/NorgKit/Models')
| -rw-r--r-- | Sources/NorgKit/Models/DetachedModifier.swift | 61 | ||||
| -rw-r--r-- | Sources/NorgKit/Models/InlineLink.swift | 15 | ||||
| -rw-r--r-- | Sources/NorgKit/Models/InlineSpan.swift | 12 | ||||
| -rw-r--r-- | Sources/NorgKit/Models/InlineStyle.swift | 28 | ||||
| -rw-r--r-- | Sources/NorgKit/Models/NorgBlock.swift | 108 | ||||
| -rw-r--r-- | Sources/NorgKit/Models/NorgDocument.swift | 16 | ||||
| -rw-r--r-- | Sources/NorgKit/Models/NorgNode.swift | 10 | ||||
| -rw-r--r-- | Sources/NorgKit/Models/NorgTask.swift | 20 | ||||
| -rw-r--r-- | Sources/NorgKit/Models/TaskStatus.swift | 33 |
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 + } + } +} |