╭─────────────╮ │scroll to top│ ╰─────────────╯ ╭───────────────────────────────────────────────────────────────╮ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ ╰───────────────────────────────────────────────────────────────╯
╭ find file ─ ─ ─ ─ ─ ─ ─ ─ ─ ─╮ ╭────╮ ╭ clone ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─╮ │ │ │http│ ssh │ │ ╰ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─╯ ╰────╯ ╰ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─╯
60 // how deeply parens nest in an inline link's destination 61 const max_paren_depth = 32; 62 63 const Parser = struct { 64 arena: std.mem.Allocator, 65 // how many quotes and lists enclose the blocks being parsed 66 depth: usize = 0, 67 68 fn parseBlocks(p: *Parser, lines: []const []const u8) anyerror![]const Block { 69 var blocks: std.ArrayList(Block) = .empty; 70 var i: usize = 0; 71 while (i < lines.len) { 72 const line = lines[i]; 73 if (isBlank(line)) { 74 i += 1; 75 continue; 76 } 77 if (indentOf(line) >= 4) { 78 var end = i; 79 var j = i; 80 while (j < lines.len and (isBlank(lines[j]) or indentOf(lines[j]) >= 4)) : (j += 1) { 81 if (!isBlank(lines[j])) end = j + 1; 82 } 83 const code = try p.arena.alloc([]const u8, end - i); 84 for (lines[i..end], code) |l, *c| c.* = dropIndent(l, 4); 85 try blocks.append(p.arena, .{ .code = code }); 86 i = end; 87 continue; 88 } 89 if (fenceStart(line)) |fence| { 90 var code: std.ArrayList([]const u8) = .empty; 91 var j = i + 1; 92 while (j < lines.len and !isFenceClose(lines[j], fence)) : (j += 1) { 93 try code.append(p.arena, dropIndent(lines[j], fence.indent)); 94 } 95 try blocks.append(p.arena, .{ .code = code.items }); 96 i = @min(j + 1, lines.len); 97 continue; 98 } 99 if (atxHeading(line)) |heading| { 100 try blocks.append(p.arena, .{ .heading = .{ .level = heading.level, .inlines = try parseInlines(p.arena, heading.text) } }); 101 i += 1; 102 continue; 103 } 104 if (isThematicBreak(line)) { 105 try blocks.append(p.arena, .rule); 106 i += 1; 107 continue; 108 } 109 // deeper nesting reads as paragraph text, bounding the recursion 110 const can_nest = p.depth < max_depth; 111 if (can_nest and quoteContent(line) != null) { 112 i = try p.parseQuote(lines, i, &blocks); 113 continue; 114 } 115 if (can_nest and listMarker(line) != null) { 116 i = try p.parseList(lines, i, &blocks); 117 continue; 118 } 119 if (isTableStart(lines, i)) { 120 var j = i; 121 while (j < lines.len and !isBlank(lines[j]) and std.mem.indexOfScalar(u8, lines[j], '|') != null) : (j += 1) {} 122 try blocks.append(p.arena, .{ .raw = lines[i..j] }); 123 i = j; 124 continue; 125 } 126 if (isHtmlStart(line)) { 127 var j = i; 128 while (j < lines.len and !isBlank(lines[j])) : (j += 1) {} 129 try blocks.append(p.arena, .{ .raw = lines[i..j] }); 130 i = j; 131 continue; 132 } 133 i = try p.parseParagraph(lines, i, &blocks); 134 } 135 return blocks.items; 136 } 137 138 fn parseQuote(p: *Parser, lines: []const []const u8, start: usize, blocks: *std.ArrayList(Block)) !usize { 139 var content: std.ArrayList([]const u8) = .empty; 140 var fence: ?Fence = null; 141 var j = start; 142 while (j < lines.len) : (j += 1) { 143 const line = lines[j]; 144 const c = quoteContent(line) orelse lazy: { 145 // lazy continuation of a paragraph inside the quote 146 if (fence != null or isBlank(content.items[content.items.len - 1]) or interruptsParagraph(line)) break; 147 break :lazy line; 148 }; 149 fence = trackFence(fence, c); 150 try content.append(p.arena, c); 151 } 152 try blocks.append(p.arena, .{ .quote = try p.nested(content.items) }); 153 return j; 154 } 155 156 fn parseList(p: *Parser, lines: []const []const u8, start: usize, blocks: *std.ArrayList(Block)) !usize { 157 const first = listMarker(lines[start]) orelse unreachable; 158 var items: std.ArrayList(Item) = .empty; 159 var i = start; 160 while (i < lines.len) { 161 if (isThematicBreak(lines[i])) break; 162 const marker = listMarker(lines[i]) orelse break; 163 if (marker.ordered != first.ordered or marker.char != first.char) break; 164 165 var content: std.ArrayList([]const u8) = .empty; 166 try content.append(p.arena, lines[i][marker.content_start..]); 167 var fence = trackFence(null, content.items[0]); 168 var j = i + 1; 169 while (j < lines.len) : (j += 1) { 170 const line = lines[j]; 171 if (isBlank(line)) { 172 // an item may begin with at most one blank line 173 if (marker.empty and content.items.len == 1) break; 174 try content.append(p.arena, ""); 175 continue; 176 } 177 if (indentOf(line) >= marker.indent) { 178 const c = dropIndent(line, marker.indent); 179 fence = trackFence(fence, c); 180 try content.append(p.arena, c); 181 continue; 182 } 183 // a less indented line ends the item unless it lazily 184 // continues a paragraph 185 if (listMarker(line) != null or fence != null or isBlank(content.items[content.items.len - 1]) or interruptsParagraph(line)) break; 186 try content.append(p.arena, std.mem.trimStart(u8, line, " ")); 187 } 188 189 // trailing blank lines sit between items 190 var len = content.items.len; 191 while (len > 0 and isBlank(content.items[len - 1])) len -= 1; 192 const item_lines = content.items[0..len]; 193 var task: ?bool = null; 194 if (item_lines.len > 0) { 195 if (taskMarker(item_lines[0])) |t| { 196 task = t.checked; 197 item_lines[0] = t.rest; 198 } 199 } 200 try items.append(p.arena, .{ .task = task, .blocks = try p.nested(item_lines) }); 201 i = j; 202 } 203 try blocks.append(p.arena, .{ .list = .{ .ordered = first.ordered, .start = first.start, .items = items.items } }); 204 return i; 205 } 206 207 fn nested(p: *Parser, lines: []const []const u8) ![]const Block { 208 p.depth += 1; 209 defer p.depth -= 1; 210 return p.parseBlocks(lines); 211 } 212 213 fn parseParagraph(p: *Parser, lines: []const []const u8, start: usize, blocks: *std.ArrayList(Block)) !usize { 214 const i = start; 215 var j = i + 1; 216 var setext: ?u3 = null; 217 while (j < lines.len) : (j += 1) { 218 const line = lines[j]; 219 if (isBlank(line)) break; 220 if (setextLevel(line)) |level| { 221 setext = level; 222 break; 223 } 224 if (interruptsParagraph(line)) break; 225 } 226 const inlines_ = try parseInlines(p.arena, try joinLines(p.arena, lines[i..j])); 227 if (setext) |level| { 228 try blocks.append(p.arena, .{ .heading = .{ .level = level, .inlines = inlines_ } }); 229 return j + 1; 230 } 231 try blocks.append(p.arena, .{ .paragraph = inlines_ }); 232 return j; 233 } 234 }; 235 236 fn parseInlines(arena: std.mem.Allocator, src: []const u8) ![]const Inline { 237 var scanner: InlineScanner = .{ .arena = arena, .src = src }; 238 return scanner.run(); 239 } 240 241 // one piece of inline content; a delimiter run keeps its matching state 242 // until emphasis is resolved, after which any unmatched chars are text. 243 const Node = struct { 244 text: []const u8, 245 style: Style = .{}, 246 link: ?[]const u8 = null, 247 // '*', '_' or '~' for a delimiter run, else 0 248 delim: u8 = 0, 249 // the run's unmatched length, then its original length 250 count: usize = 0, 251 orig: usize = 0, 252 can_open: bool = false, 253 can_close: bool = false, 254 255 // drop the delimiter state, keeping its unmatched chars as text 256 fn literal(self: *Node) void { 257 if (self.delim == 0) return; 258 self.text = self.text[0..self.count]; 259 self.delim = 0; 260 } 261 }; 262 263 const Bracket = struct { 264 node: usize, 265 image: bool, 266 }; 267 268 const InlineScanner = struct { 269 arena: std.mem.Allocator, 270 src: []const u8, 271 nodes: std.ArrayList(Node) = .empty, 272 brackets: std.ArrayList(Bracket) = .empty, 273 // the start of the plain text not yet pushed as a node 274 text_start: usize = 0, 275 // brackets below this stack index can't open links, being inside one 276 no_links_below: usize = 0, 277 // backtick run lengths with no closer left in the source 278 unclosed_runs: std.AutoHashMapUnmanaged(usize, void) = .empty, 279 // the index of a `)` for link titles to end at, see inlineLink 280 next_close: usize = 0, 281 282 fn run(s: *InlineScanner) ![]const Inline { 283 const src = s.src; 284 var i: usize = 0; 285 while (i < src.len) { 286 const next = try s.special(i); 287 if (next) |n| { 288 s.text_start = n; 289 i = n; 290 } else i += 1; 291 } 292 try s.flush(src.len); 293 processEmphasis(s.nodes.items, 0); 294 return merge(s.arena, s.nodes.items); 295 } 296 297 fn flush(s: *InlineScanner, end: usize) !void { 298 if (end > s.text_start) try s.nodes.append(s.arena, .{ .text = s.src[s.text_start..end] }); 299 s.text_start = end; 300 } 301 302 fn push(s: *InlineScanner, at: usize, node: Node) !void { 303 try s.flush(at); 304 try s.nodes.append(s.arena, node); 305 } 306 307 // handle the construct starting at `i`, returning where scanning resumes, 308 // or null when the char is plain text. 309 fn special(s: *InlineScanner, i: usize) !?usize { 310 const src = s.src; 311 const arena = s.arena; 312 switch (src[i]) { 313 '\\' => if (i + 1 < src.len and isPunct(src[i + 1])) { 314 try s.push(i, .{ .text = src[i + 1 .. i + 2] }); 315 return i + 2; 316 }, 317 '`' => { 318 const n = runLength(src, i, '`'); 319 // a run with no closer is literal text 320 const close = (if (s.unclosed_runs.contains(n)) null else findBacktickRun(src, i + n, n)) orelse { 321 try s.unclosed_runs.put(arena, n, {}); 322 try s.push(i, .{ .text = src[i .. i + n] }); 323 return i + n; 324 }; 325 try s.push(i, .{ .text = try codeContent(arena, src[i + n .. close]), .style = .{ .code = true } }); 326 return close + n; 327 }, 328 '*', '_', '~' => { 329 const c = src[i]; 330 const n = runLength(src, i, c); 331 if (c == '~' and n > 2) { 332 try s.push(i, .{ .text = src[i .. i + n] }); 333 return i + n; 334 } 335 const before: u8 = if (i == 0) ' ' else src[i - 1]; 336 const after: u8 = if (i + n >= src.len) ' ' else src[i + n]; 337 const left = !isSpace(after) and (!isPunct(after) or isSpace(before) or isPunct(before)); 338 const right = !isSpace(before) and (!isPunct(before) or isSpace(after) or isPunct(after)); 339 var node: Node = .{ .text = src[i .. i + n], .delim = c, .count = n, .orig = n, .can_open = left, .can_close = right }; 340 if (c == '_') { 341 node.can_open = left and (!right or isPunct(before)); 342 node.can_close = right and (!left or isPunct(after)); 343 } 344 try s.push(i, node); 345 return i + n; 346 }, 347 '!' => if (i + 1 < src.len and src[i + 1] == '[') { 348 try s.push(i, .{ .text = src[i .. i + 2] }); 349 try s.pushBracket(true); 350 return i + 2; 351 }, 352 '[' => { 353 try s.push(i, .{ .text = src[i .. i + 1] }); 354 try s.pushBracket(false); 355 return i + 1; 356 }, 357 ']' => { 358 try s.flush(i); 359 if (try s.closeBracket(i)) |end| return end; 360 try s.push(i, .{ .text = src[i .. i + 1] }); 361 return i + 1; 362 }, 363 '<' => if (autolink(src[i..])) |url| { 364 try s.push(i, .{ .text = url, .link = url }); 365 return i + url.len + 2; 366 }, 367 'h', 'H' => if (i == 0 or isAutolinkBoundary(src[i - 1])) { 368 if (bareAutolink(src[i..])) |len| { 369 try s.push(i, .{ .text = src[i .. i + len], .link = src[i .. i + len] }); 370 return i + len; 371 } 372 }, 373 else => {}, 374 } 375 return null; 376 } 377 378 fn pushBracket(s: *InlineScanner, image: bool) !void { 379 s.no_links_below = @min(s.no_links_below, s.brackets.items.len); 380 try s.brackets.append(s.arena, .{ .node = s.nodes.items.len - 1, .image = image }); 381 } 382 383 // resolve the `]` at `i` against the innermost bracket, returning where 384 // scanning resumes when it closes a link. 385 fn closeBracket(s: *InlineScanner, i: usize) !?usize { 386 const src = s.src; 387 const bracket = s.brackets.pop() orelse return null; 388 if (!bracket.image and s.brackets.items.len < s.no_links_below) return null; 389 if (i + 1 >= src.len or src[i + 1] != '(') return null; 390 const link = (try inlineLink(s.arena, src, i + 1, &s.next_close)) orelse return null; 391 392 const nodes = s.nodes.items; 393 processEmphasis(nodes, bracket.node + 1); 394 for (nodes[bracket.node + 1 ..]) |*node| { 395 node.link = link.dest; 396 // an image shows its alt text as an italic link 397 if (bracket.image) node.style.italic = true; 398 } 399 nodes[bracket.node].text = ""; 400 // links can't contain links 401 if (!bracket.image) s.no_links_below = s.brackets.items.len; 402 return link.end; 403 } 404 }; 405 406 // resolve `*`, `_` and `~` runs from `bottom` on with the CommonMark 407 // delimiter algorithm, styling the nodes between each matched pair. 408 fn processEmphasis(nodes: []Node, bottom: usize) void { 409 // per closer kind, the lowest index an opener may still be found at 410 var openers_bottom: [3][2][3]usize = @splat(@splat(@splat(bottom))); 411 412 var ci = bottom; 413 while (ci < nodes.len) { 414 const closer = &nodes[ci]; 415 if (closer.delim == 0 or !closer.can_close or closer.count == 0) { 416 ci += 1; 417 continue; 418 } 419 const kind: usize = switch (closer.delim) { 420 '*' => 0, 421 '_' => 1, 422 else => 2, 423 }; 424 const lowest = &openers_bottom[kind][@intFromBool(closer.can_open)][closer.orig % 3]; 425 426 var found: ?usize = null; 427 var oi = ci; 428 while (oi > lowest.*) { 429 oi -= 1; 430 const opener = nodes[oi]; 431 if (opener.delim != closer.delim or !opener.can_open or opener.count == 0) continue; 432 if (closer.delim == '~') { 433 if (opener.count != closer.count) continue; 434 } else if ((opener.can_close or closer.can_open) and (opener.orig + closer.orig) % 3 == 0 and 435 !(opener.orig % 3 == 0 and closer.orig % 3 == 0)) 436 { 437 // the rule of 3 438 continue; 439 } 440 found = oi; 441 break; 442 } 443 444 const oi_found = found orelse { 445 lowest.* = ci; 446 if (!closer.can_open) closer.literal(); 447 ci += 1; 448 continue; 449 }; 450 const opener = &nodes[oi_found]; 451 const use: usize = if (closer.delim == '~') closer.count else if (opener.count >= 2 and closer.count >= 2) 2 else 1; 452 for (nodes[oi_found + 1 .. ci]) |*node| { 453 // delimiters between the pair can no longer match 454 node.literal(); 455 if (closer.delim == '~') { 456 node.style.strike = true; 457 } else if (use == 2) { 458 node.style.bold = true; 459 } else { 460 node.style.italic = true; 461 } 462 } 463 opener.count -= use; 464 closer.count -= use; 465 if (closer.count == 0) ci += 1; 466 } 467 468 for (nodes[bottom..]) |*node| node.literal(); 469 } 470 471 // join adjacent nodes that share a style and link into runs. 472 fn merge(arena: std.mem.Allocator, nodes: []const Node) ![]const Inline { 473 var runs: std.ArrayList(Inline) = .empty; 474 var text: std.ArrayList(u8) = .empty; 475 var style: Style = .{}; 476 var link: ?[]const u8 = null; 477 for (nodes) |node| { 478 if (node.text.len == 0) continue; 479 const same_link = if (link) |l| (if (node.link) |nl| std.mem.eql(u8, l, nl) else false) else node.link == null; 480 if (text.items.len > 0 and (!std.meta.eql(style, node.style) or !same_link)) { 481 try runs.append(arena, .{ .text = try text.toOwnedSlice(arena), .style = style, .link = link }); 482 } 483 style = node.style; 484 link = node.link; 485 try text.appendSlice(arena, node.text); 486 } 487 if (text.items.len > 0) try runs.append(arena, .{ .text = try text.toOwnedSlice(arena), .style = style, .link = link }); 488 return runs.items; 489 } 490 491 // join a paragraph's lines: a soft break becomes a space and a hard break 492 // (two trailing spaces or a trailing backslash) becomes '\n'. 493 fn joinLines(arena: std.mem.Allocator, lines: []const []const u8) ![]const u8 { 494 var out: std.ArrayList(u8) = .empty; 495 for (lines, 0..) |line, i| { 496 const trimmed = std.mem.trimStart(u8, line, " "); 497 const content = std.mem.trimEnd(u8, trimmed, " "); 498 const last = i + 1 == lines.len; 499 if (!last and content.len > 0 and content[content.len - 1] == '\\' and runLengthBack(content, content.len, '\\') % 2 == 1) { 500 try out.appendSlice(arena, content[0 .. content.len - 1]); 501 try out.append(arena, '\n'); 502 } else { 503 try out.appendSlice(arena, content); 504 if (!last) try out.append(arena, if (trimmed.len - content.len >= 2) '\n' else ' '); 505 } 506 } 507 return out.items; 508 } 509 510 fn expandTabs(arena: std.mem.Allocator, line: []const u8) ![]const u8 { 511 const trimmed = std.mem.trimEnd(u8, line, "\r"); 512 if (std.mem.indexOfScalar(u8, trimmed, '\t') == null) return trimmed; 513 var out: std.ArrayList(u8) = .empty; 514 for (trimmed) |c| { 515 if (c == '\t') { 516 try out.appendNTimes(arena, ' ', 4 - out.items.len % 4); 517 } else try out.append(arena, c); 518 } 519 return out.items; 520 } 521 522 fn isBlank(line: []const u8) bool { 523 return std.mem.trimStart(u8, line, " ").len == 0; 524 } 525 526 fn indentOf(line: []const u8) usize { 527 return line.len - std.mem.trimStart(u8, line, " ").len; 528 } 529 530 // `line` without up to `n` leading spaces 531 fn dropIndent(line: []const u8, n: usize) []const u8 { 532 return line[@min(indentOf(line), n)..]; 533 } 534 535 fn runLength(s: []const u8, start: usize, c: u8) usize { 536 var n: usize = 0; 537 while (start + n < s.len and s[start + n] == c) n += 1; 538 return n; 539 } 540 541 // how many `c` end at `end` 542 fn runLengthBack(s: []const u8, end: usize, c: u8) usize { 543 var n: usize = 0; 544 while (n < end and s[end - n - 1] == c) n += 1; 545 return n; 546 } 547 548 fn isSpace(c: u8) bool { 549 return c == ' ' or c == '\n'; 550 } 551 552 fn isPunct(c: u8) bool { 553 return std.ascii.isPrint(c) and !std.ascii.isAlphanumeric(c) and c != ' '; 554 } 555 556 fn interruptsParagraph(line: []const u8) bool { 557 if (isBlank(line)) return true; 558 if (indentOf(line) >= 4) return false; 559 if (atxHeading(line) != null or fenceStart(line) != null or quoteContent(line) != null or isThematicBreak(line)) return true; 560 const marker = listMarker(line) orelse return false; 561 return !marker.empty and (!marker.ordered or marker.start == 1); 562 } 563 564 const Fence = struct { 565 char: u8, 566 len: usize, 567 indent: usize, 568 }; 569 570 fn fenceStart(line: []const u8) ?Fence { 571 const indent = indentOf(line); 572 if (indent >= 4 or indent >= line.len) return null; 573 const c = line[indent]; 574 if (c != '`' and c != '~') return null; 575 const n = runLength(line, indent, c); 576 if (n < 3) return null; 577 if (c == '`' and std.mem.indexOfScalar(u8, line[indent + n ..], '`') != null) return null; 578 return .{ .char = c, .len = n, .indent = indent }; 579 } 580 581 fn isFenceClose(line: []const u8, fence: Fence) bool { 582 const indent = indentOf(line); 583 if (indent >= 4) return false; 584 const n = runLength(line, indent, fence.char); 585 return n >= fence.len and isBlank(line[indent + n ..]); 586 } 587 588 // the fence still open after `line`, given the one open before it 589 fn trackFence(open: ?Fence, line: []const u8) ?Fence { 590 if (open) |fence| return if (isFenceClose(line, fence)) null else fence; 591 return fenceStart(line); 592 } 593 594 fn atxHeading(line: []const u8) ?struct { level: u3, text: []const u8 } { 595 const indent = indentOf(line); 596 if (indent >= 4) return null; 597 const n = runLength(line, indent, '#'); 598 if (n == 0 or n > 6) return null; 599 const after = line[indent + n ..]; 600 if (after.len > 0 and after[0] != ' ') return null; 601 var text = std.mem.trim(u8, after, " "); 602 // an optional closing sequence of #s 603 const closing = runLengthBack(text, text.len, '#'); 604 if (closing == text.len) { 605 text = ""; 606 } else if (closing > 0 and text[text.len - closing - 1] == ' ') { 607 text = std.mem.trimEnd(u8, text[0 .. text.len - closing], " "); 608 } 609 return .{ .level = @intCast(n), .text = text }; 610 } 611 612 fn isThematicBreak(line: []const u8) bool { 613 const indent = indentOf(line); 614 if (indent >= 4 or indent >= line.len) return false; 615 const c = line[indent]; 616 if (c != '-' and c != '*' and c != '_') return false; 617 var count: usize = 0; 618 for (line[indent..]) |x| { 619 if (x == c) { 620 count += 1; 621 } else if (x != ' ') return false; 622 } 623 return count >= 3; 624 } 625 626 fn setextLevel(line: []const u8) ?u3 { 627 const indent = indentOf(line); 628 if (indent >= 4 or indent >= line.len) return null; 629 const c = line[indent]; 630 if (c != '=' and c != '-') return null; 631 const n = runLength(line, indent, c); 632 if (!isBlank(line[indent + n ..])) return null; 633 return if (c == '=') 1 else 2; 634 } 635 636 // the content after a block quote marker and its optional space 637 fn quoteContent(line: []const u8) ?[]const u8 { 638 const indent = indentOf(line); 639 if (indent >= 4 or indent >= line.len or line[indent] != '>') return null; 640 const rest = line[indent + 1 ..]; 641 return if (rest.len > 0 and rest[0] == ' ') rest[1..] else rest; 642 } 643 644 const Marker = struct { 645 ordered: bool, 646 // the bullet char or the ordered delimiter 647 char: u8, 648 start: usize, 649 // where the first line's content begins 650 content_start: usize, 651 // the column continuation lines must reach to belong to the item 652 indent: usize, 653 empty: bool, 654 }; 655 656 fn listMarker(line: []const u8) ?Marker { 657 const indent = indentOf(line); 658 if (indent >= 4 or indent >= line.len) return null; 659 var pos = indent; 660 var marker: Marker = .{ .ordered = false, .char = line[pos], .start = 0, .content_start = 0, .indent = 0, .empty = false }; 661 switch (line[pos]) { 662 '-', '+', '*' => pos += 1, 663 else => { 664 const digits = runDigits(line, pos); 665 if (digits == 0 or digits > 9) return null; 666 pos += digits; 667 if (pos >= line.len or (line[pos] != '.' and line[pos] != ')')) return null; 668 marker.ordered = true; 669 marker.char = line[pos]; 670 marker.start = std.fmt.parseInt(usize, line[indent..pos], 10) catch unreachable; 671 pos += 1; 672 }, 673 } 674 if (isBlank(line[pos..])) { 675 marker.empty = true; 676 marker.content_start = line.len; 677 marker.indent = pos + 1; 678 return marker; 679 } 680 if (line[pos] != ' ') return null; 681 var spaces = runLength(line, pos, ' '); 682 // five or more spaces start indented code after a single space 683 if (spaces > 4) spaces = 1; 684 marker.content_start = pos + spaces; 685 marker.indent = pos + spaces; 686 return marker; 687 } 688 689 fn runDigits(s: []const u8, start: usize) usize { 690 var n: usize = 0; 691 while (start + n < s.len and std.ascii.isDigit(s[start + n])) n += 1; 692 return n; 693 } 694 695 fn taskMarker(line: []const u8) ?struct { checked: bool, rest: []const u8 } { 696 if (line.len < 3 or line[0] != '[' or line[2] != ']') return null; 697 const checked = switch (line[1]) { 698 ' ' => false, 699 'x', 'X' => true, 700 else => return null, 701 }; 702 if (line.len > 3 and line[3] != ' ') return null; 703 return .{ .checked = checked, .rest = line[@min(4, line.len)..] }; 704 } 705 706 fn isTableStart(lines: []const []const u8, i: usize) bool { 707 if (i + 1 >= lines.len) return false; 708 if (!std.mem.startsWith(u8, std.mem.trimStart(u8, lines[i], " "), "|")) return false; 709 const row = std.mem.trim(u8, lines[i + 1], " "); 710 if (std.mem.indexOfScalar(u8, row, '-') == null or std.mem.indexOfScalar(u8, row, '|') == null) return false; 711 for (row) |c| switch (c) { 712 '|', ':', '-', ' ' => {}, 713 else => return false, 714 }; 715 return true; 716 } 717 718 fn isHtmlStart(line: []const u8) bool { 719 const indent = indentOf(line); 720 if (indent >= 4 or indent + 1 >= line.len or line[indent] != '<') return false; 721 const c = line[indent + 1]; 722 if (!std.ascii.isAlphabetic(c) and c != '/' and c != '!' and c != '?') return false; 723 return autolink(line[indent..]) == null; 724 } 725 726 // `s` with its backslash escapes resolved 727 fn unescape(arena: std.mem.Allocator, s: []const u8) ![]const u8 { 728 if (std.mem.indexOfScalar(u8, s, '\\') == null) return s; 729 var out: std.ArrayList(u8) = .empty; 730 var i: usize = 0; 731 while (i < s.len) : (i += 1) { 732 if (s[i] == '\\' and i + 1 < s.len and isPunct(s[i + 1])) i += 1; 733 try out.append(arena, s[i]); 734 } 735 return out.items; 736 } 737 738 // the index of the next run of exactly `n` backticks at or after `start` 739 fn findBacktickRun(s: []const u8, start: usize, n: usize) ?usize { 740 var i = start; 741 while (i < s.len) { 742 if (s[i] != '`') { 743 i += 1; 744 continue; 745 } 746 const len = runLength(s, i, '`'); 747 if (len == n) return i; 748 i += len; 749 } 750 return null; 751 } 752 753 fn codeContent(arena: std.mem.Allocator, raw: []const u8) ![]const u8 { 754 var content = raw; 755 if (std.mem.indexOfScalar(u8, content, '\n') != null) { 756 const copy = try arena.dupe(u8, content); 757 std.mem.replaceScalar(u8, copy, '\n', ' '); 758 content = copy; 759 } 760 // one space is stripped from each side of content that isn't all spaces 761 if (content.len >= 2 and content[0] == ' ' and content[content.len - 1] == ' ' and !isBlank(content)) { 762 content = content[1 .. content.len - 1]; 763 } 764 return content; 765 } 766 767 // parse the `(dest "title")` of an inline link starting at the `(` at `open`, 768 // ignoring the title. `next_close` caches the index of the next `)` (or 769 // src.len for none), so links scanned left to right never rescan for it. 770 fn inlineLink(arena: std.mem.Allocator, src: []const u8, open: usize, next_close: *usize) !?struct { dest: []const u8, end: usize } { 771 var j = open + 1; 772 while (j < src.len and isSpace(src[j])) j += 1; 773 const start = j; 774 var depth: usize = 0; 775 while (j < src.len) { 776 const c = src[j]; 777 if (c == ' ' or c < 0x20) break; 778 if (c == '\\' and j + 1 < src.len and isPunct(src[j + 1])) { 779 j += 2; 780 continue; 781 } 782 if (c == '(') { 783 // deeper nesting fails, which bounds the rescanning 784 if (depth == max_paren_depth) return null; 785 depth += 1; 786 } 787 if (c == ')') { 788 if (depth == 0) break; 789 depth -= 1; 790 } 791 j += 1; 792 } 793 if (depth != 0) return null; 794 const dest = src[start..j]; 795 const before_title = j; 796 while (j < src.len and isSpace(src[j])) j += 1; 797 // a title follows the destination after a space and runs to the `)` 798 if (j < src.len and src[j] != ')' and j > before_title) { 799 if (next_close.* < j) next_close.* = std.mem.indexOfScalarPos(u8, src, j, ')') orelse src.len; 800 j = next_close.*; 801 } 802 if (j >= src.len or src[j] != ')') return null; 803 return .{ .dest = try unescape(arena, dest), .end = j + 1 }; 804 } 805 806 // an autolink `<scheme:...>` or `<user@host>` at the start of `s` 807 fn autolink(s: []const u8) ?[]const u8 { 808 if (s.len < 3 or s[0] != '<') return null; 809 var close: usize = 1; 810 while (close < s.len and s[close] > ' ' and s[close] != '<' and s[close] != '>') close += 1; 811 if (close >= s.len or s[close] != '>') return null; 812 const body = s[1..close]; 813 814 const colon = std.mem.indexOfScalar(u8, body, ':') orelse return null; 815 const scheme = body[0..colon]; 816 if (scheme.len < 2 or scheme.len > 32 or !std.ascii.isAlphabetic(scheme[0])) return null; 817 for (scheme) |c| { 818 if (!std.ascii.isAlphanumeric(c) and c != '+' and c != '.' and c != '-') return null; 819 } 820 return body; 821 } 822 823 fn isAutolinkBoundary(c: u8) bool { 824 return switch (c) { 825 ' ', '\n', '*', '_', '~', '(' => true, 826 else => false, 827 }; 828 } 829 830 // the length of a bare `http://` or `https://` link at the start of `s` 831 fn bareAutolink(s: []const u8) ?usize { 832 const prefix_len: usize = if (std.ascii.startsWithIgnoreCase(s, "https://")) 8 else if (std.ascii.startsWithIgnoreCase(s, "http://")) 7 else return null; 833 var end = prefix_len; 834 while (end < s.len and !isSpace(s[end]) and s[end] != '<') end += 1; 835 var unmatched_closes = @as(isize, @intCast(std.mem.count(u8, s[0..end], ")"))) - @as(isize, @intCast(std.mem.count(u8, s[0..end], "("))); 836 // trailing punctuation is left out, as is an unbalanced closing paren 837 while (end > prefix_len) : (end -= 1) { 838 const c = s[end - 1]; 839 if (c == ')' and unmatched_closes > 0) { 840 unmatched_closes -= 1; 841 } else if (std.mem.indexOfScalar(u8, "?!.,:;*_~'\"", c) == null) break; 842 } 843 return if (end > prefix_len) end else null; 844 } 845