diff options
| author | Ruben Beltran del Rio <jj@r.bdr.sh> | 2026-06-15 14:35:36 +0200 |
|---|---|---|
| committer | Ruben Beltran del Rio <jj@r.bdr.sh> | 2026-06-16 14:47:30 +0200 |
| commit | 657aa83a5ca24dc35572c040df64a0abb85e8612 (patch) | |
| tree | fe0b6fd425d8cad9dc6a28ada4ad8600e42b367e /Sources/NorgUI/Extensions | |
Initial extraction from Norganize
Diffstat (limited to 'Sources/NorgUI/Extensions')
| -rw-r--r-- | Sources/NorgUI/Extensions/AttributedString+Norg.swift | 74 | ||||
| -rw-r--r-- | Sources/NorgUI/Extensions/TaskStatus+UI.swift | 24 |
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)) + } +} |