Commit 505bd429eb

505bd429ebb57a21484c0a7054e8a5aadfda8269

parent: 8f8ad4e852

Unsigned

cmc <hello@cleberg.net> · 2026-03-30 21:44 UTC

feat(markdown): finalize migration to apple/swift-markdown renderer

Layout: unified · split

Hutch.xcodeproj/project.pbxproj +2 −6
@@ -11,7 +11,6 @@
11 8B2F89672F69DEB900FC0253 /* README.md in Resources */ = {isa = PBXBuildFile; fileRef = 8B2F89642F69DEB900FC0253 /* README.md */; }; 11 8B2F89672F69DEB900FC0253 /* README.md in Resources */ = {isa = PBXBuildFile; fileRef = 8B2F89642F69DEB900FC0253 /* README.md */; };
12 8B2F89682F69DEB900FC0253 /* SECURITY.md in Resources */ = {isa = PBXBuildFile; fileRef = 8B2F89652F69DEB900FC0253 /* SECURITY.md */; }; 12 8B2F89682F69DEB900FC0253 /* SECURITY.md in Resources */ = {isa = PBXBuildFile; fileRef = 8B2F89652F69DEB900FC0253 /* SECURITY.md */; };
13 8BE082012F80000100000001 /* HutchWidgetExtension.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = 8BE081F62F80000100000001 /* HutchWidgetExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; 13 8BE082012F80000100000001 /* HutchWidgetExtension.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = 8BE081F62F80000100000001 /* HutchWidgetExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
14 8BE09E792F7B20CB00F88693 /* TEST_README.md in Resources */ = {isa = PBXBuildFile; fileRef = 8BE09E782F7B20CB00F88693 /* TEST_README.md */; };
15 8BF100032F9A000100000001 /* Markdown in Frameworks */ = {isa = PBXBuildFile; productRef = 8BF100022F9A000100000001 /* Markdown */; }; 14 8BF100032F9A000100000001 /* Markdown in Frameworks */ = {isa = PBXBuildFile; productRef = 8BF100022F9A000100000001 /* Markdown */; };
16/* End PBXBuildFile section */ 15/* End PBXBuildFile section */
17 16
@@ -55,7 +54,6 @@
55 8B8182C22F6B742B000AE049 /* HutchTests */ = {isa = PBXFileReference; lastKnownFileType = folder; path = HutchTests; sourceTree = "<group>"; }; 54 8B8182C22F6B742B000AE049 /* HutchTests */ = {isa = PBXFileReference; lastKnownFileType = folder; path = HutchTests; sourceTree = "<group>"; };
56 8BDCA5272F6B76B20066AA29 /* HutchTests.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = HutchTests.xctestplan; sourceTree = "<group>"; }; 55 8BDCA5272F6B76B20066AA29 /* HutchTests.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = HutchTests.xctestplan; sourceTree = "<group>"; };
57 8BE081F62F80000100000001 /* HutchWidgetExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = HutchWidgetExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; }; 56 8BE081F62F80000100000001 /* HutchWidgetExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = HutchWidgetExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; };
58 8BE09E782F7B20CB00F88693 /* TEST_README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = TEST_README.md; sourceTree = "<group>"; };
59/* End PBXFileReference section */ 57/* End PBXFileReference section */
60 58
61/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */ 59/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */
@@ -143,7 +141,6 @@
143 8BE081F52F80000100000001 /* HutchWidgetExtension */, 141 8BE081F52F80000100000001 /* HutchWidgetExtension */,
144 8B8182B82F6B73F3000AE049 /* HutchTests */, 142 8B8182B82F6B73F3000AE049 /* HutchTests */,
145 8B4B28D22F6704280045FA19 /* Products */, 143 8B4B28D22F6704280045FA19 /* Products */,
146 8BE09E782F7B20CB00F88693 /* TEST_README.md */,
147 ); 144 );
148 sourceTree = "<group>"; 145 sourceTree = "<group>";
149 }; 146 };
@@ -281,7 +278,6 @@
281 buildActionMask = 2147483647; 278 buildActionMask = 2147483647;
282 files = ( 279 files = (
283 8B2F89662F69DEB900FC0253 /* LICENSE in Resources */, 280 8B2F89662F69DEB900FC0253 /* LICENSE in Resources */,
284 8BE09E792F7B20CB00F88693 /* TEST_README.md in Resources */,
285 8B2F89672F69DEB900FC0253 /* README.md in Resources */, 281 8B2F89672F69DEB900FC0253 /* README.md in Resources */,
286 8B2F89682F69DEB900FC0253 /* SECURITY.md in Resources */, 282 8B2F89682F69DEB900FC0253 /* SECURITY.md in Resources */,
287 ); 283 );
@@ -486,7 +482,7 @@
486 "$(inherited)", 482 "$(inherited)",
487 "@executable_path/Frameworks", 483 "@executable_path/Frameworks",
488 ); 484 );
489 MARKETING_VERSION = 2.4.2; 485 MARKETING_VERSION = 2.5.0;
490 PRODUCT_BUNDLE_IDENTIFIER = net.cleberg.Hutch; 486 PRODUCT_BUNDLE_IDENTIFIER = net.cleberg.Hutch;
491 PRODUCT_NAME = "$(TARGET_NAME)"; 487 PRODUCT_NAME = "$(TARGET_NAME)";
492 STRING_CATALOG_GENERATE_SYMBOLS = YES; 488 STRING_CATALOG_GENERATE_SYMBOLS = YES;
@@ -523,7 +519,7 @@
523 "$(inherited)", 519 "$(inherited)",
524 "@executable_path/Frameworks", 520 "@executable_path/Frameworks",
525 ); 521 );
526 MARKETING_VERSION = 2.4.2; 522 MARKETING_VERSION = 2.5.0;
527 PRODUCT_BUNDLE_IDENTIFIER = net.cleberg.Hutch; 523 PRODUCT_BUNDLE_IDENTIFIER = net.cleberg.Hutch;
528 PRODUCT_NAME = "$(TARGET_NAME)"; 524 PRODUCT_NAME = "$(TARGET_NAME)";
529 STRING_CATALOG_GENERATE_SYMBOLS = YES; 525 STRING_CATALOG_GENERATE_SYMBOLS = YES;
Hutch/Views/Repositories/MarkdownHTMLRenderer.swift +38 −4
@@ -10,7 +10,9 @@ private struct MarkdownHTMLRenderer: MarkupVisitor {
10 typealias Result = String 10 typealias Result = String
11 11
12 nonisolated(unsafe) let imageURLResolver: ((String) -> String?)? 12 nonisolated(unsafe) let imageURLResolver: ((String) -> String?)?
13 private var isRenderingTableHead = false 13 nonisolated(unsafe) private var isRenderingTableHead = false
14 nonisolated(unsafe) private var currentTableAlignments: [Markdown.Table.ColumnAlignment?] = []
15 nonisolated(unsafe) private var currentTableColumnIndex = 0
14 16
15 nonisolated init(imageURLResolver: ((String) -> String?)?) { 17 nonisolated init(imageURLResolver: ((String) -> String?)?) {
16 self.imageURLResolver = imageURLResolver 18 self.imageURLResolver = imageURLResolver
@@ -140,7 +142,14 @@ private struct MarkdownHTMLRenderer: MarkupVisitor {
140 } 142 }
141 143
142 nonisolated mutating func visitTable(_ table: Markdown.Table) -> String { 144 nonisolated mutating func visitTable(_ table: Markdown.Table) -> String {
143 "<table>\n\(visitChildren(of: table))</table>\n" 145 let previousAlignments = currentTableAlignments
146 let previousColumnIndex = currentTableColumnIndex
147 currentTableAlignments = table.columnAlignments
148 currentTableColumnIndex = 0
149 let content = visitChildren(of: table)
150 currentTableAlignments = previousAlignments
151 currentTableColumnIndex = previousColumnIndex
152 return "<table>\n\(content)</table>\n"
144 } 153 }
145 154
146 nonisolated mutating func visitTableHead(_ tableHead: Markdown.Table.Head) -> String { 155 nonisolated mutating func visitTableHead(_ tableHead: Markdown.Table.Head) -> String {
@@ -160,12 +169,18 @@ private struct MarkdownHTMLRenderer: MarkupVisitor {
160 } 169 }
161 170
162 nonisolated mutating func visitTableRow(_ tableRow: Markdown.Table.Row) -> String { 171 nonisolated mutating func visitTableRow(_ tableRow: Markdown.Table.Row) -> String {
163 "<tr>\(visitChildren(of: tableRow))</tr>\n" 172 let previousColumnIndex = currentTableColumnIndex
173 currentTableColumnIndex = 0
174 let content = visitChildren(of: tableRow)
175 currentTableColumnIndex = previousColumnIndex
176 return "<tr>\(content)</tr>\n"
164 } 177 }
165 178
166 nonisolated mutating func visitTableCell(_ tableCell: Markdown.Table.Cell) -> String { 179 nonisolated mutating func visitTableCell(_ tableCell: Markdown.Table.Cell) -> String {
167 let tagName = isRenderingTableHead ? "th" : "td" 180 let tagName = isRenderingTableHead ? "th" : "td"
168 return "<\(tagName)>\(visitChildren(of: tableCell))</\(tagName)>" 181 let styleAttribute = alignmentStyleAttribute(forColumn: currentTableColumnIndex)
182 currentTableColumnIndex += 1
183 return "<\(tagName)\(styleAttribute)>\(visitChildren(of: tableCell))</\(tagName)>"
169 } 184 }
170 185
171 nonisolated private mutating func visitChildren(of markup: Markup) -> String { 186 nonisolated private mutating func visitChildren(of markup: Markup) -> String {
@@ -195,6 +210,25 @@ private struct MarkdownHTMLRenderer: MarkupVisitor {
195 } 210 }
196 } 211 }
197 212
213 nonisolated private func alignmentStyleAttribute(forColumn column: Int) -> String {
214 guard column < currentTableAlignments.count,
215 let alignment = currentTableAlignments[column] else {
216 return ""
217 }
218
219 let textAlignment: String
220 switch alignment {
221 case .left:
222 textAlignment = "left"
223 case .center:
224 textAlignment = "center"
225 case .right:
226 textAlignment = "right"
227 }
228
229 return " style=\"text-align: \(textAlignment);\""
230 }
231
198 nonisolated private func checkboxHTML(for checkbox: Checkbox) -> String { 232 nonisolated private func checkboxHTML(for checkbox: Checkbox) -> String {
199 switch checkbox { 233 switch checkbox {
200 case .checked: 234 case .checked:
HutchTests/ReadmeViewTests.swift +10
@@ -54,6 +54,16 @@ struct MarkdownRenderingTests {
54 #expect(html.contains("<th>")) 54 #expect(html.contains("<th>"))
55 } 55 }
56 56
57 @Test
58 func markdownTableAlignment() {
59 let input = "| Left | Center | Right |\n|:-----|:------:|------:|\n| a | b | c |"
60 let html = markdownToHTML(input)
61
62 #expect(html.contains("text-align: left;"))
63 #expect(html.contains("text-align: center;"))
64 #expect(html.contains("text-align: right;"))
65 }
66
57 @Test 67 @Test
58 func markdownStrikethrough() { 68 func markdownStrikethrough() {
59 let html = markdownToHTML("~~deleted~~") 69 let html = markdownToHTML("~~deleted~~")