diff --git a/src/apiweb.zig b/src/apiweb.zig index e45a9790..ad825044 100644 --- a/src/apiweb.zig +++ b/src/apiweb.zig @@ -27,6 +27,7 @@ const XHR = @import("xhr/xhr.zig"); const Storage = @import("storage/storage.zig"); const URL = @import("url/url.zig"); const Iterators = @import("iterator/iterator.zig"); +const XMLSerializer = @import("xmlserializer/xmlserializer.zig"); pub const HTMLDocument = @import("html/document.zig").HTMLDocument; @@ -40,6 +41,7 @@ pub const Interfaces = generate.Tuple(.{ Storage.Interfaces, URL.Interfaces, Iterators.Interfaces, + XMLSerializer.Interfaces, }); pub const UserContext = @import("user_context.zig").UserContext; diff --git a/src/browser/dump.zig b/src/browser/dump.zig index 7b705d1b..74fea1d0 100644 --- a/src/browser/dump.zig +++ b/src/browser/dump.zig @@ -25,82 +25,86 @@ const Walker = @import("../dom/walker.zig").WalkerChildren; // writer must be a std.io.Writer pub fn writeHTML(doc: *parser.Document, writer: anytype) !void { try writer.writeAll("\n"); - try writeNode(parser.documentToNode(doc), writer); + try writeChildren(parser.documentToNode(doc), writer); try writer.writeAll("\n"); } +pub fn writeNode(node: *parser.Node, writer: anytype) anyerror!void { + switch (try parser.nodeType(node)) { + .element => { + // open the tag + const tag = try parser.nodeLocalName(node); + try writer.writeAll("<"); + try writer.writeAll(tag); + + // write the attributes + const map = try parser.nodeGetAttributes(node); + const ln = try parser.namedNodeMapGetLength(map); + var i: u32 = 0; + while (i < ln) { + const attr = try parser.namedNodeMapItem(map, i) orelse break; + try writer.writeAll(" "); + try writer.writeAll(try parser.attributeGetName(attr)); + try writer.writeAll("=\""); + try writer.writeAll(try parser.attributeGetValue(attr) orelse ""); + try writer.writeAll("\""); + i += 1; + } + + try writer.writeAll(">"); + + // void elements can't have any content. + if (try isVoid(parser.nodeToElement(node))) return; + + // write the children + // TODO avoid recursion + try writeChildren(node, writer); + + // close the tag + try writer.writeAll(""); + }, + .text => { + const v = try parser.nodeValue(node) orelse return; + try writer.writeAll(v); + }, + .cdata_section => { + const v = try parser.nodeValue(node) orelse return; + try writer.writeAll(""); + }, + .comment => { + const v = try parser.nodeValue(node) orelse return; + try writer.writeAll(""); + }, + // TODO handle processing instruction dump + .processing_instruction => return, + // document fragment is outside of the main document DOM, so we + // don't output it. + .document_fragment => return, + // document will never be called, but required for completeness. + .document => return, + // done globally instead, but required for completeness. + .document_type => return, + // deprecated + .attribute => return, + .entity_reference => return, + .entity => return, + .notation => return, + } +} + // writer must be a std.io.Writer -pub fn writeNode(root: *parser.Node, writer: anytype) !void { +pub fn writeChildren(root: *parser.Node, writer: anytype) !void { const walker = Walker{}; var next: ?*parser.Node = null; while (true) { next = try walker.get_next(root, next) orelse break; - switch (try parser.nodeType(next.?)) { - .element => { - // open the tag - const tag = try parser.nodeLocalName(next.?); - try writer.writeAll("<"); - try writer.writeAll(tag); - - // write the attributes - const map = try parser.nodeGetAttributes(next.?); - const ln = try parser.namedNodeMapGetLength(map); - var i: u32 = 0; - while (i < ln) { - const attr = try parser.namedNodeMapItem(map, i) orelse break; - try writer.writeAll(" "); - try writer.writeAll(try parser.attributeGetName(attr)); - try writer.writeAll("=\""); - try writer.writeAll(try parser.attributeGetValue(attr) orelse ""); - try writer.writeAll("\""); - i += 1; - } - - try writer.writeAll(">"); - - // void elements can't have any content. - if (try isVoid(parser.nodeToElement(next.?))) continue; - - // write the children - // TODO avoid recursion - try writeNode(next.?, writer); - - // close the tag - try writer.writeAll(""); - }, - .text => { - const v = try parser.nodeValue(next.?) orelse continue; - try writer.writeAll(v); - }, - .cdata_section => { - const v = try parser.nodeValue(next.?) orelse continue; - try writer.writeAll(""); - }, - .comment => { - const v = try parser.nodeValue(next.?) orelse continue; - try writer.writeAll(""); - }, - // TODO handle processing instruction dump - .processing_instruction => continue, - // document fragment is outside of the main document DOM, so we - // don't output it. - .document_fragment => continue, - // document will never be called, but required for completeness. - .document => continue, - // done globally instead, but required for completeness. - .document_type => continue, - // deprecated - .attribute => continue, - .entity_reference => continue, - .entity => continue, - .notation => continue, - } + try writeNode(next.?, writer); } } diff --git a/src/dom/element.zig b/src/dom/element.zig index 60043e50..030630da 100644 --- a/src/dom/element.zig +++ b/src/dom/element.zig @@ -26,7 +26,7 @@ const checkCases = jsruntime.test_utils.checkCases; const Variadic = jsruntime.Variadic; const collection = @import("html_collection.zig"); -const writeNode = @import("../browser/dump.zig").writeNode; +const dump = @import("../browser/dump.zig"); const css = @import("css.zig"); const Node = @import("node.zig").Node; @@ -102,7 +102,17 @@ pub const Element = struct { var buf = std.ArrayList(u8).init(alloc); defer buf.deinit(); - try writeNode(parser.elementToNode(self), buf.writer()); + try dump.writeChildren(parser.elementToNode(self), buf.writer()); + // TODO express the caller owned the slice. + // https://github.com/lightpanda-io/jsruntime-lib/issues/195 + return buf.toOwnedSlice(); + } + + pub fn get_outerHTML(self: *parser.Element, alloc: std.mem.Allocator) ![]const u8 { + var buf = std.ArrayList(u8).init(alloc); + defer buf.deinit(); + + try dump.writeNode(parser.elementToNode(self), buf.writer()); // TODO express the caller owned the slice. // https://github.com/lightpanda-io/jsruntime-lib/issues/195 return buf.toOwnedSlice(); @@ -470,4 +480,9 @@ pub fn testExecFn( .{ .src = "document.getElementById('para-empty').innerHTML.trim()", .ex = "" }, }; try checkCases(js_env, &innerHTML); + + var outerHTML = [_]Case{ + .{ .src = "document.getElementById('para').outerHTML", .ex = "

