authorgravatar for ian@ianjohnson.devIan Johnson <ian@ianjohnson.dev> 2024-03-18 22:12:06-04:00
committergravatar for ian@ianjohnson.devIan Johnson <ian@ianjohnson.dev> 2024-03-22 20:03:32-04:00
logd3ca9d55d9bae81aa3d01cd9936ff66bb26a8e9c
tree81da60ff9f20902d0031e8b81644733630331e9d
parent13a9d94a8038727469cf11b72273ce4ea6d89faa

Autodoc: implement Markdown autolinks

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 balanced75//! 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 is84//! - **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}
712718
719test "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
713test "images" {743test "images" {
714 try testRender(744 try testRender(
715 \\![Alt text](https://example.com/image.png)745 \\![Alt text](https://example.com/image.png)
lib/docs/wasm/markdown/Document.zig+2
...@@ -51,6 +51,8 @@ pub const Node = struct {...@@ -51,6 +51,8 @@ pub const Node = struct {
51 // Inlines51 // 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 }
10781079
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 after1127 /// 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 },