Skip to content

Commit

Permalink
implement XMLSerializer
Browse files Browse the repository at this point in the history
  • Loading branch information
krichprollsch committed Jan 30, 2025
1 parent 27f9963 commit 0cbbb08
Show file tree
Hide file tree
Showing 5 changed files with 147 additions and 70 deletions.
2 changes: 2 additions & 0 deletions src/apiweb.zig
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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;
140 changes: 72 additions & 68 deletions src/browser/dump.zig
Original file line number Diff line number Diff line change
Expand Up @@ -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("<!DOCTYPE html>\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("</");
try writer.writeAll(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("<![CDATA[");
try writer.writeAll(v);
try writer.writeAll("]]>");
},
.comment => {
const v = try parser.nodeValue(node) orelse return;
try writer.writeAll("<!--");
try writer.writeAll(v);
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("</");
try writer.writeAll(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("<![CDATA[");
try writer.writeAll(v);
try writer.writeAll("]]>");
},
.comment => {
const v = try parser.nodeValue(next.?) orelse continue;
try writer.writeAll("<!--");
try writer.writeAll(v);
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);
}
}

Expand Down
4 changes: 2 additions & 2 deletions src/dom/element.zig
Original file line number Diff line number Diff line change
Expand Up @@ -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 writeChildren = @import("../browser/dump.zig").writeChildren;
const css = @import("css.zig");

const Node = @import("node.zig").Node;
Expand Down Expand Up @@ -102,7 +102,7 @@ pub const Element = struct {
var buf = std.ArrayList(u8).init(alloc);
defer buf.deinit();

try writeNode(parser.elementToNode(self), buf.writer());
try 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();
Expand Down
1 change: 1 addition & 0 deletions src/run_tests.zig
Original file line number Diff line number Diff line change
Expand Up @@ -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| {
Expand Down
70 changes: 70 additions & 0 deletions src/xmlserializer/xmlserializer.zig
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
// Copyright (C) 2023-2025 Lightpanda (Selecy SAS)
//
// Francis Bouvier <[email protected]>
// Pierre Tachoire <[email protected]>
//
// 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 <https://www.gnu.org/licenses/>.
//
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());
}
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 = "<p id=\"para\"> And</p>" },
};
try checkCases(js_env, &serializer);
}

0 comments on commit 0cbbb08

Please sign in to comment.