Commit dd00288f15

dd00288f1595e197e1426776c108e32895d195ad

parent: 8d2421afbe

Unsigned

cmc <hello@cleberg.net> · 2026-08-31 18:26 UTC

Render org READMEs natively instead of in a web view

ReadmeView now uses OrgSwiftUI's OrgView for .org content. Selectable
text, Dynamic Type and VoiceOver, and no WKWebView per README — which also
removes the document.body.scrollHeight round-trip that sized it and the
re-render on every theme change.

OrgSyntaxStyler replaces OrgReadmeWebView's Highlightr bridge. Highlightr
already returns an NSAttributedString, so the HTML-span flattening and the
hand-written stylesheet are gone.

org-swift moves from tracking main to the 0.2.0 tag. The revision it
pinned before no longer exists; main was rewritten.

OrgView is qualified at the call site because gitbay already has an
OrgView for organizations.

Layout: unified · split

gitbay.xcodeproj/project.pbxproj +12 −4
@@ -8,6 +8,7 @@
8 8
9/* Begin PBXBuildFile section */ 9/* Begin PBXBuildFile section */
10 0F0F0F0F0F0F0F0F00000003 /* OrgSwift in Frameworks */ = {isa = PBXBuildFile; productRef = 0F0F0F0F0F0F0F0F00000002 /* OrgSwift */; }; 10 0F0F0F0F0F0F0F0F00000003 /* OrgSwift in Frameworks */ = {isa = PBXBuildFile; productRef = 0F0F0F0F0F0F0F0F00000002 /* OrgSwift */; };
11 0F0F0F0F0F0F0F0F00000005 /* OrgSwiftUI in Frameworks */ = {isa = PBXBuildFile; productRef = 0F0F0F0F0F0F0F0F00000004 /* OrgSwiftUI */; };
11 8B289850303FF914001BF44A /* Highlightr in Frameworks */ = {isa = PBXBuildFile; productRef = 8B2898B1303FF3E5001BF44A /* Highlightr */; }; 12 8B289850303FF914001BF44A /* Highlightr in Frameworks */ = {isa = PBXBuildFile; productRef = 8B2898B1303FF3E5001BF44A /* Highlightr */; };
12/* End PBXBuildFile section */ 13/* End PBXBuildFile section */
13 14
@@ -59,6 +60,7 @@
59 files = ( 60 files = (
60 8B289850303FF914001BF44A /* Highlightr in Frameworks */, 61 8B289850303FF914001BF44A /* Highlightr in Frameworks */,
61 0F0F0F0F0F0F0F0F00000003 /* OrgSwift in Frameworks */, 62 0F0F0F0F0F0F0F0F00000003 /* OrgSwift in Frameworks */,
63 0F0F0F0F0F0F0F0F00000005 /* OrgSwiftUI in Frameworks */,
62 ); 64 );
63 runOnlyForDeploymentPostprocessing = 0; 65 runOnlyForDeploymentPostprocessing = 0;
64 }; 66 };
@@ -121,6 +123,7 @@
121 packageProductDependencies = ( 123 packageProductDependencies = (
122 8B2898B1303FF3E5001BF44A /* Highlightr */, 124 8B2898B1303FF3E5001BF44A /* Highlightr */,
123 0F0F0F0F0F0F0F0F00000002 /* OrgSwift */, 125 0F0F0F0F0F0F0F0F00000002 /* OrgSwift */,
126 0F0F0F0F0F0F0F0F00000004 /* OrgSwiftUI */,
124 ); 127 );
125 productName = gitbay; 128 productName = gitbay;
126 productReference = 8B289812303FF3E5001BF44A /* gitbay.app */; 129 productReference = 8B289812303FF3E5001BF44A /* gitbay.app */;
@@ -413,9 +416,9 @@
413 DEVELOPMENT_TEAM = ZCNAX3VL9D; 416 DEVELOPMENT_TEAM = ZCNAX3VL9D;
414 ENABLE_PREVIEWS = YES; 417 ENABLE_PREVIEWS = YES;
415 GENERATE_INFOPLIST_FILE = YES; 418 GENERATE_INFOPLIST_FILE = YES;
419 INFOPLIST_KEY_ITSAppUsesNonExemptEncryption = NO;
416 INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; 420 INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
417 INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; 421 INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
418 INFOPLIST_KEY_ITSAppUsesNonExemptEncryption = NO;
419 INFOPLIST_KEY_UILaunchScreen_Generation = YES; 422 INFOPLIST_KEY_UILaunchScreen_Generation = YES;
420 INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 423 INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
421 INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 424 INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
@@ -446,9 +449,9 @@
446 DEVELOPMENT_TEAM = ZCNAX3VL9D; 449 DEVELOPMENT_TEAM = ZCNAX3VL9D;
447 ENABLE_PREVIEWS = YES; 450 ENABLE_PREVIEWS = YES;
448 GENERATE_INFOPLIST_FILE = YES; 451 GENERATE_INFOPLIST_FILE = YES;
452 INFOPLIST_KEY_ITSAppUsesNonExemptEncryption = NO;
449 INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; 453 INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
450 INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; 454 INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
451 INFOPLIST_KEY_ITSAppUsesNonExemptEncryption = NO;
452 INFOPLIST_KEY_UILaunchScreen_Generation = YES; 455 INFOPLIST_KEY_UILaunchScreen_Generation = YES;
453 INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 456 INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
454 INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 457 INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
@@ -587,8 +590,8 @@
587 isa = XCRemoteSwiftPackageReference; 590 isa = XCRemoteSwiftPackageReference;
588 repositoryURL = "ssh://git@gitbay.org/krz/org-swift.git"; 591 repositoryURL = "ssh://git@gitbay.org/krz/org-swift.git";
589 requirement = { 592 requirement = {
590 branch = main; 593 kind = upToNextMinorVersion;
591 kind = branch; 594 minimumVersion = 0.2.0;
592 }; 595 };
593 }; 596 };
594 8B2898B0303FF3E5001BF44A /* XCRemoteSwiftPackageReference "Highlightr" */ = { 597 8B2898B0303FF3E5001BF44A /* XCRemoteSwiftPackageReference "Highlightr" */ = {
@@ -607,6 +610,11 @@
607 package = 0F0F0F0F0F0F0F0F00000001 /* XCRemoteSwiftPackageReference "org-swift" */; 610 package = 0F0F0F0F0F0F0F0F00000001 /* XCRemoteSwiftPackageReference "org-swift" */;
608 productName = OrgSwift; 611 productName = OrgSwift;
609 }; 612 };
613 0F0F0F0F0F0F0F0F00000004 /* OrgSwiftUI */ = {
614 isa = XCSwiftPackageProductDependency;
615 package = 0F0F0F0F0F0F0F0F00000001 /* XCRemoteSwiftPackageReference "org-swift" */;
616 productName = OrgSwiftUI;
617 };
610 8B2898B1303FF3E5001BF44A /* Highlightr */ = { 618 8B2898B1303FF3E5001BF44A /* Highlightr */ = {
611 isa = XCSwiftPackageProductDependency; 619 isa = XCSwiftPackageProductDependency;
612 package = 8B2898B0303FF3E5001BF44A /* XCRemoteSwiftPackageReference "Highlightr" */; 620 package = 8B2898B0303FF3E5001BF44A /* XCRemoteSwiftPackageReference "Highlightr" */;
gitbay.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +2 −2
@@ -15,8 +15,8 @@
15 "kind" : "remoteSourceControl", 15 "kind" : "remoteSourceControl",
16 "location" : "ssh://git@gitbay.org/krz/org-swift.git", 16 "location" : "ssh://git@gitbay.org/krz/org-swift.git",
17 "state" : { 17 "state" : {
18 "branch" : "main", 18 "revision" : "ca2a0de2077b96dc2405d59def877f53e1933063",
19 "revision" : "47ac630c479b79dbfa8944c91b22b7bb6974cadc" 19 "version" : "0.2.0"
20 } 20 }
21 } 21 }
22 ], 22 ],
gitbay/Views/Repos/OrgReadmeWebView.swift deleted −239
@@ -1,239 +0,0 @@
1import Highlightr
2import OrgSwift
3import SwiftUI
4import UIKit
5import WebKit
6
7/// Renders an Org README with the shared `OrgSwift` package and shows the resulting HTML in
8/// a self-sizing, non-scrolling `WKWebView` embedded in the surrounding list. Code blocks are
9/// highlighted through Highlightr; links open in the system browser.
10struct OrgReadmeWebView: View {
11 let source: String
12 var options: OrgRenderOptions = .init()
13
14 @Environment(\.colorScheme) private var colorScheme
15 @State private var html = ""
16 @State private var height: CGFloat = 1
17
18 var body: some View {
19 OrgWebRepresentable(html: html, height: $height)
20 .frame(height: max(height, 1))
21 .task(id: colorScheme) { await render(for: colorScheme) }
22 }
23
24 private func render(for scheme: ColorScheme) async {
25 // READMEs are small, and Highlightr (JavaScriptCore) is thread-confined; rendering
26 // on the main actor keeps it simple and matches how the app highlights elsewhere.
27 let highlighter = OrgCodeHighlighter(colorScheme: scheme)
28 let body = OrgRenderer.renderToHTML(source, options: options, highlighter: highlighter)
29 html = OrgReadmeDocument.wrap(body, colorScheme: scheme)
30 }
31}
32
33// MARK: - Highlightr → OrgSwift.CodeHighlighter
34
35/// Bridges Highlightr to `OrgSwift.CodeHighlighter`: the highlighted attributed string is
36/// flattened to color-styled `<span>`s, the inner HTML of a `<code>` block. Colors are
37/// inlined, so a new instance is made per theme.
38private final class OrgCodeHighlighter: CodeHighlighter {
39 private let highlightr: Highlightr?
40 private let supported: Set<String>
41
42 init(colorScheme: ColorScheme) {
43 let engine = Highlightr()
44 engine?.setTheme(to: colorScheme == .dark ? "atom-one-dark" : "xcode")
45 highlightr = engine
46 supported = Set(engine?.supportedLanguages() ?? [])
47 }
48
49 func highlightedHTML(code: String, language: String?) -> String? {
50 guard let highlightr, let language = resolvedLanguage(language) else { return nil }
51 highlightr.theme.setCodeFont(.monospacedSystemFont(ofSize: 13, weight: .regular))
52 guard let attributed = highlightr.highlight(code, as: language, fastRender: true) else {
53 return nil
54 }
55 return Self.html(from: attributed)
56 }
57
58 private func resolvedLanguage(_ raw: String?) -> String? {
59 guard let trimmed = raw?.trimmingCharacters(in: .whitespacesAndNewlines).lowercased(),
60 !trimmed.isEmpty else { return nil }
61 let mapped = Self.aliases[trimmed] ?? trimmed
62 return supported.contains(mapped) ? mapped : nil
63 }
64
65 private static let aliases: [String: String] = [
66 "js": "javascript", "jsx": "javascript", "ts": "typescript", "tsx": "typescript",
67 "py": "python", "rb": "ruby", "rs": "rust", "sh": "bash", "shell": "bash", "zsh": "bash",
68 "yml": "yaml", "c++": "cpp", "cc": "cpp", "kt": "kotlin", "cs": "csharp",
69 "objc": "objectivec", "objective-c": "objectivec", "html": "xml", "htm": "xml",
70 ]
71
72 private static func html(from attributed: NSAttributedString) -> String {
73 var html = ""
74 let range = NSRange(location: 0, length: attributed.length)
75 attributed.enumerateAttribute(.foregroundColor, in: range) { value, subrange, _ in
76 let fragment = (attributed.string as NSString).substring(with: subrange)
77 let escaped = fragment
78 .replacingOccurrences(of: "&", with: "&amp;")
79 .replacingOccurrences(of: "<", with: "&lt;")
80 .replacingOccurrences(of: ">", with: "&gt;")
81 if let color = value as? UIColor, let hex = color.hexRGBString {
82 html += "<span style=\"color:\(hex)\">\(escaped)</span>"
83 } else {
84 html += escaped
85 }
86 }
87 return html
88 }
89}
90
91private extension UIColor {
92 var hexRGBString: String? {
93 var red: CGFloat = 0, green: CGFloat = 0, blue: CGFloat = 0, alpha: CGFloat = 0
94 guard getRed(&red, green: &green, blue: &blue, alpha: &alpha) else { return nil }
95 return String(format: "#%02x%02x%02x",
96 Int(round(red * 255)), Int(round(green * 255)), Int(round(blue * 255)))
97 }
98}
99
100// MARK: - HTML document
101
102private enum OrgReadmeDocument {
103 /// Wrap OrgSwift's body HTML in a themed document. The palette follows the app's
104 /// light/dark scheme; layout is deliberately close to the native README styling.
105 static func wrap(_ body: String, colorScheme: ColorScheme) -> String {
106 let dark = colorScheme == .dark
107 let text = dark ? "#e6e6e6" : "#1a1a1a"
108 let muted = dark ? "#9aa0a6" : "#606060"
109 let link = dark ? "#6cb6ff" : "#0a58ca"
110 let rule = dark ? "#333" : "#e0e0e0"
111 let codeBg = dark ? "#1c1c1e" : "#f4f4f5"
112 let quoteBar = dark ? "#3a3a3c" : "#d0d0d0"
113
114 return """
115 <!DOCTYPE html>
116 <html>
117 <head>
118 <meta charset="utf-8">
119 <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
120 <style>
121 :root { color-scheme: \(dark ? "dark" : "light"); }
122 html, body { margin: 0; padding: 0; background: transparent; }
123 body {
124 font: 16px/1.6 -apple-system, system-ui, sans-serif;
125 color: \(text);
126 -webkit-text-size-adjust: 100%;
127 word-wrap: break-word;
128 }
129 a { color: \(link); text-decoration: none; }
130 h1, h2, h3, h4, h5, h6 { line-height: 1.25; margin: 1.2em 0 0.5em; font-weight: 600; }
131 h1 { font-size: 1.5em; } h2 { font-size: 1.3em; } h3 { font-size: 1.15em; }
132 h4, h5, h6 { font-size: 1em; }
133 p, ul, ol, dl, blockquote, pre, table, figure { margin: 0 0 0.85em; }
134 :first-child { margin-top: 0; }
135 ul, ol { padding-left: 1.4em; }
136 li { margin: 0.2em 0; }
137 dt { font-weight: 600; margin-top: 0.5em; }
138 dd { margin: 0 0 0.4em 1.2em; }
139 code {
140 font-family: ui-monospace, "SF Mono", Menlo, monospace;
141 font-size: 0.88em;
142 background: \(codeBg);
143 padding: 0.1em 0.35em;
144 border-radius: 3px;
145 }
146 pre {
147 background: \(codeBg);
148 padding: 12px;
149 border-radius: 4px;
150 overflow-x: auto;
151 }
152 pre code { background: none; padding: 0; font-size: 0.82em; line-height: 1.45; }
153 blockquote {
154 margin-left: 0;
155 padding-left: 1em;
156 border-left: 3px solid \(quoteBar);
157 color: \(muted);
158 }
159 hr { border: none; border-top: 1px solid \(rule); margin: 1.4em 0; }
160 table { border-collapse: collapse; display: block; overflow-x: auto; }
161 th, td { border: 1px solid \(rule); padding: 6px 10px; text-align: left; }
162 th { font-weight: 600; }
163 img { max-width: 100%; height: auto; }
164 figure { margin: 0 0 0.85em; }
165 figcaption { color: \(muted); font-size: 0.9em; margin-top: 0.3em; }
166 .tag {
167 font-size: 0.75em; color: \(muted);
168 border: 1px solid \(rule); border-radius: 3px;
169 padding: 0 0.35em; margin-left: 0.3em;
170 }
171 sup a { text-decoration: none; }
172 .footnotes { font-size: 0.9em; color: \(muted); }
173 time { color: inherit; }
174 </style>
175 </head>
176 <body>
177 \(body)
178 </body>
179 </html>
180 """
181 }
182}
183
184// MARK: - WKWebView representable
185
186private struct OrgWebRepresentable: UIViewRepresentable {
187 let html: String
188 @Binding var height: CGFloat
189 @Environment(\.openURL) private var openURL
190
191 func makeCoordinator() -> Coordinator { Coordinator(self) }
192
193 func makeUIView(context: Context) -> WKWebView {
194 let config = WKWebViewConfiguration()
195 config.defaultWebpagePreferences.allowsContentJavaScript = false
196 let webView = WKWebView(frame: .zero, configuration: config)
197 webView.isOpaque = false
198 webView.backgroundColor = .clear
199 webView.scrollView.isScrollEnabled = false
200 webView.scrollView.contentInsetAdjustmentBehavior = .never
201 webView.navigationDelegate = context.coordinator
202 return webView
203 }
204
205 func updateUIView(_ webView: WKWebView, context: Context) {
206 context.coordinator.parent = self
207 guard !html.isEmpty, context.coordinator.loadedHTML != html else { return }
208 context.coordinator.loadedHTML = html
209 webView.loadHTMLString(html, baseURL: nil)
210 }
211
212 @MainActor
213 final class Coordinator: NSObject, WKNavigationDelegate {
214 var parent: OrgWebRepresentable
215 var loadedHTML: String?
216
217 init(_ parent: OrgWebRepresentable) { self.parent = parent }
218
219 func webView(_ webView: WKWebView, didFinish _: WKNavigation!) {
220 webView.evaluateJavaScript("document.body.scrollHeight") { [weak self] result, _ in
221 guard let self, let value = result as? NSNumber else { return }
222 self.parent.height = CGFloat(value.doubleValue)
223 }
224 }
225
226 func webView(
227 _ webView: WKWebView,
228 decidePolicyFor action: WKNavigationAction,
229 decisionHandler: @escaping @MainActor (WKNavigationActionPolicy) -> Void
230 ) {
231 if action.navigationType == .linkActivated, let url = action.request.url {
232 parent.openURL(url)
233 decisionHandler(.cancel)
234 return
235 }
236 decisionHandler(.allow)
237 }
238 }
239}
gitbay/Views/Repos/OrgSyntaxStyler.swift added +59
@@ -0,0 +1,59 @@
1import Highlightr
2import OrgSwiftUI
3import SwiftUI
4import UIKit
5
6/// Highlights `#+begin_src` blocks in org content, bridging Highlightr to `OrgCodeStyler`.
7///
8/// Highlightr already returns an `NSAttributedString`, so this walks its color runs straight
9/// into an `AttributedString`. Colors are baked in, so a new instance is made per theme.
10@MainActor
11final class OrgSyntaxStyler: OrgCodeStyler {
12 private let highlightr: Highlightr?
13 private let supported: Set<String>
14
15 init(colorScheme: ColorScheme) {
16 let engine = Highlightr()
17 engine?.setTheme(to: colorScheme == .dark ? "atom-one-dark" : "xcode")
18 highlightr = engine
19 supported = Set(engine?.supportedLanguages() ?? [])
20 }
21
22 func highlighted(code: String, language: String?) -> AttributedString? {
23 guard let highlightr, let language = resolvedLanguage(language) else { return nil }
24 highlightr.theme.setCodeFont(.monospacedSystemFont(ofSize: 13, weight: .regular))
25 guard let attributed = highlightr.highlight(code, as: language, fastRender: true) else {
26 return nil
27 }
28 return Self.attributedString(from: attributed)
29 }
30
31 private func resolvedLanguage(_ raw: String?) -> String? {
32 guard let trimmed = raw?.trimmingCharacters(in: .whitespacesAndNewlines).lowercased(),
33 !trimmed.isEmpty else { return nil }
34 let mapped = Self.aliases[trimmed] ?? trimmed
35 return supported.contains(mapped) ? mapped : nil
36 }
37
38 private static let aliases: [String: String] = [
39 "js": "javascript", "jsx": "javascript", "ts": "typescript", "tsx": "typescript",
40 "py": "python", "rb": "ruby", "rs": "rust", "sh": "bash", "shell": "bash", "zsh": "bash",
41 "yml": "yaml", "c++": "cpp", "cc": "cpp", "kt": "kotlin", "cs": "csharp",
42 "objc": "objectivec", "objective-c": "objectivec", "html": "xml", "htm": "xml",
43 ]
44
45 /// Carry Highlightr's colors across, in a monospaced font at the block's own size.
46 private static func attributedString(from attributed: NSAttributedString) -> AttributedString {
47 var result = AttributedString()
48 let range = NSRange(location: 0, length: attributed.length)
49 attributed.enumerateAttribute(.foregroundColor, in: range) { value, subrange, _ in
50 var fragment = AttributedString((attributed.string as NSString).substring(with: subrange))
51 fragment.font = .system(.footnote, design: .monospaced)
52 if let color = value as? UIColor {
53 fragment.foregroundColor = Color(uiColor: color)
54 }
55 result += fragment
56 }
57 return result
58 }
59}
gitbay/Views/Repos/ReadmeView.swift +9 −3
@@ -1,9 +1,10 @@
1import Foundation 1import Foundation
2import OrgSwift 2import OrgSwift
3import OrgSwiftUI
3import SwiftUI 4import SwiftUI
4 5
5/// README rendering follows the file's format. Org is rendered by the shared `OrgSwift` 6/// README rendering follows the file's format. Org is rendered natively by the shared
6/// package (in a web view); everything else keeps the established Markdown renderer. 7/// `OrgSwiftUI` package; everything else keeps the established Markdown renderer.
7struct ReadmeView: View { 8struct ReadmeView: View {
8 let name: String 9 let name: String
9 let content: String 10 let content: String
@@ -12,12 +13,17 @@ struct ReadmeView: View {
12 var host: String = "" 13 var host: String = ""
13 var repoPath: String = "" 14 var repoPath: String = ""
14 15
16 @Environment(\.colorScheme) private var colorScheme
17
15 /// Format follows the file name, never a guess at the content. 18 /// Format follows the file name, never a guess at the content.
16 var isOrg: Bool { name.lowercased().hasSuffix(".org") } 19 var isOrg: Bool { name.lowercased().hasSuffix(".org") }
17 20
18 var body: some View { 21 var body: some View {
19 if isOrg { 22 if isOrg {
20 OrgReadmeWebView(source: content, options: orgOptions) 23 // Qualified: `OrgView` here is org-mode, not the organization view.
24 OrgSwiftUI.OrgView(
25 content, options: orgOptions, styler: OrgSyntaxStyler(colorScheme: colorScheme)
26 )
21 } else { 27 } else {
22 MarkdownView(markdown: content) 28 MarkdownView(markdown: content)
23 } 29 }