ter-styles-3/#typedef-counter-style-name */ protected function isValidCounterStyleName(string $name): bool { return $name !== "none" && !in_array($name, self::CUSTOM_IDENT_FORBIDDEN, true); } /** * Parse a property value with 1 to 4 components into 4 values, as required * by shorthand properties such as `margin`, `padding`, and `border-radius`. * * @param string $prop The shorthand property with exactly 4 sub-properties to handle. * @param string $value The property value to parse. * * @return string[] */ protected function set_quad_shorthand(string $prop, string $value): array { $v = $this->parse_property_value($value); switch (\count($v)) { case 1: $values = [$v[0], $v[0], $v[0], $v[0]]; break; case 2: $values = [$v[0], $v[1], $v[0], $v[1]]; break; case 3: $values = [$v[0], $v[1], $v[2], $v[1]]; break; case 4: $values = [$v[0], $v[1], $v[2], $v[3]]; break; default: return []; } return array_combine(self::$_props_shorthand[$prop], $values); } /*======================*/ /** * @link https://www.w3.org/TR/CSS21/visuren.html#display-prop */ protected function _compute_display(string $val) { $val = strtolower($val); // Make sure that common valid, but unsupported display types have an // appropriate fallback display type switch ($val) { case "flow-root": case "flex": case "grid": case "table-caption": $val = "block"; break; case "inline-flex": case "inline-grid": $val = "inline-block"; break; } if (!isset(self::$valid_display_types[$val])) { return null; } // https://www.w3.org/TR/CSS21/visuren.html#dis-pos-flo if ($this->is_in_flow()) { return $val; } else { switch ($val) { case "inline": case "inline-block": // case "table-row-group": // case "table-header-group": // case "table-footer-group": // case "table-row": // case "table-cell": // case "table-column-group": // case "table-column": // case "table-caption": return "block"; case "inline-table": return "table"; default: return $val; } } } /** * @link https://www.w3.org/TR/CSS21/colors.html#propdef-color */ protected function _compute_color(string $color) { return $this->compute_color_value($color); } /** * @link https://www.w3.org/TR/CSS21/colors.html#propdef-background-color */ protected function _compute_background_color(string $color) { return $this->compute_color_value($color); } /** * @link https://www.w3.org/TR/CSS21/colors.html#propdef-background-image */ protected function _compute_background_image(string $val) { $parsed_val = $this->_stylesheet->resolve_url($val); if ($parsed_val === "none") { return "none"; } else { return "url(\"" . str_replace("\"", "\\\"", $parsed_val) . "\")"; } } /** * @link https://www.w3.org/TR/CSS21/colors.html#propdef-background-repeat */ protected function _compute_background_repeat(string $val) { $keywords = ["repeat", "repeat-x", "repeat-y", "no-repeat"]; $val = strtolower($val); return \in_array($val, $keywords, true) ? $val : null; } /** * @link https://www.w3.org/TR/CSS21/colors.html#propdef-background-attachment */ protected function _compute_background_attachment(string $val) { $keywords = ["scroll", "fixed"]; $val = strtolower($val); return \in_array($val, $keywords, true) ? $val : null; } /** * @link https://www.w3.org/TR/CSS21/colors.html#propdef-background-position */ protected function _compute_background_position(string $val) { $val = strtolower($val); $parts = $this->parse_property_value($val); $count = \count($parts); if ($count === 0 || $count > 2) { return null; } $v1 = $parts[0]; $v2 = $parts[1] ?? "center"; [$x, $y] = $this->computeBackgroundPositionTransformOrigin($v1, $v2); if ($x === null || $y === null) { return null; } return [$x, $y]; } /** * Compute `background-size`. * * Computes to one of the following values: * * `cover` * * `contain` * * `[width, height]`, each being a length, percentage, or `auto` * * @link https://www.w3.org/TR/css-backgrounds-3/#background-size */ protected function _compute_background_size(string $val) { $val = strtolower($val); if ($val === "cover" || $val === "contain") { return $val; } $parts = $this->parse_property_value($val); $count = \count($parts); if ($count === 0 || $count > 2) { return null; } $width = $parts[0]; if ($width !== "auto") { $width = $this->compute_length_percentage_positive($width); } $height = $parts[1] ?? "auto"; if ($height !== "auto") { $height = $this->compute_length_percentage_positive($height); } if ($width === null || $height === null) { return null; } return [$width, $height]; } /** * @link https://www.w3.org/TR/css-backgrounds-3/#propdef-background */ protected function _set_background(string $value): array { $components = $this->parse_property_value($value); $props = []; $pos_size = []; foreach ($components as $val) { $lower = strtolower($val); if ($lower === "none") { $props["background_image"] = $lower; } elseif (strncmp($lower, "url(", 4) === 0) { $props["background_image"] = $val; } elseif ($lower === "scroll" || $lower === "fixed") { $props["background_attachment"] = $lower; } elseif ($lower === "repeat" || $lower === "repeat-x" || $lower === "repeat-y" || $lower === "no-repeat") { $props["background_repeat"] = $lower; } elseif ($this->is_color_value($lower)) { $props["background_color"] = $lower; } else { $pos_size[] = $lower; } } if (\count($pos_size)) { // Split value list at "/" $index = array_search("/", $pos_size, true); if ($index !== false) { $pos = \array_slice($pos_size, 0, $index); $size = \array_slice($pos_size, $index + 1); } else { $pos = $pos_size; $size = []; } $props["background_position"] = implode(" ", $pos); if (\count($size)) { $props["background_size"] = implode(" ", $size); } } return $props; } /** * @link https://www.w3.org/TR/CSS21/fonts.html#propdef-font-family */ protected function _compute_font_family(string $val) { return array_map( function ($name) { return trim($name, " '\""); }, preg_split("/\s*,\s*/", $val) ); } /** * @link https://www.w3.org/TR/CSS21/fonts.html#propdef-font-size */ protected function _compute_font_size(string $val) { $val = strtolower($val); $parentFontSize = isset($this->parent_style) ? $this->parent_style->__get("font_size") : self::$default_font_size; switch ($val) { case "xx-small": case "x-small": case "small": case "medium": case "large": case "x-large": case "xx-large": $computed = self::$default_font_size * self::$font_size_keywords[$val]; break; case "smaller": $computed = 8 / 9 * $parentFontSize; break; case "larger": $computed = 6 / 5 * $parentFontSize; break; default: $computed = $this->single_length_in_pt($val, $parentFontSize, $parentFontSize); // Negative non-`calc` values are invalid if ($computed === null || ($computed < 0 && !preg_match("/^-?[_a-zA-Z]/", $val)) ) { return null; } break; } return $computed; } /** * @link https://www.w3.org/TR/CSS21/fonts.html#propdef-font-style */ protected function _compute_font_style(string $val) { $val = strtolower($val); return $val === "normal" || $val === "italic" || $val === "oblique" ? $val : null; } /** * @link https://www.w3.org/TR/css-fonts-4/#propdef-font-weight */ protected function _compute_font_weight(string $val) { $val = strtolower($val); switch ($val) { case "normal": return 400; case "bold": return 700; case "bolder": // https://www.w3.org/TR/css-fonts-4/#relative-weights $w = isset($this->parent_style) ? $this->parent_style->__get("font_weight") : 400; if ($w < 350) { return 400; } elseif ($w < 550) { return 700; } elseif ($w < 900) { return 900; } else { return $w; } case "lighter": // https://www.w3.org/TR/css-fonts-4/#relative-weights $w = isset($this->parent_style) ? $this->parent_style->__get("font_weight") : 400; if ($w < 100) { return $w; } elseif ($w < 550) { return 100; } elseif ($w < 750) { return 400; } else { return 700; } default: $number = self::CSS_NUMBER; $weight = preg_match("/^$number$/", $val) ? (int) $val : null; return $weight !== null && $weight >= 1 && $weight <= 1000 ? $weight : null; } } /** * @link https://www.w3.org/TR/css-fonts-4/#src-desc */ protected function _compute_src(string $val) { return $val; } /** * Handle the `font` shorthand property. * * `[ font-style || font-variant || font-weight ] font-size [ / line-height ] font-family` * * @link https://www.w3.org/TR/CSS21/fonts.html#font-shorthand */ protected function _set_font(string $value): array { $value = strtolower($value); $components = $this->parse_property_value($value); $props = []; $number = self::CSS_NUMBER; $unit = "pt|px|pc|rem|em|ex|in|cm|mm|%"; $sizePattern = "/^(xx-small|x-small|small|medium|large|x-large|xx-large|smaller|larger|$number(?:$unit)|0)$/"; $sizeIndex = null; // Find index of font-size to split the component list foreach ($components as $i => $val) { if (preg_match($sizePattern, $val)) { $sizeIndex = $i; $props["font_size"] = $val; break; } } // `font-size` is mandatory if ($sizeIndex === null) { return []; } // `font-style`, `font-variant`, `font-weight` in any order $styleVariantWeight = \array_slice($components, 0, $sizeIndex); $stylePattern = "/^(italic|oblique)$/"; $variantPattern = "/^(small-caps)$/"; $weightPattern = "/^(bold|bolder|lighter|$number)$/"; if (\count($styleVariantWeight) > 3) { return []; } foreach ($styleVariantWeight as $val) { if ($val === "normal") { // Ignore any `normal` value, as it is valid and the initial // value for all three properties } elseif (!isset($props["font_style"]) && preg_match($stylePattern, $val)) { $props["font_style"] = $val; } elseif (!isset($props["font_variant"]) && preg_match($variantPattern, $val)) { $props["font_variant"] = $val; } elseif (!isset($props["font_weight"]) && preg_match($weightPattern, $val)) { $props["font_weight"] = $val; } else { // Duplicates and other values disallowed here return []; } } // Optional slash + `line-height` followed by mandatory `font-family` $lineFamily = \array_slice($components, $sizeIndex + 1); $hasLineHeight = $lineFamily !== [] && $lineFamily[0] === "/"; $lineHeight = $hasLineHeight ? \array_slice($lineFamily, 1, 1) : []; $fontFamily = $hasLineHeight ? \array_slice($lineFamily, 2) : $lineFamily; $lineHeightPattern = "/^(normal|$number(?:$unit)?)$/"; // Missing `font-family` or `line-height` after slash if ($fontFamily === [] || ($hasLineHeight && !preg_match($lineHeightPattern, $lineHeight[0])) ) { return []; } if ($hasLineHeight) { $props["line_height"] = $lineHeight[0]; } $props["font_family"] = implode("", $fontFamily); return $props; } /** * Compute `text-align`. * * If no alignment is set on the element and the direction is rtl then * the property is set to "right", otherwise it is set to "left". * * @link https://www.w3.org/TR/CSS21/text.html#propdef-text-align */ protected function _compute_text_align(string $val) { $alignment = strtolower($val); if ($alignment === "") { $alignment = $this->__get("direction") === "rtl" ? "right" : "left"; } if (!\in_array($alignment, self::TEXT_ALIGN_KEYWORDS, true)) { return null; } return $alignment; } /** * @link https://www.w3.org/TR/css-text-4/#word-spacing-property */ protected function _compute_word_spacing(string $val) { $val = strtolower($val); if ($val === "normal") { return 0.0; } return $this->compute_length_percentage($val); } /** * @link https://www.w3.org/TR/css-text-4/#letter-spacing-property */ protected function _compute_letter_spacing(string $val) { $val = strtolower($val); if ($val === "normal") { return 0.0; } return $this->compute_length_percentage($val); } /** * @link https://www.w3.org/TR/CSS21/visudet.html#propdef-line-height */ protected function _compute_line_height(string $val) { $val = strtolower($val); if ($val === "normal") { return $val; } // Compute number values to string and lengths to float (in pt) if (is_numeric($val)) { return (string) $val; } $font_size = $this->__get("font_size"); $computed = $this->single_length_in_pt($val, $font_size); // Negative non-`calc` values are invalid if ($computed === null || ($computed < 0 && !preg_match("/^-?[_a-zA-Z]/", $val)) ) { return null; } return $computed; } /** * @link https://www.w3.org/TR/css-text-3/#text-indent-property */ protected function _compute_text_indent(string $val) { return $this->compute_length_percentage($val); } /** * @link https://www.w3.org/TR/CSS21/page.html#propdef-page-break-before */ protected function _compute_page_break_before(string $val) { $break = strtolower($val); if ($break === "left" || $break === "right") { $break = "always"; } return $break; } /** * @link https://www.w3.org/TR/CSS21/page.html#propdef-page-break-after */ protected function _compute_page_break_after(string $val) { $break = strtolower($val); if ($break === "left" || $break === "right") { $break = "always"; } return $break; } /** * @link https://www.w3.org/TR/CSS21/visudet.html#propdef-width */ protected function _compute_width(string $val) { $val = strtolower($val); if ($val === "auto") { return $val; } return $this->compute_length_percentage_positive($val); } /** * @link https://www.w3.org/TR/CSS21/visudet.html#propdef-height */ protected function _compute_height(string $val) { $val = strtolower($val); if ($val === "auto") { return $val; } return $this->compute_length_percentage_positive($val); } /** * @link https://www.w3.org/TR/CSS21/visudet.html#propdef-min-width */ protected function _compute_min_width(string $val) { $val = strtolower($val); // Legacy support for `none`, not covered by spec if ($val === "auto" || $val === "none") { return "auto"; } return $this->compute_length_percentage_positive($val); } /** * @link https://www.w3.org/TR/CSS21/visudet.html#propdef-min-height */ protected function _compute_min_height(string $val) { $val = strtolower($val); // Legacy support for `none`, not covered by spec if ($val === "auto" || $val === "none") { return "auto"; } return $this->compute_length_percentage_positive($val); } /** * @link https://www.w3.org/TR/CSS21/visudet.html#propdef-max-width */ protected function _compute_max_width(string $val) { $val = strtolower($val); // Legacy support for `auto`, not covered by spec if ($val === "none" || $val === "auto") { return "none"; } return $this->compute_length_percentage_positive($val); } /** * @link https://www.w3.org/TR/CSS21/visudet.html#propdef-max-height */ protected function _compute_max_height(string $val) { $val = strtolower($val); // Legacy support for `auto`, not covered by spec if ($val === "none" || $val === "auto") { return "none"; } return $this->compute_length_percentage_positive($val); } /** * @link https://www.w3.org/TR/css-position-3/#inset-properties * @link https://www.w3.org/TR/css-position-3/#propdef-inset */ protected function _set_inset(string $val): array { return $this->set_quad_shorthand("inset", $val); } /** * @param string $val * @return float|string|null */ protected function compute_box_inset(string $val) { $val = strtolower($val); if ($val === "auto") { return $val; } return $this->compute_length_percentage($val); } protected function _compute_top(string $val) { return $this->compute_box_inset($val); } protected function _compute_right(string $val) { return $this->compute_box_inset($val); } protected function _compute_bottom(string $val) { return $this->compute_box_inset($val); } protected function _compute_left(string $val) { return $this->compute_box_inset($val); } /** * @link https://www.w3.org/TR/CSS21/box.html#margin-properties * @link https://www.w3.org/TR/CSS21/box.html#propdef-margin */ protected function _set_margin(string $val): array { return $this->set_quad_shorthand("margin", $val); } /** * @param string $val * @return float|string|null */ protected function compute_margin(string $val) { $val = strtolower($val); // Legacy support for `none` keyword, not covered by spec if ($val === "none") { return 0.0; } if ($val === "auto") { return $val; } return $this->compute_length_percentage($val); } protected function _compute_margin_top(string $val) { return $this->compute_margin($val); } protected function _compute_margin_right(string $val) { return $this->compute_margin($val); } protected function _compute_margin_bottom(string $val) { return $this->compute_margin($val); } protected function _compute_margin_left(string $val) { return $this->compute_margin($val); } /** * @link https://www.w3.org/TR/CSS21/box.html#padding-properties * @link https://www.w3.org/TR/CSS21/box.html#propdef-padding */ protected function _set_padding(string $val): array { return $this->set_quad_shorthand("padding", $val); } /** * @param string $val * @return float|string|null */ protected function compute_padding(string $val) { $val = strtolower($val); // Legacy support for `none` keyword, not covered by spec if ($val === "none") { return 0.0; } return $this->compute_length_percentage_positive($val); } protected function _compute_padding_top(string $val) { return $this->compute_padding($val); } protected function _compute_padding_right(string $val) { return $this->compute_padding($val); } protected function _compute_padding_bottom(string $val) { return $this->compute_padding($val); } protected function _compute_padding_left(string $val) { return $this->compute_padding($val); } /** * @param string $value `width || style || color` * @param string[] $styles The list of border styles to accept. * * @return string[]|null Array of `[width, style, color]`, or `null` if the declaration is invalid. */ protected function parse_border_side(string $value, array $styles = self::BORDER_STYLES): ?array { $value = strtolower($value); $components = $this->parse_property_value($value); $width = null; $style = null; $color = null; foreach ($components as $val) { if ($style === null && \in_array($val, $styles, true)) { $style = $val; } elseif ($color === null && $this->is_color_value($val)) { $color = $val; } elseif ($width === null) { // Assume width $width = $val; } else { // Duplicates are not allowed return null; } } return [$width, $style, $color]; } /** * @link https://www.w3.org/TR/CSS21/box.html#border-properties * @link https://www.w3.org/TR/CSS21/box.html#propdef-border */ protected function _set_border(string $value): array { $values = $this->parse_border_side($value); if ($values === null) { return []; } return array_merge( array_combine(self::$_props_shorthand["border_top"], $values), array_combine(self::$_props_shorthand["border_right"], $values), array_combine(self::$_props_shorthand["border_bottom"], $values), array_combine(self::$_props_shorthand["border_left"], $values) ); } /** * @param string $prop * @param string $value * @return array */ protected function set_border_side(string $prop, string $value): array { $values = $this->parse_border_side($value); if ($values === null) { return []; } return array_combine(self::$_props_shorthand[$prop], $values); } protected function _set_border_top(string $val): array { return $this->set_border_side("border_top", $val); } protected function _set_border_right(string $val): array { return $this->set_border_side("border_right", $val); } protected function _set_border_bottom(string $val): array { return $this->set_border_side("border_bottom", $val); } protected function _set_border_left(string $val): array { return $this->set_border_side("border_left", $val); } /** * @link https://www.w3.org/TR/CSS21/box.html#propdef-border-color */ protected function _set_border_color(string $val): array { return $this->set_quad_shorthand("border_color", $val); } protected function _compute_border_top_color(string $val) { return $this->compute_color_value($val); } protected function _compute_border_right_color(string $val) { return $this->compute_color_value($val); } protected function _compute_border_bottom_color(string $val) { return $this->compute_color_value($val); } protected function _compute_border_left_color(string $val) { return $this->compute_color_value($val); } /** * @link https://www.w3.org/TR/CSS21/box.html#propdef-border-style */ protected function _set_border_style(string $val): array { return $this->set_quad_shorthand("border_style", $val); } protected function _compute_border_top_style(string $val) { return $this->compute_border_style($val); } protected function _compute_border_right_style(string $val) { return $this->compute_border_style($val); } protected function _compute_border_bottom_style(string $val) { return $this->compute_border_style($val); } protected function _compute_border_left_style(string $val) { return $this->compute_border_style($val); } /** * @link https://www.w3.org/TR/CSS21/box.html#propdef-border-width */ protected function _set_border_width(string $val): array { return $this->set_quad_shorthand("border_width", $val); } protected function _compute_border_top_width(string $val) { return $this->compute_line_width($val, "border_top_style"); } protected function _compute_border_right_width(string $val) { return $this->compute_line_width($val, "border_right_style"); } protected function _compute_border_bottom_width(string $val) { return $this->compute_line_width($val, "border_bottom_style"); } protected function _compute_border_left_width(string $val) { return $this->compute_line_width($val, "border_left_style"); } /** * @link https://www.w3.org/TR/css-backgrounds-3/#corners * @link https://www.w3.org/TR/css-backgrounds-3/#propdef-border-radius */ protected function _set_border_radius(string $val): array { return $this->set_quad_shorthand("border_radius", $val); } protected function _compute_border_top_left_radius(string $val) { return $this->compute_length_percentage_positive($val); } protected function _compute_border_top_right_radius(string $val) { return $this->compute_length_percentage_positive($val); } protected function _compute_border_bottom_right_radius(string $val) { return $this->compute_length_percentage_positive($val); } protected function _compute_border_bottom_left_radius(string $val) { return $this->compute_length_percentage_positive($val); } /** * @link https://www.w3.org/TR/css-ui-4/#outline-props * @link https://www.w3.org/TR/css-ui-4/#propdef-outline */ protected function _set_outline(string $value): array { $values = $this->parse_border_side($value, self::OUTLINE_STYLES); if ($values === null) { return []; } return array_combine(self::$_props_shorthand["outline"], $values); } protected function _compute_outline_color(string $val) { return $this->compute_color_value($val); } protected function _compute_outline_style(string $val) { $val = strtolower($val); return \in_array($val, self::OUTLINE_STYLES, true) ? $val : null; } protected function _compute_outline_width(string $val) { return $this->compute_line_width($val, "outline_style"); } /** * @link https://www.w3.org/TR/css-ui-4/#propdef-outline-offset */ protected function _compute_outline_offset(string $val) { return $this->compute_length($val); } /** * Compute `border-spacing` to two lengths of the form * `[horizontal, vertical]`. * * @link https://www.w3.org/TR/CSS21/tables.html#propdef-border-spacing */ protected function _compute_border_spacing(string $val) { $val = strtolower($val); $parts = $this->parse_property_value($val); $count = \count($parts); if ($count === 0 || $count > 2) { return null; } $h = $this->compute_length_positive($parts[0]); $v = isset($parts[1]) ? $this->compute_length_positive($parts[1]) : $h; if ($h === null || $v === null) { return null; } return [$h, $v]; } /** * @link https://www.w3.org/TR/CSS21/generate.html#propdef-list-style-image */ protected function _compute_list_style_image(string $val) { $parsed_val = $this->_stylesheet->resolve_url($val); if ($parsed_val === "none") { return "none"; } else { return "url(\"" . str_replace("\"", "\\\"", $parsed_val) . "\")"; } } /** * @link https://www.w3.org/TR/CSS21/generate.html#propdef-list-style-type */ protected function _compute_list_style_position(string $val) { $val = strtolower($val); return $val === "inside" || $val === "outside" ? $val : null; } /** * @link https://www.w3.org/TR/CSS21/generate.html#propdef-list-style-type */ protected function _compute_list_style_type(string $val) { $val = strtolower($val); if ($val === "none") { return $val; } $ident = self::CSS_IDENTIFIER; return $val !== "default" && preg_match("/^$ident$/", $val) ? $val : null; } /** * Handle the `list-style` shorthand property. * * `[ list-style-position || list-style-image || list-style-type ]` * * @link https://www.w3.org/TR/css-lists-3/#list-style-property */ protected function _set_list_style(string $value): array { $components = $this->parse_property_value($value); $none = 0; $position = null; $image = null; $type = null; foreach ($components as $val) { $lower = strtolower($val); // `none` can occur max 2 times (for image and type each) if ($none < 2 && $lower === "none") { $none++; } elseif ($position === null && ($lower === "inside" || $lower === "outside")) { $position = $lower; } elseif ($image === null && strncmp($lower, "url(", 4) === 0) { $image = $val; } elseif ($type === null) { $type = $val; } else { // Duplicates are not allowed return []; } } // From the spec: // Using a value of `none` in the shorthand is potentially ambiguous, as // `none` is a valid value for both `list-style-image` and `list-style-type`. // To resolve this ambiguity, a value of `none` in the shorthand must be // applied to whichever of the two properties aren’t otherwise set by // the shorthand. if ($none === 2) { if ($image !== null || $type !== null) { return []; } $image = "none"; $type = "none"; } elseif ($none === 1) { if ($image !== null && $type !== null) { return []; } $image = $image ?? "none"; $type = $type ?? "none"; } return [ "list_style_position" => $position, "list_style_image" => $image, "list_style_type" => $type ]; } /** * @param string $value * @param int $default * @param bool $sumDuplicates * * @return array|string|null */ protected function compute_counter_prop(string $value, int $default, bool $sumDuplicates = false) { $lower = strtolower($value); if ($lower === "none") { return $lower; } $ident = self::CSS_IDENTIFIER; $integer = self::CSS_INTEGER; $counterDef = "($ident)(?:\s+($integer))?"; $validationPattern = "/^$counterDef(\s+$counterDef)*$/"; if (!preg_match($validationPattern, $value)) { return null; } preg_match_all("/$counterDef/", $value, $matches, PREG_SET_ORDER); $counters = []; foreach ($matches as $match) { $name = $match[1]; if (!$this->isValidCounterName($name)) { return null; } $value = isset($match[2]) ? (int) $match[2] : $default; $counters[$name] = $sumDuplicates ? ($counters[$name] ?? 0) + $value : $value; } return $counters; } /** * @link https://www.w3.org/TR/CSS21/generate.html#propdef-counter-increment */ protected function _compute_counter_increment(string $val) { return $this->compute_counter_prop($val, 1, true); } /** * @link https://www.w3.org/TR/CSS21/generate.html#propdef-counter-reset */ protected function _compute_counter_reset(string $val) { return $this->compute_counter_prop($val, 0); } /** * @link https://www.w3.org/TR/css-content-3/#quotes */ protected function _compute_quotes(string $val) { $lower = strtolower($val); // `auto` is resolved in the getter, so it can inherit as is if ($lower === "none" || $lower === "auto") { return $lower; } $components = $this->parse_property_value($val); $quotes = []; foreach ($components as $value) { if (strncmp($value, '"', 1) !== 0 && strncmp($value, "'", 1) !== 0 ) { return null; } $quotes[] = $this->parse_string($value); } if ($quotes === [] || \count($quotes) % 2 !== 0) { return null; } return array_chunk($quotes, 2); } /** * @link https://www.w3.org/TR/CSS21/generate.html#propdef-content * @link https://www.w3.org/TR/css-content-3/#propdef-content */ protected function _compute_content(string $val) { $lower = strtolower($val); if ($lower === "normal" || $lower === "none") { return $lower; } $components = $this->parse_property_value($val); $parts = []; if ($components === []) { return null; } foreach ($components as $value) { // String if (strncmp($value, '"', 1) === 0 || strncmp($value, "'", 1) === 0) { $parts[] = new StringPart($this->parse_string($value)); continue; } $lower = strtolower($value); // Keywords if ($lower === "open-quote") { $parts[] = new OpenQuote; continue; } elseif ($lower === "close-quote") { $parts[] = new CloseQuote; continue; } elseif ($lower === "no-open-quote") { $parts[] = new NoOpenQuote; continue; } elseif ($lower === "no-close-quote") { $parts[] = new NoCloseQuote; continue; } // Functional components $pos = strpos($lower, "("); if ($pos === false) { return null; } // `parse_property_value` ensures that the value is of the form // `function(arguments)` at this point $function = substr($lower, 0, $pos); $arguments = trim(substr($value, $pos + 1, -1)); // attr() if ($function === "attr") { $attr = strtolower($arguments); if ($attr === "") { return null; } $parts[] = new Attr($attr); } // counter(name [, style]) elseif ($function === "counter") { $ident = self::CSS_IDENTIFIER; if (!preg_match("/^($ident)(?:\s*,\s*($ident))?$/", $arguments, $matches)) { return null; } $name = $matches[1]; $type = isset($matches[2]) ? strtolower($matches[2]) : "decimal"; if (!$this->isValidCounterName($name) || !$this->isValidCounterStyleName($type) ) { return null; } $parts[] = new Counter($name, $type); } // counters(name, string [, style]) elseif ($function === "counters") { $ident = self::CSS_IDENTIFIER; $string = self::CSS_STRING; if (!preg_match("/^($ident)\s*,\s*($string)(?:\s*,\s*($ident))?$/", $arguments, $matches)) { return null; } $name = $matches[1]; $string = $this->parse_string($matches[2]); $type = isset($matches[3]) ? strtolower($matches[3]) : "decimal"; if (!$this->isValidCounterName($name) || !$this->isValidCounterStyleName($type) ) { return null; } $parts[] = new Counters($name, $string, $type); } // url() elseif ($function === "url") { $url = $this->parse_string($arguments); $parts[] = new Url($url); } else { return null; } } return $parts; } /** * @link https://www.w3.org/TR/css-page-3/#page-size-prop */ protected function _compute_size(string $val) { $val = strtolower($val); if ($val === "auto") { return $val; } $parts = $this->parse_property_value($val); $count = \count($parts); if ($count === 0 || $count > 3) { return null; } $size = null; $orientation = null; $lengths = []; foreach ($parts as $part) { if ($size === null && isset(CPDF::$PAPER_SIZES[$part])) { $size = $part; } elseif ($orientation === null && ($part === "portrait" || $part === "landscape")) { $orientation = $part; } else { $lengths[] = $part; } } if ($size !== null && $lengths !== []) { return null; } if ($size !== null) { // Standard paper size [$l1, $l2] = \array_slice(CPDF::$PAPER_SIZES[$size], 2, 2); } elseif ($lengths === []) { // Orientation only, use default paper size $dims = $this->_stylesheet->get_dompdf()->getPaperSize(); [$l1, $l2] = \array_slice($dims, 2, 2); } else { // Custom paper size $l1 = $this->compute_length_positive($lengths[0]); $l2 = isset($lengths[1]) ? $this->compute_length_positive($lengths[1]) : $l1; if ($l1 === null || $l2 === null) { return null; } } if (($orientation === "portrait" && $l1 > $l2) || ($orientation === "landscape" && $l2 > $l1) ) { return [$l2, $l1]; } return [$l1, $l2]; } /** * @link https://www.w3.org/TR/css-transforms-1/#transform-property */ protected function _compute_transform(string $val) { $val = strtolower($val); if ($val === "none") { return []; } $parts = $this->parse_property_value($val); $transforms = []; if ($parts === []) { return null; } foreach ($parts as $part) { if (!preg_match("/^([a-z]+)\((.+)\)$/s", $part, $matches)) { return null; } $name = $matches[1]; $arguments = trim($matches[2]); $values = $this->parse_property_value($arguments); $values = array_values(array_filter($values, function ($v) { return $v !== ","; })); $count = \count($values); if ($count === 0) { return null; } switch ($name) { // case "matrix": // if ($count !== 6) { // return null; // } // $values = array_map([$this, "compute_number"], $values); // break; // units case "translate": if ($count > 2) { return null; } $values = [ $this->compute_length_percentage($values[0]), isset($values[1]) ? $this->compute_length_percentage($values[1]) : 0.0 ]; break; case "translatex": if ($count > 1) { return null; } $name = "translate"; $values = [$this->compute_length_percentage($values[0]), 0.0]; break; case "translatey": if ($count > 1) { return null; } $name = "translate"; $values = [0.0, $this->compute_length_percentage($values[0])]; break; // units case "scale": if ($count > 2) { return null; } $v0 = $this->compute_number($values[0]); $v1 = isset($values[1]) ? $this->compute_number($values[1]) : $v0; $values = [$v0, $v1]; break; case "scalex": if ($count > 1) { return null; } $name = "scale"; $values = [$this->compute_number($values[0]), 1.0]; break; case "scaley": if ($count > 1) { return null; } $name = "scale"; $values = [1.0, $this->compute_number($values[0])]; break; // units case "rotate": if ($count > 1) { return null; } $values = [$this->compute_angle_or_zero($values[0])]; break; case "skew": if ($count > 2) { return null; } $values = [ $this->compute_angle_or_zero($values[0]), isset($values[1]) ? $this->compute_angle_or_zero($values[1]) : 0.0 ]; break; case "skewx": if ($count > 1) { return null; } $name = "skew"; $values = [$this->compute_angle_or_zero($values[0]), 0.0]; break; case "skewy": if ($count > 1) { return null; } $name = "skew"; $values = [0.0, $this->compute_angle_or_zero($values[0])]; break; default: return null; } foreach ($values as $v) { if ($v === null) { return null; } } $transforms[] = [$name, $values]; } return $transforms; } /** * @link https://www.w3.org/TR/css-transforms-1/#transform-origin-property */ protected function _compute_transform_origin(string $val) { $val = strtolower($val); $parts = $this->parse_property_value($val); $count = \count($parts); if ($count === 0 || $count > 3) { return null; } $v1 = $parts[0]; $v2 = $parts[1] ?? "center"; [$x, $y] = $this->computeBackgroundPositionTransformOrigin($v1, $v2); $z = $count === 3 ? $this->compute_length($parts[2]) : 0.0; if ($x === null || $y === null || $z === null) { return null; } return [$x, $y, $z]; } /** * @param string $val * @return string|null */ protected function parse_image_resolution(string $val): ?string { // If exif data could be get: // $re = '/^\s*(\d+|normal|auto)(?:\s*,\s*(\d+|normal))?\s*$/'; $val = strtolower($val); $re = '/^\s*(\d+|normal|auto)\s*$/'; if (!preg_match($re, $val, $matches)) { return null; } return $matches[1]; } /** * auto | normal | dpi */ protected function _compute_background_image_resolution(string $val) { return $this->parse_image_resolution($val); } /** * auto | normal | dpi */ protected function _compute_image_resolution(string $val) { return $this->parse_image_resolution($val); } /** * @link https://www.w3.org/TR/css-break-3/#propdef-orphans */ protected function _compute_orphans(string $val) { return $this->compute_integer($val); } /** * @link https://www.w3.org/TR/css-break-3/#propdef-widows */ protected function _compute_widows(string $val) { return $this->compute_integer($val); } /** * @link https://www.w3.org/TR/css-color-4/#propdef-opacity */ protected function _compute_opacity(string $val) { $number = self::CSS_NUMBER; $pattern = "/^($number)(%?)$/"; if (!preg_match($pattern, $val, $matches)) { return null; } $v = (float) $matches[1]; $percent = $matches[2] === "%"; $opacity = $percent ? ($v / 100) : $v; return max(0.0, min($opacity, 1.0)); } /** * @link https://www.w3.org/TR/CSS21//visuren.html#propdef-z-index */ protected function _compute_z_index(string $val) { $val = strtolower($val); if ($val === "auto") { return $val; } return $this->compute_integer($val); } /** * @param FontMetrics $fontMetrics * @return $this */ public function setFontMetrics(FontMetrics $fontMetrics) { $this->fontMetrics = $fontMetrics; return $this; } /** * @return FontMetrics */ public function getFontMetrics() { return $this->fontMetrics; } /** * Generate a string representation of the Style * * This dumps the entire property array into a string via print_r. Useful * for debugging. * * @return string */ /*DEBUGCSS print: see below additional debugging util*/ public function __toString(): string { $parent_font_size = $this->parent_style ? $this->parent_style->font_size : self::$default_font_size; return print_r(array_merge(["parent_font_size" => $parent_font_size], $this->_props), true); } /*DEBUGCSS*/ public function debug_print(): void { $parent_font_size = $this->parent_style ? $this->parent_style->font_size : self::$default_font_size; print " parent_font_size:" . $parent_font_size . ";\n"; print " Props [\n"; print " specified [\n"; foreach ($this->_props as $prop => $val) { print ' ' . $prop . ': ' . preg_replace("/\r\n/", ' ', print_r($val, true)); if (isset($this->_important_props[$prop])) { print ' !important'; } print ";\n"; } print " ]\n"; print " computed [\n"; foreach ($this->_props_computed as $prop => $val) { print ' ' . $prop . ': ' . preg_replace("/\r\n/", ' ', print_r($val, true)); print ";\n"; } print " ]\n"; print " cached [\n"; foreach ($this->_props_used as $prop => $val) { print ' ' . $prop . ': ' . preg_replace("/\r\n/", ' ', print_r($val, true)); print ";\n"; } print " ]\n"; print " ]\n"; } }