diff --git a/src/browser/RenderTree.zig b/src/browser/RenderTree.zig index 211846b76..fe3ea26c9 100644 --- a/src/browser/RenderTree.zig +++ b/src/browser/RenderTree.zig @@ -343,7 +343,7 @@ fn measureChild(self: *const RenderTree, child: Child, where: Measure.Where, m: const el = child.node.subtype(Element); const inner: Measure.Where = .{ .shell = where.shell or dump_html.isShellElement(el), - .link = where.link or el.getTag() == .anchor, + .link = where.link or (el.getTag() == .anchor and el.getAttributeInterned("href") != null), }; if (el.is(Slot)) |slot| { var it = self.slotted(slot); diff --git a/src/browser/clutter.zig b/src/browser/clutter.zig index e7964767a..5c64fb482 100644 --- a/src/browser/clutter.zig +++ b/src/browser/clutter.zig @@ -286,9 +286,10 @@ const Pass = struct { var inner = ctx; switch (tag) { - .anchor => { + // Unlike readability: without href, is a placeholder + // (tag chips, JS buttons), and its text is not link text. + .anchor => if (el.getAttributeSafe(comptime .wrap("href"))) |href| { inner.in_link = true; - const href = el.getAttributeSafe(comptime .wrap("href")) orelse ""; inner.hash_link = href.len > 0 and href[0] == '#'; }, .h1, .h2, .h3, .h4, .h5, .h6 => inner.in_heading = true, @@ -921,6 +922,15 @@ test "clutter: qualifying sibling paragraphs come along" { try testing.expectEqual(null, std.mem.indexOf(u8, out, "[a]")); } +test "clutter: anchors without href are not link text" { + const quote = "
" ++ prose ++ "
Tags: bread
"; + // A short quote with many tags: over 0.2 link density if tags counted. + const tagged = "
I have not failed. I've just found ten thousand ways that won't work.by Thomas A. Edison" ++ + "
Tags: edison failure inspirational paraphrased
"; + const out = try extract("
" ++ quote ++ quote ++ tagged ++ quote ++ "
"); + try testing.expectEqual(true, std.mem.indexOf(u8, out, "ten thousand ways") != null); +} + test "clutter: the prune set lives in the caller's allocator" { const frame = try testing.createFrame(); defer testing.test_session.closeAllPages(); diff --git a/src/browser/markdown.zig b/src/browser/markdown.zig index c7c137f2d..c4c756eac 100644 --- a/src/browser/markdown.zig +++ b/src/browser/markdown.zig @@ -82,9 +82,11 @@ const Context = struct { // what follows after the children const Epilogue = union(enum) { none, + // A standalone anchor without href: its own line, but no link syntax. + standalone_text, element: Element.Tag, - block_anchor: struct { href: ?[]const u8, label: ?[]const u8 }, - inline_anchor: struct { href: ?[]const u8, standalone: bool }, + block_anchor: struct { href: []const u8, label: ?[]const u8 }, + inline_anchor: struct { href: []const u8, standalone: bool }, }; fn deinit(self: *Context) void { @@ -259,15 +261,18 @@ const Context = struct { }, .anchor => { const frame = self.frame; + // Without href, is a placeholder, not a hyperlink. + const href_raw = el.getAttributeInterned("href") orelse { + if (!RenderTree.isStandaloneAnchor(el, frame)) { + return self.open(.init(self.tree.content(el, boxed)), .none); + } + try self.ensureNewline(); + return self.open(.init(self.tree.content(el, boxed)), .standalone_text); + }; + const info = RenderTree.analyzeContent(el.asNode(), frame); const label = getAnchorLabel(el); - const href_raw = el.getAttributeInterned("href"); - - if (!info.has_visible and label == null and href_raw == null) { - return; - } - - const href = if (href_raw) |h| URL.resolve(frame.local_arena, frame.base(), h, .{ .encoding = frame.charset }) catch h else null; + const href = URL.resolve(frame.local_arena, frame.base(), href_raw, .{ .encoding = frame.charset }) catch href_raw; if (info.has_block) { return self.open(.init(self.tree.content(el, boxed)), .{ .block_anchor = .{ @@ -309,22 +314,23 @@ const Context = struct { fn close(self: *Context, epilogue: Epilogue) Error!void { const tag = switch (epilogue) { .none => return, + .standalone_text => { + try self.ensureNewline(); + return; + }, .element => |t| t, .block_anchor => |anchor| { - const href = anchor.href orelse return; try self.ensureNewline(); try self.writer.writeByte('['); - try self.writer.writeAll(anchor.label orelse href); + try self.writer.writeAll(anchor.label orelse anchor.href); try self.writer.writeAll("]("); - try self.writer.writeAll(href); + try self.writer.writeAll(anchor.href); try self.writer.writeAll(")\n"); return; }, .inline_anchor => |anchor| { try self.writer.writeAll("]("); - if (anchor.href) |h| { - try self.writer.writeAll(h); - } + try self.writer.writeAll(anchor.href); try self.writer.writeByte(')'); if (anchor.standalone) { try self.writer.writeByte('\n'); @@ -756,6 +762,20 @@ test "browser.markdown: anchor fallback label" { , "[](http://localhost/no-label)\n"); } +test "browser.markdown: anchor without href is plain text" { + try testMarkdownHTML( + \\

Read the glossary first.

+ , "\nRead the glossary first.\n"); + + try testMarkdownHTML( + \\ + , "Home\n**About**\n"); + + try testMarkdownHTML( + \\ + , ""); +} + test "browser.markdown: hidden elements are skipped" { try testMarkdownHTML( \\

before