╭─────────────╮ │scroll to top│ ╰─────────────╯ ╭ markdown.zig ─────────────────────────────────────────────────╮ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ ╰───────────────────────────────────────────────────────────────╯
╭────╮ ╭ clone ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╮ │http│ ssh │ │ ╰────╯ ╰ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╯
484 // (two trailing spaces or a trailing backslash) becomes '\n'. 485 fn joinLines(arena: std.mem.Allocator, lines: []const []const u8) ![]const u8 { 486 var out: std.ArrayList(u8) = .empty; 487 for (lines, 0..) |line, i| { 488 const trimmed = std.mem.trimStart(u8, line, " "); 489 const content = std.mem.trimEnd(u8, trimmed, " "); 490 const last = i + 1 == lines.len; 491 if (!last and content.len > 0 and content[content.len - 1] == '\\' and runLengthBack(content, content.len, '\\') % 2 == 1) { 492 try out.appendSlice(arena, content[0 .. content.len - 1]); 493 try out.append(arena, '\n'); 494 } else { 495 try out.appendSlice(arena, content); 496 if (!last) try out.append(arena, if (trimmed.len - content.len >= 2) '\n' else ' '); 497 } 498 } 499 return out.items; 500 } 501 502 fn expandTabs(arena: std.mem.Allocator, line: []const u8) ![]const u8 { 503 const trimmed = std.mem.trimEnd(u8, line, "\r"); 504 if (std.mem.indexOfScalar(u8, trimmed, '\t') == null) return trimmed; 505 var out: std.ArrayList(u8) = .empty; 506 for (trimmed) |c| { 507 if (c == '\t') { 508 try out.appendNTimes(arena, ' ', 4 - out.items.len % 4); 509 } else try out.append(arena, c); 510 } 511 return out.items; 512 } 513 514 fn isBlank(line: []const u8) bool { 515 return std.mem.trimStart(u8, line, " ").len == 0; 516 } 517 518 fn indentOf(line: []const u8) usize { 519 return line.len - std.mem.trimStart(u8, line, " ").len; 520 } 521 522 // `line` without up to `n` leading spaces 523 fn dropIndent(line: []const u8, n: usize) []const u8 { 524 return line[@min(indentOf(line), n)..]; 525 } 526 527 fn runLength(s: []const u8, start: usize, c: u8) usize { 528 var n: usize = 0; 529 while (start + n < s.len and s[start + n] == c) n += 1; 530 return n; 531 } 532 533 // how many `c` end at `end` 534 fn runLengthBack(s: []const u8, end: usize, c: u8) usize { 535 var n: usize = 0; 536 while (n < end and s[end - n - 1] == c) n += 1; 537 return n; 538 } 539 540 fn isSpace(c: u8) bool { 541 return c == ' ' or c == '\n'; 542 } 543 544 fn isPunct(c: u8) bool { 545 return std.ascii.isPrint(c) and !std.ascii.isAlphanumeric(c) and c != ' '; 546 } 547 548 fn interruptsParagraph(line: []const u8) bool { 549 if (isBlank(line)) return true; 550 if (indentOf(line) >= 4) return false; 551 if (atxHeading(line) != null or fenceStart(line) != null or quoteContent(line) != null or isThematicBreak(line)) return true; 552 const marker = listMarker(line) orelse return false; 553 return !marker.empty and (!marker.ordered or marker.start == 1); 554 } 555 556 const Fence = struct { 557 char: u8, 558 len: usize, 559 indent: usize, 560 }; 561 562 fn fenceStart(line: []const u8) ?Fence { 563 const indent = indentOf(line); 564 if (indent >= 4 or indent >= line.len) return null; 565 const c = line[indent]; 566 if (c != '`' and c != '~') return null; 567 const n = runLength(line, indent, c); 568 if (n < 3) return null; 569 if (c == '`' and std.mem.indexOfScalar(u8, line[indent + n ..], '`') != null) return null; 570 return .{ .char = c, .len = n, .indent = indent }; 571 } 572 573 fn isFenceClose(line: []const u8, fence: Fence) bool { 574 const indent = indentOf(line); 575 if (indent >= 4) return false; 576 const n = runLength(line, indent, fence.char); 577 return n >= fence.len and isBlank(line[indent + n ..]); 578 } 579 580 // the fence still open after `line`, given the one open before it 581 fn trackFence(open: ?Fence, line: []const u8) ?Fence { 582 if (open) |fence| return if (isFenceClose(line, fence)) null else fence; 583 return fenceStart(line); 584 } 585 586 fn atxHeading(line: []const u8) ?struct { level: u3, text: []const u8 } { 587 const indent = indentOf(line); 588 if (indent >= 4) return null; 589 const n = runLength(line, indent, '#'); 590 if (n == 0 or n > 6) return null; 591 const after = line[indent + n ..]; 592 if (after.len > 0 and after[0] != ' ') return null; 593 var text = std.mem.trim(u8, after, " "); 594 // an optional closing sequence of #s 595 const closing = runLengthBack(text, text.len, '#'); 596 if (closing == text.len) { 597 text = ""; 598 } else if (closing > 0 and text[text.len - closing - 1] == ' ') { 599 text = std.mem.trimEnd(u8, text[0 .. text.len - closing], " "); 600 } 601 return .{ .level = @intCast(n), .text = text }; 602 } 603 604 fn isThematicBreak(line: []const u8) bool { 605 const indent = indentOf(line); 606 if (indent >= 4 or indent >= line.len) return false; 607 const c = line[indent]; 608 if (c != '-' and c != '*' and c != '_') return false; 609 var count: usize = 0; 610 for (line[indent..]) |x| { 611 if (x == c) { 612 count += 1; 613 } else if (x != ' ') return false; 614 } 615 return count >= 3; 616 } 617 618 fn setextLevel(line: []const u8) ?u3 { 619 const indent = indentOf(line); 620 if (indent >= 4 or indent >= line.len) return null; 621 const c = line[indent]; 622 if (c != '=' and c != '-') return null; 623 const n = runLength(line, indent, c); 624 if (!isBlank(line[indent + n ..])) return null; 625 return if (c == '=') 1 else 2; 626 } 627 628 // the content after a block quote marker and its optional space 629 fn quoteContent(line: []const u8) ?[]const u8 { 630 const indent = indentOf(line); 631 if (indent >= 4 or indent >= line.len or line[indent] != '>') return null; 632 const rest = line[indent + 1 ..]; 633 return if (rest.len > 0 and rest[0] == ' ') rest[1..] else rest; 634 } 635 636 const Marker = struct { 637 ordered: bool, 638 // the bullet char or the ordered delimiter 639 char: u8, 640 start: usize, 641 // where the first line's content begins 642 content_start: usize, 643 // the column continuation lines must reach to belong to the item 644 indent: usize, 645 empty: bool, 646 }; 647 648 fn listMarker(line: []const u8) ?Marker { 649 const indent = indentOf(line); 650 if (indent >= 4 or indent >= line.len) return null; 651 var pos = indent; 652 var marker: Marker = .{ .ordered = false, .char = line[pos], .start = 0, .content_start = 0, .indent = 0, .empty = false }; 653 switch (line[pos]) { 654 '-', '+', '*' => pos += 1, 655 else => { 656 const digits = runDigits(line, pos); 657 if (digits == 0 or digits > 9) return null; 658 pos += digits; 659 if (pos >= line.len or (line[pos] != '.' and line[pos] != ')')) return null; 660 marker.ordered = true; 661 marker.char = line[pos]; 662 marker.start = std.fmt.parseInt(usize, line[indent..pos], 10) catch unreachable; 663 pos += 1; 664 }, 665 } 666 if (isBlank(line[pos..])) { 667 marker.empty = true; 668 marker.content_start = line.len; 669 marker.indent = pos + 1; 670 return marker; 671 } 672 if (line[pos] != ' ') return null; 673 var spaces = runLength(line, pos, ' '); 674 // five or more spaces start indented code after a single space 675 if (spaces > 4) spaces = 1; 676 marker.content_start = pos + spaces; 677 marker.indent = pos + spaces; 678 return marker; 679 } 680 681 fn runDigits(s: []const u8, start: usize) usize { 682 var n: usize = 0; 683 while (start + n < s.len and std.ascii.isDigit(s[start + n])) n += 1; 684 return n; 685 } 686 687 fn taskMarker(line: []const u8) ?struct { checked: bool, rest: []const u8 } { 688 if (line.len < 3 or line[0] != '[' or line[2] != ']') return null; 689 const checked = switch (line[1]) { 690 ' ' => false, 691 'x', 'X' => true, 692 else => return null, 693 }; 694 if (line.len > 3 and line[3] != ' ') return null; 695 return .{ .checked = checked, .rest = line[@min(4, line.len)..] }; 696 } 697 698 fn isTableStart(lines: []const []const u8, i: usize) bool { 699 if (i + 1 >= lines.len) return false; 700 if (!std.mem.startsWith(u8, std.mem.trimStart(u8, lines[i], " "), "|")) return false; 701 const row = std.mem.trim(u8, lines[i + 1], " "); 702 if (std.mem.indexOfScalar(u8, row, '-') == null or std.mem.indexOfScalar(u8, row, '|') == null) return false; 703 for (row) |c| switch (c) { 704 '|', ':', '-', ' ' => {}, 705 else => return false, 706 }; 707 return true; 708 } 709 710 fn isHtmlStart(line: []const u8) bool { 711 const indent = indentOf(line); 712 if (indent >= 4 or indent + 1 >= line.len or line[indent] != '<') return false; 713 const c = line[indent + 1]; 714 if (!std.ascii.isAlphabetic(c) and c != '/' and c != '!' and c != '?') return false; 715 return autolink(line[indent..]) == null; 716 } 717 718 // `s` with its backslash escapes resolved 719 fn unescape(arena: std.mem.Allocator, s: []const u8) ![]const u8 { 720 if (std.mem.indexOfScalar(u8, s, '\\') == null) return s; 721 var out: std.ArrayList(u8) = .empty; 722 var i: usize = 0; 723 while (i < s.len) : (i += 1) { 724 if (s[i] == '\\' and i + 1 < s.len and isPunct(s[i + 1])) i += 1; 725 try out.append(arena, s[i]); 726 } 727 return out.items; 728 } 729 730 // the index of the next run of exactly `n` backticks at or after `start` 731 fn findBacktickRun(s: []const u8, start: usize, n: usize) ?usize { 732 var i = start; 733 while (i < s.len) { 734 if (s[i] != '`') { 735 i += 1; 736 continue; 737 } 738 const len = runLength(s, i, '`'); 739 if (len == n) return i; 740 i += len; 741 } 742 return null; 743 } 744 745 fn codeContent(arena: std.mem.Allocator, raw: []const u8) ![]const u8 { 746 var content = raw; 747 if (std.mem.indexOfScalar(u8, content, '\n') != null) { 748 const copy = try arena.dupe(u8, content); 749 std.mem.replaceScalar(u8, copy, '\n', ' '); 750 content = copy; 751 } 752 // one space is stripped from each side of content that isn't all spaces 753 if (content.len >= 2 and content[0] == ' ' and content[content.len - 1] == ' ' and !isBlank(content)) { 754 content = content[1 .. content.len - 1]; 755 } 756 return content; 757 } 758 759 // parse the `(dest "title")` of an inline link starting at the `(` at `open`, 760 // ignoring the title. `next_close` caches the index of the next `)` (or 761 // src.len for none), so links scanned left to right never rescan for it. 762 fn inlineLink(arena: std.mem.Allocator, src: []const u8, open: usize, next_close: *usize) !?struct { dest: []const u8, end: usize } { 763 var j = open + 1; 764 while (j < src.len and isSpace(src[j])) j += 1; 765 const start = j; 766 var depth: usize = 0; 767 while (j < src.len) { 768 const c = src[j]; 769 if (c == ' ' or c < 0x20) break; 770 if (c == '\\' and j + 1 < src.len and isPunct(src[j + 1])) { 771 j += 2; 772 continue; 773 } 774 if (c == '(') { 775 // deeper nesting fails, which bounds the rescanning 776 if (depth == max_paren_depth) return null; 777 depth += 1; 778 } 779 if (c == ')') { 780 if (depth == 0) break; 781 depth -= 1; 782 } 783 j += 1; 784 } 785 if (depth != 0) return null; 786 const dest = src[start..j]; 787 const before_title = j; 788 while (j < src.len and isSpace(src[j])) j += 1; 789 // a title follows the destination after a space and runs to the `)` 790 if (j < src.len and src[j] != ')' and j > before_title) { 791 if (next_close.* < j) next_close.* = std.mem.indexOfScalarPos(u8, src, j, ')') orelse src.len; 792 j = next_close.*; 793 } 794 if (j >= src.len or src[j] != ')') return null; 795 return .{ .dest = try unescape(arena, dest), .end = j + 1 }; 796 } 797 798 // an autolink `<scheme:...>` or `<user@host>` at the start of `s` 799 fn autolink(s: []const u8) ?[]const u8 { 800 if (s.len < 3 or s[0] != '<') return null; 801 var close: usize = 1; 802 while (close < s.len and s[close] > ' ' and s[close] != '<' and s[close] != '>') close += 1; 803 if (close >= s.len or s[close] != '>') return null; 804 const body = s[1..close]; 805 806 const colon = std.mem.indexOfScalar(u8, body, ':') orelse return null; 807 const scheme = body[0..colon]; 808 if (scheme.len < 2 or scheme.len > 32 or !std.ascii.isAlphabetic(scheme[0])) return null; 809 for (scheme) |c| { 810 if (!std.ascii.isAlphanumeric(c) and c != '+' and c != '.' and c != '-') return null; 811 } 812 return body; 813 } 814 815 fn isAutolinkBoundary(c: u8) bool { 816 return switch (c) { 817 ' ', '\n', '*', '_', '~', '(' => true, 818 else => false, 819 }; 820 } 821 822 // the length of a bare `http://` or `https://` link at the start of `s` 823 fn bareAutolink(s: []const u8) ?usize { 824 const prefix_len: usize = if (std.ascii.startsWithIgnoreCase(s, "https://")) 8 else if (std.ascii.startsWithIgnoreCase(s, "http://")) 7 else return null; 825 var end = prefix_len; 826 while (end < s.len and !isSpace(s[end]) and s[end] != '<') end += 1; 827 var unmatched_closes = @as(isize, @intCast(std.mem.count(u8, s[0..end], ")"))) - @as(isize, @intCast(std.mem.count(u8, s[0..end], "("))); 828 // trailing punctuation is left out, as is an unbalanced closing paren 829 while (end > prefix_len) : (end -= 1) { 830 const c = s[end - 1]; 831 if (c == ')' and unmatched_closes > 0) { 832 unmatched_closes -= 1; 833 } else if (std.mem.indexOfScalar(u8, "?!.,:;*_~'\"", c) == null) break; 834 } 835 return if (end > prefix_len) end else null; 836 } 837