aboutsummaryrefslogtreecommitdiff
path: root/Sources/NorgUI/Extensions/AttributedString+Norg.swift
diff options
context:
space:
mode:
Diffstat (limited to 'Sources/NorgUI/Extensions/AttributedString+Norg.swift')
-rw-r--r--Sources/NorgUI/Extensions/AttributedString+Norg.swift74
1 files changed, 74 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)
+ }
+}