diff --git a/src/browser/Page.zig b/src/browser/Page.zig index 0169e9d5..a4feb165 100644 --- a/src/browser/Page.zig +++ b/src/browser/Page.zig @@ -722,7 +722,7 @@ pub fn appendNew(self: *Page, parent: *Node, child: Node.NodeOrText) !void { // called from the parser when the node and all its children have been added pub fn nodeComplete(self: *Page, node: *Node) !void { Node.Build.call(node, "complete", .{ node, self }) catch |err| { - log.err(.bug, "build.complete", .{ .tag = node.getTag(), .err = err }); + log.err(.bug, "build.complete", .{ .tag = node.getNodeName(self), .err = err }); return err; }; return self.nodeIsReady(true, node); @@ -979,6 +979,12 @@ pub fn createElement(self: *Page, ns_: ?[]const u8, name: []const u8, attribute_ attribute_iterator, .{ ._proto = undefined }, ), + asUint("template") => return self.createHtmlElementT( + Element.Html.Template, + namespace, + attribute_iterator, + .{ ._proto = undefined, ._content = undefined }, + ), else => {}, }, else => {}, @@ -1015,7 +1021,7 @@ fn createHtmlElementT(self: *Page, comptime E: type, namespace: Element.Namespac const node = element.asNode(); if (@hasDecl(E, "Build") and @hasDecl(E.Build, "created")) { @call(.auto, @field(E.Build, "created"), .{ node, self }) catch |err| { - log.err(.page, "build.created", .{ .tag = node.getTag(), .err = err }); + log.err(.page, "build.created", .{ .tag = node.getNodeName(self), .err = err }); return err; }; } diff --git a/src/browser/js/bridge.zig b/src/browser/js/bridge.zig index 7bf3cbc6..714407eb 100644 --- a/src/browser/js/bridge.zig +++ b/src/browser/js/bridge.zig @@ -518,6 +518,7 @@ pub const JsApis = flattenTypes(&.{ @import("../webapi/element/html/Script.zig"), @import("../webapi/element/html/Select.zig"), @import("../webapi/element/html/Style.zig"), + @import("../webapi/element/html/Template.zig"), @import("../webapi/element/html/TextArea.zig"), @import("../webapi/element/html/Title.zig"), @import("../webapi/element/html/UL.zig"), diff --git a/src/browser/parser/Parser.zig b/src/browser/parser/Parser.zig index 1515d600..efb85ab3 100644 --- a/src/browser/parser/Parser.zig +++ b/src/browser/parser/Parser.zig @@ -66,6 +66,7 @@ const Error = struct { create_comment, append_doctype_to_document, add_attrs_if_missing, + get_template_content, }; }; @@ -83,6 +84,7 @@ pub fn parse(self: *Parser, html: []const u8) void { createCommentCallback, appendDoctypeToDocument, addAttrsIfMissingCallback, + getTemplateContentsCallback ); } @@ -100,6 +102,7 @@ pub fn parseFragment(self: *Parser, html: []const u8) void { createCommentCallback, appendDoctypeToDocument, addAttrsIfMissingCallback, + getTemplateContentsCallback ); } @@ -134,6 +137,7 @@ pub const Streaming = struct { createCommentCallback, appendDoctypeToDocument, addAttrsIfMissingCallback, + getTemplateContentsCallback ) orelse return error.ParserCreationFailed; } @@ -245,6 +249,28 @@ fn _addAttrsIfMissingCallback(self: *Parser, node: *Node, attributes: h5e.Attrib } } +fn getTemplateContentsCallback(ctx: *anyopaque, target_ref: *anyopaque) callconv(.c) ?*anyopaque { + const self: *Parser = @ptrCast(@alignCast(ctx)); + return self._getTemplateContentsCallback(getNode(target_ref)) catch |err| { + self.err = .{ .err = err, .source = .get_template_content }; + return null; + }; +} + +fn _getTemplateContentsCallback(self: *Parser, node: *Node) !*anyopaque { + const element = node.as(Element); + const template = element._type.html.is(Element.Html.Template) orelse unreachable; + const content_node = template.getContent().asNode(); + + // Create a ParsedNode wrapper for the content DocumentFragment + const pn = try self.arena.create(ParsedNode); + pn.* = .{ + .data = null, + .node = content_node, + }; + return pn; +} + fn getDataCallback(ctx: *anyopaque) callconv(.c) *anyopaque { const pn: *ParsedNode = @ptrCast(@alignCast(ctx)); // For non-elements, data is null. But, we expect this to only ever diff --git a/src/browser/parser/html5ever.zig b/src/browser/parser/html5ever.zig index d03fbd8b..411d35e7 100644 --- a/src/browser/parser/html5ever.zig +++ b/src/browser/parser/html5ever.zig @@ -31,6 +31,7 @@ pub extern "c" fn html5ever_parse_document( createCommentCallback: *const fn (ctx: *anyopaque, StringSlice) callconv(.c) ?*anyopaque, appendDoctypeToDocument: *const fn (ctx: *anyopaque, StringSlice, StringSlice, StringSlice) callconv(.c) void, addAttrsIfMissingCallback: *const fn (ctx: *anyopaque, target_ref: *anyopaque, AttributeIterator) callconv(.c) void, + getTemplateContentsCallback: *const fn (ctx: *anyopaque, target_ref: *anyopaque) callconv(.c) ?*anyopaque, ) void; pub extern "c" fn html5ever_parse_fragment( @@ -46,6 +47,7 @@ pub extern "c" fn html5ever_parse_fragment( createCommentCallback: *const fn (ctx: *anyopaque, StringSlice) callconv(.c) ?*anyopaque, appendDoctypeToDocument: *const fn (ctx: *anyopaque, StringSlice, StringSlice, StringSlice) callconv(.c) void, addAttrsIfMissingCallback: *const fn (ctx: *anyopaque, target_ref: *anyopaque, AttributeIterator) callconv(.c) void, + getTemplateContentsCallback: *const fn (ctx: *anyopaque, target_ref: *anyopaque) callconv(.c) ?*anyopaque, ) void; pub extern "c" fn html5ever_attribute_iterator_next(ctx: *anyopaque) Nullable(Attribute); @@ -70,6 +72,7 @@ pub extern "c" fn html5ever_streaming_parser_create( createCommentCallback: *const fn (ctx: *anyopaque, StringSlice) callconv(.c) ?*anyopaque, appendDoctypeToDocument: *const fn (ctx: *anyopaque, StringSlice, StringSlice, StringSlice) callconv(.c) void, addAttrsIfMissingCallback: *const fn (ctx: *anyopaque, target_ref: *anyopaque, AttributeIterator) callconv(.c) void, + getTemplateContentsCallback: *const fn (ctx: *anyopaque, target_ref: *anyopaque) callconv(.c) ?*anyopaque, ) ?*anyopaque; pub extern "c" fn html5ever_streaming_parser_feed( diff --git a/src/browser/tests/document_fragment/document_fragment.html b/src/browser/tests/document_fragment/document_fragment.html index 6b20529c..663d483f 100644 --- a/src/browser/tests/document_fragment/document_fragment.html +++ b/src/browser/tests/document_fragment/document_fragment.html @@ -100,3 +100,79 @@ testing.expectEqual(test2, document.getElementById("test2")); } + + + + + + diff --git a/src/browser/tests/element/html/template.html b/src/browser/tests/element/html/template.html new file mode 100644 index 00000000..bc605584 --- /dev/null +++ b/src/browser/tests/element/html/template.html @@ -0,0 +1,168 @@ + + +
+ +This is template content
+This is template content
+hello, world
+ + + + + + + + + + + + + + + diff --git a/src/browser/webapi/DocumentFragment.zig b/src/browser/webapi/DocumentFragment.zig index 9813d922..9879f766 100644 --- a/src/browser/webapi/DocumentFragment.zig +++ b/src/browser/webapi/DocumentFragment.zig @@ -136,6 +136,24 @@ pub fn replaceChildren(self: *DocumentFragment, nodes: []const Node.NodeOrText, } } +pub fn cloneFragment(self: *DocumentFragment, deep: bool, page: *Page) !*Node { + const fragment = try DocumentFragment.init(page); + const fragment_node = fragment.asNode(); + + if (deep) { + const node = self.asNode(); + const self_is_connected = node.isConnected(); + + var child_it = node.childrenIterator(); + while (child_it.next()) |child| { + const cloned_child = try child.cloneNode(true, page); + try page.appendNode(fragment_node, cloned_child, .{ .child_already_connected = self_is_connected }); + } + } + + return fragment_node; +} + pub const JsApi = struct { pub const bridge = js.Bridge(DocumentFragment); diff --git a/src/browser/webapi/Element.zig b/src/browser/webapi/Element.zig index 0ea4783e..7b727a21 100644 --- a/src/browser/webapi/Element.zig +++ b/src/browser/webapi/Element.zig @@ -146,6 +146,7 @@ pub fn getTagNameLower(self: *const Element) []const u8 { .script => "script", .select => "select", .style => "style", + .template => "template", .text_area => "textarea", .title => "title", .ul => "ul", @@ -188,6 +189,7 @@ pub fn getTagNameSpec(self: *const Element, buf: []u8) []const u8 { .script => "SCRIPT", .select => "SELECT", .style => "STYLE", + .template => "TEMPLATE", .text_area => "TEXTAREA", .title => "TITLE", .ul => "UL", @@ -730,6 +732,7 @@ pub fn getTag(self: *const Element) Tag { .script => .script, .select => .select, .option => .option, + .template => .template, .text_area => .textarea, .input => .input, .link => .link, @@ -797,6 +800,7 @@ pub const Tag = enum { style, svg, text, + template, textarea, title, ul, diff --git a/src/browser/webapi/Node.zig b/src/browser/webapi/Node.zig index d162ae81..73c59476 100644 --- a/src/browser/webapi/Node.zig +++ b/src/browser/webapi/Node.zig @@ -196,7 +196,7 @@ pub fn setTextContent(self: *Node, data: []const u8, page: *Page) !void { } } -pub fn getNodeName(self: *const Node, page: *Page) ![]const u8 { +pub fn getNodeName(self: *const Node, page: *Page) []const u8 { return switch (self._type) { .element => |el| el.getTagNameSpec(&page.buf), .cdata => |cd| switch (cd._type) { @@ -428,7 +428,7 @@ pub fn cloneNode(self: *Node, deep_: ?bool, page: *Page) error{ OutOfMemory, Str .element => |el| return el.cloneElement(deep, page), .document => return error.NotSupported, .document_type => return error.NotSupported, - .document_fragment => return error.NotImplemented, + .document_fragment => |frag| return frag.cloneFragment(deep, page), .attribute => return error.NotSupported, } } diff --git a/src/browser/webapi/element/Html.zig b/src/browser/webapi/element/Html.zig index c418f160..854375d5 100644 --- a/src/browser/webapi/element/Html.zig +++ b/src/browser/webapi/element/Html.zig @@ -45,6 +45,7 @@ pub const Form = @import("html/Form.zig"); pub const Heading = @import("html/Heading.zig"); pub const Unknown = @import("html/Unknown.zig"); pub const Generic = @import("html/Generic.zig"); +pub const Template = @import("html/Template.zig"); pub const TextArea = @import("html/TextArea.zig"); pub const Paragraph = @import("html/Paragraph.zig"); pub const Select = @import("html/Select.zig"); @@ -81,6 +82,7 @@ pub const Type = union(enum) { script: *Script, select: Select, style: Style, + template: *Template, text_area: *TextArea, title: Title, ul: UL, @@ -119,6 +121,7 @@ pub fn className(self: *const HtmlElement) []const u8 { .generic => "[object HTMLElement]", .script => "[object HtmlScriptElement]", .select => "[object HTMLSelectElement]", + .template => "[object HTMLTemplateElement]", .option => "[object HTMLOptionElement]", .text_area => "[object HtmlTextAreaElement]", .input => "[object HtmlInputElement]", diff --git a/src/browser/webapi/element/html/Template.zig b/src/browser/webapi/element/html/Template.zig new file mode 100644 index 00000000..4529230f --- /dev/null +++ b/src/browser/webapi/element/html/Template.zig @@ -0,0 +1,49 @@ +const std = @import("std"); + +const js = @import("../../../js/js.zig"); +const Page = @import("../../../Page.zig"); +const Node = @import("../../Node.zig"); +const Element = @import("../../Element.zig"); +const HtmlElement = @import("../Html.zig"); +const DocumentFragment = @import("../../DocumentFragment.zig"); + +const Template = @This(); + +_proto: *HtmlElement, +_content: *DocumentFragment, + +pub fn asElement(self: *Template) *Element { + return self._proto._proto; +} +pub fn asNode(self: *Template) *Node { + return self.asElement().asNode(); +} + +pub fn getContent(self: *Template) *DocumentFragment { + return self._content; +} + +pub const JsApi = struct { + pub const bridge = js.Bridge(Template); + + pub const Meta = struct { + pub const name = "HTMLTemplateElement"; + pub const prototype_chain = bridge.prototypeChain(); + pub var class_id: bridge.ClassId = undefined; + }; + + pub const content = bridge.accessor(Template.getContent, null, .{}); +}; + +pub const Build = struct { + pub fn created(node: *Node, page: *Page) !void { + const self = node.as(Template); + // Create the template content DocumentFragment + self._content = try DocumentFragment.init(page); + } +}; + +const testing = @import("../../../../testing.zig"); +test "WebApi: Template" { + try testing.htmlRunner("element/html/template.html", .{}); +} diff --git a/src/html5ever/lib.rs b/src/html5ever/lib.rs index 5ef4de21..f2d06ab1 100644 --- a/src/html5ever/lib.rs +++ b/src/html5ever/lib.rs @@ -45,6 +45,7 @@ pub extern "C" fn html5ever_parse_document( create_comment_callback: CreateCommentCallback, append_doctype_to_document: AppendDoctypeToDocumentCallback, add_attrs_if_missing_callback: AddAttrsIfMissingCallback, + get_template_contents_callback: GetTemplateContentsCallback, ) -> () { if html.is_null() || len == 0 { return (); @@ -65,6 +66,7 @@ pub extern "C" fn html5ever_parse_document( create_comment_callback: create_comment_callback, append_doctype_to_document: append_doctype_to_document, add_attrs_if_missing_callback: add_attrs_if_missing_callback, + get_template_contents_callback: get_template_contents_callback, }; let bytes = unsafe { std::slice::from_raw_parts(html, len) }; @@ -87,6 +89,7 @@ pub extern "C" fn html5ever_parse_fragment( create_comment_callback: CreateCommentCallback, append_doctype_to_document: AppendDoctypeToDocumentCallback, add_attrs_if_missing_callback: AddAttrsIfMissingCallback, + get_template_contents_callback: GetTemplateContentsCallback, ) -> () { if html.is_null() || len == 0 { return (); @@ -107,6 +110,7 @@ pub extern "C" fn html5ever_parse_fragment( create_comment_callback: create_comment_callback, append_doctype_to_document: append_doctype_to_document, add_attrs_if_missing_callback: add_attrs_if_missing_callback, + get_template_contents_callback: get_template_contents_callback, }; let bytes = unsafe { std::slice::from_raw_parts(html, len) }; @@ -188,6 +192,7 @@ pub extern "C" fn html5ever_streaming_parser_create( create_comment_callback: CreateCommentCallback, append_doctype_to_document: AppendDoctypeToDocumentCallback, add_attrs_if_missing_callback: AddAttrsIfMissingCallback, + get_template_contents_callback: GetTemplateContentsCallback, ) -> *mut c_void { let arena = Box::new(typed_arena::Arena::new()); @@ -211,6 +216,7 @@ pub extern "C" fn html5ever_streaming_parser_create( create_comment_callback: create_comment_callback, append_doctype_to_document: append_doctype_to_document, add_attrs_if_missing_callback: add_attrs_if_missing_callback, + get_template_contents_callback: get_template_contents_callback, }; // Create a parser which implements TendrilSink for streaming parsing diff --git a/src/html5ever/sink.rs b/src/html5ever/sink.rs index 9976450f..27f1ef9c 100644 --- a/src/html5ever/sink.rs +++ b/src/html5ever/sink.rs @@ -56,6 +56,7 @@ pub struct Sink<'arena> { pub create_comment_callback: CreateCommentCallback, pub append_doctype_to_document: AppendDoctypeToDocumentCallback, pub add_attrs_if_missing_callback: AddAttrsIfMissingCallback, + pub get_template_contents_callback: GetTemplateContentsCallback, } impl<'arena> TreeSink for Sink<'arena> { @@ -101,8 +102,9 @@ impl<'arena> TreeSink for Sink<'arena> { } fn get_template_contents(&self, target: &Ref) -> Ref { - _ = target; - panic!("get_template_contents") + unsafe { + return (self.get_template_contents_callback)(self.ctx, *target); + } } fn is_mathml_annotation_xml_integration_point(&self, target: &Ref) -> bool { diff --git a/src/html5ever/types.rs b/src/html5ever/types.rs index fa9c6cbc..71c5f8eb 100644 --- a/src/html5ever/types.rs +++ b/src/html5ever/types.rs @@ -57,6 +57,8 @@ pub type AddAttrsIfMissingCallback = unsafe extern "C" fn( attributes: *mut c_void, ) -> (); +pub type GetTemplateContentsCallback = unsafe extern "C" fn(ctx: Ref, target: Ref) -> Ref; + pub type Ref = *const c_void; #[repr(C)]