aboutsummaryrefslogtreecommitdiff
path: root/Sources/NorgUI/Extensions
diff options
context:
space:
mode:
authorRuben Beltran del Rio <jj@r.bdr.sh>2026-06-15 14:35:36 +0200
committerRuben Beltran del Rio <jj@r.bdr.sh>2026-06-16 14:47:30 +0200
commit657aa83a5ca24dc35572c040df64a0abb85e8612 (patch)
treefe0b6fd425d8cad9dc6a28ada4ad8600e42b367e /Sources/NorgUI/Extensions
Initial extraction from Norganize
Diffstat (limited to 'Sources/NorgUI/Extensions')
-rw-r--r--Sources/NorgUI/Extensions/AttributedString+Norg.swift74
-rw-r--r--Sources/NorgUI/Extensions/TaskStatus+UI.swift24
2 files changed, 98 insertions, 0 deletions
diff --git a/Sources/NorgUI/Extensions/AttributedString+Norg.swift b/Sources/NorgUI/Extensions/AttributedString+Norg.swift
new file mode 100644
index 0000000..286e0c8
--- /dev/null
+++ b/Sources/NorgUI/Extensions/AttributedString+Norg.swift
@@ -0,0 +1,74 @@
+import NorgKit
+import SwiftUI
+
+/// Extensions to AttributedString to generate from NorgKit models.
+extension AttributedString {
+
+ /// Builds a styled attributed string from parsed Norg inline spans.
+ public init(norg spans: [InlineSpan], baseFont: Font = .body, theme: NorgTheme = .default) {
+ var result = AttributedString()
+ for span in spans {
+ result.append(Self.render(span, baseFont: baseFont, theme: theme))
+ }
+ self = result
+ }
+
+ private static func render(_ span: InlineSpan, baseFont: Font, theme: NorgTheme)
+ -> AttributedString {
+ var run = AttributedString(span.text)
+ run.font = font(for: span.styles, baseFont: baseFont)
+ applyDecorations(to: &run, styles: span.styles, theme: theme)
+ if let link = span.link {
+ applyLink(link, to: &run, theme: theme)
+ }
+ return run
+ }
+
+ private static func font(for styles: InlineStyle, baseFont: Font) -> Font {
+ var font = baseFont
+ if styles.contains(.verbatim) || styles.contains(.math) { font = font.monospaced() }
+ if styles.contains(.bold) { font = font.bold() }
+ if styles.contains(.italic) { font = font.italic() }
+ return font
+ }
+
+ private static func applyDecorations(
+ to run: inout AttributedString, styles: InlineStyle, theme: NorgTheme
+ ) {
+ if styles.contains(.underline) { run.underlineStyle = .single }
+ if styles.contains(.strikethrough) { run.strikethroughStyle = .single }
+ if styles.contains(.superscript) { run.baselineOffset = theme.superscriptOffset }
+ if styles.contains(.subscript) { run.baselineOffset = theme.subscriptOffset }
+ if styles.contains(.spoiler) {
+ run.foregroundColor = theme.spoilerForeground
+ run.backgroundColor = theme.spoilerBackground
+ }
+ if styles.contains(.verbatim) {
+ run.foregroundColor = theme.verbatimColor
+ }
+ }
+
+ private static func applyLink(
+ _ link: InlineLink, to run: inout AttributedString, theme: NorgTheme
+ ) {
+ run.foregroundColor = theme.linkColor
+ run.underlineStyle = .single
+ run.link = url(for: link)
+ }
+
+ private static func url(for link: InlineLink) -> URL? {
+ if link.kind == .link, let target = link.target, let external = externalURL(for: target) {
+ return external
+ }
+ return NorgLinkURL.encode(link)
+ }
+
+ private static func externalURL(for target: String) -> URL? {
+ let lower = target.lowercased()
+ guard lower.hasPrefix("http://") || lower.hasPrefix("https://") || lower.hasPrefix("file://")
+ else {
+ return nil
+ }
+ return URL(string: target)
+ }
+}
diff --git a/Sources/NorgUI/Extensions/TaskStatus+UI.swift b/Sources/NorgUI/Extensions/TaskStatus+UI.swift
new file mode 100644
index 0000000..e54e511
--- /dev/null
+++ b/Sources/NorgUI/Extensions/TaskStatus+UI.swift
@@ -0,0 +1,24 @@
+import Foundation
+import NorgKit
+
+/// Adds UI rendering labels for TaskStatus.
+extension TaskStatus {
+
+ /// A localized, human-readable name for the status.
+ public var displayName: LocalizedStringResource {
+ switch self {
+ case .undone: return Self.label("Undone")
+ case .done: return Self.label("Done")
+ case .needsInput: return Self.label("Needs input")
+ case .urgent: return Self.label("Urgent")
+ case .recurring: return Self.label("Recurring")
+ case .pending: return Self.label("Pending")
+ case .onHold: return Self.label("On hold")
+ case .cancelled: return Self.label("Cancelled")
+ }
+ }
+
+ private static func label(_ value: String.LocalizationValue) -> LocalizedStringResource {
+ LocalizedStringResource(value, bundle: .atURL(Bundle.module.bundleURL))
+ }
+}