From 326851ed6fc30cf9eed5b2985e5d2b0acf6d68e4 Mon Sep 17 00:00:00 2001 From: Francis Bouvier Date: Mon, 16 Jun 2025 16:06:59 -0700 Subject: [PATCH 1/2] markdown: first implementation --- src/browser/markdown.zig | 271 +++++++++++++++++++++++++++++++++++++++ src/browser/page.zig | 13 ++ src/main.zig | 22 +++- 3 files changed, 305 insertions(+), 1 deletion(-) create mode 100644 src/browser/markdown.zig diff --git a/src/browser/markdown.zig b/src/browser/markdown.zig new file mode 100644 index 000000000..f83da9340 --- /dev/null +++ b/src/browser/markdown.zig @@ -0,0 +1,271 @@ +// Copyright (C) 2023-2025 Lightpanda (Selecy SAS) +// +// Francis Bouvier +// Pierre Tachoire +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +const std = @import("std"); + +const parser = @import("netsurf.zig"); +const Walker = @import("dom/walker.zig").WalkerChildren; + +const NP = "\n\n"; + +// writer must be a std.io.Writer +pub fn writeMarkdown(doc: *parser.Document, writer: anytype) !void { + _ = try writeChildren(parser.documentToNode(doc), true, writer); + try writer.writeAll("\n"); +} + +fn writeChildren(root: *parser.Node, new_para: bool, writer: anytype) !bool { + const walker = Walker{}; + var next: ?*parser.Node = null; + var _new_para = new_para; + while (true) { + next = try walker.get_next(root, next) orelse break; + _new_para = try writeNode(next.?, _new_para, writer); + } + return _new_para; +} + +fn skipTextChild(root: *parser.Node) !*parser.Node { + const child = parser.nodeFirstChild(root) orelse return root; + const node_type = try parser.nodeType(child); + if (node_type == .text) return child; + return root; +} + +// the returned boolean can be either: +// - true if a new paragraph has been written at the end +// - false if an inline text (ie. without new paragraph) has been written at the end +// - the value of the writeChildren function if it has been called recursively at the end +// - the new_para received as argument otherwise +fn writeNode(node: *parser.Node, new_para: bool, writer: anytype) anyerror!bool { + switch (try parser.nodeType(node)) { + .element => { + const html_element: *parser.ElementHTML = @ptrCast(node); + const tag = try parser.elementHTMLGetTagType(html_element); + + // debug + // try writer.writeAll(@tagName(tag)); + // try writer.writeAll("-"); + // if (new_para) { + // try writer.writeAll("1"); + // } else { + // try writer.writeAll("0"); + // } + + switch (tag) { + + // skip element, go to children + .html, .head, .header, .footer, .meta, .link, .body => { + return try writeChildren(node, new_para, writer); + }, + + // skip element and children (eg. text) + .title, .i, .script, .noscript, .undef, .style => return new_para, + + // generic elements + .h1, .h2, .h3, .h4 => { + if (!new_para) { + try writer.writeAll(NP); + } + switch (tag) { + .h1 => try writer.writeAll("# "), + .h2 => try writer.writeAll("## "), + .h3 => try writer.writeAll("### "), + .h4 => try writer.writeAll("#### "), + else => @panic("only headers tags are supported here"), + } + const np = try writeChildren(node, false, writer); + if (!np) try writer.writeAll(NP); + return true; + }, + + // containers and dividers + .nav, .section, .article, .p, .div, .button, .form => { + if (!new_para) try writer.writeAll(NP); + const np = try writeChildren(node, true, writer); + if (!np) try writer.writeAll(NP); + return true; + }, + .span => { + return try writeChildren(node, new_para, writer); + }, + .b => { + try writer.writeAll("**"); + _ = try writeChildren(node, false, writer); + try writer.writeAll("**"); + return false; + }, + .br => { + if (!new_para) try writer.writeAll(NP); + return try writeChildren(node, true, writer); + }, + .hr => { + if (!new_para) try writer.writeAll(NP); + try writer.writeAll("---"); + try writer.writeAll(NP); + return true; + }, + + // specific elements + .a => { + const element = parser.nodeToElement(node); + if (try getAttributeValue(element, "href")) |href| { + // TODO: absolute path? + try writer.writeAll("["); + _ = try writeChildren(node, false, writer); + try writer.writeAll("]("); + try writer.writeAll(href); + try writer.writeAll(")"); + return false; + } + return try writeChildren(node, new_para, writer); + }, + .img => { + const element = parser.nodeToElement(node); + if (try getAttributeValue(element, "src")) |src| { + // TODO: absolute path? + try writer.writeAll("!["); + if (try getAttributeValue(element, "alt")) |alt| { + try writer.writeAll(alt); + } else { + try writer.writeAll(src); + } + try writer.writeAll("]("); + try writer.writeAll(src); + try writer.writeAll(")"); + return false; + } + return new_para; + }, + .ol => { + if (!new_para) try writer.writeAll(NP); + const np = try writeChildren(node, true, writer); + if (!np) try writer.writeAll(NP); + return true; + }, + .ul => { + if (!new_para) try writer.writeAll(NP); + const np = try writeChildren(node, true, writer); + if (!np) try writer.writeAll(NP); + return true; + }, + .li => { + if (!new_para) try writer.writeAll("\n"); + try writer.writeAll("- "); + return try writeChildren(node, false, writer); + }, + .input => { + const element = parser.nodeToElement(node); + if (try getAttributeValue(element, "value")) |value| { + try writer.writeAll(value); + try writer.writeAll(" "); + } + return false; + }, + else => { + try writer.writeAll("\n"); + try writer.writeAll(@tagName(tag)); + try writer.writeAll(" not supported\n"); + }, + } + // panic + }, + .text => { + const v = try parser.nodeValue(node) orelse return new_para; + const printed = try writeText(v, writer); + if (printed) return false; + return new_para; + }, + .cdata_section => { + return new_para; + }, + .comment => { + return new_para; + }, + // TODO handle processing instruction dump + .processing_instruction => return new_para, + // document fragment is outside of the main document DOM, so we + // don't output it. + .document_fragment => return new_para, + // document will never be called, but required for completeness. + .document => return new_para, + // done globally instead, but required for completeness. Only the outer DOCTYPE should be written + .document_type => return new_para, + // deprecated + .attribute, .entity_reference, .entity, .notation => return new_para, + } + return new_para; +} + +// TODO: not sure about + - . ! as they are very common characters +// I fear that we add too much escape strings +// TODO: | (pipe) +const escape = [_]u8{ '\\', '`', '*', '_', '{', '}', '[', ']', '<', '>', '(', ')', '#' }; + +fn writeText(value: []const u8, writer: anytype) !bool { + if (value.len == 0) return false; + + var last_char: u8 = ' '; + var printed: u64 = 0; + for (value) |v| { + // do not print: + // - multiple spaces + // - return line + // - tabs + if (v == last_char and v == ' ') continue; + if (v == '\n') continue; + if (v == '\t') continue; + + // escape char + for (escape) |esc| { + if (v == esc) try writer.writeAll("\\"); + } + + last_char = v; + printed += 1; + const x = [_]u8{v}; // TODO: do we have something better? + try writer.writeAll(&x); + } + if (printed > 0) return true; + return false; +} + +fn getAttributeValue(elem: *parser.Element, attr: []const u8) !?[]const u8 { + if (try parser.elementGetAttribute(elem, attr)) |value| { + if (value.len > 0) return value; + } + return null; +} + +fn writeEscapedTextNode(writer: anytype, value: []const u8) !void { + var v = value; + while (v.len > 0) { + try writer.writeAll("TEXT: "); + const index = std.mem.indexOfAnyPos(u8, v, 0, &.{ '&', '<', '>' }) orelse { + return writer.writeAll(v); + }; + try writer.writeAll(v[0..index]); + switch (v[index]) { + '&' => try writer.writeAll("&"), + '<' => try writer.writeAll("<"), + '>' => try writer.writeAll(">"), + else => unreachable, + } + v = v[index + 1 ..]; + } +} diff --git a/src/browser/page.zig b/src/browser/page.zig index 21faa208a..75c144f6b 100644 --- a/src/browser/page.zig +++ b/src/browser/page.zig @@ -22,6 +22,7 @@ const builtin = @import("builtin"); const Allocator = std.mem.Allocator; const Dump = @import("dump.zig"); +const Markdown = @import("markdown.zig"); const State = @import("State.zig"); const Env = @import("env.zig").Env; const Mime = @import("mime.zig").Mime; @@ -147,6 +148,18 @@ pub const Page = struct { try Dump.writeHTML(doc, out); } + // dump writes the page content into the given file. + pub fn markdown(self: *const Page, out: std.fs.File) !void { + if (self.raw_data) |_| { + // raw_data was set if the document was not HTML we can not convert it to Markdown, + return error.HTMLDocument; + } + + // if the page has a pointer to a document, converts the HTML in Markdown and dump it. + const doc = parser.documentHTMLToDocument(self.window.document); + try Markdown.writeMarkdown(doc, out); + } + pub fn fetchModuleSource(ctx: *anyopaque, specifier: []const u8) !?[]const u8 { const self: *Page = @ptrCast(@alignCast(ctx)); const base = if (self.current_script) |s| s.src else null; diff --git a/src/main.zig b/src/main.zig index c95f7a6d7..356391da2 100644 --- a/src/main.zig +++ b/src/main.zig @@ -103,7 +103,7 @@ fn run(alloc: Allocator) !void { }; }, .fetch => |opts| { - log.debug(.app, "startup", .{ .mode = "fetch", .dump = opts.dump, .url = opts.url }); + log.debug(.app, "startup", .{ .mode = "fetch", .dump = opts.dump, .markdown = opts.markdown, .url = opts.url }); const url = try @import("url.zig").URL.parse(opts.url, null); // browser @@ -128,6 +128,13 @@ fn run(alloc: Allocator) !void { try page.wait(); + // markdown + if (opts.markdown) { + try page.markdown(std.io.getStdOut()); + // do not dump HTML if both options are provided + return; + } + // dump if (opts.dump) { try page.dump(std.io.getStdOut()); @@ -193,6 +200,7 @@ const Command = struct { const Fetch = struct { url: []const u8, dump: bool = false, + markdown: bool = false, common: Common, }; @@ -241,6 +249,9 @@ const Command = struct { \\--dump Dumps document to stdout. \\ Defaults to false. \\ + \\--markdown Converts document in Markdown format and dumps it to stdout. + \\ Defaults to false. + \\ ++ common_options ++ \\ \\serve command @@ -317,6 +328,9 @@ fn inferMode(opt: []const u8) ?App.RunMode { if (std.mem.eql(u8, opt, "--dump")) { return .fetch; } + if (std.mem.eql(u8, opt, "--markdown")) { + return .fetch; + } if (std.mem.startsWith(u8, opt, "--") == false) { return .fetch; } @@ -402,6 +416,7 @@ fn parseFetchArgs( args: *std.process.ArgIterator, ) !Command.Fetch { var dump: bool = false; + var markdown: bool = false; var url: ?[]const u8 = null; var common: Command.Common = .{}; @@ -410,6 +425,10 @@ fn parseFetchArgs( dump = true; continue; } + if (std.mem.eql(u8, "--markdown", opt)) { + markdown = true; + continue; + } if (try parseCommonArg(allocator, opt, args, &common)) { continue; @@ -435,6 +454,7 @@ fn parseFetchArgs( return .{ .url = url.?, .dump = dump, + .markdown = markdown, .common = common, }; } From 3575f45ac06170e421a975e2fc0b81ae1db61a69 Mon Sep 17 00:00:00 2001 From: Francis Bouvier Date: Thu, 19 Jun 2025 11:22:05 -0700 Subject: [PATCH 2/2] markdown: working duckduckgo --- src/browser/markdown.zig | 296 ++++++++++++++++++++++++--------------- src/browser/page.zig | 2 +- 2 files changed, 182 insertions(+), 116 deletions(-) diff --git a/src/browser/markdown.zig b/src/browser/markdown.zig index f83da9340..b194bdd40 100644 --- a/src/browser/markdown.zig +++ b/src/browser/markdown.zig @@ -21,195 +21,254 @@ const std = @import("std"); const parser = @import("netsurf.zig"); const Walker = @import("dom/walker.zig").WalkerChildren; +const URL = @import("../url.zig").URL; + const NP = "\n\n"; +const Elem = struct { + inlin: bool = false, + list_order: ?u8 = null, + parent: ?*Elem = null, +}; + +const State = struct { + block: bool, + last_char: u8, + elem: ?*Elem = null, + + fn is_inline(state: *State) bool { + if (state.elem == null) return false; + return state.elem.?.inlin; + } + + fn last_char_space(state: *State) bool { + if (state.last_char == ' ' or state.last_char == '\n') return true; + return false; + } +}; + // writer must be a std.io.Writer -pub fn writeMarkdown(doc: *parser.Document, writer: anytype) !void { - _ = try writeChildren(parser.documentToNode(doc), true, writer); +pub fn writeMarkdown(url: URL, doc: *parser.Document, writer: anytype) !void { + var state = State{ .block = true, .last_char = '\n' }; + _ = try writeChildren(url, parser.documentToNode(doc), &state, writer); try writer.writeAll("\n"); } -fn writeChildren(root: *parser.Node, new_para: bool, writer: anytype) !bool { +fn writeChildren(url: URL, root: *parser.Node, state: *State, writer: anytype) !void { const walker = Walker{}; var next: ?*parser.Node = null; - var _new_para = new_para; while (true) { next = try walker.get_next(root, next) orelse break; - _new_para = try writeNode(next.?, _new_para, writer); + try writeNode(url, next.?, state, writer); } - return _new_para; } -fn skipTextChild(root: *parser.Node) !*parser.Node { - const child = parser.nodeFirstChild(root) orelse return root; - const node_type = try parser.nodeType(child); - if (node_type == .text) return child; - return root; +fn ensureBlock(state: *State, writer: anytype) !void { + if (state.is_inline()) return; + if (!state.block) { + try writer.writeAll(NP); + state.last_char = '\n'; + state.block = true; + } } -// the returned boolean can be either: -// - true if a new paragraph has been written at the end -// - false if an inline text (ie. without new paragraph) has been written at the end -// - the value of the writeChildren function if it has been called recursively at the end -// - the new_para received as argument otherwise -fn writeNode(node: *parser.Node, new_para: bool, writer: anytype) anyerror!bool { +fn writeInline(state: *State, text: []const u8, writer: anytype) !void { + try writer.writeAll(text); + state.last_char = text[text.len - 1]; + if (state.block) state.block = false; +} + +const order = [_][]const u8{ + "1", "2", "3", "4", "5", "6", "7", "8", "9", "10", + "11", "12", "13", "14", "15", "16", "17", "18", "19", "20", + "21", "22", "23", "24", "25", "26", "27", "28", "29", "30", + "31", "32", "33", "34", "35", "36", "37", "38", "39", "40", + "41", "42", "43", "44", "45", "46", "47", "48", "49", "50", +}; + +fn writeNode(url: URL, node: *parser.Node, state: *State, writer: anytype) anyerror!void { switch (try parser.nodeType(node)) { .element => { const html_element: *parser.ElementHTML = @ptrCast(node); const tag = try parser.elementHTMLGetTagType(html_element); // debug + // try writer.writeAll("\nstart - "); // try writer.writeAll(@tagName(tag)); - // try writer.writeAll("-"); - // if (new_para) { - // try writer.writeAll("1"); - // } else { - // try writer.writeAll("0"); - // } + // try writer.writeAll("\n"); switch (tag) { // skip element, go to children - .html, .head, .header, .footer, .meta, .link, .body => { - return try writeChildren(node, new_para, writer); + .html, .head, .meta, .link, .body, .span => { + try writeChildren(url, node, state, writer); }, - // skip element and children (eg. text) - .title, .i, .script, .noscript, .undef, .style => return new_para, + // skip element and children + .title, .i, .script, .noscript, .undef, .style => {}, // generic elements - .h1, .h2, .h3, .h4 => { - if (!new_para) { - try writer.writeAll(NP); - } - switch (tag) { - .h1 => try writer.writeAll("# "), - .h2 => try writer.writeAll("## "), - .h3 => try writer.writeAll("### "), - .h4 => try writer.writeAll("#### "), - else => @panic("only headers tags are supported here"), + .h1, .h2, .h3, .h4, .h5, .h6 => { + try ensureBlock(state, writer); + if (!state.is_inline()) { + switch (tag) { + .h1 => try writeInline(state, "# ", writer), + .h2 => try writeInline(state, "## ", writer), + .h3 => try writeInline(state, "### ", writer), + .h4 => try writeInline(state, "#### ", writer), + .h5 => try writeInline(state, "##### ", writer), + .h6 => try writeInline(state, "###### ", writer), + else => @panic("only headers tags are supported here"), + } } - const np = try writeChildren(node, false, writer); - if (!np) try writer.writeAll(NP); - return true; + try writeChildren(url, node, state, writer); + try ensureBlock(state, writer); }, // containers and dividers - .nav, .section, .article, .p, .div, .button, .form => { - if (!new_para) try writer.writeAll(NP); - const np = try writeChildren(node, true, writer); - if (!np) try writer.writeAll(NP); - return true; - }, - .span => { - return try writeChildren(node, new_para, writer); - }, - .b => { - try writer.writeAll("**"); - _ = try writeChildren(node, false, writer); - try writer.writeAll("**"); - return false; + .header, .footer, .nav, .section, .div, .article, .p, .button, .form => { + try ensureBlock(state, writer); + try writeChildren(url, node, state, writer); + try ensureBlock(state, writer); }, .br => { - if (!new_para) try writer.writeAll(NP); - return try writeChildren(node, true, writer); + try ensureBlock(state, writer); + try writeChildren(url, node, state, writer); }, .hr => { - if (!new_para) try writer.writeAll(NP); - try writer.writeAll("---"); - try writer.writeAll(NP); - return true; + try ensureBlock(state, writer); + try writeInline(state, "---", writer); + try ensureBlock(state, writer); + }, + + // styling + .b => { + var elem = Elem{ .parent = state.elem, .inlin = true }; + state.elem = &elem; + defer state.elem = elem.parent; + try writeInline(state, "**", writer); + try writeChildren(url, node, state, writer); + try writeInline(state, "**", writer); }, // specific elements .a => { + if (!state.last_char_space()) try writeInline(state, " ", writer); + var elem = Elem{ .parent = state.elem, .inlin = true }; + state.elem = &elem; + defer state.elem = elem.parent; const element = parser.nodeToElement(node); if (try getAttributeValue(element, "href")) |href| { - // TODO: absolute path? - try writer.writeAll("["); - _ = try writeChildren(node, false, writer); - try writer.writeAll("]("); - try writer.writeAll(href); - try writer.writeAll(")"); - return false; + try writeInline(state, "[", writer); + try writeChildren(url, node, state, writer); + try writeInline(state, "](", writer); + // handle relative path + if (href[0] == '/') { + try writeInline(state, url.scheme(), writer); + try writeInline(state, "://", writer); + try writeInline(state, url.host(), writer); + } + try writeInline(state, href, writer); + try writeInline(state, ")", writer); + } else { + try writeChildren(url, node, state, writer); } - return try writeChildren(node, new_para, writer); }, .img => { + var elem = Elem{ .parent = state.elem, .inlin = true }; + state.elem = &elem; + defer state.elem = elem.parent; const element = parser.nodeToElement(node); if (try getAttributeValue(element, "src")) |src| { - // TODO: absolute path? - try writer.writeAll("!["); + try writeInline(state, "![", writer); if (try getAttributeValue(element, "alt")) |alt| { - try writer.writeAll(alt); + try writeInline(state, alt, writer); } else { - try writer.writeAll(src); + try writeInline(state, src, writer); + } + try writeInline(state, "](", writer); + // handle relative path + if (src[0] == '/') { + try writeInline(state, url.scheme(), writer); + try writeInline(state, "://", writer); + try writeInline(state, url.host(), writer); } - try writer.writeAll("]("); - try writer.writeAll(src); - try writer.writeAll(")"); - return false; + try writeInline(state, src, writer); + try writeInline(state, ")", writer); } - return new_para; - }, - .ol => { - if (!new_para) try writer.writeAll(NP); - const np = try writeChildren(node, true, writer); - if (!np) try writer.writeAll(NP); - return true; }, .ul => { - if (!new_para) try writer.writeAll(NP); - const np = try writeChildren(node, true, writer); - if (!np) try writer.writeAll(NP); - return true; + var elem = Elem{ .parent = state.elem, .list_order = 0 }; + state.elem = &elem; + defer state.elem = elem.parent; + try ensureBlock(state, writer); + try writeChildren(url, node, state, writer); + try ensureBlock(state, writer); + }, + .ol => { + var elem = Elem{ .parent = state.elem, .list_order = 1 }; + state.elem = &elem; + defer state.elem = elem.parent; + try ensureBlock(state, writer); + try writeChildren(url, node, state, writer); + try ensureBlock(state, writer); }, - .li => { - if (!new_para) try writer.writeAll("\n"); - try writer.writeAll("- "); - return try writeChildren(node, false, writer); + .li => blk: { + const parent = state.elem orelse break :blk; + const list_order = parent.list_order orelse break :blk; + if (!state.block) try writer.writeAll("\n"); + if (list_order > 0) { + // ordered list + try writeInline(state, order[list_order - 1], writer); + try writeInline(state, ". ", writer); + parent.list_order = list_order + 1; + } else { + // unordered list + try writeInline(state, "- ", writer); + } + try writeChildren(url, node, state, writer); }, .input => { + var elem = Elem{ .parent = state.elem, .inlin = true }; + state.elem = &elem; + defer state.elem = elem.parent; const element = parser.nodeToElement(node); if (try getAttributeValue(element, "value")) |value| { - try writer.writeAll(value); - try writer.writeAll(" "); + try writeInline(state, value, writer); + try writeInline(state, " ", writer); } - return false; }, + else => { - try writer.writeAll("\n"); + try ensureBlock(state, writer); try writer.writeAll(@tagName(tag)); - try writer.writeAll(" not supported\n"); + try writer.writeAll(" not supported"); + try ensureBlock(state, writer); }, } - // panic + // try writer.writeAll("\nend - "); + // try writer.writeAll(@tagName(tag)); + // try writer.writeAll("\n"); }, .text => { - const v = try parser.nodeValue(node) orelse return new_para; - const printed = try writeText(v, writer); - if (printed) return false; - return new_para; - }, - .cdata_section => { - return new_para; - }, - .comment => { - return new_para; + const v = try parser.nodeValue(node) orelse return; + const printed = try writeText(state, v, writer); + if (printed) state.block = false; }, + .cdata_section => {}, + .comment => {}, // TODO handle processing instruction dump - .processing_instruction => return new_para, + .processing_instruction => {}, // document fragment is outside of the main document DOM, so we // don't output it. - .document_fragment => return new_para, + .document_fragment => {}, // document will never be called, but required for completeness. - .document => return new_para, + .document => {}, // done globally instead, but required for completeness. Only the outer DOCTYPE should be written - .document_type => return new_para, + .document_type => {}, // deprecated - .attribute, .entity_reference, .entity, .notation => return new_para, + .attribute, .entity_reference, .entity, .notation => {}, } - return new_para; } // TODO: not sure about + - . ! as they are very common characters @@ -217,12 +276,12 @@ fn writeNode(node: *parser.Node, new_para: bool, writer: anytype) anyerror!bool // TODO: | (pipe) const escape = [_]u8{ '\\', '`', '*', '_', '{', '}', '[', ']', '<', '>', '(', ')', '#' }; -fn writeText(value: []const u8, writer: anytype) !bool { +fn writeText(state: *State, value: []const u8, writer: anytype) !bool { if (value.len == 0) return false; var last_char: u8 = ' '; var printed: u64 = 0; - for (value) |v| { + for (value, 0..) |v, i| { // do not print: // - multiple spaces // - return line @@ -236,10 +295,17 @@ fn writeText(value: []const u8, writer: anytype) !bool { if (v == esc) try writer.writeAll("\\"); } + if (printed == 0 and !state.is_inline()) { + if (state.last_char != '\n' and state.last_char != ' ') { + try writer.writeAll(" "); + } + } + last_char = v; printed += 1; const x = [_]u8{v}; // TODO: do we have something better? try writer.writeAll(&x); + if (i == value.len - 1) state.last_char = v; } if (printed > 0) return true; return false; diff --git a/src/browser/page.zig b/src/browser/page.zig index 75c144f6b..d14a527ff 100644 --- a/src/browser/page.zig +++ b/src/browser/page.zig @@ -157,7 +157,7 @@ pub const Page = struct { // if the page has a pointer to a document, converts the HTML in Markdown and dump it. const doc = parser.documentHTMLToDocument(self.window.document); - try Markdown.writeMarkdown(doc, out); + try Markdown.writeMarkdown(self.url, doc, out); } pub fn fetchModuleSource(ctx: *anyopaque, specifier: []const u8) !?[]const u8 {