Skip to content

Processing instructions gain an extra "?" when an HTML-parsed document is serialized with html()/innerHTML() #65

Description

@jakejackson1

Description of the bug

When a document is parsed with the libxml HTML parser (qp() on a .html file, or htmlqp()), any processing instruction in it — most commonly a <?php … ?> block — gains an extra ? every time the document is serialized with html(), innerHTML(), innerXML(), html5(), or innerHTML5().

<?php echo $title; ?>   ->   <?php echo $title; ??>

Round-trip the document twice and you get ???>, and so on. The output is no longer valid PHP.

writeHTML() and writeHTML5() are not affected, and neither is a document parsed by html5qp() or qp() in XML mode.

Root cause

libxml's HTML parser stores the closing ? of a PI as part of the node's data, while its XML parser does not:

$d = new DOMDocument();
$d->loadHTML('<html><body><h1><?php echo $t; ?></h1></body></html>');
// data === 'echo $t; ?'      <-- trailing "?" retained

$d2 = new DOMDocument();
$d2->loadXML('<root><?php echo $t; ?></root>');
// data === 'echo $t; '       <-- no trailing "?"

saveHTML() compensates for this and emits a single ?. saveXML() does not — it appends ?> to data that already ends in ?:

$d->saveHTML($body);  // <body><h1><?php echo $t; ?></h1></body>    correct
$d->saveXML($body);   // <body><h1><?php echo $t; ??></h1></body>   corrupted

QueryPath hits this because DOMQuery::html() serializes a non-root node with saveXML() (src/DOMQuery.php:707), and the html5() family hands the same node to the Masterminds serializer, which likewise does not compensate.

Suggested fix

In the HTML-oriented serializers, either use saveHTML($node) rather than saveXML($node) for a document that was HTML-parsed, or normalise the PI data on load by stripping a single trailing ?. The second option also fixes the reading side — $pi->data currently returns PHP source with a stray ? glued to the end, which callers have to trim themselves.

QueryPath version

4.1.0 (main, 3036f97)

PHP Version and environment

PHP 8.3.16 (cli), libxml 2.9.13, macOS. Not version-specific — it reproduces anywhere libxml behaves this way.

Minimal reproducible PHP+HTML snippet to replicate bug

<?php
require 'vendor/autoload.php';

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

$qp = htmlqp($html);

echo $qp->top()->find('body')->innerHTML(), "\n";
// actual:   <h1><?php echo $title; ??></h1>
// expected: <h1><?php echo $title; ?></h1>

$qp->top()->writeHTML();
// writeHTML() is correct:
// <html><body><h1><?php echo $title; ?></h1></body></html>

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions