Commit 47eb92311e
Verified · cmc
Layout: unified · split
gitbay/Views/Repos/MarkdownView.swift +31 −6
| @@ -86,9 +86,14 @@ struct MarkdownView: View { | ||
| 86 | 86 | } |
| 87 | 87 | if trimmed.hasPrefix("- ") || trimmed.hasPrefix("* ") || trimmed.hasPrefix("+ ") { |
| 88 | 88 | var items: [String] = [] |
| 89 | while let next = lines.first?.trimmingCharacters(in: .whitespaces), | |
| 90 | next.hasPrefix("- ") || next.hasPrefix("* ") || next.hasPrefix("+ ") { | |
| 91 | items.append(String(next.dropFirst(2))) | |
| 89 | while let next = lines.first?.trimmingCharacters(in: .whitespaces) { | |
| 90 | if next.hasPrefix("- ") || next.hasPrefix("* ") || next.hasPrefix("+ ") { | |
| 91 | items.append(String(next.dropFirst(2))) | |
| 92 | } else if Self.continuesItem(lines) { | |
| 93 | items[items.count - 1] += " " + next | |
| 94 | } else { | |
| 95 | break | |
| 96 | } | |
| 92 | 97 | lines = lines.dropFirst() |
| 93 | 98 | } |
| 94 | 99 | blocks.append(.bullet(items)) |
| @@ -96,9 +101,14 @@ struct MarkdownView: View { | ||
| 96 | 101 | } |
| 97 | 102 | if trimmed.range(of: #"^\d+\. "#, options: .regularExpression) != nil { |
| 98 | 103 | var items: [String] = [] |
| 99 | while let next = lines.first?.trimmingCharacters(in: .whitespaces), | |
| 100 | let range = next.range(of: #"^\d+\. "#, options: .regularExpression) { | |
| 101 | items.append(String(next[range.upperBound...])) | |
| 104 | while let next = lines.first?.trimmingCharacters(in: .whitespaces) { | |
| 105 | if let range = next.range(of: #"^\d+\. "#, options: .regularExpression) { | |
| 106 | items.append(String(next[range.upperBound...])) | |
| 107 | } else if Self.continuesItem(lines) { | |
| 108 | items[items.count - 1] += " " + next | |
| 109 | } else { | |
| 110 | break | |
| 111 | } | |
| 102 | 112 | lines = lines.dropFirst() |
| 103 | 113 | } |
| 104 | 114 | blocks.append(.ordered(items)) |
| @@ -140,6 +150,19 @@ struct MarkdownView: View { | ||
| 140 | 150 | return blocks |
| 141 | 151 | } |
| 142 | 152 | |
| 153 | /// A line that wraps the list item above it: anything but a blank line or | |
| 154 | /// the start of another block, indented or not, as in CommonMark. | |
| 155 | private static func continuesItem(_ lines: ArraySlice<String>) -> Bool { | |
| 156 | guard let line = lines.first?.trimmingCharacters(in: .whitespaces), !line.isEmpty | |
| 157 | else { return false } | |
| 158 | let startsBlock = line.hasPrefix("#") || line.hasPrefix("```") || line.hasPrefix(">") | |
| 159 | || line.hasPrefix("- ") || line.hasPrefix("* ") || line.hasPrefix("+ ") | |
| 160 | || line == "---" || line == "***" || line == "___" | |
| 161 | || line.range(of: #"^\d+\. "#, options: .regularExpression) != nil | |
| 162 | || tableStart(lines) != nil | |
| 163 | return !startsBlock | |
| 164 | } | |
| 165 | ||
| 143 | 166 | // MARK: - Tables |
| 144 | 167 | |
| 145 | 168 | /// A table begins where a row of cells is followed by a separator row with the |
| @@ -224,6 +247,7 @@ struct MarkdownView: View { | ||
| 224 | 247 | HStack(alignment: .firstTextBaseline, spacing: 8) { |
| 225 | 248 | Text("•") |
| 226 | 249 | Self.inline(item, linkOptions: linkOptions) |
| 250 | .fixedSize(horizontal: false, vertical: true) | |
| 227 | 251 | } |
| 228 | 252 | } |
| 229 | 253 | } |
| @@ -233,6 +257,7 @@ struct MarkdownView: View { | ||
| 233 | 257 | HStack(alignment: .firstTextBaseline, spacing: 8) { |
| 234 | 258 | Text("\(index + 1).").monospacedDigit() |
| 235 | 259 | Self.inline(item, linkOptions: linkOptions) |
| 260 | .fixedSize(horizontal: false, vertical: true) | |
| 236 | 261 | } |
| 237 | 262 | } |
| 238 | 263 | } |
gitbayTests/MarkdownTests.swift +40
| @@ -135,3 +135,43 @@ struct MarkdownLinkTests { | ||
| 135 | 135 | == ["ARCHITECTURE.md"]) |
| 136 | 136 | } |
| 137 | 137 | } |
| 138 | ||
| 139 | @MainActor | |
| 140 | struct MarkdownListTests { | |
| 141 | ||
| 142 | private func blocks(_ markdown: String) -> [MarkdownView.Block] { | |
| 143 | MarkdownView(markdown: markdown).blocks | |
| 144 | } | |
| 145 | ||
| 146 | @Test func indentedContinuationLinesStayInTheItem() { | |
| 147 | let parsed = blocks(""" | |
| 148 | - Org source files in `content/`. | |
| 149 | That directory is the URL root. | |
| 150 | - HTML templates. | |
| 151 | """) | |
| 152 | #expect(parsed == [.bullet([ | |
| 153 | "Org source files in `content/`. That directory is the URL root.", | |
| 154 | "HTML templates.", | |
| 155 | ])]) | |
| 156 | } | |
| 157 | ||
| 158 | @Test func unindentedContinuationLinesStayInTheItem() { | |
| 159 | let parsed = blocks("- one\ncontinued\n- two") | |
| 160 | #expect(parsed == [.bullet(["one continued", "two"])]) | |
| 161 | } | |
| 162 | ||
| 163 | @Test func orderedItemsTakeContinuationLines() { | |
| 164 | let parsed = blocks("1. first\n more\n2. second") | |
| 165 | #expect(parsed == [.ordered(["first more", "second"])]) | |
| 166 | } | |
| 167 | ||
| 168 | @Test func aListEndsAtABlankLine() { | |
| 169 | let parsed = blocks("- one\n\nAfter the list.") | |
| 170 | #expect(parsed == [.bullet(["one"]), .paragraph("After the list.")]) | |
| 171 | } | |
| 172 | ||
| 173 | @Test func aHeadingEndsAList() { | |
| 174 | let parsed = blocks("- one\n## Next") | |
| 175 | #expect(parsed == [.bullet(["one"]), .heading(level: 2, text: "Next")]) | |
| 176 | } | |
| 177 | } | |