| author | |
| committer | |
| log | d3ca9d55d9bae81aa3d01cd9936ff66bb26a8e9c |
| tree | 81da60ff9f20902d0031e8b81644733630331e9d |
| parent | 13a9d94a8038727469cf11b72273ce4ea6d89faa |
Closes #19265
This commit implements support for Markdown autolinks delimited by angle
brackets. The precise syntax accepted is documented in the doc comment
of `markdown.zig`.4 files changed, 84 insertions(+), 1 deletions(-)
lib/docs/wasm/markdown.zig+30| ... | @@ -75,6 +75,12 @@ | ... | @@ -75,6 +75,12 @@ |
| 75 | //! content. `target` may contain `\`-escaped characters and balanced | 75 | //! content. `target` may contain `\`-escaped characters and balanced |
| 76 | //! parentheses. | 76 | //! parentheses. |
| 77 | //! | 77 | //! |
| 78 | //! - **Autolink** - an abbreviated link, of the format `<target>`, where | ||
| 79 | //! `target` serves as both the link target and text. `target` may not | ||
| 80 | //! contain spaces or `<`, and any `\` in it are interpreted literally (not as | ||
| 81 | //! escapes). `target` is expected to be an absolute URI: an autolink will not | ||
| 82 | //! be recognized unless `target` starts with a URI scheme followed by a `:`. | ||
| 83 | //! | ||
| 78 | //! - **Image** - a link directly preceded by a `!`. The link text is | 84 | //! - **Image** - a link directly preceded by a `!`. The link text is |
| 79 | //! interpreted as the alt text of the image. | 85 | //! interpreted as the alt text of the image. |
| 80 | //! | 86 | //! |
| ... | @@ -710,6 +716,30 @@ test "links" { | ... | @@ -710,6 +716,30 @@ test "links" { |
| 710 | ); | 716 | ); |
| 711 | } | 717 | } |
| 712 | 718 | ||
| 719 | test "autolinks" { | ||
| 720 | try testRender( | ||
| 721 | \\<https://example.com> | ||
| 722 | \\**This is important: <https://example.com/strong>** | ||
| 723 | \\<https://example.com?query=abc.123#page(parens)> | ||
| 724 | \\<placeholder> | ||
| 725 | \\<data:> | ||
| 726 | \\1 < 2 | ||
| 727 | \\4 > 3 | ||
| 728 | \\Unclosed: < | ||
| 729 | \\ | ||
| 730 | , | ||
| 731 | \\<p><a href="https://example.com">https://example.com</a> | ||
| 732 | \\<strong>This is important: <a href="https://example.com/strong">https://example.com/strong</a></strong> | ||
| 733 | \\<a href="https://example.com?query=abc.123#page(parens)">https://example.com?query=abc.123#page(parens)</a> | ||
| 734 | \\&lt;placeholder&gt; | ||
| 735 | \\<a href="data:">data:</a> | ||
| 736 | \\1 &lt; 2 | ||
| 737 | \\4 &gt; 3 | ||
| 738 | \\Unclosed: &lt;</p> | ||
| 739 | \\ | ||
| 740 | ); | ||
| 741 | } | ||
| 742 | |||
| 713 | test "images" { | 743 | test "images" { |
| 714 | try testRender( | 744 | try testRender( |
| 715 | \\ | 745 | \\ |
lib/docs/wasm/markdown/Document.zig+2| ... | @@ -51,6 +51,8 @@ pub const Node = struct { | ... | @@ -51,6 +51,8 @@ pub const Node = struct { |
| 51 | // Inlines | 51 | // Inlines |
| 52 | /// Data is `link`. | 52 | /// Data is `link`. |
| 53 | link, | 53 | link, |
| 54 | /// Data is `text`. | ||
| 55 | autolink, | ||
| 54 | /// Data is `link`. | 56 | /// Data is `link`. |
| 55 | image, | 57 | image, |
| 56 | /// Data is `container`. | 58 | /// Data is `container`. |
lib/docs/wasm/markdown/Parser.zig+47| ... | @@ -985,6 +985,7 @@ const InlineParser = struct { | ... | @@ -985,6 +985,7 @@ const InlineParser = struct { |
| 985 | ip.pos += 1; | 985 | ip.pos += 1; |
| 986 | }, | 986 | }, |
| 987 | ']' => try ip.parseLink(), | 987 | ']' => try ip.parseLink(), |
| 988 | '<' => try ip.parseAutolink(), | ||
| 988 | '*', '_' => try ip.parseEmphasis(), | 989 | '*', '_' => try ip.parseEmphasis(), |
| 989 | '`' => try ip.parseCodeSpan(), | 990 | '`' => try ip.parseCodeSpan(), |
| 990 | else => {}, | 991 | else => {}, |
| ... | @@ -1076,6 +1077,52 @@ const InlineParser = struct { | ... | @@ -1076,6 +1077,52 @@ const InlineParser = struct { |
| 1076 | return @enumFromInt(string_top); | 1077 | return @enumFromInt(string_top); |
| 1077 | } | 1078 | } |
| 1078 | 1079 | ||
| 1080 | /// Parses an autolink, starting at the opening `<`. `ip.pos` is left at the | ||
| 1081 | /// closing `>`, or remains unchanged at the opening `<` if there is none. | ||
| 1082 | fn parseAutolink(ip: *InlineParser) !void { | ||
| 1083 | const start = ip.pos; | ||
| 1084 | ip.pos += 1; | ||
| 1085 | var state: enum { | ||
| 1086 | start, | ||
| 1087 | scheme, | ||
| 1088 | target, | ||
| 1089 | } = .start; | ||
| 1090 | while (ip.pos < ip.content.len) : (ip.pos += 1) { | ||
| 1091 | switch (state) { | ||
| 1092 | .start => switch (ip.content[ip.pos]) { | ||
| 1093 | 'A'...'Z', 'a'...'z' => state = .scheme, | ||
| 1094 | else => break, | ||
| 1095 | }, | ||
| 1096 | .scheme => switch (ip.content[ip.pos]) { | ||
| 1097 | 'A'...'Z', 'a'...'z', '0'...'9', '+', '.', '-' => {}, | ||
| 1098 | ':' => state = .target, | ||
| 1099 | else => break, | ||
| 1100 | }, | ||
| 1101 | .target => switch (ip.content[ip.pos]) { | ||
| 1102 | '<', ' ', '\t', '\n' => break, // Not allowed in autolinks | ||
| 1103 | '>' => { | ||
| 1104 | // Backslash escapes are not recognized in autolink targets. | ||
| 1105 | const target = try ip.parent.addString(ip.content[start + 1 .. ip.pos]); | ||
| 1106 | const node = try ip.parent.addNode(.{ | ||
| 1107 | .tag = .autolink, | ||
| 1108 | .data = .{ .text = .{ | ||
| 1109 | .content = target, | ||
| 1110 | } }, | ||
| 1111 | }); | ||
| 1112 | try ip.completed_inlines.append(ip.parent.allocator, .{ | ||
| 1113 | .node = node, | ||
| 1114 | .start = start, | ||
| 1115 | .len = ip.pos - start + 1, | ||
| 1116 | }); | ||
| 1117 | return; | ||
| 1118 | }, | ||
| 1119 | else => {}, | ||
| 1120 | }, | ||
| 1121 | } | ||
| 1122 | } | ||
| 1123 | ip.pos = start; | ||
| 1124 | } | ||
| 1125 | |||
| 1079 | /// Parses emphasis, starting at the beginning of a run of `*` or `_` | 1126 | /// Parses emphasis, starting at the beginning of a run of `*` or `_` |
| 1080 | /// characters. `ip.pos` is left at the last character in the run after | 1127 | /// characters. `ip.pos` is left at the last character in the run after |
| 1081 | /// parsing. | 1128 | /// parsing. |
lib/docs/wasm/markdown/renderer.zig+5-1| ... | @@ -140,6 +140,10 @@ pub fn Renderer(comptime Writer: type, comptime Context: type) type { | ... | @@ -140,6 +140,10 @@ pub fn Renderer(comptime Writer: type, comptime Context: type) type { |
| 140 | } | 140 | } |
| 141 | try writer.writeAll("</a>"); | 141 | try writer.writeAll("</a>"); |
| 142 | }, | 142 | }, |
| 143 | .autolink => { | ||
| 144 | const target = doc.string(data.text.content); | ||
| 145 | try writer.print("<a href=\"{0}\">{0}</a>", .{fmtHtml(target)}); | ||
| 146 | }, | ||
| 143 | .image => { | 147 | .image => { |
| 144 | const target = doc.string(data.link.target); | 148 | const target = doc.string(data.link.target); |
| 145 | try writer.print("<img src=\"{}\" alt=\"", .{fmtHtml(target)}); | 149 | try writer.print("<img src=\"{}\" alt=\"", .{fmtHtml(target)}); |
| ... | @@ -215,7 +219,7 @@ pub fn renderInlineNodeText( | ... | @@ -215,7 +219,7 @@ pub fn renderInlineNodeText( |
| 215 | try renderInlineNodeText(doc, child, writer); | 219 | try renderInlineNodeText(doc, child, writer); |
| 216 | } | 220 | } |
| 217 | }, | 221 | }, |
| 218 | .code_span, .text => { | 222 | .autolink, .code_span, .text => { |
| 219 | const content = doc.string(data.text.content); | 223 | const content = doc.string(data.text.content); |
| 220 | try writer.print("{}", .{fmtHtml(content)}); | 224 | try writer.print("{}", .{fmtHtml(content)}); |
| 221 | }, | 225 | }, |