╭─────────────╮ │scroll to top│ ╰─────────────╯ ╭ markdown.zig ─────────────────────────────────────────────────╮ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ ╰───────────────────────────────────────────────────────────────╯
╭ find file ─ ─ ─ ─ ─ ─ ─ ─ ─ ─╮ ╭────╮ ╭ clone ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─╮ │ │ │http│ ssh │ │ ╰ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─╯ ╰────╯ ╰ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─╯
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