Skip to content

Keep comments and DOCTYPE before the root element when there is no XML declaration - #146

Open
yxshee wants to merge 1 commit into
antchfx:masterfrom
yxshee:leading-comments
Open

yxshee wants to merge 1 commit into
antchfx:masterfrom
yxshee:leading-comments

Conversation

@yxshee

@yxshee yxshee commented Nov 9, 2026

Copy link
Copy Markdown

When a document has no XML declaration, a comment or DOCTYPE that comes before the root element is dropped from the parsed tree:

doc, _ := xmlquery.Parse(strings.NewReader(`<!-- license --><!DOCTYPE r><r/>`))
fmt.Println(doc.OutputXML(true))
// <?xml version="1.0"?><r></r>
fmt.Println(len(xmlquery.Find(doc, "//comment()")))
// 0

With a declaration first (<?xml version="1.0"?><!-- license --><!DOCTYPE r><r/>) both nodes are kept, and a processing instruction in the same position (<?pi?><r/>) is kept as well.

In parser.parse, the xml.Comment and xml.Directive cases attach the node relative to p.prev. Before anything else has been seen, p.prev is the document node and p.level == p.prev.level == 0, so the node is added with AddSibling(p.prev, node): it becomes a sibling of the document node and is unreachable from it. The xml.ProcInst case already handles this position by making the node a child of the document at level 1.

I noticed it while comparing xmlquery's OutputXML against libxml2's canonical form on generated documents; documents that started with a comment or a DOCTYPE and had no declaration lost those nodes.

The comment and directive cases now do what the processing-instruction case does when p.prev is the document node: add the node as a child of the document at level 1 and make it p.prev. The following root element is then attached as its sibling, as it is after a leading processing instruction today, so no synthetic <?xml version="1.0"?> is inserted in front of it (the same as for <?pi?><r/> on master).

Documents with a declaration, or with nothing before the root element, are unaffected: TestMissDeclaration still gets the synthetic declaration, TestDirective and TestMultipleProcInst are unchanged.

TestLeadingCommentAndDoctypeWithoutDeclaration checks the document children, OutputXML, //comment() and that the root's own comment child is untouched; TestLeadingCommentWithWhitespace covers a CRLF between the comment and the root. Both fail on master and pass here; go test ./... and go vet ./... are clean.

…aration

A comment or directive seen while p.prev was still the document node was
added with AddSibling(p.prev, ...), which makes it a sibling of the document
node itself, so it was dropped from the tree. A processing instruction in the
same position already becomes a child of the document; do the same for
comments and directives.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant