Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
85.19% covered (warning)
85.19%
23 / 27
50.00% covered (danger)
50.00%
1 / 2
CRAP
0.00% covered (danger)
0.00%
0 / 1
DomXmlSerializer
85.19% covered (warning)
85.19%
23 / 27
50.00% covered (danger)
50.00%
1 / 2
10.33
0.00% covered (danger)
0.00%
0 / 1
 serialize
66.67% covered (warning)
66.67%
8 / 12
0.00% covered (danger)
0.00%
0 / 1
2.15
 buildElement
100.00% covered (success)
100.00%
15 / 15
100.00% covered (success)
100.00%
1 / 1
8
1<?php
2
3declare(strict_types=1);
4
5namespace Phpdftk\HtmlToPdf\ForeignContent;
6
7use Phpdftk\Html\Dom\Element as HtmlElement;
8use Phpdftk\Html\Dom\Text as HtmlText;
9
10/**
11 * Serialise an HTML-DOM subtree into an XML string the foreign-content
12 * parsers ({@see \Phpdftk\Svg\Parser}, {@see \Phpdftk\Mathml\Parser})
13 * can ingest.
14 *
15 * The HTML parser produces nodes in its own type system
16 * ({@see HtmlElement} and friends, not `\DOMElement`), but the SVG
17 * and MathML parsers only accept XML strings. This class bridges the
18 * gap once per inline-foreign subtree:
19 *
20 *   1. Build a fresh `\DOMDocument` mirroring the HTML DOM subtree.
21 *   2. Declare the foreign namespace explicitly on the root via
22 *      `createElementNS()` so the parser's namespace check passes
23 *      (Svg / MathML parsers reject documents whose root carries
24 *      the wrong namespace, with an error message that includes
25 *      `unexpected namespace …`).
26 *   3. Call `saveXML()` and hand the string back.
27 *
28 * The shared shape is just the walk + the namespace plumbing.
29 * Adapters that wrap this class (`InlineSvgAdapter`,
30 * `InlineMathmlAdapter`) keep format-specific concerns — namespace /
31 * localName validation, caching, parser dispatch — close to their
32 * call sites.
33 *
34 * Design note: this lives inside `html-to-pdf` rather than a new
35 * shared package because (a) it depends on `Phpdftk\Html\Dom`, which
36 * is already a dependency of `html-to-pdf`, and (b) `Phpdftk\Svg` /
37 * `Phpdftk\Mathml` shouldn't grow a dep on `phpdftk/html`. Lifting
38 * this to a new package would force inter-package coupling for no
39 * measurable gain.
40 */
41final class DomXmlSerializer
42{
43    /**
44     * Serialise `$root` into an XML string, declaring `$namespaceUri`
45     * as the default namespace on the root element.
46     *
47     * Sibling namespaces inside the subtree (e.g. MathML's
48     * `<annotation-xml>` inside a `<foreignObject>`) are deliberately
49     * collapsed into `$namespaceUri` — the consumer parser falls back
50     * to its `GenericElement` for anything it doesn't recognise, so
51     * preserving the original namespace would only complicate the
52     * round-trip without changing the rendered output.
53     *
54     * @throws \RuntimeException When libxml's `saveXML()` refuses
55     *   (malformed attribute name, invalid character data, …). The
56     *   message preserves enough context to identify the foreign
57     *   subtree.
58     */
59    public function serialize(HtmlElement $root, string $namespaceUri): string
60    {
61        $dom = new \DOMDocument('1.0', 'UTF-8');
62        $dom->preserveWhiteSpace = true;
63        $dom->formatOutput = false;
64        $domRoot = $this->buildElement($dom, $root, $namespaceUri);
65        $dom->appendChild($domRoot);
66        $xml = $dom->saveXML($domRoot);
67        if ($xml === false) {
68            throw new \RuntimeException(sprintf(
69                'Failed to serialise inline-<%s> subtree to XML.',
70                $root->localName,
71            ));
72        }
73        return $xml;
74    }
75
76    private function buildElement(
77        \DOMDocument $dom,
78        HtmlElement $src,
79        string $namespaceUri,
80    ): \DOMElement {
81        $node = $dom->createElementNS($namespaceUri, $src->localName);
82        // attributes() returns a list of Attr objects in source order.
83        // (allAttributes() returns a name=>value map but loses prefix
84        // info, which we need to distinguish e.g. xlink:href from href.)
85        foreach ($src->attributes() as $attr) {
86            // Skip xmlns redeclarations — saveXML adds the inherited
87            // one on the root and we don't want duplicate declarations
88            // peppered through the tree.
89            if ($attr->localName === 'xmlns'
90                || $attr->prefix === 'xmlns'
91            ) {
92                continue;
93            }
94            // Keep prefixed attrs (e.g. xlink:href) intact via qualified
95            // name. setAttribute() takes a qualified name; libxml will
96            // create the prefix declaration if it's new.
97            $node->setAttribute($attr->qualifiedName(), $attr->value);
98        }
99        // children() filters to Element only, but we need text nodes
100        // too (e.g. `<text>hello</text>` for SVG, `<mn>2</mn>` for
101        // MathML). Walk the live sibling chain so Text + Element nodes
102        // both arrive in document order.
103        for ($child = $src->firstChild; $child !== null; $child = $child->nextSibling) {
104            if ($child instanceof HtmlText) {
105                if ($child->data !== '') {
106                    $node->appendChild($dom->createTextNode($child->data));
107                }
108                continue;
109            }
110            if ($child instanceof HtmlElement) {
111                $node->appendChild($this->buildElement($dom, $child, $namespaceUri));
112                continue;
113            }
114            // Other node types (Comment, DocumentType, processing
115            // instructions, …) are skipped — both SVG and MathML
116            // parsers drop them at their tree-walk layer.
117        }
118        return $node;
119    }
120}