a native ios client for gitbay

client ios swift

https://gitbay.org

Commit 3bdfe61a93

3bdfe61a93c9d4046cae3b5b4c4fafac0d4da8ad

parent: 4328d08603

Verified · cmc

cmc <hello@cleberg.net> · 2026-08-28T18:47:02Z

Render Org READMEs with the shared OrgSwift package

Route .org READMEs through OrgSwift (added as a local SwiftPM package) into a
self-sizing WKWebView with themed light/dark CSS, replacing the thin native Org
renderer. Code blocks are highlighted via a Highlightr-backed CodeHighlighter;
links open in the system browser. Removes OrgDocument/OrgDocumentView and its
tests — OrgSwift carries its own suite and conformance corpus.
gitbay.xcodeproj/project.pbxproj +16
@@ -7,6 +7,7 @@
77 objects = {
88
99/* Begin PBXBuildFile section */
10 0F0F0F0F0F0F0F0F00000003 /* OrgSwift in Frameworks */ = {isa = PBXBuildFile; productRef = 0F0F0F0F0F0F0F0F00000002 /* OrgSwift */; };
1011 8B289850303FF914001BF44A /* Highlightr in Frameworks */ = {isa = PBXBuildFile; productRef = 8B2898B1303FF3E5001BF44A /* Highlightr */; };
1112/* End PBXBuildFile section */
1213
@@ -57,6 +58,7 @@
5758 buildActionMask = 2147483647;
5859 files = (
5960 8B289850303FF914001BF44A /* Highlightr in Frameworks */,
61 0F0F0F0F0F0F0F0F00000003 /* OrgSwift in Frameworks */,
6062 );
6163 runOnlyForDeploymentPostprocessing = 0;
6264 };
@@ -118,6 +120,7 @@
118120 name = gitbay;
119121 packageProductDependencies = (
120122 8B2898B1303FF3E5001BF44A /* Highlightr */,
123 0F0F0F0F0F0F0F0F00000002 /* OrgSwift */,
121124 );
122125 productName = gitbay;
123126 productReference = 8B289812303FF3E5001BF44A /* gitbay.app */;
@@ -203,6 +206,7 @@
203206 minimizedProjectReferenceProxies = 1;
204207 packageReferences = (
205208 8B2898B0303FF3E5001BF44A /* XCRemoteSwiftPackageReference "Highlightr" */,
209 0F0F0F0F0F0F0F0F00000001 /* XCLocalSwiftPackageReference "../org-swift" */,
206210 );
207211 preferredProjectObjectVersion = 77;
208212 productRefGroup = 8B289813303FF3E5001BF44A /* Products */;
@@ -576,6 +580,13 @@
576580 };
577581/* End XCConfigurationList section */
578582
583/* Begin XCLocalSwiftPackageReference section */
584 0F0F0F0F0F0F0F0F00000001 /* XCLocalSwiftPackageReference "../org-swift" */ = {
585 isa = XCLocalSwiftPackageReference;
586 relativePath = "../org-swift";
587 };
588/* End XCLocalSwiftPackageReference section */
589
579590/* Begin XCRemoteSwiftPackageReference section */
580591 8B2898B0303FF3E5001BF44A /* XCRemoteSwiftPackageReference "Highlightr" */ = {
581592 isa = XCRemoteSwiftPackageReference;
@@ -588,6 +599,11 @@
588599/* End XCRemoteSwiftPackageReference section */
589600
590601/* Begin XCSwiftPackageProductDependency section */
602 0F0F0F0F0F0F0F0F00000002 /* OrgSwift */ = {
603 isa = XCSwiftPackageProductDependency;
604 package = 0F0F0F0F0F0F0F0F00000001 /* XCLocalSwiftPackageReference "../org-swift" */;
605 productName = OrgSwift;
606 };
591607 8B2898B1303FF3E5001BF44A /* Highlightr */ = {
592608 isa = XCSwiftPackageProductDependency;
593609 package = 8B2898B0303FF3E5001BF44A /* XCRemoteSwiftPackageReference "Highlightr" */;
gitbay/Views/Repos/OrgReadmeWebView.swift added +238
@@ -0,0 +1,238 @@
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
13 @Environment(\.colorScheme) private var colorScheme
14 @State private var html = ""
15 @State private var height: CGFloat = 1
16
17 var body: some View {
18 OrgWebRepresentable(html: html, height: $height)
19 .frame(height: max(height, 1))
20 .task(id: colorScheme) { await render(for: colorScheme) }
21 }
22
23 private func render(for scheme: ColorScheme) async {
24 // READMEs are small, and Highlightr (JavaScriptCore) is thread-confined; rendering
25 // on the main actor keeps it simple and matches how the app highlights elsewhere.
26 let highlighter = OrgCodeHighlighter(colorScheme: scheme)
27 let body = OrgRenderer.renderToHTML(source, highlighter: highlighter)
28 html = OrgReadmeDocument.wrap(body, colorScheme: scheme)
29 }
30}
31
32// MARK: - Highlightr OrgSwift.CodeHighlighter
33
34/// Bridges Highlightr to `OrgSwift.CodeHighlighter`: the highlighted attributed string is
35/// flattened to color-styled `<span>`s, the inner HTML of a `<code>` block. Colors are
36/// inlined, so a new instance is made per theme.
37private final class OrgCodeHighlighter: CodeHighlighter {
38 private let highlightr: Highlightr?
39 private let supported: Set<String>
40
41 init(colorScheme: ColorScheme) {
42 let engine = Highlightr()
43 engine?.setTheme(to: colorScheme == .dark ? "atom-one-dark" : "xcode")
44 highlightr = engine
45 supported = Set(engine?.supportedLanguages() ?? [])
46 }
47
48 func highlightedHTML(code: String, language: String?) -> String? {
49 guard let highlightr, let language = resolvedLanguage(language) else { return nil }
50 highlightr.theme.setCodeFont(.monospacedSystemFont(ofSize: 13, weight: .regular))
51 guard let attributed = highlightr.highlight(code, as: language, fastRender: true) else {
52 return nil
53 }
54 return Self.html(from: attributed)
55 }
56
57 private func resolvedLanguage(_ raw: String?) -> String? {
58 guard let trimmed = raw?.trimmingCharacters(in: .whitespacesAndNewlines).lowercased(),
59 !trimmed.isEmpty else { return nil }
60 let mapped = Self.aliases[trimmed] ?? trimmed
61 return supported.contains(mapped) ? mapped : nil
62 }
63
64 private static let aliases: [String: String] = [
65 "js": "javascript", "jsx": "javascript", "ts": "typescript", "tsx": "typescript",
66 "py": "python", "rb": "ruby", "rs": "rust", "sh": "bash", "shell": "bash", "zsh": "bash",
67 "yml": "yaml", "c++": "cpp", "cc": "cpp", "kt": "kotlin", "cs": "csharp",
68 "objc": "objectivec", "objective-c": "objectivec", "html": "xml", "htm": "xml",
69 ]
70
71 private static func html(from attributed: NSAttributedString) -> String {
72 var html = ""
73 let range = NSRange(location: 0, length: attributed.length)
74 attributed.enumerateAttribute(.foregroundColor, in: range) { value, subrange, _ in
75 let fragment = (attributed.string as NSString).substring(with: subrange)
76 let escaped = fragment
77 .replacingOccurrences(of: "&", with: "&amp;")
78 .replacingOccurrences(of: "<", with: "&lt;")
79 .replacingOccurrences(of: ">", with: "&gt;")
80 if let color = value as? UIColor, let hex = color.hexRGBString {
81 html += "<span style=\"color:\(hex)\">\(escaped)</span>"
82 } else {
83 html += escaped
84 }
85 }
86 return html
87 }
88}
89
90private extension UIColor {
91 var hexRGBString: String? {
92 var red: CGFloat = 0, green: CGFloat = 0, blue: CGFloat = 0, alpha: CGFloat = 0
93 guard getRed(&red, green: &green, blue: &blue, alpha: &alpha) else { return nil }
94 return String(format: "#%02x%02x%02x",
95 Int(round(red * 255)), Int(round(green * 255)), Int(round(blue * 255)))
96 }
97}
98
99// MARK: - HTML document
100
101private enum OrgReadmeDocument {
102 /// Wrap OrgSwift's body HTML in a themed document. The palette follows the app's
103 /// light/dark scheme; layout is deliberately close to the native README styling.
104 static func wrap(_ body: String, colorScheme: ColorScheme) -> String {
105 let dark = colorScheme == .dark
106 let text = dark ? "#e6e6e6" : "#1a1a1a"
107 let muted = dark ? "#9aa0a6" : "#606060"
108 let link = dark ? "#6cb6ff" : "#0a58ca"
109 let rule = dark ? "#333" : "#e0e0e0"
110 let codeBg = dark ? "#1c1c1e" : "#f4f4f5"
111 let quoteBar = dark ? "#3a3a3c" : "#d0d0d0"
112
113 return """
114 <!DOCTYPE html>
115 <html>
116 <head>
117 <meta charset="utf-8">
118 <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
119 <style>
120 :root { color-scheme: \(dark ? "dark" : "light"); }
121 html, body { margin: 0; padding: 0; background: transparent; }
122 body {
123 font: 16px/1.6 -apple-system, system-ui, sans-serif;
124 color: \(text);
125 -webkit-text-size-adjust: 100%;
126 word-wrap: break-word;
127 }
128 a { color: \(link); text-decoration: none; }
129 h1, h2, h3, h4, h5, h6 { line-height: 1.25; margin: 1.2em 0 0.5em; font-weight: 600; }
130 h1 { font-size: 1.5em; } h2 { font-size: 1.3em; } h3 { font-size: 1.15em; }
131 h4, h5, h6 { font-size: 1em; }
132 p, ul, ol, dl, blockquote, pre, table, figure { margin: 0 0 0.85em; }
133 :first-child { margin-top: 0; }
134 ul, ol { padding-left: 1.4em; }
135 li { margin: 0.2em 0; }
136 dt { font-weight: 600; margin-top: 0.5em; }
137 dd { margin: 0 0 0.4em 1.2em; }
138 code {
139 font-family: ui-monospace, "SF Mono", Menlo, monospace;
140 font-size: 0.88em;
141 background: \(codeBg);
142 padding: 0.1em 0.35em;
143 border-radius: 3px;
144 }
145 pre {
146 background: \(codeBg);
147 padding: 12px;
148 border-radius: 4px;
149 overflow-x: auto;
150 }
151 pre code { background: none; padding: 0; font-size: 0.82em; line-height: 1.45; }
152 blockquote {
153 margin-left: 0;
154 padding-left: 1em;
155 border-left: 3px solid \(quoteBar);
156 color: \(muted);
157 }
158 hr { border: none; border-top: 1px solid \(rule); margin: 1.4em 0; }
159 table { border-collapse: collapse; display: block; overflow-x: auto; }
160 th, td { border: 1px solid \(rule); padding: 6px 10px; text-align: left; }
161 th { font-weight: 600; }
162 img { max-width: 100%; height: auto; }
163 figure { margin: 0 0 0.85em; }
164 figcaption { color: \(muted); font-size: 0.9em; margin-top: 0.3em; }
165 .tag {
166 font-size: 0.75em; color: \(muted);
167 border: 1px solid \(rule); border-radius: 3px;
168 padding: 0 0.35em; margin-left: 0.3em;
169 }
170 sup a { text-decoration: none; }
171 .footnotes { font-size: 0.9em; color: \(muted); }
172 time { color: inherit; }
173 </style>
174 </head>
175 <body>
176 \(body)
177 </body>
178 </html>
179 """
180 }
181}
182
183// MARK: - WKWebView representable
184
185private struct OrgWebRepresentable: UIViewRepresentable {
186 let html: String
187 @Binding var height: CGFloat
188 @Environment(\.openURL) private var openURL
189
190 func makeCoordinator() -> Coordinator { Coordinator(self) }
191
192 func makeUIView(context: Context) -> WKWebView {
193 let config = WKWebViewConfiguration()
194 config.defaultWebpagePreferences.allowsContentJavaScript = false
195 let webView = WKWebView(frame: .zero, configuration: config)
196 webView.isOpaque = false
197 webView.backgroundColor = .clear
198 webView.scrollView.isScrollEnabled = false
199 webView.scrollView.contentInsetAdjustmentBehavior = .never
200 webView.navigationDelegate = context.coordinator
201 return webView
202 }
203
204 func updateUIView(_ webView: WKWebView, context: Context) {
205 context.coordinator.parent = self
206 guard !html.isEmpty, context.coordinator.loadedHTML != html else { return }
207 context.coordinator.loadedHTML = html
208 webView.loadHTMLString(html, baseURL: nil)
209 }
210
211 @MainActor
212 final class Coordinator: NSObject, WKNavigationDelegate {
213 var parent: OrgWebRepresentable
214 var loadedHTML: String?
215
216 init(_ parent: OrgWebRepresentable) { self.parent = parent }
217
218 func webView(_ webView: WKWebView, didFinish _: WKNavigation!) {
219 webView.evaluateJavaScript("document.body.scrollHeight") { [weak self] result, _ in
220 guard let self, let value = result as? NSNumber else { return }
221 self.parent.height = CGFloat(value.doubleValue)
222 }
223 }
224
225 func webView(
226 _ webView: WKWebView,
227 decidePolicyFor action: WKNavigationAction,
228 decisionHandler: @escaping (WKNavigationActionPolicy) -> Void
229 ) {
230 if action.navigationType == .linkActivated, let url = action.request.url {
231 parent.openURL(url)
232 decisionHandler(.cancel)
233 return
234 }
235 decisionHandler(.allow)
236 }
237 }
238}
gitbay/Views/Repos/ReadmeView.swift +3 −199
@@ -1,8 +1,8 @@
11 import Foundation
22 import SwiftUI
33
4/// README rendering follows the file's format. Org is parsed as Org;
5/// everything else keeps the established Markdown renderer.
4/// README rendering follows the file's format. Org is rendered by the shared `OrgSwift`
5/// package (in a web view); everything else keeps the established Markdown renderer.
66 struct ReadmeView: View {
77 let name: String
88 let content: String
@@ -12,205 +12,9 @@ struct ReadmeView: View {
1212
1313 var body: some View {
1414 if isOrg {
15 OrgDocumentView(document: OrgDocument.parse(content))
15 OrgReadmeWebView(source: content)
1616 } else {
1717 MarkdownView(markdown: content)
1818 }
1919 }
2020 }
21
22nonisolated struct OrgDocument: Sendable, Hashable {
23 let blocks: [OrgBlock]
24
25 static func parse(_ source: String) -> OrgDocument {
26 var result: [OrgBlock] = []
27 var lines = source.components(separatedBy: "\n")[...]
28
29 while let raw = lines.first {
30 let line = raw.trimmingCharacters(in: .whitespaces)
31 if line.isEmpty {
32 lines = lines.dropFirst()
33 continue
34 }
35 let lower = line.lowercased()
36 if lower.hasPrefix("#+title:") {
37 result.append(.heading(level: 1, text: String(line.dropFirst(8)).trimmingCharacters(in: .whitespaces)))
38 lines = lines.dropFirst()
39 continue
40 }
41 if lower.hasPrefix("#+begin_src") || lower.hasPrefix("#+begin_example") {
42 let language = lower.hasPrefix("#+begin_src")
43 ? String(line.dropFirst(11)).trimmingCharacters(in: .whitespaces)
44 : ""
45 lines = lines.dropFirst()
46 var body: [String] = []
47 while let next = lines.first,
48 !next.trimmingCharacters(in: .whitespaces).lowercased().hasPrefix("#+end_") {
49 body.append(next)
50 lines = lines.dropFirst()
51 }
52 if !lines.isEmpty { lines = lines.dropFirst() }
53 result.append(.code(language: language, text: body.joined(separator: "\n")))
54 continue
55 }
56 if lower == "#+begin_quote" {
57 lines = lines.dropFirst()
58 var body: [String] = []
59 while let next = lines.first,
60 next.trimmingCharacters(in: .whitespaces).lowercased() != "#+end_quote" {
61 body.append(next.trimmingCharacters(in: .whitespaces))
62 lines = lines.dropFirst()
63 }
64 if !lines.isEmpty { lines = lines.dropFirst() }
65 result.append(.quote(body.joined(separator: " ")))
66 continue
67 }
68 if line.first == "*" {
69 let stars = line.prefix(while: { $0 == "*" }).count
70 if line.dropFirst(stars).first == " " {
71 result.append(.heading(
72 level: min(stars, 4),
73 text: String(line.dropFirst(stars)).trimmingCharacters(in: .whitespaces)
74 ))
75 lines = lines.dropFirst()
76 continue
77 }
78 }
79 if line.hasPrefix("- ") || line.hasPrefix("+ ") {
80 var items: [String] = []
81 while let next = lines.first?.trimmingCharacters(in: .whitespaces),
82 next.hasPrefix("- ") || next.hasPrefix("+ ") {
83 items.append(String(next.dropFirst(2)))
84 lines = lines.dropFirst()
85 }
86 result.append(.bullet(items))
87 continue
88 }
89 if line.range(of: #"^\d+[.)] "#, options: .regularExpression) != nil {
90 var items: [String] = []
91 while let next = lines.first?.trimmingCharacters(in: .whitespaces),
92 let range = next.range(of: #"^\d+[.)] "#, options: .regularExpression) {
93 items.append(String(next[range.upperBound...]))
94 lines = lines.dropFirst()
95 }
96 result.append(.ordered(items))
97 continue
98 }
99 if lower.hasPrefix("#+") {
100 lines = lines.dropFirst() // document metadata, not prose
101 continue
102 }
103
104 var paragraph: [String] = []
105 while let next = lines.first {
106 let trimmed = next.trimmingCharacters(in: .whitespaces)
107 let nextLower = trimmed.lowercased()
108 if trimmed.isEmpty || trimmed.hasPrefix("* ") || trimmed.hasPrefix("- ")
109 || trimmed.hasPrefix("+ ") || nextLower.hasPrefix("#+") {
110 break
111 }
112 paragraph.append(trimmed)
113 lines = lines.dropFirst()
114 }
115 result.append(.paragraph(paragraph.joined(separator: " ")))
116 }
117 return OrgDocument(blocks: result)
118 }
119}
120
121nonisolated enum OrgBlock: Sendable, Hashable {
122 case heading(level: Int, text: String)
123 case code(language: String, text: String)
124 case quote(String)
125 case bullet([String])
126 case ordered([String])
127 case paragraph(String)
128}
129
130private struct OrgDocumentView: View {
131 let document: OrgDocument
132
133 var body: some View {
134 VStack(alignment: .leading, spacing: 12) {
135 ForEach(Array(document.blocks.enumerated()), id: \.offset) { _, block in
136 blockView(block)
137 }
138 }
139 }
140
141 @ViewBuilder
142 private func blockView(_ block: OrgBlock) -> some View {
143 switch block {
144 case .heading(let level, let text):
145 inline(text)
146 .font(headingFont(level))
147 .padding(.top, level <= 2 ? 8 : 4)
148 case .code(let language, let text):
149 VStack(alignment: .leading, spacing: 4) {
150 if !language.isEmpty {
151 Text(language)
152 .font(.gbMono(.caption2))
153 .foregroundStyle(.secondary)
154 }
155 ScrollView(.horizontal) {
156 Text(text)
157 .font(.gbMono(.caption))
158 .padding(10)
159 }
160 }
161 .background(Color.gbCodeBackground, in: RoundedRectangle(cornerRadius: 2))
162 case .quote(let text):
163 HStack(spacing: 10) {
164 RoundedRectangle(cornerRadius: 2).fill(.tertiary).frame(width: 3)
165 inline(text).foregroundStyle(.secondary)
166 }
167 .fixedSize(horizontal: false, vertical: true)
168 case .bullet(let items):
169 list(items, ordered: false)
170 case .ordered(let items):
171 list(items, ordered: true)
172 case .paragraph(let text):
173 inline(text)
174 }
175 }
176
177 private func list(_ items: [String], ordered: Bool) -> some View {
178 VStack(alignment: .leading, spacing: 4) {
179 ForEach(Array(items.enumerated()), id: \.offset) { index, item in
180 HStack(alignment: .firstTextBaseline, spacing: 8) {
181 Text(ordered ? "\(index + 1)." : "")
182 inline(item)
183 }
184 }
185 }
186 }
187
188 private func inline(_ source: String) -> Text {
189 var markdown = source
190 markdown = replacing(markdown, pattern: #"\[\[([^\]]+)\]\[([^\]]+)\]\]"#, template: "[$2]($1)")
191 markdown = replacing(markdown, pattern: #"\[\[([^\]]+)\]\]"#, template: "<$1>")
192 markdown = replacing(markdown, pattern: #"=([^=]+)="#, template: "`$1`")
193 if let attributed = try? AttributedString(
194 markdown: markdown,
195 options: .init(interpretedSyntax: .inlineOnlyPreservingWhitespace)
196 ) {
197 return Text(attributed)
198 }
199 return Text(source)
200 }
201
202 private func replacing(_ source: String, pattern: String, template: String) -> String {
203 guard let expression = try? NSRegularExpression(pattern: pattern) else { return source }
204 let range = NSRange(source.startIndex..., in: source)
205 return expression.stringByReplacingMatches(in: source, range: range, withTemplate: template)
206 }
207
208 private func headingFont(_ level: Int) -> Font {
209 switch level {
210 case 1: .gbSans(.title2).bold()
211 case 2: .gbSans(.title3).bold()
212 case 3: .gbSans(.headline)
213 default: .gbSans(.subheadline).bold()
214 }
215 }
216}
gitbayTests/ReadmeOrgTests.swift deleted −98
@@ -1,98 +0,0 @@
1import Foundation
2import Testing
3@testable import gitbay
4
5/// README.org rendered as Org, not Markdown (krz/gitbay-ios#4). The
6/// markdown block parser turned `#+title:` into a heading and left
7/// `[[link]]` raw.
8struct OrgDocumentTests {
9
10 @Test func titleAndStarHeadingsBecomeHeadings() {
11 let doc = OrgDocument.parse("""
12 #+title: orgo
13 #+author: krz
14
15 * Install
16 ** From source
17 """)
18
19 #expect(doc.blocks == [
20 .heading(level: 1, text: "orgo"),
21 .heading(level: 1, text: "Install"),
22 .heading(level: 2, text: "From source"),
23 ])
24 }
25
26 @Test func metadataOtherThanTitleIsDropped() {
27 let doc = OrgDocument.parse("#+options: toc:nil\n#+startup: showall\n\nProse.")
28 #expect(doc.blocks == [.paragraph("Prose.")])
29 }
30
31 @Test func sourceBlocksKeepTheirLanguageAndBody() {
32 let doc = OrgDocument.parse("""
33 #+begin_src sh
34 gitbay repo list
35 gitbay mr create
36 #+end_src
37 """)
38 #expect(doc.blocks == [
39 .code(language: "sh", text: "gitbay repo list\ngitbay mr create"),
40 ])
41 }
42
43 @Test func examplesAndQuotesAreTheirOwnBlocks() {
44 let doc = OrgDocument.parse("""
45 #+begin_example
46 $ gitbay whoami
47 #+end_example
48
49 #+begin_quote
50 SSH is the API.
51 #+end_quote
52 """)
53 #expect(doc.blocks == [
54 .code(language: "", text: "$ gitbay whoami"),
55 .quote("SSH is the API."),
56 ])
57 }
58
59 @Test func bothListFormsParse() {
60 let doc = OrgDocument.parse("""
61 - one
62 + two
63
64 1. first
65 2) second
66 """)
67 #expect(doc.blocks == [
68 .bullet(["one", "two"]),
69 .ordered(["first", "second"]),
70 ])
71 }
72
73 @Test func aBoldStarLineIsNotAHeading() {
74 // "*bold*" has no space after the star, so it is prose.
75 let doc = OrgDocument.parse("*bold* opening line")
76 #expect(doc.blocks == [.paragraph("*bold* opening line")])
77 }
78
79 @Test func paragraphsRunUntilABlankLineOrBlockStart() {
80 let doc = OrgDocument.parse("""
81 One sentence
82 continued here.
83 * Heading
84 """)
85 #expect(doc.blocks == [
86 .paragraph("One sentence continued here."),
87 .heading(level: 1, text: "Heading"),
88 ])
89 }
90
91 @Test func readmeViewPicksTheRendererByExtension() {
92 // Only the name decides; content is not sniffed.
93 #expect(ReadmeView(name: "README.org", content: "* x").isOrg)
94 #expect(ReadmeView(name: "readme.ORG", content: "* x").isOrg)
95 #expect(!ReadmeView(name: "README.md", content: "# x").isOrg)
96 #expect(!ReadmeView(name: "README", content: "x").isOrg)
97 }
98}