diff --git a/CHANGELOG-GRAVITYPDF.md b/CHANGELOG-GRAVITYPDF.md index d3e8fa21e..fc5b6036f 100644 --- a/CHANGELOG-GRAVITYPDF.md +++ b/CHANGELOG-GRAVITYPDF.md @@ -117,7 +117,7 @@ New features * **`:not()`, `:is()` and `:where()`.** Each takes a selector list whose selectors may have combinators, such as `p:not(.note, div > p)` or `:is(h2, h3) + p`. `:is()` and `:where()` leave out a selector they cannot read, as browsers do. `:not()` and `:is()` weigh as their most specific argument, and `:where()` as nothing. Rules using them were dropped before. [#538] [#628] * **Pseudo-classes that look at what follows an element.** `:last-child`, `:nth-last-child()`, `:only-child`, `:last-of-type`, `:nth-last-of-type()`, `:only-of-type` and `:empty` match in the flow, in tables, headers and footers, and positioned blocks. Rules using them were dropped before, so a document that has them changes. Set `'cssMode' => \Mpdf\CssMode::LEGACY` to keep dropping them. [#537] [#634] * `WriteHTML()` reads ahead through the HTML it is given to count each element's children. An element still open at the end of a call that leaves it open is not known in full, so these do not match its children in that call, and nor does `:not()` of them. - * `:empty` matches as in browsers: an element holding white space is not empty. In the default mode mPDF's CSS reader turns each HTML comment into a space, so an element holding only a comment is not empty either. A table cell holding only white space is empty, as mPDF strips it. + * `:empty` matches as in browsers: an element holding white space is not empty, and one holding only a comment is. A table cell holding only white space is empty, as mPDF strips it. Performance ----------- @@ -178,6 +178,9 @@ Bugfixes * A byte order mark at the start of a stylesheet, as `WriteHTML(file_get_contents('style.css'), HTMLParserMode::HEADER_CSS)` passes one on, no longer spoils the first rule. * An `@import` is loaded only from the stylesheet's own rules, not from a comment, a string or a block. * A `style` attribute is split the same way, so `style="font-family: 'a;b', serif; color: red"` applies both, and a value may run over several lines. +* An HTML comment between two words drew a space: `Bookkeeper` read "Book keeper", and `

` held a space. A comment now leaves nothing in its place, as in a browser and as `HTMLParserMode::HTML_BODY` already had it, in either `cssMode`. A document that relied on the space can put one next to the comment. [#636] [#637] + * In the default parser mode, a ` markers and all HTML comments. + * Removes the markers, keeping what is between them, then every HTML comment, leaving nothing + * in its place, as a browser does. A ab', 'ab'], + 'a script tag in a comment' => ['ab', 'ab'], + 'no comment' => ['

a b

', '

a b

'], + ]; + } + +} diff --git a/tests/Mpdf/HtmlCommentTest.php b/tests/Mpdf/HtmlCommentTest.php new file mode 100644 index 000000000..8824ca924 --- /dev/null +++ b/tests/Mpdf/HtmlCommentTest.php @@ -0,0 +1,97 @@ +draw($html, $mode, $cssMode); + + $this->assertNotEmpty($drawn[0]); + $this->assertSame($this->draw($asRead, $mode, $cssMode), $drawn); + } + + /** + * Each document with a comment, and the same document as a browser reads it, in each parser mode and CSS mode + * + * @return array[] Each [html, as read, parser mode, CSS mode] + */ + public function comments() + { + $documents = [ + 'between two words' => ['

ab

', '

ab

'], + 'at the start of a text run' => ['

ab

', '

ab

'], + 'at the end of a text run' => ['

ab

', '

ab

'], + 'between inline elements' => ['

ab

', '

ab

'], + 'over several lines' => ["

ab

", '

ab

'], + 'with line breaks around it' => ["

a\n\nb

", "

a\nb

"], + 'inside pre, between two words' => ["
ab\nc
", "
ab\nc
"], + 'inside pre, keeping the line breaks around it' => ["
a\n\nb
", "
a\n\nb
"], + 'inside a table cell' => ['
ab
', '
ab
'], + 'mPDF content, whose markers go and whose content stays' => ['

ac

', '

a b c

'], + 'a comment inside mPDF content' => ['

ac mpdf-->d

', '

a bc d

'], + 'a conditional comment' => ['

ab

', '

ab

'], + 'a conditional comment that shows its content' => ['

abc

', '

abc

'], + 'a comment marker in a script' => ['

ab

', '

ab

'], + 'a script tag in a comment' => ['

ab

c

', '

ab

c

'], + ]; + + $cases = []; + foreach ($documents as $name => $document) { + foreach ($this->modes() as $modeName => $mode) { + $cases[$name . ', ' . $modeName] = array_merge($document, $mode); + } + } + + return $cases; + } + + /** + * Each parser mode that reads a document, with each CSS mode + * + * @return array[] Each [parser mode, CSS mode] + */ + private function modes() + { + return [ + 'default mode, standard' => [HTMLParserMode::DEFAULT_MODE, CssMode::STANDARD], + 'default mode, legacy' => [HTMLParserMode::DEFAULT_MODE, CssMode::LEGACY], + 'body mode, standard' => [HTMLParserMode::HTML_BODY, CssMode::STANDARD], + 'body mode, legacy' => [HTMLParserMode::HTML_BODY, CssMode::LEGACY], + ]; + } + + /** + * @param string $html + * @param int $mode The parser mode, an HTMLParserMode constant + * @param string $cssMode + * + * @return array[] The text of each line drawn, and the page, left and right edges and top of each + */ + private function draw($html, $mode, $cssMode) + { + $mpdf = new TextRecordingMpdf(['mode' => 'c', 'cssMode' => $cssMode]); + $mpdf->WriteHTML($html, $mode); + + return [$mpdf->drawnText, $mpdf->drawnBoxes]; + } + +} diff --git a/tests/Mpdf/LookAheadSelectorTest.php b/tests/Mpdf/LookAheadSelectorTest.php index 5110f0d51..3338390fc 100644 --- a/tests/Mpdf/LookAheadSelectorTest.php +++ b/tests/Mpdf/LookAheadSelectorTest.php @@ -239,10 +239,10 @@ public function testTheKeptBlockUnwindsForEveryCase() } /** - * :empty matches an element with no child elements and no text, as browsers have it: white space is text. A - * comment is not, but in the default mode the CSS reader turns each comment into a space before the HTML is read, - * so an element holding one is not empty; the body mode, which does not read CSS, drops comments outright. The - * white space at the end of a table cell is stripped before the HTML is read, so a cell holding only that is empty + * :empty matches an element with no child elements and no text, as browsers have it: white space is text, and a + * comment is not, so an element holding only a comment is empty and one holding comments and white space is not. + * The white space at the end of a table cell is stripped before the HTML is read, so a cell holding only that is + * empty * * @dataProvider emptyElements * @@ -257,8 +257,8 @@ public function testWhatCountsAsEmpty($element, $empty) } /** - * HTML written in the body mode does not go through the CSS reader, so a comment is dropped and the element that - * held only the comment is empty, as in a browser + * HTML written in the body mode drops a comment too, so the element that held only the comment is empty, as in a + * browser */ public function testAnElementHoldingOnlyACommentInTheBodyMode() { @@ -280,7 +280,7 @@ public function emptyElements() 'nothing in it' => ['

', true], 'a space' => ['

', false], 'line breaks and tabs' => ["

\n\t\n

", false], - 'a comment, which the CSS reader turns into a space' => ['

', false], + 'a comment, which leaves nothing in its place' => ['

', true], 'comments and white space' => ['

', false], 'a closed start tag' => ['
', true], 'a void element' => ['
', true], diff --git a/tests/Snapshots/HtmlCommentSnapshotTest.php b/tests/Snapshots/HtmlCommentSnapshotTest.php new file mode 100644 index 000000000..a2b24aafc --- /dev/null +++ b/tests/Snapshots/HtmlCommentSnapshotTest.php @@ -0,0 +1,81 @@ + + body { font-size: 11pt; } + h1 { font-size: 15pt; margin: 0 0 2mm 0; } + h2 { margin: 5mm 0 1mm 0; font-size: 10pt; } + p.caption { margin: 0 0 2mm 0; font-size: 8pt; color: #606060; } + div.case { border: 0.2mm solid #999999; padding: 1mm 2mm; } + div.case p { margin: 1mm 0; } + pre { margin: 1mm 0; } + table { border-collapse: collapse; } + td { border: 0.2mm solid #999999; padding: 1mm 3mm; } + + +

HTML comments

+ +

Between two words

+

Reads "Bookkeeper", with no space.

+

Bookkeeper

+ +

Between inline elements

+

Reads "Bookkeeper", with "Book" in bold and "keeper" in italics, and no space between them.

+

Bookkeeper

+ +

In a table cell

+

The cell reads "Bookkeeper", with no space.

+
Bookkeeper
+ +

In preformatted text

+

Three lines: "Bookkeeper" with no space, an empty line, and "keeper".

+
Bookkeeper
+
+keeper
+ +

A conditional comment

+

Reads "Bookkeeper", with no space and no "hidden".

+

Bookkeeper

+ +

A comment marker in a script

+

Reads "Bookkeeper", with no space and none of the script.

+ +

Bookkeeper

+ +

mPDF content

+

Reads "Shown only by mPDF, and Bookkeeper": the markers go and their content stays, and the comment inside it leaves no space.

+
keeper

mpdf-->
+ '; + + $this->mpdf = $this->createMpdf(); + + $this->mpdf->WriteHTML($html); + } + +} diff --git a/tests/data/snapshots/html-comment.pdf b/tests/data/snapshots/html-comment.pdf new file mode 100644 index 000000000..42b04fd58 Binary files /dev/null and b/tests/data/snapshots/html-comment.pdf differ