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 @@
7575//! content. `target` may contain `\`-escaped characters and balanced
7676//! parentheses.
7777//!
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//!
7884//! - **Image** - a link directly preceded by a `!`. The link text is
7985//! interpreted as the alt text of the image.
8086//!
......@@ -710,6 +716,30 @@ test "links" {
710716 );
711717}
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
713743test "images" {
714744 try testRender(
715745 \\![Alt text](https://example.com/image.png)
lib/docs/wasm/markdown/Document.zig+2
......@@ -51,6 +51,8 @@ pub const Node = struct {
5151 // Inlines
5252 /// Data is `link`.
5353 link,
54 /// Data is `text`.
55 autolink,
5456 /// Data is `link`.
5557 image,
5658 /// Data is `container`.
lib/docs/wasm/markdown/Parser.zig+47
......@@ -985,6 +985,7 @@ const InlineParser = struct {
985985 ip.pos += 1;
986986 },
987987 ']' => try ip.parseLink(),
988 '<' => try ip.parseAutolink(),
988989 '*', '_' => try ip.parseEmphasis(),
989990 '`' => try ip.parseCodeSpan(),
990991 else => {},
......@@ -1076,6 +1077,52 @@ const InlineParser = struct {
10761077 return @enumFromInt(string_top);
10771078 }
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
10791126 /// Parses emphasis, starting at the beginning of a run of `*` or `_`
10801127 /// characters. `ip.pos` is left at the last character in the run after
10811128 /// parsing.
lib/docs/wasm/markdown/renderer.zig+5-1
......@@ -140,6 +140,10 @@ pub fn Renderer(comptime Writer: type, comptime Context: type) type {
140140 }
141141 try writer.writeAll("</a>");
142142 },
143 .autolink => {
144 const target = doc.string(data.text.content);
145 try writer.print("<a href=\"{0}\">{0}</a>", .{fmtHtml(target)});
146 },
143147 .image => {
144148 const target = doc.string(data.link.target);
145149 try writer.print("<img src=\"{}\" alt=\"", .{fmtHtml(target)});
......@@ -215,7 +219,7 @@ pub fn renderInlineNodeText(
215219 try renderInlineNodeText(doc, child, writer);
216220 }
217221 },
218 .code_span, .text => {
222 .autolink, .code_span, .text => {
219223 const content = doc.string(data.text.content);
220224 try writer.print("{}", .{fmtHtml(content)});
221225 },