And

" }, + }; + try checkCases(js_env, &outerHTML); } diff --git a/src/run_tests.zig b/src/run_tests.zig index bc0b5631..5e569f26 100644 --- a/src/run_tests.zig +++ b/src/run_tests.zig @@ -147,6 +147,7 @@ fn testsAllExecFn( @import("html/navigator.zig").testExecFn, @import("html/history.zig").testExecFn, @import("html/location.zig").testExecFn, + @import("xmlserializer/xmlserializer.zig").testExecFn, }; inline for (testFns) |testFn| { diff --git a/src/xmlserializer/xmlserializer.zig b/src/xmlserializer/xmlserializer.zig new file mode 100644 index 00000000..f39745f1 --- /dev/null +++ b/src/xmlserializer/xmlserializer.zig @@ -0,0 +1,72 @@ +// 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 jsruntime = @import("jsruntime"); +const Case = jsruntime.test_utils.Case; +const checkCases = jsruntime.test_utils.checkCases; +const generate = @import("../generate.zig"); + +const DOMError = @import("netsurf").DOMError; + +const parser = @import("netsurf"); +const dump = @import("../browser/dump.zig"); + +pub const Interfaces = generate.Tuple(.{ + XMLSerializer, +}); + +// https://w3c.github.io/DOM-Parsing/#dom-xmlserializer-constructor +pub const XMLSerializer = struct { + pub const mem_guarantied = true; + + pub fn constructor() !XMLSerializer { + return .{}; + } + + pub fn deinit(_: *XMLSerializer, _: std.mem.Allocator) void {} + + pub fn _serializeToString(_: XMLSerializer, alloc: std.mem.Allocator, root: *parser.Node) ![]const u8 { + var buf = std.ArrayList(u8).init(alloc); + defer buf.deinit(); + + if (try parser.nodeType(root) == .document) { + try dump.writeHTML(@as(*parser.Document, @ptrCast(root)), buf.writer()); + } else { + try dump.writeNode(root, buf.writer()); + } + // TODO express the caller owned the slice. + // https://github.com/lightpanda-io/jsruntime-lib/issues/195 + return try buf.toOwnedSlice(); + } +}; + +// Tests +// ----- + +pub fn testExecFn( + _: std.mem.Allocator, + js_env: *jsruntime.Env, +) anyerror!void { + var serializer = [_]Case{ + .{ .src = "const s = new XMLSerializer()", .ex = "undefined" }, + .{ .src = "s.serializeToString(document.getElementById('para'))", .ex = "

And

" }, + }; + try checkCases(js_env, &serializer); +}