Commit 385ad813bb

385ad813bb1a50050a9c2f309762845c78a21b19

parent: 9a3cb9402a

Unsigned

cmc <hello@cleberg.net> · 2026-09-30 02:59 UTC

Markdown: a heading needs a space after its #s (!144)

A line like `#12 fixed this` rendered as a heading and lost its reference link. ATX headings now need one to six `#` followed by a space, tab or end of line (CommonMark); paragraph and list-item continuation use the same check. 5 new tests in MarkdownHeadingTests.

Layout: unified · split

gitbay/Views/Repos/MarkdownView.swift +13 −5
@@ -65,9 +65,7 @@ struct MarkdownView: View {
6565 blocks.append(.code(code.joined(separator: "\n")))
6666 continue
6767 }
68 if trimmed.hasPrefix("#") {
69 let level = trimmed.prefix(while: { $0 == "#" }).count
70 let text = trimmed.drop(while: { $0 == "#" }).trimmingCharacters(in: .whitespaces)
68 if let (level, text) = Self.atxHeading(trimmed) {
7169 blocks.append(.heading(level: min(level, 4), text: text))
7270 lines = lines.dropFirst()
7371 continue
@@ -140,7 +138,7 @@ struct MarkdownView: View {
140138 var paragraph: [String] = []
141139 while let next = lines.first {
142140 let t = next.trimmingCharacters(in: .whitespaces)
143 if t.isEmpty || t.hasPrefix("#") || t.hasPrefix("```") || t.hasPrefix(">")
141 if t.isEmpty || Self.atxHeading(t) != nil || t.hasPrefix("```") || t.hasPrefix(">")
144142 || t.hasPrefix("- ") || t.hasPrefix("* ") || Self.tableStart(lines) != nil {
145143 break
146144 }
@@ -152,12 +150,22 @@ struct MarkdownView: View {
152150 return blocks
153151 }
154152
153 /// An ATX heading: one to six `#` followed by a space, a tab or the end
154 /// of the line, as in CommonMark. `#12 fixed this` is a paragraph.
155 private static func atxHeading(_ line: String) -> (level: Int, text: String)? {
156 let level = line.prefix(while: { $0 == "#" }).count
157 guard (1...6).contains(level) else { return nil }
158 let rest = line.dropFirst(level)
159 guard rest.isEmpty || rest.first == " " || rest.first == "\t" else { return nil }
160 return (level, rest.trimmingCharacters(in: .whitespaces))
161 }
162
155163 /// A line that wraps the list item above it: anything but a blank line or
156164 /// the start of another block, indented or not, as in CommonMark.
157165 private static func continuesItem(_ lines: ArraySlice<String>) -> Bool {
158166 guard let line = lines.first?.trimmingCharacters(in: .whitespaces), !line.isEmpty
159167 else { return false }
160 let startsBlock = line.hasPrefix("#") || line.hasPrefix("```") || line.hasPrefix(">")
168 let startsBlock = atxHeading(line) != nil || line.hasPrefix("```") || line.hasPrefix(">")
161169 || line.hasPrefix("- ") || line.hasPrefix("* ") || line.hasPrefix("+ ")
162170 || line == "---" || line == "***" || line == "___"
163171 || line.range(of: #"^\d+\. "#, options: .regularExpression) != nil
gitbayTests/MarkdownTests.swift +29
@@ -175,3 +175,32 @@ struct MarkdownListTests {
175175 #expect(parsed == [.bullet(["one"]), .heading(level: 2, text: "Next")])
176176 }
177177}
178
179@MainActor
180struct MarkdownHeadingTests {
181
182 private func blocks(_ markdown: String) -> [MarkdownView.Block] {
183 MarkdownView(markdown: markdown).blocks
184 }
185
186 @Test func hashWithoutSpaceIsAParagraph() {
187 #expect(blocks("#12 fixed this") == [.paragraph("#12 fixed this")])
188 }
189
190 @Test func hashReferenceDoesNotEndAParagraph() {
191 #expect(blocks("see\n#12 for context") == [.paragraph("see #12 for context")])
192 }
193
194 @Test func hashReferenceContinuesAListItem() {
195 #expect(blocks("- fixed in\n#12") == blocks("- fixed in #12"))
196 }
197
198 @Test func headingNeedsSpaceOrEndOfLine() {
199 #expect(blocks("## Usage") == [.heading(level: 2, text: "Usage")])
200 #expect(blocks("#") == [.heading(level: 1, text: "")])
201 }
202
203 @Test func sevenHashesIsAParagraph() {
204 #expect(blocks("####### x") == [.paragraph("####### x")])
205 }
206}