mirror of
https://github.com/lightpanda-io/browser.git
synced 2026-03-22 04:34:44 +00:00
Merge pull request #1717 from lightpanda-io/improve-markdown-links
Improve markdown links
This commit is contained in:
@@ -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("<a href=\"https://lightpanda.io\">Lightpanda</a>", "[Lightpanda](https://lightpanda.io)\n");
|
||||
try testMarkdownHTML("<a href=\"/relative\">Link</a>", "[Link](http://localhost/relative)\n");
|
||||
}
|
||||
|
||||
test "browser.markdown: images" {
|
||||
try testMarkdownHTML("<img src=\"logo.png\" alt=\"Logo\">", "\n");
|
||||
try testMarkdownHTML("<img src=\"logo.png\" alt=\"Logo\">", "\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" {
|
||||
\\ <a href="2">Link 2</a>
|
||||
\\</main>
|
||||
,
|
||||
\\[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 <a href="1">Link 1</a>.
|
||||
\\</main>
|
||||
,
|
||||
\\Welcome [Link 1](1).
|
||||
\\Welcome [Link 1](http://localhost/1).
|
||||
\\
|
||||
);
|
||||
}
|
||||
|
||||
test "browser.markdown: skip empty links" {
|
||||
try testMarkdownHTML(
|
||||
\\<a href="/"></a>
|
||||
\\<a href="/"><svg></svg></a>
|
||||
,
|
||||
\\[](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(),
|
||||
\\<a href="b">Link</a>
|
||||
\\<img src="../c.png" alt="Img">
|
||||
\\<a href="/my page">Space</a>
|
||||
);
|
||||
|
||||
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)
|
||||
\\
|
||||
\\[Space](https://example.com/my%20page)
|
||||
\\
|
||||
, aw.written());
|
||||
}
|
||||
|
||||
test "browser.markdown: anchor fallback label" {
|
||||
try testMarkdownHTML(
|
||||
\\<a href="/discord" aria-label="Discord Server"><svg></svg></a>
|
||||
, "[Discord Server](http://localhost/discord)\n");
|
||||
|
||||
try testMarkdownHTML(
|
||||
\\<a href="/search" title="Search Site"><svg></svg></a>
|
||||
, "[Search Site](http://localhost/search)\n");
|
||||
|
||||
try testMarkdownHTML(
|
||||
\\<a href="/no-label"><svg></svg></a>
|
||||
, "[](http://localhost/no-label)\n");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user