Repository navigation
Track the stack of open elements while parsing HTML #533
Description
Part of #520 (step 13). Changes nothing on the page; it only adds state that #538, #537, #546 and #547 read.
>, +, ~, :first-child on any element, and descendant rules through inline ancestors all need the element's ancestors and earlier siblings. Today only block elements and table parts push a level (blk[], CssManager::$tablecascadeCSS), and nothing records siblings.
Design. Keep a stack of frames on Mpdf. StateSnapshot captures Mpdf's own scalar and array properties, plus Form and TableOfContents. CssManager is an object and is not captured, so a stack kept there would survive a measuring pass unchanged.
Each frame holds:
- the tag, id, classes, lang and attributes
- the element's position among its element siblings, and among siblings of the same tag
- a short record of the siblings already closed, for
+and~ - for Inherit from the parent element's computed values under the opt-in cascade #547, the element's computed values of the inherited properties
Maintain it in WriteHTML()'s token loop, on real tokens only (end tags at src/Mpdf.php:14897, start tags at :15082), not in Tag::OpenTag() and Tag::CloseTag(). Forced page breaks close every open block through Tag::CloseTag() (_preForcedPagebreak(), src/Mpdf.php:3460) and open them again through Tag::OpenTag() (_postForcedPagebreak(), :3495); these are called from BlockTag, Table and FormFeed. Frames pushed and popped there would count each reopened block as a new sibling.
- An end tag pops to the nearest frame with its name, and is ignored if there is none, as a browser treats a stray end tag.
- Implied end tags follow the optional end-tag rules at the top of
Tag::OpenTag():p,li,dt,dd,option,td,th,tr. - Void elements (
img,br,input,hr) are never pushed. - Keep it independent of
blk[].
Hard parts.
page-break-inside: avoid. The snapshot is taken insideBlockTag::open()(src/Tag/BlockTag.php:479).close()restores it (:1370) and rewinds the parser to the opening token. Push the frame afterOpenTag()returns, and pop it beforeCloseTag()is called. Otherwise the reparsed token pushes a second frame, or the pop removes the parent after the restore.- Headers and footers are written in the middle of the flow, when a page is added (
WriteHTML(…, HTML_HEADER_BUFFER)atsrc/Mpdf.php:10857,:10929,:13653). Set the stack aside and put it back around them, rather than starting them empty on top of the flow's open elements. - Positioned blocks are buffered and replayed through
WriteHTML()later (Match descendant selectors inside an absolutely positioned block #474). Snapshot the stack when the block opens, asfixedPosBlockCascadeCSSis snapshotted, and replay against it. WriteHTML()called in parts keeps the stack across calls, as it keepsblk[].
Tests. The stack's depth and top frame after documents with omitted end tags, stray end tags, nested tables, a forced page break inside nested blocks, a kept block that unwinds, a header written while elements are open in the flow, and a positioned block. The snapshot suite passes with no fixture updated.
Activity