diff options
| author | Christian Cleberg <[email protected]> | 2026-08-07 02:45:31 -0500 |
|---|---|---|
| committer | Christian Cleberg <[email protected]> | 2026-08-07 02:45:31 -0500 |
| commit | b410fd16b76146f4116538c5b82330818dd47da9 (patch) | |
| tree | 649488b2e5d3d901db8c792c0dc215c835fa9d38 /Hutch/Views/Repositories/ReadmeView.swift | |
| parent | 0a3e55c15cfb9ce1bd3df0c7b352760bc2ee33c3 (diff) | |
| download | hutch-b410fd16b76146f4116538c5b82330818dd47da9.tar.gz hutch-b410fd16b76146f4116538c5b82330818dd47da9.tar.bz2 hutch-b410fd16b76146f4116538c5b82330818dd47da9.zip | |
Add multi-language syntax highlighting (#16)
Splash (Swift-only, since removed) left code rendering unhighlighted
everywhere. Add Highlightr (highlight.js) behind a small SyntaxHighlighter
service and use it on both surfaces:
- Native file viewer: highlight NSAttributedString by language inferred
from the filename, picking a light/dark theme from the trait style and
re-highlighting on style changes. Falls back to plain text for unknown
languages or very large files.
- Markdown/org code blocks in the WKWebView (READMEs, tickets, comments):
the web view disables JavaScript, so highlight server-side into inline
color-styled spans. The color scheme is now part of the render/cache
key since colors are baked into the HTML.
Covered by unit tests for language mapping and highlighted output.
Diffstat (limited to 'Hutch/Views/Repositories/ReadmeView.swift')
| -rw-r--r-- | Hutch/Views/Repositories/ReadmeView.swift | 28 |
1 files changed, 22 insertions, 6 deletions
diff --git a/Hutch/Views/Repositories/ReadmeView.swift b/Hutch/Views/Repositories/ReadmeView.swift index b8b9622..722af8d 100644 --- a/Hutch/Views/Repositories/ReadmeView.swift +++ b/Hutch/Views/Repositories/ReadmeView.swift @@ -234,15 +234,18 @@ struct RenderedMarkupContentView: View { @State private var renderedHTML: String? private var cacheKey: String { + // Highlighted code colors are baked into the HTML, so the theme is part + // of the cache identity — light and dark must not share an entry. + let theme = colorScheme == .dark ? "dark" : "light" switch content { case .html(let html): - "html:\(readmePath ?? "custom"):\(html)" + return "html:\(readmePath ?? "custom"):\(html)" case .markdown(let text): - "markdown:\(readmePath ?? ""):\(text)" + return "markdown:\(theme):\(readmePath ?? ""):\(text)" case .org(let text): - "org:\(readmePath ?? ""):\(text)" + return "org:\(theme):\(readmePath ?? ""):\(text)" case .plainText(let text): - "plain:\(readmePath ?? ""):\(text)" + return "plain:\(readmePath ?? ""):\(text)" } } @@ -277,9 +280,11 @@ struct RenderedMarkupContentView: View { renderedHTML = cached return } + let theme = SyntaxHighlightTheme(colorScheme: colorScheme) let html = await Task.detached(priority: .userInitiated) { markdownToHTML( text, + codeTheme: theme, imageURLResolver: { source in resolveRepositoryAssetURL( source, @@ -308,9 +313,11 @@ struct RenderedMarkupContentView: View { renderedHTML = cached return } + let theme = SyntaxHighlightTheme(colorScheme: colorScheme) let html = await Task.detached(priority: .userInitiated) { orgToHTML( text, + codeTheme: theme, imageURLResolver: { source in resolveRepositoryAssetURL( source, @@ -462,9 +469,11 @@ nonisolated func processInline( nonisolated func orgToHTML( _ text: String, + codeTheme: SyntaxHighlightTheme = .light, imageURLResolver: ((String) -> String?)? = nil, linkURLResolver: ((String) -> String?)? = nil ) -> String { + let highlighter = SyntaxHighlighter(theme: codeTheme) let normalizedText = text .replacingOccurrences(of: "\r\n", with: "\n") .replacingOccurrences(of: "\r", with: "\n") @@ -496,6 +505,7 @@ nonisolated func orgToHTML( var inQuoteBlock = false var inPropertyDrawer = false var srcLanguage: String? + var srcLines: [String] = [] var inExampleBlock = false var inCenterBlock = false var inVerseBlock = false @@ -593,9 +603,14 @@ nonisolated func orgToHTML( } func closeSourceBlock() { - if srcLanguage != nil { + if let language = srcLanguage { + let code = srcLines.joined(separator: "\n") + if !code.isEmpty { + html += (highlighter.highlightedHTML(for: code, language: language) ?? escapeHTML(code)) + "\n" + } html += "</code></pre>\n" srcLanguage = nil + srcLines = [] closePendingBlockWrapper() } } @@ -659,7 +674,7 @@ nonisolated func orgToHTML( if trimmed.lowercased() == "#+end_src" { closeSourceBlock() } else { - html += escapeHTML(line) + "\n" + srcLines.append(line) } continue } @@ -730,6 +745,7 @@ nonisolated func orgToHTML( let classAttribute = language.map { " class=\"language-\(escapeHTMLAttribute($0))\"" } ?? "" html += "<pre><code\(classAttribute)>" srcLanguage = language ?? "" + srcLines = [] continue } |
