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 = "";
+ // 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" ++
+ "
";
+ 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