Skip to content

Keep the closing "?" of processing instructions in html5qp() documents - #113

Merged
jakejackson1 merged 2 commits into
mainfrom
fix/issue-86
Oct 26, 2026
Merged

jakejackson1 merged 2 commits into
mainfrom
fix/issue-86

Conversation

@gravitypdf-bot

Copy link
Copy Markdown
Contributor

The bug

html() and writeHTML() dropped the closing ? of every processing instruction in an html5qp() document:

$html = '<html><body><h1><?php echo $title; ?></h1></body></html>';

html5qp($html)->top()->html();
// was:  <h1><?php echo $title; ></h1>
// now:  <h1><?php echo $title; ?></h1>

libxml's HTML serializer expects the ? to already be in the PI data. #68 handles that in saveDocumentHTML(), but only for documents that are a QueryPath\Document. Masterminds builds a plain DOMDocument, so the ? was never put back.

The fix

QueryPath::withHTML5() now retypes the document Masterminds builds as a QueryPath\Document. It calls registerNodeClass(DOMDocument::class, Document::class), releases the original wrapper, and fetches the document again through its doctype node. PHP builds the new wrapper from the registered class. DOM::createDocument() already depends on this same rebuild behaviour.

The document is otherwise exactly what Masterminds produces. On PHP 7.4, 8.2 and 8.5, html(), html5(), xml(), the inner*() methods, text() and writeHTML() gave byte-identical output before and after the change for a set of documents: non-ASCII text, an HTML 4 doctype, a document with no <html>, SVG/MathML, tests/data.html and the file-path route. The document properties (encoding, documentURI, standalone, ...) matched as well.

Why not the issue's two candidates

  • target_document: Masterminds only adds the doctype to a document it creates itself. Appending a doctype from DOMImplementation::createDocumentType() to the target looks fine ($doc->doctype is set), but on PHP < 8.4 it doesn't set libxml's intSubset, so saveHTML() still leaves the doctype out. Seeding the target with loadXML('<!DOCTYPE html><html/>') does work, but it sets documentURI to the working directory, and resetting it gives "" instead of NULL. That is a smaller change, but still a change.
  • registerNodeClass('DOMElement', …): this would change the class of every element callers get back.

The retype changes nothing except the class of the document.

Behaviour change

html5qp(...)->document() now returns a QueryPath\Document, which extends DOMDocument, just like documents from qp() and htmlqp(). A Masterminds document the caller builds and passes to qp() stays a plain DOMDocument and is still written as-is.

Tests

tests/Issues/Issue86Test.php covers:

  • Every cell in the html5qp() column of the issue's table: html(), html5(), xml(), innerHTML(), innerHTML5(), innerXML(), writeHTML() (printed and to a file), writeHTML5() and writeXML(). It also covers loading from a file path.
  • html() and writeHTML() still emit <!DOCTYPE html>.
  • The document type survives being reached through ownerDocument, content ending in ??> round-trips, and writeHTML() leaves the PI data unchanged.
  • htmlqp(), qp(), and caller-supplied DOMDocuments (from libxml and from Masterminds) behave as before.

Six of these tests fail without the fix. The full suite and both lints pass.

Fixes #86.

🤖 Generated with Claude Code

html()/writeHTML() go through libxml's HTML serializer, which relies on
the terminator being in the PI data. Masterminds never leaves it there,
and saveDocumentHTML() only restores it for a QueryPath\Document. Retype
the Masterminds document as a Document so it gets the same treatment.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@codecov

codecov Bot commented Oct 26, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 90.98%. Comparing base (6ae6acf) to head (55d3c2d).
⚠️ Report is 8 commits behind head on main.

Additional details and impacted files
@@             Coverage Diff              @@
##               main     #113      +/-   ##
============================================
+ Coverage     90.69%   90.98%   +0.28%     
- Complexity     1424     1425       +1     
============================================
  Files            25       25              
  Lines          3170     3171       +1     
============================================
+ Hits           2875     2885      +10     
+ Misses          295      286       -9     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@jakejackson1 jakejackson1 added the bug Something isn't working label Oct 26, 2026
@jakejackson1
jakejackson1 merged commit 3920a77 into main Oct 26, 2026
14 checks passed
@jakejackson1
jakejackson1 deleted the fix/issue-86 branch October 26, 2026 00:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

html5qp() documents lose the closing "?" of a PHP block when serialized with html()/writeHTML()

2 participants