diff --git a/lib/docs/wasm/markdown.zig b/lib/docs/wasm/markdown.zig index 4ce1ee15b462..092906c46afc 100644 --- a/lib/docs/wasm/markdown.zig +++ b/lib/docs/wasm/markdown.zig @@ -75,6 +75,12 @@ //! content. `target` may contain `\`-escaped characters and balanced //! parentheses. //! +//! - **Autolink** - an abbreviated link, of the format ``, where +//! `target` serves as both the link target and text. `target` may not +//! contain spaces or `<`, and any `\` in it are interpreted literally (not as +//! escapes). `target` is expected to be an absolute URI: an autolink will not +//! be recognized unless `target` starts with a URI scheme followed by a `:`. +//! //! - **Image** - a link directly preceded by a `!`. The link text is //! interpreted as the alt text of the image. //! @@ -710,6 +716,30 @@ test "links" { ); } +test "autolinks" { + try testRender( + \\ + \\**This is important: ** + \\ + \\ + \\ + \\1 < 2 + \\4 > 3 + \\Unclosed: < + \\ + , + \\

https://example.com + \\This is important: https://example.com/strong + \\https://example.com?query=abc.123#page(parens) + \\<placeholder> + \\data: + \\1 < 2 + \\4 > 3 + \\Unclosed: <

+ \\ + ); +} + test "images" { try testRender( \\![Alt text](https://example.com/image.png) diff --git a/lib/docs/wasm/markdown/Document.zig b/lib/docs/wasm/markdown/Document.zig index 9e43e357957b..f3c0fdeed064 100644 --- a/lib/docs/wasm/markdown/Document.zig +++ b/lib/docs/wasm/markdown/Document.zig @@ -51,6 +51,8 @@ pub const Node = struct { // Inlines /// Data is `link`. link, + /// Data is `text`. + autolink, /// Data is `link`. image, /// Data is `container`. diff --git a/lib/docs/wasm/markdown/Parser.zig b/lib/docs/wasm/markdown/Parser.zig index 7cee59674635..5a52882e4821 100644 --- a/lib/docs/wasm/markdown/Parser.zig +++ b/lib/docs/wasm/markdown/Parser.zig @@ -985,6 +985,7 @@ const InlineParser = struct { ip.pos += 1; }, ']' => try ip.parseLink(), + '<' => try ip.parseAutolink(), '*', '_' => try ip.parseEmphasis(), '`' => try ip.parseCodeSpan(), else => {}, @@ -1076,6 +1077,52 @@ const InlineParser = struct { return @enumFromInt(string_top); } + /// Parses an autolink, starting at the opening `<`. `ip.pos` is left at the + /// closing `>`, or remains unchanged at the opening `<` if there is none. + fn parseAutolink(ip: *InlineParser) !void { + const start = ip.pos; + ip.pos += 1; + var state: enum { + start, + scheme, + target, + } = .start; + while (ip.pos < ip.content.len) : (ip.pos += 1) { + switch (state) { + .start => switch (ip.content[ip.pos]) { + 'A'...'Z', 'a'...'z' => state = .scheme, + else => break, + }, + .scheme => switch (ip.content[ip.pos]) { + 'A'...'Z', 'a'...'z', '0'...'9', '+', '.', '-' => {}, + ':' => state = .target, + else => break, + }, + .target => switch (ip.content[ip.pos]) { + '<', ' ', '\t', '\n' => break, // Not allowed in autolinks + '>' => { + // Backslash escapes are not recognized in autolink targets. + const target = try ip.parent.addString(ip.content[start + 1 .. ip.pos]); + const node = try ip.parent.addNode(.{ + .tag = .autolink, + .data = .{ .text = .{ + .content = target, + } }, + }); + try ip.completed_inlines.append(ip.parent.allocator, .{ + .node = node, + .start = start, + .len = ip.pos - start + 1, + }); + return; + }, + else => {}, + }, + } + } + ip.pos = start; + } + /// Parses emphasis, starting at the beginning of a run of `*` or `_` /// characters. `ip.pos` is left at the last character in the run after /// parsing. diff --git a/lib/docs/wasm/markdown/renderer.zig b/lib/docs/wasm/markdown/renderer.zig index fd361a379e37..1e6041399a80 100644 --- a/lib/docs/wasm/markdown/renderer.zig +++ b/lib/docs/wasm/markdown/renderer.zig @@ -140,6 +140,10 @@ pub fn Renderer(comptime Writer: type, comptime Context: type) type { } try writer.writeAll(""); }, + .autolink => { + const target = doc.string(data.text.content); + try writer.print("{0}", .{fmtHtml(target)}); + }, .image => { const target = doc.string(data.link.target); try writer.print("\"", { + .autolink, .code_span, .text => { const content = doc.string(data.text.content); try writer.print("{}", .{fmtHtml(content)}); },