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