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 std = @import("std");
|
||||||
|
|
||||||
const Page = @import("Page.zig");
|
const Page = @import("Page.zig");
|
||||||
|
const URL = @import("URL.zig");
|
||||||
const CData = @import("webapi/CData.zig");
|
const CData = @import("webapi/CData.zig");
|
||||||
const Element = @import("webapi/Element.zig");
|
const Element = @import("webapi/Element.zig");
|
||||||
const Node = @import("webapi/Node.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 {
|
fn isAllWhitespace(text: []const u8) bool {
|
||||||
return for (text) |c| {
|
return for (text) |c| {
|
||||||
if (!std.ascii.isWhitespace(c)) break false;
|
if (!std.ascii.isWhitespace(c)) break false;
|
||||||
@@ -119,6 +124,21 @@ fn hasBlockDescendant(node: *Node) bool {
|
|||||||
} else false;
|
} 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 {
|
fn ensureNewline(state: *State, writer: *std.Io.Writer) !void {
|
||||||
if (!state.last_char_was_newline) {
|
if (!state.last_char_was_newline) {
|
||||||
try writer.writeByte('\n');
|
try writer.writeByte('\n');
|
||||||
@@ -278,20 +298,29 @@ fn renderElement(el: *Element, state: *State, writer: *std.Io.Writer, page: *Pag
|
|||||||
}
|
}
|
||||||
try writer.writeAll("](");
|
try writer.writeAll("](");
|
||||||
if (el.getAttributeSafe(comptime .wrap("src"))) |src| {
|
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(")");
|
try writer.writeAll(")");
|
||||||
state.last_char_was_newline = false;
|
state.last_char_was_newline = false;
|
||||||
return;
|
return;
|
||||||
},
|
},
|
||||||
.anchor => {
|
.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 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) {
|
if (has_block) {
|
||||||
try renderChildren(el.asNode(), state, writer, page);
|
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');
|
if (!state.last_char_was_newline) try writer.writeByte('\n');
|
||||||
try writer.writeAll("([Link](");
|
try writer.writeAll("([](");
|
||||||
try writer.writeAll(href);
|
try writer.writeAll(h);
|
||||||
try writer.writeAll("))\n");
|
try writer.writeAll("))\n");
|
||||||
state.last_char_was_newline = true;
|
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 (isStandaloneAnchor(el)) {
|
||||||
if (!state.last_char_was_newline) try writer.writeByte('\n');
|
if (!state.last_char_was_newline) try writer.writeByte('\n');
|
||||||
try writer.writeByte('[');
|
try writer.writeByte('[');
|
||||||
|
if (has_content) {
|
||||||
try renderChildren(el.asNode(), state, writer, page);
|
try renderChildren(el.asNode(), state, writer, page);
|
||||||
|
} else {
|
||||||
|
try writer.writeAll(label orelse "");
|
||||||
|
}
|
||||||
try writer.writeAll("](");
|
try writer.writeAll("](");
|
||||||
if (el.getAttributeSafe(comptime .wrap("href"))) |href| {
|
if (href) |h| {
|
||||||
try writer.writeAll(href);
|
try writer.writeAll(h);
|
||||||
}
|
}
|
||||||
try writer.writeAll(")\n");
|
try writer.writeAll(")\n");
|
||||||
state.last_char_was_newline = true;
|
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 writer.writeByte('[');
|
||||||
|
if (has_content) {
|
||||||
try renderChildren(el.asNode(), state, writer, page);
|
try renderChildren(el.asNode(), state, writer, page);
|
||||||
|
} else {
|
||||||
|
try writer.writeAll(label orelse "");
|
||||||
|
}
|
||||||
try writer.writeAll("](");
|
try writer.writeAll("](");
|
||||||
if (el.getAttributeSafe(comptime .wrap("href"))) |href| {
|
if (href) |h| {
|
||||||
try writer.writeAll(href);
|
try writer.writeAll(h);
|
||||||
}
|
}
|
||||||
try writer.writeByte(')');
|
try writer.writeByte(')');
|
||||||
state.last_char_was_newline = false;
|
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 testing = @import("../testing.zig");
|
||||||
const page = try testing.test_session.createPage();
|
const page = try testing.test_session.createPage();
|
||||||
defer testing.test_session.removePage();
|
defer testing.test_session.removePage();
|
||||||
|
page.url = "http://localhost/";
|
||||||
|
|
||||||
const doc = page.window._document;
|
const doc = page.window._document;
|
||||||
|
|
||||||
const div = try doc.createElement("div", null, page);
|
const div = try doc.createElement("div", null, page);
|
||||||
@@ -520,11 +559,11 @@ test "browser.markdown: blockquote" {
|
|||||||
}
|
}
|
||||||
|
|
||||||
test "browser.markdown: links" {
|
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" {
|
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" {
|
test "browser.markdown: headings" {
|
||||||
@@ -565,7 +604,7 @@ test "browser.markdown: block link" {
|
|||||||
\\### Title
|
\\### Title
|
||||||
\\
|
\\
|
||||||
\\Description
|
\\Description
|
||||||
\\([Link](https://example.com))
|
\\([](https://example.com))
|
||||||
\\
|
\\
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -588,8 +627,8 @@ test "browser.markdown: standalone anchors" {
|
|||||||
\\ <a href="2">Link 2</a>
|
\\ <a href="2">Link 2</a>
|
||||||
\\</main>
|
\\</main>
|
||||||
,
|
,
|
||||||
\\[Link 1](1)
|
\\[Link 1](http://localhost/1)
|
||||||
\\[Link 2](2)
|
\\[Link 2](http://localhost/2)
|
||||||
\\
|
\\
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -601,7 +640,58 @@ test "browser.markdown: mixed anchors in main" {
|
|||||||
\\ Welcome <a href="1">Link 1</a>.
|
\\ Welcome <a href="1">Link 1</a>.
|
||||||
\\</main>
|
\\</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