/** * Determine if a page break is allowed before $frame * http://www.w3.org/TR/CSS21/page.html#allowed-page-breaks * * In the normal flow, page breaks can occur at the following places: * * 1. In the vertical margin between block boxes. When a page * break occurs here, the used values of the relevant * 'margin-top' and 'margin-bottom' properties are set to '0'. * 2. Between line boxes inside a block box. * * These breaks are subject to the following rules: * * * Rule A: Breaking at (1) is allowed only if the * 'page-break-after' and 'page-break-before' properties of * all the elements generating boxes that meet at this margin * allow it, which is when at least one of them has the value * 'always', 'left', or 'right', or when all of them are * 'auto'. * * * Rule B: However, if all of them are 'auto' and the * nearest common ancestor of all the elements has a * 'page-break-inside' value of 'avoid', then breaking here is * not allowed. * * * Rule C: Breaking at (2) is allowed only if the number of * line boxes between the break and the start of the enclosing * block box is the value of 'orphans' or more, and the number * of line boxes between the break and the end of the box is * the value of 'widows' or more. * * * Rule D: In addition, breaking at (2) is allowed only if * the 'page-break-inside' property is 'auto'. * * If the above doesn't provide enough break points to keep * content from overflowing the page boxes, then rules B and D are * dropped in order to find additional breakpoints. * * If that still does not lead to sufficient break points, rules A * and C are dropped as well, to find still more break points. * * We will also allow breaks between table rows. However, when * splitting a table, the table headers should carry over to the * next page (but they don't yet). * * @param Frame $frame the frame to check * @return bool true if a break is allowed, false otherwise */ protected function _page_break_allowed(Frame $frame) { $block_types = array("block", "list-item", "table", "-dompdf-image"); dompdf_debug("page-break", "_page_break_allowed(" . $frame->get_node()->nodeName . ")"); $display = $frame->get_style()->display; // Block Frames (1): if (in_array($display, $block_types)) { // Avoid breaks within table-cells if ($this->_in_table) { dompdf_debug("page-break", "In table: " . $this->_in_table); return false; } // Rules A & B if ($frame->get_style()->page_break_before === "avoid") { dompdf_debug("page-break", "before: avoid"); return false; } // Find the preceeding block-level sibling $prev = $frame->get_prev_sibling(); while ($prev && !in_array($prev->get_style()->display, $block_types)) { $prev = $prev->get_prev_sibling(); } // Does the previous element allow a page break after? if ($prev && $prev->get_style()->page_break_after === "avoid") { dompdf_debug("page-break", "after: avoid"); return false; } // If both $prev & $frame have the same parent, check the parent's // page_break_inside property. $parent = $frame->get_parent(); if ($prev && $parent && $parent->get_style()->page_break_inside === "avoid") { dompdf_debug("page-break", "parent inside: avoid"); return false; } // To prevent cascading page breaks when a top-level element has // page-break-inside: avoid, ensure that at least one frame is // on the page before splitting. if ($parent->get_node()->nodeName === "body" && !$prev) { // We are the body's first child dompdf_debug("page-break", "Body's first child."); return false; } // If the frame is the first block-level frame, use the value from // $frame's parent instead. if (!$prev && $parent) { return $this->_page_break_allowed($parent); } dompdf_debug("page-break", "block: break allowed"); return true; } else { if (in_array($display, Style::$INLINE_TYPES)) { // Avoid breaks within table-cells if ($this->_in_table) { dompdf_debug("page-break", "In table: " . $this->_in_table); return false; } // Rule C $block_parent = $frame->find_block_parent(); if (count($block_parent->get_line_boxes()) < $frame->get_style()->orphans) { dompdf_debug("page-break", "orphans"); return false; } // FIXME: Checking widows is tricky without having laid out the // remaining line boxes. Just ignore it for now... // Rule D $p = $block_parent; while ($p) { if ($p->get_style()->page_break_inside === "avoid") { dompdf_debug("page-break", "parent->inside: avoid"); return false; } $p = $p->find_block_parent(); } // To prevent cascading page breaks when a top-level element has // page-break-inside: avoid, ensure that at least one frame with // some content is on the page before splitting. $prev = $frame->get_prev_sibling(); while ($prev && ($prev->is_text_node() && trim($prev->get_node()->nodeValue) == "")) { $prev = $prev->get_prev_sibling(); } if ($block_parent->get_node()->nodeName === "body" && !$prev) { // We are the body's first child dompdf_debug("page-break", "Body's first child."); return false; } // Skip breaks on empty text nodes if ($frame->is_text_node() && $frame->get_node()->nodeValue == "") { return false; } dompdf_debug("page-break", "inline: break allowed"); return true; // Table-rows } else { if ($display === "table-row") { // Simply check if the parent table's page_break_inside property is // not 'avoid' $p = Table_Frame_Decorator::find_parent_table($frame); while ($p) { if ($p->get_style()->page_break_inside === "avoid") { dompdf_debug("page-break", "parent->inside: avoid"); return false; } $p = $p->find_block_parent(); } // Avoid breaking after the first row of a table if ($p && $p->get_first_child() === $frame) { dompdf_debug("page-break", "table: first-row"); return false; } // If this is a nested table, prevent the page from breaking if ($this->_in_table > 1) { dompdf_debug("page-break", "table: nested table"); return false; } dompdf_debug("page-break", "table-row/row-groups: break allowed"); return true; } else { if (in_array($display, Table_Frame_Decorator::$ROW_GROUPS)) { // Disallow breaks at row-groups: only split at row boundaries return false; } else { dompdf_debug("page-break", "? " . $frame->get_style()->display . ""); return false; } } } } }
/** * Determine if a page break is allowed before $frame * * @param Frame $frame the frame to check * @return bool true if a break is allowed, false otherwise */ protected function _page_break_allowed(Frame $frame) { /** * * http://www.w3.org/TR/CSS21/page.html#allowed-page-breaks * /* * In the normal flow, page breaks can occur at the following places: * * 1. In the vertical margin between block boxes. When a page * break occurs here, the used values of the relevant * 'margin-top' and 'margin-bottom' properties are set to '0'. * 2. Between line boxes inside a block box. * * These breaks are subject to the following rules: * * * Rule A: Breaking at (1) is allowed only if the * 'page-break-after' and 'page-break-before' properties of * all the elements generating boxes that meet at this margin * allow it, which is when at least one of them has the value * 'always', 'left', or 'right', or when all of them are * 'auto'. * * * Rule B: However, if all of them are 'auto' and the * nearest common ancestor of all the elements has a * 'page-break-inside' value of 'avoid', then breaking here is * not allowed. * * * Rule C: Breaking at (2) is allowed only if the number of * line boxes between the break and the start of the enclosing * block box is the value of 'orphans' or more, and the number * of line boxes between the break and the end of the box is * the value of 'widows' or more. * * * Rule D: In addition, breaking at (2) is allowed only if * the 'page-break-inside' property is 'auto'. * * If the above doesn't provide enough break points to keep * content from overflowing the page boxes, then rules B and D are * dropped in order to find additional breakpoints. * * If that still does not lead to sufficient break points, rules A * and C are dropped as well, to find still more break points. * * [endquote] * * We will also allow breaks between table rows. However, when * splitting a table, the table headers should carry over to the * next page (but they don't yet). */ $block_types = array("block", "list-item", "table"); // echo "\nbreak_allowed: " . $frame->get_node()->nodeName ."\n"; $display = $frame->get_style()->display; // Block Frames (1): if (in_array($display, $block_types)) { // Avoid breaks within table-cells if ($this->_in_table) { // echo "In table: " . $this->_in_table ."\n"; return false; } // Rules A & B if ($frame->get_style()->page_break_before == "avoid") { // echo "before: avoid\n"; return false; } // Find the preceeding block-level sibling $prev = $frame->get_prev_sibling(); while ($prev && !in_array($prev->get_style()->display, $block_types)) { $prev = $prev->get_prev_sibling(); } // Does the previous element allow a page break after? if ($prev && $prev->get_style()->page_break_after == "avoid") { // echo "after: avoid\n"; return false; } // If both $prev & $frame have the same parent, check the parent's // page_break_inside property. $parent = $frame->get_parent(); if ($prev && $parent && $parent->get_style()->page_break_inside == "avoid") { // echo "parent inside: avoid\n"; return false; } // To prevent cascading page breaks when a top-level element has // page-break-inside: avoid, ensure that at least one frame is // on the page before splitting. if ($parent->get_node()->nodeName == "body" && !$prev) { // We are the body's first child // echo "Body's first child.\n"; return false; } // If the frame is the first block-level frame, use the value from // $frame's parent instead. if (!$prev && $parent) { return $this->_page_break_allowed($parent); } // echo "block: break allowed\n"; return true; } else { if (in_array($display, Style::$INLINE_TYPES)) { // Avoid breaks within table-cells if ($this->_in_table) { // echo "In table: " . $this->_in_table ."\n"; return false; } // Rule C $block_parent = $frame->find_block_parent(); if (count($block_parent->get_lines()) < $frame->get_style()->orphans) { // echo "orphans\n"; return false; } // FIXME: Checking widows is tricky without having laid out the // remaining line boxes. Just ignore it for now... // Rule D if ($block_parent->get_style()->page_break_inside == "avoid") { // echo "parent->inside: avoid\n"; return false; } // To prevent cascading page breaks when a top-level element has // page-break-inside: avoid, ensure that at least one frame with // some content is on the page before splitting. $prev = $frame->get_prev_sibling(); while ($prev && ($prev->get_node()->nodeName == "#text" && trim($prev->get_node()->nodeValue) == "")) { $prev = $prev->get_prev_sibling(); } if ($block_parent->get_node()->nodeName == "body" && !$prev) { // We are the body's first child // echo "Body's first child.\n"; return false; } // Skip breaks on empty text nodes if ($frame->get_node()->nodeName == "#text" && $frame->get_node()->nodeValue == "") { return false; } // echo "inline: break allowed\n"; return true; // Table-rows } else { if ($display == "table-row") { // Simply check if the parent table's page_break_inside property is // not 'avoid' $p = Table_Frame_Decorator::find_parent_table($frame); if ($p->get_style()->page_break_inside == "avoid") { // echo "table: page-break-inside: avoid\n"; return false; } // Check the table's parent element $table_parent = $p->get_parent(); if ($table_parent->get_style()->page_break_inside == "avoid") { // echo "table->parent: page-break-inside: avoid\n"; return false; } // Avoid breaking after the first row of a table if ($p->get_first_child() === $frame) { // echo "table: first-row\n"; return false; } // echo "table-row/row-groups: break allowed\n"; return true; } else { if (in_array($display, Table_Frame_Decorator::$ROW_GROUPS)) { // Disallow breaks at row-groups: only split at row boundaries return false; } else { // echo "? " . $frame->get_style()->display . "\n"; return false; } } } } }
/** * Parses the CSS "content" property * * @return string The resulting string */ protected function _parse_content() { // Matches generated content $re = "/\n" . "\\s(counters?\\([^)]*\\))|\n" . "\\A(counters?\\([^)]*\\))|\n" . "\\s([\"']) ( (?:[^\"']|\\\\[\"'])+ )(?<!\\\\)\\3|\n" . "\\A([\"']) ( (?:[^\"']|\\\\[\"'])+ )(?<!\\\\)\\5|\n" . "\\s([^\\s\"']+)|\n" . "\\A([^\\s\"']+)\n" . "/xi"; $content = $this->_frame->get_style()->content; $quotes = $this->_parse_quotes(); // split on spaces, except within quotes if (!preg_match_all($re, $content, $matches, PREG_SET_ORDER)) { return; } $text = ""; foreach ($matches as $match) { if (isset($match[2]) && $match[2] !== "") { $match[1] = $match[2]; } if (isset($match[6]) && $match[6] !== "") { $match[4] = $match[6]; } if (isset($match[8]) && $match[8] !== "") { $match[7] = $match[8]; } if (isset($match[1]) && $match[1] !== "") { // counters?(...) $match[1] = mb_strtolower(trim($match[1])); // Handle counter() references: // http://www.w3.org/TR/CSS21/generate.html#content $i = mb_strpos($match[1], ")"); if ($i === false) { continue; } $args = explode(",", mb_substr($match[1], 8, $i - 8)); $counter_id = $args[0]; if ($match[1][7] === "(") { // counter(name [,style]) if (isset($args[1])) { $type = trim($args[1]); } else { $type = null; } $p = $this->_frame->find_block_parent(); $text .= $p->counter_value($counter_id, $type); } else { if ($match[1][7] === "s") { // counters(name, string [,style]) if (isset($args[1])) { $string = $this->_parse_string(trim($args[1])); } else { $string = ""; } if (isset($args[2])) { $type = $args[2]; } else { $type = null; } $p = $this->_frame->find_block_parent(); $tmp = ""; while ($p) { $tmp = $p->counter_value($counter_id, $type) . $string . $tmp; $p = $p->find_block_parent(); } $text .= $tmp; } else { // countertops? continue; } } } else { if (isset($match[4]) && $match[4] !== "") { // String match $text .= $this->_parse_string($match[4]); } else { if (isset($match[7]) && $match[7] !== "") { // Directive match if ($match[7] === "open-quote") { // FIXME: do something here $text .= $quotes[0][0]; } else { if ($match[7] === "close-quote") { // FIXME: do something else here $text .= $quotes[0][1]; } else { if ($match[7] === "no-open-quote") { // FIXME: } else { if ($match[7] === "no-close-quote") { // FIXME: } else { if (mb_strpos($match[7], "attr(") === 0) { $i = mb_strpos($match[7], ")"); if ($i === false) { continue; } $attr = mb_substr($match[7], 5, $i - 5); if ($attr == "") { continue; } $text .= $this->_frame->get_parent()->get_node()->getAttribute($attr); } else { continue; } } } } } } } } } return $text; }
protected function _page_break_allowed(Frame $frame) { $block_types = array("block", "list-item", "table", "-dompdf-image"); dompdf_debug("page-break", "_page_break_allowed(" . $frame->get_node()->nodeName . ")"); $display = $frame->get_style()->display; if (in_array($display, $block_types)) { if ($this->_in_table) { dompdf_debug("page-break", "In table: " . $this->_in_table); return false; } if ($frame->get_style()->page_break_before === "avoid") { dompdf_debug("page-break", "before: avoid"); return false; } $prev = $frame->get_prev_sibling(); while ($prev && !in_array($prev->get_style()->display, $block_types)) { $prev = $prev->get_prev_sibling(); } if ($prev && $prev->get_style()->page_break_after === "avoid") { dompdf_debug("page-break", "after: avoid"); return false; } $parent = $frame->get_parent(); if ($prev && $parent && $parent->get_style()->page_break_inside === "avoid") { dompdf_debug("page-break", "parent inside: avoid"); return false; } if ($parent->get_node()->nodeName === "body" && !$prev) { dompdf_debug("page-break", "Body's first child."); return false; } if (!$prev && $parent) { return $this->_page_break_allowed($parent); } dompdf_debug("page-break", "block: break allowed"); return true; } else { if (in_array($display, Style::$INLINE_TYPES)) { if ($this->_in_table) { dompdf_debug("page-break", "In table: " . $this->_in_table); return false; } $block_parent = $frame->find_block_parent(); if (count($block_parent->get_line_boxes()) < $frame->get_style()->orphans) { dompdf_debug("page-break", "orphans"); return false; } $p = $block_parent; while ($p) { if ($p->get_style()->page_break_inside === "avoid") { dompdf_debug("page-break", "parent->inside: avoid"); return false; } $p = $p->find_block_parent(); } $prev = $frame->get_prev_sibling(); while ($prev && ($prev->is_text_node() && trim($prev->get_node()->nodeValue) == "")) { $prev = $prev->get_prev_sibling(); } if ($block_parent->get_node()->nodeName === "body" && !$prev) { dompdf_debug("page-break", "Body's first child."); return false; } if ($frame->is_text_node() && $frame->get_node()->nodeValue == "") { return false; } dompdf_debug("page-break", "inline: break allowed"); return true; } else { if ($display === "table-row") { $p = Table_Frame_Decorator::find_parent_table($frame); while ($p) { if ($p->get_style()->page_break_inside === "avoid") { dompdf_debug("page-break", "parent->inside: avoid"); return false; } $p = $p->find_block_parent(); } if ($p && $p->get_first_child() === $frame) { dompdf_debug("page-break", "table: first-row"); return false; } if ($this->_in_table > 1) { dompdf_debug("page-break", "table: nested table"); return false; } dompdf_debug("page-break", "table-row/row-groups: break allowed"); return true; } else { if (in_array($display, Table_Frame_Decorator::$ROW_GROUPS)) { return false; } else { dompdf_debug("page-break", "? " . $frame->get_style()->display . ""); return false; } } } } }