diff --git a/src/browser/markdown.zig b/src/browser/markdown.zig index d1bb860e..91e04196 100644 --- a/src/browser/markdown.zig +++ b/src/browser/markdown.zig @@ -19,6 +19,7 @@ const std = @import("std"); const Page = @import("Page.zig"); +const URL = @import("URL.zig"); const CData = @import("webapi/CData.zig"); const Element = @import("webapi/Element.zig"); const Node = @import("webapi/Node.zig"); @@ -103,6 +104,10 @@ fn isVisibleElement(el: *Element) bool { }; } +fn getAnchorLabel(el: *Element) ?[]const u8 { + return el.getAttributeSafe(comptime .wrap("aria-label")) orelse el.getAttributeSafe(comptime .wrap("title")); +} + fn isAllWhitespace(text: []const u8) bool { return for (text) |c| { if (!std.ascii.isWhitespace(c)) break false; @@ -119,6 +124,21 @@ fn hasBlockDescendant(node: *Node) bool { } else false; } +fn hasVisibleContent(node: *Node) bool { + var it = node.childrenIterator(); + while (it.next()) |child| { + if (isSignificantText(child)) return true; + if (child.is(Element)) |el| { + if (!isVisibleElement(el)) continue; + // Images are visible + if (el.getTag() == .img) return true; + // Recursive check + if (hasVisibleContent(child)) return true; + } + } + return false; +} + fn ensureNewline(state: *State, writer: *std.Io.Writer) !void { if (!state.last_char_was_newline) { try writer.writeByte('\n'); @@ -278,20 +298,29 @@ fn renderElement(el: *Element, state: *State, writer: *std.Io.Writer, page: *Pag } try writer.writeAll("]("); if (el.getAttributeSafe(comptime .wrap("src"))) |src| { - try writer.writeAll(src); + const absolute_src = URL.resolve(page.call_arena, page.base(), src, .{ .encode = true }) catch src; + try writer.writeAll(absolute_src); } try writer.writeAll(")"); state.last_char_was_newline = false; return; }, .anchor => { + const has_content = hasVisibleContent(el.asNode()); + const label = getAnchorLabel(el); + const href_raw = el.getAttributeSafe(comptime .wrap("href")); + + if (!has_content and label == null and href_raw == null) return; + const has_block = hasBlockDescendant(el.asNode()); + const href = if (href_raw) |h| URL.resolve(page.call_arena, page.base(), h, .{ .encode = true }) catch h else null; + if (has_block) { try renderChildren(el.asNode(), state, writer, page); - if (el.getAttributeSafe(comptime .wrap("href"))) |href| { + if (href) |h| { if (!state.last_char_was_newline) try writer.writeByte('\n'); - try writer.writeAll("([Link]("); - try writer.writeAll(href); + try writer.writeAll("([]("); + try writer.writeAll(h); try writer.writeAll("))\n"); state.last_char_was_newline = true; } @@ -301,10 +330,14 @@ fn renderElement(el: *Element, state: *State, writer: *std.Io.Writer, page: *Pag if (isStandaloneAnchor(el)) { if (!state.last_char_was_newline) try writer.writeByte('\n'); try writer.writeByte('['); - try renderChildren(el.asNode(), state, writer, page); + if (has_content) { + try renderChildren(el.asNode(), state, writer, page); + } else { + try writer.writeAll(label orelse ""); + } try writer.writeAll("]("); - if (el.getAttributeSafe(comptime .wrap("href"))) |href| { - try writer.writeAll(href); + if (href) |h| { + try writer.writeAll(h); } try writer.writeAll(")\n"); state.last_char_was_newline = true; @@ -312,10 +345,14 @@ fn renderElement(el: *Element, state: *State, writer: *std.Io.Writer, page: *Pag } try writer.writeByte('['); - try renderChildren(el.asNode(), state, writer, page); + if (has_content) { + try renderChildren(el.asNode(), state, writer, page); + } else { + try writer.writeAll(label orelse ""); + } try writer.writeAll("]("); - if (el.getAttributeSafe(comptime .wrap("href"))) |href| { - try writer.writeAll(href); + if (href) |h| { + try writer.writeAll(h); } try writer.writeByte(')'); state.last_char_was_newline = false; @@ -452,6 +489,8 @@ fn testMarkdownHTML(html: []const u8, expected: []const u8) !void { const testing = @import("../testing.zig"); const page = try testing.test_session.createPage(); defer testing.test_session.removePage(); + page.url = "http://localhost/"; + const doc = page.window._document; const div = try doc.createElement("div", null, page); @@ -520,11 +559,11 @@ test "browser.markdown: blockquote" { } test "browser.markdown: links" { - try testMarkdownHTML("Lightpanda", "[Lightpanda](https://lightpanda.io)\n"); + try testMarkdownHTML("Link", "[Link](http://localhost/relative)\n"); } test "browser.markdown: images" { - try testMarkdownHTML("\"Logo\"", "![Logo](logo.png)\n"); + try testMarkdownHTML("\"Logo\"", "![Logo](http://localhost/logo.png)\n"); } test "browser.markdown: headings" { @@ -565,7 +604,7 @@ test "browser.markdown: block link" { \\### Title \\ \\Description - \\([Link](https://example.com)) + \\([](https://example.com)) \\ ); } @@ -588,8 +627,8 @@ test "browser.markdown: standalone anchors" { \\ Link 2 \\ , - \\[Link 1](1) - \\[Link 2](2) + \\[Link 1](http://localhost/1) + \\[Link 2](http://localhost/2) \\ ); } @@ -601,7 +640,58 @@ test "browser.markdown: mixed anchors in main" { \\ Welcome Link 1. \\ , - \\Welcome [Link 1](1). + \\Welcome [Link 1](http://localhost/1). \\ ); } + +test "browser.markdown: skip empty links" { + try testMarkdownHTML( + \\ + \\ + , + \\[](http://localhost/) + \\[](http://localhost/) + \\ + ); +} + +test "browser.markdown: resolve links" { + const testing = @import("../testing.zig"); + const page = try testing.test_session.createPage(); + defer testing.test_session.removePage(); + page.url = "https://example.com/a/index.html"; + + const doc = page.window._document; + const div = try doc.createElement("div", null, page); + try page.parseHtmlAsChildren(div.asNode(), + \\Link + \\Img + \\Space + ); + + var aw: std.Io.Writer.Allocating = .init(testing.allocator); + defer aw.deinit(); + try dump(div.asNode(), .{}, &aw.writer, page); + + try testing.expectString( + \\[Link](https://example.com/a/b) + \\![Img](https://example.com/c.png) + \\[Space](https://example.com/my%20page) + \\ + , aw.written()); +} + +test "browser.markdown: anchor fallback label" { + try testMarkdownHTML( + \\ + , "[Discord Server](http://localhost/discord)\n"); + + try testMarkdownHTML( + \\ + , "[Search Site](http://localhost/search)\n"); + + try testMarkdownHTML( + \\ + , "[](http://localhost/no-label)\n"); +}