diff --git a/src/browser/frame/parse.zig b/src/browser/frame/parse.zig index ea00bdece..109114216 100644 --- a/src/browser/frame/parse.zig +++ b/src/browser/frame/parse.zig @@ -117,7 +117,7 @@ pub fn xmlDocument(frame: *Frame, xml: []const u8) !?*Document.XMLDocument { return error.ExecutionTerminated; } - if (parser.err != null or doc_node.firstChild() == null) { + if (parser.err != null or parser.xml_error or doc_node.firstChild() == null) { return null; } diff --git a/src/browser/parser/Parser.zig b/src/browser/parser/Parser.zig index f735916ad..903636adb 100644 --- a/src/browser/parser/Parser.zig +++ b/src/browser/parser/Parser.zig @@ -77,6 +77,7 @@ buf: std.ArrayList(u8), // innerHTML and DOMParser (per spec). Set from Options at init. allow_declarative_shadow: bool = false, +xml_error: bool = false, terminated: bool = false, appends_until_terminate_check: u16 = TERMINATE_CHECK_INTERVAL, @@ -253,7 +254,7 @@ pub fn parseXML(self: *Parser, xml: []const u8) void { createXMLElementCallback, getDataCallback, appendCallback, - parseErrorCallback, + xmlParseErrorCallback, popCallback, createCommentCallback, createProcessingInstruction, @@ -430,6 +431,21 @@ fn parseErrorCallback(ctx: *anyopaque, err: h5e.StringSlice) callconv(.c) void { // std.debug.print("PEC: {s}\n", .{err.slice()}); } +// xml5ever xml error callback indicating a well-formedness violating. +fn xmlParseErrorCallback(ctx: *anyopaque, err: h5e.StringSlice) callconv(.c) void { + const self: *Parser = @ptrCast(@alignCast(ctx)); + const msg = err.slice(); + if (std.mem.startsWith(u8, msg, "Bad character") and msg.len > "Bad character".len) { + // discouraged by legal code point + return; + } + if (std.mem.startsWith(u8, msg, "Invalid character reference")) { + // xml5ever doesn't know about this entity, but it could be valid + return; + } + self.xml_error = true; +} + fn popCallback(ctx: *anyopaque, node_ref: *anyopaque) callconv(.c) void { const self: *Parser = @ptrCast(@alignCast(ctx)); if (self.terminated) { @@ -488,10 +504,21 @@ fn _createElementCallbackWithDefaultnamespace(ctx: *anyopaque, data: *anyopaque, } fn _createElementCallback(self: *Parser, data: *anyopaque, qname: h5e.QualName, attributes: h5e.AttributeIterator, default_namespace: Element.Namespace) !*anyopaque { const frame = self.frame; - const name = qname.local.slice(); + const local = qname.local.slice(); + // Elements are keyed by qualified name (prefix/localName split on ':'), + // like createElementNS. html5ever never sets a prefix; xml5ever does. + const name = if (qname.prefix.unwrap()) |prefix| blk: { + if (prefix.len == 0) break :blk local; + break :blk try std.fmt.allocPrint(frame.local_arena, "{s}:{s}", .{ prefix.slice(), local }); + } else local; const namespace_string = qname.ns.slice(); const namespace = if (namespace_string.len == 0) default_namespace else Element.Namespace.parse(namespace_string); const node = try Frame.node_factory.createElementNS(frame, namespace, name, attributes); + if (namespace == .unknown and namespace_string.len > 0) { + // Same as Document.createElementNS: keep the URI so namespaceURI and + // lookupNamespaceURI can return it. + try frame._element_namespace_uris.put(frame.arena, node.as(Element), try frame.dupeString(namespace_string)); + } const pn = try self.arena.create(ParsedNode); pn.* = .{ diff --git a/src/browser/tests/domparser.html b/src/browser/tests/domparser.html index 9bb5ccde0..40e05d3ac 100644 --- a/src/browser/tests/domparser.html +++ b/src/browser/tests/domparser.html @@ -442,3 +442,96 @@ testing.expectEqual(true, r.hasAttributeNS('http://www.w3.org/XML/1998/namespace', 'lang')); } + + + + + + diff --git a/src/browser/webapi/DOMParser.zig b/src/browser/webapi/DOMParser.zig index bd1babfb1..00729d6b5 100644 --- a/src/browser/webapi/DOMParser.zig +++ b/src/browser/webapi/DOMParser.zig @@ -23,6 +23,7 @@ const js = @import("../js/js.zig"); const Frame = @import("../Frame.zig"); const Parser = @import("../parser/Parser.zig"); +const Node = @import("Node.zig"); const HTMLDocument = @import("HTMLDocument.zig"); const Document = @import("Document.zig"); @@ -41,15 +42,9 @@ pub fn parseFromString( mime_type: []const u8, frame: *Frame, ) !*Document { - const target_mime = std.meta.stringToEnum(enum { - @"text/html", - @"text/xml", - @"application/xml", - @"application/xhtml+xml", - @"image/svg+xml", - }, mime_type) orelse return error.NotSupported; + const target_mime = std.meta.stringToEnum(SupportedType, mime_type) orelse return error.NotSupported; - return switch (target_mime) { + switch (target_mime) { .@"text/html" => { const arena = try frame.getArena(.medium, "DOMParser.parseFromString"); defer arena.release(); @@ -89,13 +84,34 @@ pub fn parseFromString( return doc.asDocument(); }, else => { - const doc = (try Frame.parse.xmlDocument(frame, html)) orelse blk: { - // Return a document with a element per spec. - break :blk (try Frame.parse.xmlDocument(frame, "error")).?; - }; - return doc.asDocument(); + const xml_doc = (try Frame.parse.xmlDocument(frame, html)) orelse try parserErrorDocument(frame); + const doc = xml_doc.asDocument(); + doc._content_type = @tagName(target_mime); + return doc; }, - }; + } +} + +const SupportedType = enum { + @"text/html", + @"text/xml", + @"application/xml", + @"application/xhtml+xml", + @"image/svg+xml", +}; + +const parsererror_ns = "http://www.mozilla.org/newlayout/xml/parsererror.xml"; + +// Per spec, a well-formedness error yields a document whose only child is +// in the Mozilla error namespace. +fn parserErrorDocument(frame: *Frame) !*Document.XMLDocument { + const doc = try frame._factory.document(Document.XMLDocument{ ._proto = undefined }); + const root = try Frame.node_factory.createElementNS(frame, .unknown, "parsererror", null); + try frame._element_namespace_uris.put(frame.arena, root.as(Node.Element), parsererror_ns); + const text = try Frame.node_factory.createTextNode(frame, "error"); + _ = try root.appendChild(text, frame); + _ = try doc.asNode().appendChild(root, frame); + return doc; } pub const JsApi = struct { diff --git a/src/html5ever/lib.rs b/src/html5ever/lib.rs index 4b1e23cec..d69ffe7c3 100644 --- a/src/html5ever/lib.rs +++ b/src/html5ever/lib.rs @@ -807,7 +807,121 @@ pub extern "C" fn xml5ever_parse_document( }; let bytes = unsafe { std::slice::from_raw_parts(xml, len) }; - xml5ever::driver::parse_document(sink, xml5ever::driver::XmlParseOpts::default()) - .from_utf8() - .one(bytes); + let bytes = strip_doctype_internal_subset(bytes); + let tb = xml5ever::tree_builder::XmlTreeBuilder::new(sink, Default::default()); + let tokenizer = xml5ever::tokenizer::XmlTokenizer::new( + UnclosedTagSink { tb, depth: Cell::new(0) }, + Default::default(), + ); + html5ever::tendril::stream::Utf8LossyDecoder::new(XmlDocumentParser { + tokenizer, + input_buffer: Default::default(), + }) + .one(&*bytes); +} + +// xml5ever's tokenizer has no notion of a DOCTYPE internal subset +// (` ]>`, as old Illustrator exports +// emit): the `[` ends the doctype as bogus and the declarations tokenize as +// junk before the root, each step a parse error. Nothing in the subset is used +// anyway (no DTD support), so cut it out before tokenizing. Quotes are +// respected; nested `[`/`]` are not (they don't occur in the subset syntax). +fn strip_doctype_internal_subset(bytes: &[u8]) -> std::borrow::Cow<'_, [u8]> { + let Some(doctype) = bytes.windows(9).position(|w| w == b" = None; + let mut open = None; + while i < bytes.len() { + let c = bytes[i]; + match quote { + Some(q) => { + if c == q { + quote = None + } + }, + None => match c { + b'"' | b'\'' => quote = Some(c), + b'>' if open.is_none() => return std::borrow::Cow::Borrowed(bytes), + b'[' if open.is_none() => open = Some(i), + b']' if open.is_some() => { + let mut out = Vec::with_capacity(bytes.len()); + out.extend_from_slice(&bytes[..open.unwrap()]); + out.extend_from_slice(&bytes[i + 1..]); + return std::borrow::Cow::Owned(out); + }, + _ => {}, + }, + } + i += 1; + } + std::borrow::Cow::Borrowed(bytes) +} + +// xml5ever's tree builder silently closes elements still open at EOF, and +// its tokenizer reports nothing either, so a truncated document +// (`text`) parses "cleanly". Browsers reject it. This sits between +// the tokenizer and the tree builder, tracks tag nesting from the raw token +// stream, and reports an error when EOF arrives with tags still open. +struct UnclosedTagSink<'arena> { + tb: xml5ever::tree_builder::XmlTreeBuilder>, + depth: Cell, +} + +impl<'arena> xml5ever::tokenizer::TokenSink for UnclosedTagSink<'arena> { + type Handle = Ref; + + fn process_token( + &self, + token: xml5ever::tokenizer::Token, + ) -> xml5ever::tokenizer::ProcessResult { + use xml5ever::tokenizer::{TagKind, Token}; + match &token { + Token::Tag(tag) => match tag.kind { + TagKind::StartTag => self.depth.set(self.depth.get() + 1), + TagKind::EndTag | TagKind::ShortTag => { + self.depth.set(self.depth.get().saturating_sub(1)) + }, + TagKind::EmptyTag => {}, + }, + Token::EndOfFile => { + if self.depth.get() > 0 { + use xml5ever::tree_builder::TreeSink; + self.tb.sink.parse_error(std::borrow::Cow::Borrowed("Unclosed element at EOF")); + } + }, + _ => {}, + } + self.tb.process_token(token) + } + + fn end(&self) { + self.tb.end() + } +} + +// xml5ever::driver::XmlParser, minus the tree-builder-typed tokenizer so the +// UnclosedTagSink can sit in between. +struct XmlDocumentParser<'arena> { + tokenizer: xml5ever::tokenizer::XmlTokenizer>, + input_buffer: xml5ever::buffer_queue::BufferQueue, +} + +impl<'arena> TendrilSink for XmlDocumentParser<'arena> { + type Output = (); + + fn process(&mut self, t: StrTendril) { + self.input_buffer.push_back(t); + while let xml5ever::TokenizerResult::Script(_) = self.tokenizer.feed(&self.input_buffer) {} + } + + fn error(&mut self, desc: std::borrow::Cow<'static, str>) { + use xml5ever::tree_builder::TreeSink; + self.tokenizer.sink.tb.sink.parse_error(desc) + } + + fn finish(self) -> () { + self.tokenizer.end(); + } }