Commit 47eb92311e
Verified · cmc
Layout: unified · split
gitbay/Views/Repos/MarkdownView.swift +31 −6
| @@ -86,9 +86,14 @@ struct MarkdownView: View { | |||
| 86 | } | 86 | } |
| 87 | if trimmed.hasPrefix("- ") || trimmed.hasPrefix("* ") || trimmed.hasPrefix("+ ") { | 87 | if trimmed.hasPrefix("- ") || trimmed.hasPrefix("* ") || trimmed.hasPrefix("+ ") { |
| 88 | var items: [String] = [] | 88 | var items: [String] = [] |
| 89 | while let next = lines.first?.trimmingCharacters(in: .whitespaces), | 89 | while let next = lines.first?.trimmingCharacters(in: .whitespaces) { |
| 90 | next.hasPrefix("- ") || next.hasPrefix("* ") || next.hasPrefix("+ ") { | 90 | if next.hasPrefix("- ") || next.hasPrefix("* ") || next.hasPrefix("+ ") { |
| 91 | items.append(String(next.dropFirst(2))) | 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 | lines = lines.dropFirst() | 97 | lines = lines.dropFirst() |
| 93 | } | 98 | } |
| 94 | blocks.append(.bullet(items)) | 99 | blocks.append(.bullet(items)) |
| @@ -96,9 +101,14 @@ struct MarkdownView: View { | |||
| 96 | } | 101 | } |
| 97 | if trimmed.range(of: #"^\d+\. "#, options: .regularExpression) != nil { | 102 | if trimmed.range(of: #"^\d+\. "#, options: .regularExpression) != nil { |
| 98 | var items: [String] = [] | 103 | var items: [String] = [] |
| 99 | while let next = lines.first?.trimmingCharacters(in: .whitespaces), | 104 | while let next = lines.first?.trimmingCharacters(in: .whitespaces) { |
| 100 | let range = next.range(of: #"^\d+\. "#, options: .regularExpression) { | 105 | if let range = next.range(of: #"^\d+\. "#, options: .regularExpression) { |
| 101 | items.append(String(next[range.upperBound...])) | 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 | lines = lines.dropFirst() | 112 | lines = lines.dropFirst() |
| 103 | } | 113 | } |
| 104 | blocks.append(.ordered(items)) | 114 | blocks.append(.ordered(items)) |
| @@ -140,6 +150,19 @@ struct MarkdownView: View { | |||
| 140 | return blocks | 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 | // MARK: - Tables | 166 | // MARK: - Tables |
| 144 | 167 | ||
| 145 | /// A table begins where a row of cells is followed by a separator row with the | 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 | HStack(alignment: .firstTextBaseline, spacing: 8) { | 247 | HStack(alignment: .firstTextBaseline, spacing: 8) { |
| 225 | Text("•") | 248 | Text("•") |
| 226 | Self.inline(item, linkOptions: linkOptions) | 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 | HStack(alignment: .firstTextBaseline, spacing: 8) { | 257 | HStack(alignment: .firstTextBaseline, spacing: 8) { |
| 234 | Text("\(index + 1).").monospacedDigit() | 258 | Text("\(index + 1).").monospacedDigit() |
| 235 | Self.inline(item, linkOptions: linkOptions) | 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 | == ["ARCHITECTURE.md"]) | 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 | } | ||