<?php

namespace Google\Web_Stories_Dependencies;

/**
 * Class AMP_Style_Sanitizer
 *
 * @package AMP
 */
use Google\Web_Stories_Dependencies\AmpProject\Amp;
use Google\Web_Stories_Dependencies\AmpProject\AmpWP\Icon;
use Google\Web_Stories_Dependencies\AmpProject\AmpWP\Option;
use Google\Web_Stories_Dependencies\AmpProject\AmpWP\RemoteRequest\CachedRemoteGetRequest;
use Google\Web_Stories_Dependencies\AmpProject\AmpWP\RemoteRequest\WpHttpRemoteGetRequest;
use Google\Web_Stories_Dependencies\AmpProject\AmpWP\ValidationExemption;
use Google\Web_Stories_Dependencies\AmpProject\DevMode;
use Google\Web_Stories_Dependencies\AmpProject\Dom\Document;
use Google\Web_Stories_Dependencies\AmpProject\Dom\Element;
use Google\Web_Stories_Dependencies\AmpProject\Exception\FailedToGetFromRemoteUrl;
use Google\Web_Stories_Dependencies\AmpProject\Html\Attribute;
use Google\Web_Stories_Dependencies\AmpProject\Html\RequestDestination;
use Google\Web_Stories_Dependencies\AmpProject\Html\Tag;
use Google\Web_Stories_Dependencies\AmpProject\RemoteGetRequest;
use Google\Web_Stories_Dependencies\Sabberworm\CSS\CSSList\AtRuleBlockList;
use Google\Web_Stories_Dependencies\Sabberworm\CSS\CSSList\CSSList;
use Google\Web_Stories_Dependencies\Sabberworm\CSS\CSSList\Document as CSSDocument;
use Google\Web_Stories_Dependencies\Sabberworm\CSS\CSSList\KeyFrame;
use Google\Web_Stories_Dependencies\Sabberworm\CSS\OutputFormat;
use Google\Web_Stories_Dependencies\Sabberworm\CSS\Parsing\SourceException;
use Google\Web_Stories_Dependencies\Sabberworm\CSS\Property\AtRule;
use Google\Web_Stories_Dependencies\Sabberworm\CSS\Property\Import;
use Google\Web_Stories_Dependencies\Sabberworm\CSS\Property\Selector;
use Google\Web_Stories_Dependencies\Sabberworm\CSS\RuleSet\AtRuleSet;
use Google\Web_Stories_Dependencies\Sabberworm\CSS\RuleSet\DeclarationBlock;
use Google\Web_Stories_Dependencies\Sabberworm\CSS\RuleSet\RuleSet;
use Google\Web_Stories_Dependencies\Sabberworm\CSS\Rule\Rule;
use Google\Web_Stories_Dependencies\Sabberworm\CSS\Value\CSSFunction;
use Google\Web_Stories_Dependencies\Sabberworm\CSS\Value\RuleValueList;
use Google\Web_Stories_Dependencies\Sabberworm\CSS\Value\URL;
use Google\Web_Stories_Dependencies\Sabberworm\CSS\Value\Value;
/**
 * Class AMP_Style_Sanitizer
 *
 * Collects inline styles and outputs them in the amp-custom stylesheet.
 *
 * @internal
 */
class AMP_Style_Sanitizer extends \Google\Web_Stories_Dependencies\AMP_Base_Sanitizer
{
    const STYLE_AMP_CUSTOM_SPEC_NAME = 'style amp-custom';
    const STYLE_AMP_KEYFRAMES_SPEC_NAME = 'style[amp-keyframes]';
    const ORIGINAL_STYLE_ATTRIBUTE_NAME = 'data-amp-original-style';
    const STYLE_AMP_CUSTOM_GROUP_INDEX = 0;
    const STYLE_AMP_KEYFRAMES_GROUP_INDEX = 1;
    // Error codes raised during parsing CSS. See get_css_parser_validation_error_codes.
    const CSS_SYNTAX_INVALID_AT_RULE = 'CSS_SYNTAX_INVALID_AT_RULE';
    const CSS_SYNTAX_INVALID_DECLARATION = 'CSS_SYNTAX_INVALID_DECLARATION';
    const CSS_SYNTAX_INVALID_PROPERTY = 'CSS_SYNTAX_INVALID_PROPERTY';
    const CSS_SYNTAX_INVALID_PROPERTY_NOLIST = 'CSS_SYNTAX_INVALID_PROPERTY_NOLIST';
    const CSS_SYNTAX_INVALID_IMPORTANT = 'CSS_SYNTAX_INVALID_IMPORTANT';
    const CSS_SYNTAX_PARSE_ERROR = 'CSS_SYNTAX_PARSE_ERROR';
    const CSS_DISALLOWED_SELECTOR = 'CSS_DISALLOWED_SELECTOR';
    const STYLESHEET_FETCH_ERROR = 'STYLESHEET_FETCH_ERROR';
    const STYLESHEET_TOO_LONG = 'STYLESHEET_TOO_LONG';
    // Error code when encountering 'i-amphtml-' prefixing a class name in an HTML class attribute.
    const DISALLOWED_ATTR_CLASS_NAME = 'DISALLOWED_ATTR_CLASS_NAME';
    // These are internal to the sanitizer and not exposed as validation error codes.
    const STYLESHEET_DISALLOWED_FILE_EXT = 'STYLESHEET_DISALLOWED_FILE_EXT';
    const STYLESHEET_EXTERNAL_FILE_URL = 'STYLESHEET_EXTERNAL_FILE_URL';
    const STYLESHEET_FILE_PATH_NOT_FOUND = 'STYLESHEET_FILE_PATH_NOT_FOUND';
    const STYLESHEET_FILE_PATH_NOT_ALLOWED = 'STYLESHEET_FILE_PATH_NOT_ALLOWED';
    const STYLESHEET_URL_SYNTAX_ERROR = 'STYLESHEET_URL_SYNTAX_ERROR';
    const STYLESHEET_INVALID_RELATIVE_PATH = 'STYLESHEET_INVALID_RELATIVE_PATH';
    /**
     * Percentage at which the used CSS budget becomes a warning.
     *
     * @var int
     */
    const CSS_BUDGET_WARNING_PERCENTAGE = 80;
    /**
     * Inline style selector's specificity multiplier, i.e. used to generate the number of ':not(#_)' placeholders.
     *
     * @var int
     */
    const INLINE_SPECIFICITY_MULTIPLIER = 5;
    // @todo The correctness of using "5" should be validated.
    /**
     * Array index for tag names extracted from a selector.
     *
     * @private
     * @since 1.1
     * @see \AMP_Style_Sanitizer::parse_stylesheet()
     */
    const SELECTOR_EXTRACTED_TAGS = 0;
    /**
     * Array index for class names extracted from a selector.
     *
     * @private
     * @since 1.1
     * @see \AMP_Style_Sanitizer::parse_stylesheet()
     */
    const SELECTOR_EXTRACTED_CLASSES = 1;
    /**
     * Array index for IDs extracted from a selector.
     *
     * @private
     * @since 1.1
     * @see \AMP_Style_Sanitizer::parse_stylesheet()
     */
    const SELECTOR_EXTRACTED_IDS = 2;
    /**
     * Array index for attributes extracted from a selector.
     *
     * @private
     * @since 1.1
     * @see \AMP_Style_Sanitizer::parse_stylesheet()
     */
    const SELECTOR_EXTRACTED_ATTRIBUTES = 3;
    /**
     * Array of flags used to control sanitization.
     *
     * @var array {
     *      @type bool     $disable_style_processing       Whether arbitrary styles should be allowed. When enabled, external stylesheet links, style elements, and style attributes will all be unprocessed and marked as PX-verified.
     *      @type string[] $dynamic_element_selectors      Selectors for elements (or their ancestors) which contain dynamic content; selectors containing these will not be filtered.
     *      @type bool     $use_document_element           Whether the root of the document should be used rather than the body.
     *      @type bool     $require_https_src              Require HTTPS URLs.
     *      @type callable $validation_error_callback      Function to call when a validation error is encountered.
     *      @type bool     $should_locate_sources          Whether to locate the sources when reporting validation errors.
     *      @type string   $parsed_cache_variant           Additional value by which to vary parsed cache.
     *      @type string[] $focus_within_classes           Class names in selectors that should be replaced with :focus-within pseudo classes.
     *      @type string[] $low_priority_plugins           Plugin slugs of the plugins to deprioritize when hitting the CSS limit.
     *      @type bool     $allow_transient_caching        Whether to allow caching parsed CSS in transients. This may need to be disabled when there is highly-variable CSS content.
     *      @type bool     $skip_tree_shaking              Whether tree shaking should be skipped.
     *      @type bool     $allow_excessive_css            Whether to allow CSS to exceed the allowed max bytes (without raising validation errors).
     *      @type bool     $transform_important_qualifiers Whether !important rules should be transformed. This also necessarily transform inline style attributes.
     *      @type string[] $font_face_display_overrides    Array of the font family names and the font-display value they should each have.
     * }
     */
    protected $args;
    /**
     * Default args.
     *
     * @var array
     */
    protected $DEFAULT_ARGS = ['disable_style_processing' => \false, 'dynamic_element_selectors' => ['amp-img', 'amp-anim', 'amp-list', 'amp-live-list', '[submit-error]', '[submit-success]', 'amp-script', 'amp-story-captions'], 'should_locate_sources' => \false, 'parsed_cache_variant' => null, 'focus_within_classes' => ['focus'], 'low_priority_plugins' => ['query-monitor'], 'allow_transient_caching' => \true, 'skip_tree_shaking' => \false, 'allow_excessive_css' => \false, 'transform_important_qualifiers' => \true, 'font_face_display_overrides' => ['NonBreakingSpaceOverride' => 'optional', 'Inter var' => 'optional', 'Genericons' => 'block']];
    /**
     * List of stylesheet parts prior to selector/rule removal (tree shaking).
     *
     * Keys are MD5 hashes of stylesheets.
     *
     * @since 1.0
     * @var array[] {
     *     @type int                $group         Either STYLE_AMP_CUSTOM_GROUP_INDEX or STYLE_AMP_KEYFRAMES_GROUP_INDEX.
     *     @type int                $original_size The byte size of the stylesheet prior to parsing and tree shaking.
     *     @type int                $final_size    The byte size of the stylesheet after parsing and tree shaking.
     *     @type DOMElement|DOMAttr $element       Origin element for the styles.
     *     @type string             $origin        Either 'style_element', 'style_attribute', or 'link_element'.
     *     @type array              $sources       Source stack.
     *     @type int                $priority      Priority of the stylesheet.
     *     @type array|null         $tokens        Stylesheet tokens, with declaration blocks being represented as arrays. Null after shaking occurs.
     *     @type array|null         $shaken_tokens Shaken stylesheet tokens, where first array index of each array item is whether the token is included. Null until shaking occurs.
     *     @type string             $serialized    Stylesheet tokens serialized into CSS.
     *     @type string             $hash          MD5 hash of the parsed stylesheet tokens, prior to tree-shaking.
     *     @type array              $sources       Sources for the node.
     *     @type bool               $keyframes     Whether an amp-keyframes.
     *     @type float              $parse_time    The time duration it took to parse the stylesheet, in milliseconds.
     *     @type bool               $cached        Whether the parsed stylesheet was retrieved from cache.
     * }
     */
    private $pending_stylesheets = [];
    /**
     * Spec for style[amp-custom] cdata.
     *
     * @since 1.0
     * @var array
     */
    private $style_custom_cdata_spec;
    /**
     * The style[amp-custom] element.
     *
     * @var DOMElement
     */
    private $amp_custom_style_element;
    /**
     * Spec for style[amp-keyframes] cdata.
     *
     * @since 1.0
     * @var array
     */
    private $style_keyframes_cdata_spec;
    /**
     * Regex for allowed font stylesheet URL.
     *
     * @var string
     */
    private $allowed_font_src_regex;
    /**
     * Base URL for styles.
     *
     * Full URL with trailing slash.
     *
     * @var string
     */
    private $base_url;
    /**
     * URL of the content directory.
     *
     * @var string
     */
    private $content_url;
    /**
     * Class names used in document.
     *
     * This list includes all class names that AMP can dynamically add.
     *
     * @link https://www.ampproject.org/docs/reference/components/amp-dynamic-css-classes
     * @since 1.0
     * @var array|null
     */
    private $used_class_names;
    /**
     * Regular expression pattern to match focus class names in selectors.
     *
     * The computed pattern is cached to prevent re-constructing for each processed selector.
     *
     * @var string|null
     */
    private $focus_class_name_selector_pattern;
    /**
     * Attributes used in the document.
     *
     * This is initially populated with boolean attributes which can be mutated by AMP at runtime,
     * since they can by dynamically added at any time.
     *
     * @todo With the exception of 'hidden' (which can be on any element), the values here could be removed in favor of
     *       checking to see if any of the related elements exist in the page in `\AMP_Style_Sanitizer::has_used_attributes()`.
     *       Nevertheless, selectors mentioning these attributes are very numerous, so tree-shaking improvements will be marginal.
     *
     * @see \AMP_Style_Sanitizer::has_used_attributes()
     *
     * @since 1.1
     * @var array
     */
    private $used_attributes = ['autofocus' => \true, 'checked' => \true, 'controls' => \true, 'disabled' => \true, 'hidden' => \true, 'loop' => \true, 'multiple' => \true, 'readonly' => \true, 'required' => \true, 'selected' => \true];
    /**
     * Tag names used in document.
     *
     * @since 1.0
     * @var array|null
     */
    private $used_tag_names;
    /**
     * Current node being processed.
     *
     * @var DOMElement|DOMAttr
     */
    private $current_node;
    /**
     * Current sources for a given node.
     *
     * @var array|null
     */
    private $current_sources;
    /**
     * Log of the stylesheet URLs that have been imported to guard against infinite loops.
     *
     * @var array
     */
    private $processed_imported_stylesheet_urls = [];
    /**
     * Mapping of HTML element selectors to AMP selector elements.
     *
     * @var array
     */
    private $selector_mappings = [];
    /**
     * Elements in extensions which use the video-manager, and thus the video-autoplay.css.
     *
     * @var array
     */
    private $video_autoplay_elements = ['amp-3q-player', 'amp-brid-player', 'amp-brightcove', 'amp-dailymotion', 'amp-delight-player', 'amp-ima-video', 'amp-mowplayer', 'amp-nexxtv-player', 'amp-ooyala-player', 'amp-powr-player', 'amp-story-auto-ads', 'amp-video', 'amp-video-iframe', 'amp-vimeo', 'amp-viqeo-player', 'amp-wistia-player', 'amp-youtube'];
    /**
     * Remote request instance.
     *
     * @var RemoteGetRequest
     */
    private $remote_request;
    /**
     * All current sanitizers.
     *
     * @see AMP_Style_Sanitizer::init()
     * @var AMP_Base_Sanitizer[]
     */
    private $sanitizers = [];
    /**
     * Get error codes that can be raised during parsing of CSS.
     *
     * This is used to determine which validation errors should be taken into account
     * when determining which validation errors should vary the parse cache.
     *
     * @return array
     */
    public static function get_css_parser_validation_error_codes()
    {
        return [self::CSS_SYNTAX_INVALID_AT_RULE, self::CSS_SYNTAX_INVALID_DECLARATION, self::CSS_SYNTAX_INVALID_PROPERTY, self::CSS_SYNTAX_INVALID_PROPERTY_NOLIST, self::CSS_SYNTAX_INVALID_IMPORTANT, self::CSS_SYNTAX_PARSE_ERROR, self::CSS_DISALLOWED_SELECTOR, self::STYLESHEET_FETCH_ERROR, self::STYLESHEET_TOO_LONG];
    }
    /**
     * Determine whether the version of PHP-CSS-Parser loaded has all required features for tree shaking and CSS processing.
     *
     * @since 1.0.2
     *
     * @return bool Returns true if the plugin's forked version of PHP-CSS-Parser is loaded by Composer.
     */
    public static function has_required_php_css_parser()
    {
        $reflection = new \ReflectionClass('Google\\Web_Stories_Dependencies\\Sabberworm\\CSS\\OutputFormat');
        $has_output_format_extensions = $reflection->hasProperty('sBeforeAtRuleBlock') && $reflection->hasProperty('sAfterAtRuleBlock') && $reflection->hasProperty('sBeforeDeclarationBlock') && $reflection->hasProperty('sAfterDeclarationBlockSelectors') && $reflection->hasProperty('sAfterDeclarationBlock');
        if (!$has_output_format_extensions) {
            return \false;
        }
        return \true;
    }
    /**
     * AMP_Base_Sanitizer constructor.
     *
     * @since 0.7
     *
     * @param Document $dom  Represents the HTML document to sanitize.
     * @param array    $args Args.
     */
    public function __construct($dom, array $args = [])
    {
        parent::__construct($dom, $args);
        foreach (\Google\Web_Stories_Dependencies\AMP_Allowed_Tags_Generated::get_allowed_tag('style') as $spec_rule) {
            if (!isset($spec_rule[\Google\Web_Stories_Dependencies\AMP_Rule_Spec::TAG_SPEC]['spec_name'])) {
                continue;
            }
            if (self::STYLE_AMP_KEYFRAMES_SPEC_NAME === $spec_rule[\Google\Web_Stories_Dependencies\AMP_Rule_Spec::TAG_SPEC]['spec_name']) {
                $this->style_keyframes_cdata_spec = $spec_rule[\Google\Web_Stories_Dependencies\AMP_Rule_Spec::CDATA];
            } elseif (self::STYLE_AMP_CUSTOM_SPEC_NAME === $spec_rule[\Google\Web_Stories_Dependencies\AMP_Rule_Spec::TAG_SPEC]['spec_name']) {
                $this->style_custom_cdata_spec = $spec_rule[\Google\Web_Stories_Dependencies\AMP_Rule_Spec::CDATA];
            }
        }
        $spec_name = 'link rel=stylesheet for fonts';
        // phpcs:ignore WordPress.WP.EnqueuedResources.NonEnqueuedStylesheet
        foreach (\Google\Web_Stories_Dependencies\AMP_Allowed_Tags_Generated::get_allowed_tag('link') as $spec_rule) {
            if (isset($spec_rule[\Google\Web_Stories_Dependencies\AMP_Rule_Spec::TAG_SPEC]['spec_name']) && $spec_name === $spec_rule[\Google\Web_Stories_Dependencies\AMP_Rule_Spec::TAG_SPEC]['spec_name']) {
                $this->allowed_font_src_regex = '/^(' . $spec_rule[\Google\Web_Stories_Dependencies\AMP_Rule_Spec::ATTR_SPEC_LIST]['href']['value_regex'] . ')$/';
                break;
            }
        }
        $guessurl = \site_url();
        if (!$guessurl) {
            $guessurl = \wp_guess_url();
        }
        $this->base_url = \untrailingslashit($guessurl);
        $this->content_url = \WP_CONTENT_URL;
        $this->remote_request = new CachedRemoteGetRequest(new WpHttpRemoteGetRequest());
    }
    /**
     * Get list of CSS styles in HTML content of Dom\Document ($this->dom).
     *
     * @since 0.4
     * @codeCoverageIgnore
     * @deprecated As of 1.0, use get_stylesheets().
     *
     * @return array[] Mapping CSS selectors to array of properties, or mapping of keys starting with 'stylesheet:' with value being the stylesheet.
     */
    public function get_styles()
    {
        return [];
    }
    /**
     * Get stylesheets for amp-custom.
     *
     * @since 0.7
     * @return array Values are the CSS stylesheets.
     */
    public function get_stylesheets()
    {
        return \wp_list_pluck(\array_filter($this->pending_stylesheets, static function ($pending_stylesheet) {
            return $pending_stylesheet['included'] && self::STYLE_AMP_CUSTOM_GROUP_INDEX === $pending_stylesheet['group'];
        }), 'serialized');
    }
    /**
     * Get list of all the class names used in the document, including those used in [class] attributes.
     *
     * @since 1.0
     * @return array Used class names.
     */
    private function get_used_class_names()
    {
        if (isset($this->used_class_names)) {
            return $this->used_class_names;
        }
        $dynamic_class_names = [
            /*
             * See <https://www.ampproject.org/docs/reference/components/amp-dynamic-css-classes>.
             * Note that amp-referrer-* class names are handled in has_used_class_name() below.
             */
            'amp-viewer',
            // Classes added based on input mode. See <https://github.com/ampproject/amphtml/blob/bd29b0eb1b28d900d4abed2c1883c6980f18db8e/spec/amp-css-classes.md#input-mode-classes>.
            'amp-mode-touch',
            'amp-mode-mouse',
            'amp-mode-keyboard-active',
        ];
        $classes = [];
        $i_amphtml_prefix = 'i-amphtml-';
        $i_amphtml_prefix_length = 10;
        foreach ($this->dom->xpath->query('//*/@class') as $class_attribute) {
            /** @var DOMAttr $class_attribute */
            $mutated = \false;
            $element_classes = [];
            foreach (\array_filter(\preg_split('/\\s+/', \trim($class_attribute->nodeValue))) as $class_name) {
                if (\substr($class_name, 0, $i_amphtml_prefix_length) === $i_amphtml_prefix) {
                    $error = ['code' => self::DISALLOWED_ATTR_CLASS_NAME, 'type' => \Google\Web_Stories_Dependencies\AMP_Validation_Error_Taxonomy::HTML_ATTRIBUTE_ERROR_TYPE, 'class_name' => $class_name];
                    $sanitized = $this->should_sanitize_validation_error($error, ['node' => $class_attribute]);
                    if ($sanitized) {
                        $mutated = \true;
                        continue;
                    } else {
                        ValidationExemption::mark_node_as_amp_unvalidated($class_attribute);
                    }
                }
                $element_classes[] = $class_name;
            }
            if ($mutated) {
                $class_attribute->nodeValue = \implode(' ', $element_classes);
            }
            $classes = \array_merge($classes, $element_classes);
        }
        // Find all [class] attributes and capture the contents of any single- or double-quoted strings.
        foreach ($this->dom->xpath->query('//*/@' . Amp::BIND_DATA_ATTR_PREFIX . 'class') as $bound_class_attribute) {
            if (\preg_match_all('/([\'"])([^\\1]*?)\\1/', $bound_class_attribute->nodeValue, $matches)) {
                $classes = \array_merge($classes, \preg_split('/\\s+/', \trim(\implode(' ', $matches[2]))));
            }
        }
        $class_names = \array_merge($dynamic_class_names, \array_unique(\array_filter($classes)));
        // Find all instances of the toggleClass() action to prevent the class name from being tree-shaken.
        foreach ($this->dom->xpath->query('//*/@on[ contains( ., "toggleClass" ) ]') as $on_attribute) {
            if (\preg_match_all('/\\.\\s*toggleClass\\s*\\(\\s*class\\s*=\\s*(([\'"])([^\\1]*?)\\2|[a-zA-Z0-9_\\-]+)/', $on_attribute->nodeValue, $matches)) {
                $class_names = \array_merge($class_names, \array_map(static function ($match) {
                    return \trim($match, '"\'');
                }, $matches[1]));
            }
        }
        // If using the toggleTheme component, get the theme's dark mode class.
        // See usage of toggleTheme in <https://github.com/ampproject/amphtml/pull/36958>.
        $dark_mode_class = $this->dom->body->getAttribute('data-prefers-dark-mode-class');
        // Prevent dark mode class from being tree-shaken.
        if ($dark_mode_class) {
            $class_names[] = $dark_mode_class;
        } else {
            $class_names[] = 'amp-dark-mode';
        }
        $this->used_class_names = \array_fill_keys($class_names, \true);
        return $this->used_class_names;
    }
    /**
     * Determine if all the supplied class names are used.
     *
     * @since 1.1
     *
     * @param string[] $class_names Class names.
     * @return bool All used.
     */
    private function has_used_class_name($class_names)
    {
        if (empty($this->used_class_names)) {
            $this->get_used_class_names();
        }
        foreach ($class_names as $class_name) {
            // Bail early with a common case scenario.
            if (isset($this->used_class_names[$class_name])) {
                continue;
            }
            // Check exact matches first, as they are faster.
            switch ($class_name) {
                /*
                 * Common class names used for amp-user-notification and amp-live-list.
                 * See <https://www.ampproject.org/docs/reference/components/amp-user-notification#styling>.
                 * See <https://www.ampproject.org/docs/reference/components/amp-live-list#styling>.
                 */
                case 'amp-active':
                case 'amp-hidden':
                    if (!$this->has_used_tag_names(['amp-live-list', 'amp-user-notification'])) {
                        return \false;
                    }
                    continue 2;
                // Class names for amp-image-lightbox, see <https://www.ampproject.org/docs/reference/components/amp-image-lightbox#styling>.
                case 'amp-image-lightbox-caption':
                    if (!$this->has_used_tag_names(['amp-image-lightbox'])) {
                        return \false;
                    }
                    continue 2;
                // Class names for amp-form, see <https://www.ampproject.org/docs/reference/components/amp-form#classes-and-css-hooks>.
                case 'user-valid':
                case 'user-invalid':
                    if (!$this->has_used_tag_names(['form'])) {
                        return \false;
                    }
                    continue 2;
            }
            // Only do AMP element-specific checks on an AMP components with the corresponding prefix.
            if ('amp-' === \substr($class_name, 0, 4)) {
                // Class names for amp-geo, see <https://www.ampproject.org/docs/reference/components/amp-geo#generated-css-classes>.
                if ('amp-geo-' === \substr($class_name, 0, 8)) {
                    if (!$this->has_used_tag_names(['amp-geo'])) {
                        return \false;
                    }
                    continue;
                }
                // Class names for amp-form, see <https://www.ampproject.org/docs/reference/components/amp-form#classes-and-css-hooks>.
                if ('amp-form-' === \substr($class_name, 0, 9)) {
                    if (!$this->has_used_tag_names(['form'])) {
                        return \false;
                    }
                    continue;
                }
                // Class names for extensions which use the video-manager, and thus video-autoplay.css.
                if ('amp-video-' === \substr($class_name, 0, 10)) {
                    foreach ($this->video_autoplay_elements as $video_autoplay_element) {
                        if ($this->has_used_tag_names([$video_autoplay_element])) {
                            continue 2;
                        }
                    }
                    return \false;
                }
                switch (\substr($class_name, 0, 11)) {
                    /*
                     * Class names for amp-access and amp-access-laterpay.
                     * See <https://www.ampproject.org/docs/reference/components/amp-access>.
                     * See <https://www.ampproject.org/docs/reference/components/amp-access-laterpay#styling>
                     */
                    case 'amp-access-':
                        if (!$this->has_used_attributes(['amp-access'])) {
                            return \false;
                        }
                        continue 2;
                    // Class names for amp-video-docking, see <https://amp.dev/documentation/components/amp-video-docking/#styling>.
                    case 'amp-docked-':
                        if (!$this->has_used_attributes(['dock'])) {
                            return \false;
                        }
                        continue 2;
                }
                // Class names for amp-sidebar, see <https://www.ampproject.org/docs/reference/components/amp-sidebar#styling-toolbar>.
                if ('amp-sidebar-' === \substr($class_name, 0, 12)) {
                    if (!$this->has_used_tag_names(['amp-sidebar'])) {
                        return \false;
                    }
                    continue;
                }
                switch (\substr($class_name, 0, 13)) {
                    // Class names for amp-dynamic-css-classes, see <https://www.ampproject.org/docs/reference/components/amp-dynamic-css-classes>.
                    case 'amp-referrer-':
                        continue 2;
                    // Class names for amp-carousel, see <https://www.ampproject.org/docs/reference/components/amp-carousel#styling>.
                    case 'amp-carousel-':
                        if (!$this->has_used_tag_names(['amp-carousel'])) {
                            return \false;
                        }
                        continue 2;
                }
                switch (\substr($class_name, 0, 14)) {
                    // Class names for amp-sticky-ad, see <https://www.ampproject.org/docs/reference/components/amp-sticky-ad#styling>.
                    case 'amp-sticky-ad-':
                        if (!$this->has_used_tag_names(['amp-sticky-ad'])) {
                            return \false;
                        }
                        continue 2;
                    // Class names for amp-live-list, see <https://www.ampproject.org/docs/reference/components/amp-live-list#styling>.
                    case 'amp-live-list-':
                        if (!$this->has_used_tag_names(['amp-live-list'])) {
                            return \false;
                        }
                        continue 2;
                    // Class names for amp-next-page, see <https://amp.dev/documentation/components/amp-next-page/#styling>.
                    case 'amp-next-page-':
                        if (!$this->has_used_tag_names(['amp-next-page'])) {
                            return \false;
                        }
                        continue 2;
                }
                switch (\substr($class_name, 0, 16)) {
                    // Class names for amp-date-picker, see <https://www.ampproject.org/docs/reference/components/amp-date-picker>.
                    case 'amp-date-picker-':
                        if (!$this->has_used_tag_names(['amp-date-picker'])) {
                            return \false;
                        }
                        continue 2;
                    // Class names for amp-geo, see <https://www.ampproject.org/docs/reference/components/amp-geo#generated-css-classes>.
                    case 'amp-iso-country-':
                        if (!$this->has_used_tag_names(['amp-geo'])) {
                            return \false;
                        }
                        continue 2;
                }
            } elseif (\ctype_upper($class_name[0]) && $this->has_used_tag_names(['amp-date-picker']) && $this->is_class_allowed_in_amp_date_picker($class_name)) {
                // If the document has an amp-date-picker tag, check if this class is an allowed child of it.
                // That component's child classes won't be present yet in the document, so prevent tree-shaking valid classes.
                // The ctype_upper() check is an optimization since we know up front that all class names in React Dates are
                // in CamelCase form, thus we can short-circut if the first character of the class name is not upper-case.
                continue;
            }
            if (!isset($this->used_class_names[$class_name])) {
                return \false;
            }
        }
        return \true;
    }
    /**
     * Get list of all the tag names used in the document.
     *
     * @since 1.0
     * @return array Used tag names.
     */
    private function get_used_tag_names()
    {
        if (!isset($this->used_tag_names)) {
            $this->used_tag_names = [];
            foreach ($this->dom->getElementsByTagName('*') as $el) {
                $this->used_tag_names[$el->tagName] = \true;
            }
        }
        return $this->used_tag_names;
    }
    /**
     * Determine if all the supplied tag names are used.
     *
     * @since 1.1
     *
     * @param string[] $tag_names Tag names.
     * @return bool All used.
     */
    private function has_used_tag_names($tag_names)
    {
        if (empty($this->used_tag_names)) {
            $this->get_used_tag_names();
        }
        foreach ($tag_names as $tag_name) {
            if (!isset($this->used_tag_names[$tag_name])) {
                return \false;
            }
        }
        return \true;
    }
    /**
     * Check whether the attributes exist.
     *
     * @since 1.1
     * @todo Make $attribute_names into $attributes as an associative array and implement lookups of specific values. Since attribute values can vary (e.g. with amp-bind), this may not be feasible.
     *
     * @param string[] $attribute_names Attribute names.
     * @return bool Whether all supplied attributes are used.
     */
    private function has_used_attributes($attribute_names)
    {
        foreach ($attribute_names as $attribute_name) {
            if (!isset($this->used_attributes[$attribute_name])) {
                $expression = \sprintf('(//@%s)[1]', $attribute_name);
                $this->used_attributes[$attribute_name] = 0 !== $this->dom->xpath->query($expression)->length;
            }
            // Attributes for amp-accordion, see <https://amp.dev/documentation/components/amp-accordion/#styling>.
            if ('expanded' === $attribute_name) {
                if (!$this->has_used_tag_names(['amp-accordion'])) {
                    return \false;
                }
                continue;
            }
            // Attributes for amp-sidebar, see <https://amp.dev/documentation/components/amp-sidebar/#styling>.
            if ('open' === $attribute_name) {
                // The 'open' attribute is also used by the HTML5 <details> attribute.
                if (!$this->has_used_tag_names(['amp-sidebar']) && !$this->has_used_tag_names(['details'])) {
                    return \false;
                }
                continue;
            }
            // Attributes for amp-live-list, see <https://amp.dev/documentation/components/amp-live-list/#styling>.
            if ('data-tombstone' === $attribute_name) {
                if (!$this->has_used_tag_names(['amp-live-list'])) {
                    return \false;
                }
                continue;
            }
            // Attributes for amp-experiment begin with 'amp-x-', see <https://amp.dev/documentation/examples/components/amp-experiment/>.
            if ('amp-x-' === \substr($attribute_name, 0, 6)) {
                if (!$this->has_used_tag_names(['amp-experiment'])) {
                    return \false;
                }
                continue;
            }
            if (!$this->used_attributes[$attribute_name]) {
                return \false;
            }
        }
        return \true;
    }
    /**
     * Whether a given class is allowed to be styled in <amp-date-picker>.
     *
     * That component has child classes that won't be present in the document yet.
     * So get whether a class is an allowed child.
     *
     * @since 1.5.0
     * @link https://github.com/airbnb/react-dates/tree/05356/src/components
     *
     * @param string $class The name of the class to evaluate.
     * @return bool Whether the class is allowed as a child of <amp-date-picker>.
     */
    private function is_class_allowed_in_amp_date_picker($class)
    {
        static $class_prefixes = ['CalendarDay', 'CalendarMonth', 'CalendarMonthGrid', 'DayPicker', 'DayPickerKeyboardShortcuts', 'DayPickerNavigation', 'KeyboardShortcutRow'];
        return \in_array(\strtok($class, '_'), $class_prefixes, \true);
    }
    /**
     * Run logic before any sanitizers are run.
     *
     * After the sanitizers are instantiated but before calling sanitize on each of them, this
     * method is called with list of all the instantiated sanitizers.
     *
     * @param AMP_Base_Sanitizer[] $sanitizers Sanitizers.
     */
    public function init($sanitizers)
    {
        parent::init($sanitizers);
        $this->sanitizers = $sanitizers;
    }
    /**
     * Sanitize CSS styles within the HTML contained in this instance's Dom\Document.
     *
     * @since 0.4
     */
    public function sanitize()
    {
        // When style processing is disabled, simply mark all the CSS elements/attributes as PX-verified.
        if ($this->args['disable_style_processing']) {
            foreach ($this->dom->xpath->query('//link[ @rel = "stylesheet" and @href ] | //style | //*/@style') as $node) {
                ValidationExemption::mark_node_as_px_verified($node);
                // Since stylesheet links are allowed in the HEAD if they are for fonts, mark the href specifically as being the exempted attribute.
                if ($node instanceof Element && Tag::LINK === $node->tagName) {
                    ValidationExemption::mark_node_as_px_verified($node->getAttributeNode(Attribute::HREF));
                    ValidationExemption::mark_node_as_px_verified($node->getAttributeNode(Attribute::REL));
                }
            }
            return;
        }
        // Capture the selector conversion mappings from the other sanitizers.
        foreach ($this->sanitizers as $sanitizer) {
            foreach ($sanitizer->get_selector_conversion_mapping() as $html_selectors => $amp_selectors) {
                if (!isset($this->selector_mappings[$html_selectors])) {
                    $this->selector_mappings[$html_selectors] = $amp_selectors;
                } else {
                    $this->selector_mappings[$html_selectors] = \array_unique(\array_merge($this->selector_mappings[$html_selectors], $amp_selectors));
                }
                // Prevent selectors like `amp-img img` getting deleted since `img` does not occur in the DOM.
                if ($sanitizer->has_light_shadow_dom()) {
                    $this->args['dynamic_element_selectors'] = \array_merge($this->args['dynamic_element_selectors'], $this->selector_mappings[$html_selectors]);
                }
            }
        }
        $elements = [];
        $this->focus_class_name_selector_pattern = !empty($this->args['focus_within_classes']) ? self::get_class_name_selector_pattern($this->args['focus_within_classes']) : null;
        /*
         * Note that xpath is used to query the DOM so that the link and style elements will be
         * in document order. DOMNode::compareDocumentPosition() is not yet implemented.
         */
        // @todo Also consider skipping the processing of link and style elements that have data-px-verified-tag.
        $dev_mode_predicate = '';
        if (DevMode::isActiveForDocument($this->dom)) {
            $dev_mode_predicate = \sprintf(' and not ( @%s )', \Google\Web_Stories_Dependencies\AMP_Rule_Spec::DEV_MODE_ATTRIBUTE);
        }
        $lower_case = 'translate( %s, "ABCDEFGHIJKLMNOPQRSTUVWXYZ", "abcdefghijklmnopqrstuvwxyz" )';
        // In XPath 2.0 this is lower-case().
        $predicates = [\sprintf('( self::style and not( @amp-boilerplate ) and ( not( @type ) or %s = "text/css" ) %s )', \sprintf($lower_case, '@type'), $dev_mode_predicate), \sprintf('( self::link and @href and %s = "stylesheet" %s )', \sprintf($lower_case, '@rel'), $dev_mode_predicate)];
        foreach ($this->dom->xpath->query('//*[ ' . \implode(' or ', $predicates) . ' ]') as $element) {
            $elements[] = $element;
        }
        // If 'width' attribute is present for 'col' tag, convert to proper CSS rule.
        // @todo The width attribute on the <col> tag is probably something that should just be allowed in AMP.
        foreach ($this->dom->getElementsByTagName('col') as $col) {
            /**
             * Col element.
             *
             * @var DOMElement $col
             */
            $width_attr = $col->getAttribute('width');
            if (!empty($width_attr) && \false === \strpos($width_attr, '*')) {
                $width_style = 'width: ' . $width_attr;
                if (\is_numeric($width_attr)) {
                    $width_style .= 'px';
                }
                if ($col->hasAttribute('style')) {
                    $col->setAttribute('style', $width_style . ';' . $col->getAttribute('style'));
                } else {
                    $col->setAttribute('style', $width_style);
                }
                $col->removeAttribute('width');
            }
        }
        /**
         * Element.
         *
         * @var DOMElement $element
         */
        foreach ($elements as $element) {
            $node_name = \strtolower($element->nodeName);
            if ('style' === $node_name) {
                $this->process_style_element($element);
            } elseif ('link' === $node_name) {
                $this->process_link_element($element);
                // If the element is still in the document, it is a font stylesheet; make sure it gets moved to the head as required.
                if ($element->parentNode && 'head' !== $element->parentNode->nodeName) {
                    $this->dom->head->appendChild($element->parentNode->removeChild($element));
                }
            }
        }
        $styled_elements = $this->dom->xpath->query("//*[ @style {$dev_mode_predicate} ]");
        if ($this->args['transform_important_qualifiers']) {
            foreach (\iterator_to_array($styled_elements) as $element) {
                $this->collect_inline_styles($element);
            }
        } else {
            foreach ($styled_elements as $element) {
                /** @var DOMElement $element */
                $attr = $element->getAttributeNode(Attribute::STYLE);
                if ($attr && \preg_match('/!\\s*important/i', $attr->value)) {
                    ValidationExemption::mark_node_as_px_verified($attr);
                }
            }
        }
        $this->finalize_styles();
        $this->did_convert_elements = \true;
        $parse_css_duration = 0.0;
        $shake_css_duration = 0.0;
        foreach ($this->pending_stylesheets as $pending_stylesheet) {
            if (!$pending_stylesheet['cached']) {
                $parse_css_duration += $pending_stylesheet['parse_time'];
            }
            $shake_css_duration += $pending_stylesheet['shake_time'];
        }
        // TODO: These cannot use actions when we extract the sanitizers into an external library.
        /**
         * Logs the server-timing measurement for the CSS parsing.
         *
         * @since 2.0
         * @internal
         *
         * @param string   $event_name        Name of the event to log.
         * @param string   $event_description Description of the event to log.
         * @param string[] $properties        Optional. Additional properties to add
         *                                    to the logged record.
         * @param bool     $verbose_only      Optional. Whether to only show the
         *                                    event in verbose mode.
         */
        \do_action('amp_server_timing_log', 'amp_parse_css', '', ['dur' => $parse_css_duration * 1000], \true);
        /**
         * Logs the server-timing measurement for the CSS tree-shaking.
         *
         * @since 2.0
         * @internal
         *
         * @param string   $event_name        Name of the event to log.
         * @param string   $event_description Description of the event to log.
         * @param string[] $properties        Optional. Additional properties to add
         *                                    to the logged record.
         * @param bool     $verbose_only      Optional. Whether to only show the
         *                                    event in verbose mode.
         */
        \do_action('amp_server_timing_log', 'amp_shake_css', '', ['dur' => $shake_css_duration * 1000], \true);
    }
    /**
     * Get the priority of the stylesheet associated with the given element.
     *
     * As with hooks, lower priorities mean they should be included first.
     * The higher the priority value, the more likely it will be that the
     * stylesheet will be among those excluded due to STYLESHEET_TOO_LONG when
     * concatenated CSS reaches 75KB.
     *
     * @todo This will eventually need to be abstracted to not be CMS-specific, allowing for the prioritization scheme to be defined by configuration.
     *
     * @param DOMNode|DOMElement|DOMAttr $node Node.
     * @return int Priority.
     */
    private function get_stylesheet_priority(\DOMNode $node)
    {
        $print_priority_base = 100;
        $admin_bar_priority = 200;
        $remove_url_scheme = static function ($url) {
            return \preg_replace('/^https?:/', '', $url);
        };
        if ($node instanceof \DOMElement && 'link' === $node->tagName) {
            $element_id = (string) $node->getAttribute('id');
            $schemeless_href = $remove_url_scheme($node->getAttribute('href'));
            $plugin = null;
            if (\preg_match(\sprintf('#^(?:%s|%s)(?<plugin>[^/]+)#i', \preg_quote($remove_url_scheme(\trailingslashit(\WP_PLUGIN_URL)), '#'), \preg_quote($remove_url_scheme(\trailingslashit(\WPMU_PLUGIN_URL)), '#')), $schemeless_href, $matches)) {
                $plugin = $matches['plugin'];
            }
            $style_handle = null;
            if (\preg_match('/^(.+)-css$/', $element_id, $matches)) {
                $style_handle = $matches[1];
            }
            $core_frontend_handles = ['wp-block-library', 'wp-block-library-theme'];
            $non_amp_handles = ['mediaelement', 'wp-mediaelement', 'thickbox'];
            if (\in_array($style_handle, $non_amp_handles, \true)) {
                // Styles are for non-AMP JS only so not be used in AMP at all.
                $priority = 1000;
            } elseif ('admin-bar' === $style_handle) {
                // Admin bar has lowest priority. If it gets excluded, then the entire admin bar should be removed.
                $priority = $admin_bar_priority;
            } elseif ('dashicons' === $style_handle) {
                // Dashicons could be used by the theme, but low priority compared to other styles.
                $priority = 90;
            } elseif (\false !== \strpos($schemeless_href, $remove_url_scheme(\trailingslashit(\get_template_directory_uri())))) {
                // Highest priority are parent theme styles.
                $priority = 1;
            } elseif (\false !== \strpos($schemeless_href, $remove_url_scheme(\trailingslashit(\get_stylesheet_directory_uri())))) {
                // Penultimate highest priority are child theme styles.
                $priority = 10;
            } elseif (\in_array($style_handle, $core_frontend_handles, \true)) {
                // Styles from wp-includes which are enqueued for themes are next highest priority.
                $priority = 20;
            } elseif ($plugin) {
                // Styles from plugins are next-highest priority, unless they are in the list of low-priority plugins.
                $priority = \in_array($plugin, $this->args['low_priority_plugins'], \true) ? 150 : 30;
            } elseif (0 === \strpos($schemeless_href, $remove_url_scheme(\includes_url()))) {
                // Other styles from wp-includes come next.
                $priority = 40;
            } else {
                // Everything else, perhaps wp-admin styles or stylesheets from remote servers.
                $priority = 50;
            }
            if ('print' === $node->getAttribute('media')) {
                $priority += $print_priority_base;
            }
        } elseif ($node instanceof \DOMElement && 'style' === $node->tagName && $node->hasAttribute('id')) {
            $id = $node->getAttribute('id');
            $dependency = null;
            if (\preg_match('/^(?<handle>.+)-inline-css$/', $id, $matches)) {
                $dependency = \wp_styles()->query($matches['handle'], 'registered');
            }
            if ($dependency && (0 === \strpos($dependency->src, \get_template_directory_uri()) || 'twenty-twenty-one-style' === $dependency->handle)) {
                // Parent theme inline style.
                $priority = 2;
            } elseif ($dependency && \get_stylesheet() !== \get_template() && 0 === \strpos($dependency->src, \get_stylesheet_directory_uri())) {
                // Child theme inline style.
                $priority = 12;
            } elseif ('admin-bar-inline-css' === $id) {
                $priority = $admin_bar_priority;
            } elseif ('wp-custom-css' === $id) {
                // Additional CSS from Customizer.
                $priority = 60;
            } else {
                // Other style elements, including from Recent Comments widget.
                $priority = 70;
            }
            if ('print' === $node->getAttribute('media')) {
                $priority += $print_priority_base;
            }
        } else {
            // Style attribute.
            $priority = 70;
        }
        return $priority;
    }
    /**
     * Eliminate relative segments (../ and ./) from a path.
     *
     * @param string $path Path with relative segments. This is not a URL, so no host and no query string.
     * @return string|WP_Error Unrelativized path or WP_Error if there is too much relativity.
     */
    private function unrelativize_path($path)
    {
        // Eliminate current directory relative paths, like <foo/./bar/./baz.css> => <foo/bar/baz.css>.
        do {
            $path = \preg_replace('#/\\./#', '/', $path, -1, $count);
        } while (0 !== $count);
        // Collapse relative paths, like <foo/bar/../../baz.css> => <baz.css>.
        do {
            $path = \preg_replace('#(?<=/)(?!\\.\\./)[^/]+/\\.\\./#', '', $path, 1, $count);
        } while (0 !== $count);
        if (\preg_match('#(^|/)\\.+/#', $path)) {
            return new \WP_Error(self::STYLESHEET_INVALID_RELATIVE_PATH);
        }
        return $path;
    }
    /**
     * Construct a URL from a parsed one.
     *
     * @param array $parsed_url Parsed URL.
     * @return string Reconstructed URL.
     */
    private function reconstruct_url($parsed_url)
    {
        $url = '';
        if (!empty($parsed_url['host'])) {
            if (!empty($parsed_url['scheme'])) {
                $url .= $parsed_url['scheme'] . ':';
            }
            $url .= '//';
            $url .= $parsed_url['host'];
            if (!empty($parsed_url['port'])) {
                $url .= ':' . $parsed_url['port'];
            }
        }
        if (!empty($parsed_url['path'])) {
            $url .= $parsed_url['path'];
        }
        if (!empty($parsed_url['query'])) {
            $url .= '?' . $parsed_url['query'];
        }
        if (!empty($parsed_url['fragment'])) {
            $url .= '#' . $parsed_url['fragment'];
        }
        return $url;
    }
    /**
     * Generate a URL's fully-qualified file path.
     *
     * @since 0.7
     * @see WP_Styles::_css_href()
     *
     * @param string   $url The file URL.
     * @param string[] $allowed_extensions Allowed file extensions for local files.
     * @return string|WP_Error Style's absolute validated filesystem path, or WP_Error when error.
     */
    public function get_validated_url_file_path($url, $allowed_extensions = [])
    {
        if (!\is_string($url)) {
            return new \WP_Error(self::STYLESHEET_URL_SYNTAX_ERROR);
        }
        $needs_base_url = !\preg_match('|^(https?:)?//|', $url) && !($this->content_url && 0 === \strpos($url, $this->content_url));
        if ($needs_base_url) {
            $url = $this->base_url . '/' . \ltrim($url, '/');
        }
        $parsed_url = \wp_parse_url($url);
        if (empty($parsed_url['host'])) {
            return new \WP_Error(self::STYLESHEET_URL_SYNTAX_ERROR);
        }
        if (empty($parsed_url['path'])) {
            return new \WP_Error(self::STYLESHEET_URL_SYNTAX_ERROR);
        }
        $path = $this->unrelativize_path($parsed_url['path']);
        if (\is_wp_error($path)) {
            return $path;
        }
        $parsed_url['path'] = $path;
        $remove_url_scheme = static function ($schemed_url) {
            return \preg_replace('#^\\w+:(?=//)#', '', $schemed_url);
        };
        unset($parsed_url['scheme'], $parsed_url['query'], $parsed_url['fragment']);
        $url = $this->reconstruct_url($parsed_url);
        $includes_url = $remove_url_scheme(\includes_url('/'));
        $content_url = $remove_url_scheme(\content_url('/'));
        $admin_url = $remove_url_scheme(\get_admin_url(null, '/'));
        $site_url = $remove_url_scheme(\site_url('/'));
        $allowed_hosts = [\wp_parse_url($includes_url, \PHP_URL_HOST), \wp_parse_url($content_url, \PHP_URL_HOST), \wp_parse_url($admin_url, \PHP_URL_HOST)];
        // Validate file extensions.
        if (!empty($allowed_extensions)) {
            $pattern = \sprintf('/\\.(%s)$/i', \implode('|', $allowed_extensions));
            if (!\preg_match($pattern, $url)) {
                /* translators: %s: the file URL. */
                return new \WP_Error(self::STYLESHEET_DISALLOWED_FILE_EXT);
            }
        }
        if (!\in_array($parsed_url['host'], $allowed_hosts, \true)) {
            /* translators: %s: the file URL */
            return new \WP_Error(self::STYLESHEET_EXTERNAL_FILE_URL);
        }
        $base_path = null;
        $file_path = null;
        $wp_content = 'wp-content';
        if (0 === \strpos($url, $content_url)) {
            $base_path = \WP_CONTENT_DIR;
            $file_path = \substr($url, \strlen($content_url) - 1);
        } elseif (0 === \strpos($url, $includes_url)) {
            $base_path = \ABSPATH . \WPINC;
            $file_path = \substr($url, \strlen($includes_url) - 1);
        } elseif (0 === \strpos($url, $admin_url)) {
            $base_path = \ABSPATH . 'wp-admin';
            $file_path = \substr($url, \strlen($admin_url) - 1);
        } elseif (0 === \strpos($url, $site_url . \trailingslashit($wp_content))) {
            // Account for loading content from original wp-content directory not WP_CONTENT_DIR which can happen via register_theme_directory().
            $base_path = \ABSPATH . $wp_content;
            $file_path = \substr($url, \strlen($site_url) + \strlen($wp_content));
        }
        if (!$file_path || \false !== \strpos($file_path, '../') || \false !== \strpos($file_path, '..\\')) {
            return new \WP_Error(self::STYLESHEET_FILE_PATH_NOT_ALLOWED);
        }
        if (!\file_exists($base_path . $file_path)) {
            return new \WP_Error(self::STYLESHEET_FILE_PATH_NOT_FOUND);
        }
        return $base_path . $file_path;
    }
    /**
     * Set the current node (and its sources when required).
     *
     * @since 1.0
     * @param DOMElement|DOMAttr|null $node Current node, or null to reset.
     */
    private function set_current_node($node)
    {
        if ($this->current_node === $node) {
            return;
        }
        $this->current_node = $node;
        if (empty($node)) {
            $this->current_sources = null;
        } elseif (!empty($this->args['should_locate_sources'])) {
            $this->current_sources = \Google\Web_Stories_Dependencies\AMP_Validation_Manager::locate_sources($node);
        }
    }
    /**
     * Process style element.
     *
     * @param DOMElement $element Style element.
     */
    private function process_style_element(\DOMElement $element)
    {
        $this->set_current_node($element);
        // And sources when needing to be located.
        // @todo Any @keyframes rules could be removed from amp-custom and instead added to amp-keyframes.
        $is_keyframes = $element->hasAttribute('amp-keyframes');
        $stylesheet = \trim($element->textContent);
        $cdata_spec = $is_keyframes ? $this->style_keyframes_cdata_spec : $this->style_custom_cdata_spec;
        // Honor the style's media attribute.
        $media = $element->getAttribute('media');
        if ($media && 'all' !== $media) {
            $stylesheet = \sprintf('@media %s { %s }', $media, $stylesheet);
        }
        // @todo If ValidationExemption::is_px_verified_for_node( $element ) then keep !important.
        // @todo If ValidationExemption::is_amp_unvalidated_for_node( $element ) then keep invalid markup.
        $parsed = $this->get_parsed_stylesheet($stylesheet, ['allowed_at_rules' => $cdata_spec['css_spec']['allowed_at_rules'], 'property_allowlist' => $cdata_spec['css_spec']['declaration'], 'validate_keyframes' => $cdata_spec['css_spec']['validate_keyframes'], 'spec_name' => $is_keyframes ? self::STYLE_AMP_KEYFRAMES_SPEC_NAME : self::STYLE_AMP_CUSTOM_SPEC_NAME]);
        if ($parsed['viewport_rules']) {
            $this->create_meta_viewport($element, $parsed['viewport_rules']);
        }
        $this->pending_stylesheets[] = ['group' => $is_keyframes ? self::STYLE_AMP_KEYFRAMES_GROUP_INDEX : self::STYLE_AMP_CUSTOM_GROUP_INDEX, 'original_size' => (int) \strlen($stylesheet), 'final_size' => null, 'element' => $element, 'origin' => 'style_element', 'sources' => $this->current_sources, 'priority' => $this->get_stylesheet_priority($element), 'tokens' => $parsed['tokens'], 'hash' => $parsed['hash'], 'parse_time' => $parsed['parse_time'], 'shake_time' => null, 'cached' => $parsed['cached'], 'imported_font_urls' => $parsed['imported_font_urls'], 'important_count' => $parsed['important_count'], 'kept_error_count' => $parsed['kept_error_count'], 'preload_font_urls' => $parsed['preload_font_urls']];
        // Remove from DOM since we'll be adding it to a newly-created style[amp-custom] element later.
        $element->parentNode->removeChild($element);
        $this->set_current_node(null);
    }
    /**
     * Process link element.
     *
     * @param DOMElement $element Link element.
     */
    private function process_link_element(\DOMElement $element)
    {
        $href = $element->getAttribute('href');
        // Allow font URLs, including protocol-less URLs and recognized URLs that use HTTP instead of HTTPS.
        $normalized_url = \preg_replace('#^(http:)?(?=//)#', 'https:', $href);
        if ($this->allowed_font_src_regex && \preg_match($this->allowed_font_src_regex, $normalized_url)) {
            if ($href !== $normalized_url) {
                $element->setAttribute('href', $normalized_url);
            }
            /*
             * Make sure rel=preconnect link is present for Google Fonts stylesheet.
             * Note that core themes normally do this already, per <https://core.trac.wordpress.org/ticket/37171>.
             * But not always, per <https://core.trac.wordpress.org/ticket/44668>.
             * This also ensures that other themes will get the preconnect link when
             * they don't implement the resource hint.
             */
            $needs_preconnect_link = 'https://fonts.googleapis.com/' === \substr($normalized_url, 0, 29) && 0 === $this->dom->xpath->query('//link[ @rel = "preconnect" and @crossorigin and starts-with( @href, "https://fonts.gstatic.com" ) ]', $this->dom->head)->length;
            if ($needs_preconnect_link) {
                $link = \Google\Web_Stories_Dependencies\AMP_DOM_Utils::create_node($this->dom, 'link', ['rel' => 'preconnect', 'href' => 'https://fonts.gstatic.com/', 'crossorigin' => '']);
                $this->dom->head->insertBefore($link);
                // Note that \AMP_Theme_Support::ensure_required_markup() will put this in the optimal order.
            }
            return;
        }
        $stylesheet = $this->get_stylesheet_from_url($href);
        if ($stylesheet instanceof \WP_Error) {
            $this->remove_invalid_child($element, ['code' => self::STYLESHEET_FETCH_ERROR, 'type' => \Google\Web_Stories_Dependencies\AMP_Validation_Error_Taxonomy::CSS_ERROR_TYPE, 'url' => $normalized_url, 'message' => $stylesheet->get_error_message()]);
            return;
        }
        // Honor the link's media attribute.
        $media = $element->getAttribute('media');
        if ($media && 'all' !== $media) {
            $stylesheet = \sprintf('@media %s { %s }', $media, $stylesheet);
        }
        $this->set_current_node($element);
        // And sources when needing to be located.
        // @todo If ValidationExemption::is_px_verified_for_node( $element ) then keep !important.
        // @todo If ValidationExemption::is_amp_unvalidated_for_node( $element ) then keep invalid markup.
        $parsed = $this->get_parsed_stylesheet($stylesheet, ['allowed_at_rules' => $this->style_custom_cdata_spec['css_spec']['allowed_at_rules'], 'property_allowlist' => $this->style_custom_cdata_spec['css_spec']['declaration'], 'stylesheet_url' => $href, 'spec_name' => self::STYLE_AMP_CUSTOM_SPEC_NAME]);
        if ($parsed['viewport_rules']) {
            $this->create_meta_viewport($element, $parsed['viewport_rules']);
        }
        $this->pending_stylesheets[] = [
            'group' => self::STYLE_AMP_CUSTOM_GROUP_INDEX,
            'original_size' => \strlen($stylesheet),
            'final_size' => null,
            'element' => $element,
            'origin' => 'link_element',
            'sources' => $this->current_sources,
            // Needed because node is removed below.
            'priority' => $this->get_stylesheet_priority($element),
            'tokens' => $parsed['tokens'],
            'hash' => $parsed['hash'],
            'parse_time' => $parsed['parse_time'],
            'shake_time' => null,
            'cached' => $parsed['cached'],
            'imported_font_urls' => $parsed['imported_font_urls'],
            'important_count' => $parsed['important_count'],
            'kept_error_count' => $parsed['kept_error_count'],
            'preload_font_urls' => $parsed['preload_font_urls'],
        ];
        // Remove now that styles have been processed.
        $element->parentNode->removeChild($element);
        $this->set_current_node(null);
    }
    /**
     * Get stylesheet from URL.
     *
     * @since 1.5.0
     *
     * @param string $stylesheet_url Stylesheet URL.
     * @return string|WP_Error Stylesheet string on success, or WP_Error on failure.
     */
    private function get_stylesheet_from_url($stylesheet_url)
    {
        $stylesheet = \false;
        $css_file_path = $this->get_validated_url_file_path($stylesheet_url, ['css', 'less', 'scss', 'sass']);
        if (!\is_wp_error($css_file_path)) {
            $stylesheet = \file_get_contents($css_file_path);
            // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents -- It's a local filesystem path not a remote request.
        }
        if (\is_string($stylesheet)) {
            return $stylesheet;
        }
        // Fall back to doing an HTTP request for the stylesheet is not accessible directly from the filesystem.
        return $this->fetch_external_stylesheet($stylesheet_url);
    }
    /**
     * Fetch external stylesheet.
     *
     * @param string $url External stylesheet URL.
     * @return string|WP_Error Stylesheet contents or WP_Error.
     */
    private function fetch_external_stylesheet($url)
    {
        // Prepend schemeless stylesheet URL with the same URL scheme as the current site.
        if ('//' === \substr($url, 0, 2)) {
            $url = \wp_parse_url(\home_url(), \PHP_URL_SCHEME) . ':' . $url;
        }
        try {
            $response = $this->remote_request->get($url);
        } catch (\Exception $exception) {
            if ($exception instanceof FailedToGetFromRemoteUrl && $exception->hasStatusCode()) {
                return new \WP_Error("http_{$exception->getStatusCode()}", $exception->getMessage());
            }
            /* translators: %1$s: the fetched URL, %2$s the error message that was returned */
            return new \WP_Error('http_error', \sprintf(\__('Failed to fetch: %1$s (%2$s)', 'amp'), $url, $exception->getMessage()));
        }
        $status = $response->getStatusCode();
        if ($status < 200 || $status >= 300) {
            /* translators: %1$s: the URL, %2$d: the HTTP status code, %3$s: the HTTP status message */
            return new \WP_Error("http_{$status}", \sprintf(\__('Failed to fetch: %1$s (HTTP %2$d: %3$s)', 'amp'), $url, $status, \get_status_header_desc($status)));
        }
        $content_type = (array) $response->getHeader('content-type');
        if (!empty($content_type) && !\preg_match('#^text/css#', $content_type[0])) {
            return new \WP_Error('no_css_content_type', \__('Response did not contain the expected text/css content type.', 'amp'));
        }
        return $response->getBody();
    }
    /**
     * Get parsed stylesheet (from cache).
     *
     * If the sanitization status has changed for the validation errors in the cached stylesheet since it was cached,
     * then the cache is invalidated, as the parsed stylesheet needs to be re-constructed.
     *
     * @since 1.0
     * @see \AMP_Style_Sanitizer::parse_stylesheet()
     *
     * @param string $stylesheet Stylesheet.
     * @param array  $options {
     *     Options.
     *
     *     @type string[] $property_allowlist Exclusively-allowed properties.
     *     @type string[] $property_denylist  Disallowed properties.
     *     @type string   $stylesheet_url     Original URL for stylesheet when originating via link or @import.
     *     @type array    $allowed_at_rules   Allowed @-rules.
     *     @type bool     $validate_keyframes Whether keyframes should be validated.
     *     @type string   $spec_name          Spec name.
     * }
     * @return array {
     *    Processed stylesheet.
     *
     *    @type array    $tokens             Stylesheet tokens, where arrays are tuples for declaration blocks.
     *    @type string   $hash               MD5 hash of the parsed stylesheet.
     *    @type array    $validation_results Validation results, array containing arrays with error and sanitized keys.
     *    @type array    $imported_font_urls Imported font stylesheet URLs.
     *    @type int      $priority           The priority of the stylesheet.
     *    @type float    $parse_time         The time duration it took to parse the stylesheet, in milliseconds.
     *    @type float    $shake_time         The time duration it took to tree-shake the stylesheet, in milliseconds.
     *    @type bool     $cached             Whether the parsed stylesheet was cached.
     *    @type int      $important_count    Number of !important qualifiers.
     *    @type int      $kept_error_count   Number of instances of invalid markup causing validation errors which are kept.
     *    @type string[] $preload_font_urls  Font URLs to preload.
     * }
     */
    private function get_parsed_stylesheet($stylesheet, $options = [])
    {
        $cached = \true;
        $cache_group = 'amp-parsed-stylesheet-v40';
        // This should be bumped whenever the PHP-CSS-Parser is updated or parsed format is updated.
        $use_transients = $this->should_use_transient_caching();
        // @todo If ValidationExemption::is_px_verified_for_node( $this->current_node ) then keep !important.
        // @todo If ValidationExemption::is_amp_unvalidated_for_node( $this->current_node ) then keep invalid markup.
        $cache_impacting_options = \array_merge(\wp_array_slice_assoc($options, ['property_allowlist', 'property_denylist', 'stylesheet_url', 'allowed_at_rules']), \wp_array_slice_assoc($this->args, ['should_locate_sources', 'parsed_cache_variant', 'dynamic_element_selectors', 'transform_important_qualifiers', 'font_face_display_overrides']), [
            'language' => \get_bloginfo('language'),
            // Used to tree-shake html[lang] selectors.
            'selector_mappings' => $this->selector_mappings,
        ]);
        $cache_key = \md5($stylesheet . \wp_json_encode($cache_impacting_options));
        if ($use_transients) {
            $parsed = \get_transient($cache_group . '-' . $cache_key);
        } else {
            $parsed = \wp_cache_get($cache_key, $cache_group);
        }
        /*
         * Make sure that the parsed stylesheet was cached with current sanitizations.
         * The should_sanitize_validation_error method prevents duplicates from being reported.
         */
        if (!empty($parsed['validation_results'])) {
            foreach ($parsed['validation_results'] as $validation_result) {
                $sanitized = $this->should_sanitize_validation_error($validation_result['error']);
                if ($sanitized !== $validation_result['sanitized']) {
                    $parsed = null;
                    // Change to sanitization of validation error detected, so cache cannot be used.
                    break;
                }
            }
        }
        if (!$parsed || !isset($parsed['tokens']) || !\is_array($parsed['tokens'])) {
            $parsed = $this->parse_stylesheet($stylesheet, $options);
            $cached = \false;
            /*
             * When an object cache is not available, we cache with an expiration to prevent the options table from
             * getting filled infinitely. On the other hand, if an external object cache is available then we don't
             * set an expiration because it should implement LRU cache expulsion policy.
             */
            if ($use_transients) {
                // The expiration is to ensure transient doesn't stick around forever since no LRU flushing like with external object cache.
                \set_transient($cache_group . '-' . $cache_key, $parsed, \MONTH_IN_SECONDS);
            } else {
                \wp_cache_set($cache_key, $parsed, $cache_group);
            }
        }
        $parsed['kept_error_count'] = 0;
        foreach ($parsed['validation_results'] as $validation_result) {
            if (!$validation_result['sanitized']) {
                $parsed['kept_error_count']++;
            }
        }
        $parsed['cached'] = $cached;
        return $parsed;
    }
    /**
     * Check whether transient caching for stylesheets should be used.
     *
     * @return bool Whether transient caching should be used.
     */
    private function should_use_transient_caching()
    {
        if (\wp_using_ext_object_cache()) {
            return \false;
        }
        if (!$this->args['allow_transient_caching']) {
            return \false;
        }
        if (\Google\Web_Stories_Dependencies\AMP_Options_Manager::get_option(Option::DISABLE_CSS_TRANSIENT_CACHING, \false)) {
            return \false;
        }
        return \true;
    }
    /**
     * Parse imported stylesheet and replace the `@import` rule with the imported rules in the provided CSS list (in place).
     *
     * @param Import  $item     Import object.
     * @param CSSList $css_list CSS List.
     * @param array   $options {
     *     Options.
     *
     *     @type string $stylesheet_url Original URL for stylesheet when originating via link or @import.
     * }
     * @return array {
     *     Results.
     *
     *     @type array    $validation_results Validation results.
     *     @type array    $imported_font_urls Imported font URLs.
     *     @type array    $viewport_rules     Extracted viewport rules.
     *     @type int      $important_count    Number of !important qualifiers.
     *     @type string[] $preload_font_urls  Font URLs to preload.
     * }
     */
    private function splice_imported_stylesheet(Import $item, CSSList $css_list, $options)
    {
        $validation_results = [];
        $imported_font_urls = [];
        $viewport_rules = [];
        $at_rule_args = $item->atRuleArgs();
        $location = \array_shift($at_rule_args);
        $media_query = \array_shift($at_rule_args);
        $important_count = 0;
        $preload_font_urls = [];
        if (isset($options['stylesheet_url'])) {
            $this->real_path_urls([$location], $options['stylesheet_url']);
        }
        $import_stylesheet_url = $location->getURL()->getString();
        // Prevent importing something that has already been imported, and avoid infinite recursion.
        if (isset($this->processed_imported_stylesheet_urls[$import_stylesheet_url])) {
            $css_list->remove($item);
            return \compact('validation_results', 'imported_font_urls', 'viewport_rules', 'important_count', 'preload_font_urls');
        }
        $this->processed_imported_stylesheet_urls[$import_stylesheet_url] = \true;
        // Prevent importing font stylesheets from allowed font CDNs. These will get added to the document as links instead.
        $https_import_stylesheet_url = \preg_replace('#^(http:)?(?=//)#', 'https:', $import_stylesheet_url);
        if ($this->allowed_font_src_regex && \preg_match($this->allowed_font_src_regex, $https_import_stylesheet_url)) {
            $imported_font_urls[] = $https_import_stylesheet_url;
            $css_list->remove($item);
            \_doing_it_wrong('wp_enqueue_style', \esc_html(\sprintf(
                /* translators: 1: @import. 2: wp_enqueue_style(). 3: font CDN URL. */
                \__('It is not a best practice to use %1$s to load font CDN stylesheets. Please use %2$s to enqueue %3$s as its own separate script.', 'amp'),
                '@import',
                'wp_enqueue_style()',
                $import_stylesheet_url
            )), '1.0');
            return \compact('validation_results', 'imported_font_urls', 'viewport_rules', 'important_count', 'preload_font_urls');
        }
        $stylesheet = $this->get_stylesheet_from_url($import_stylesheet_url);
        if ($stylesheet instanceof \WP_Error) {
            $error = ['code' => self::STYLESHEET_FETCH_ERROR, 'type' => \Google\Web_Stories_Dependencies\AMP_Validation_Error_Taxonomy::CSS_ERROR_TYPE, 'url' => $import_stylesheet_url, 'message' => $stylesheet->get_error_message()];
            $sanitized = $this->should_sanitize_validation_error($error);
            if ($sanitized) {
                $css_list->remove($item);
            }
            $validation_results[] = \compact('error', 'sanitized');
            return \compact('validation_results', 'imported_font_urls', 'viewport_rules', 'important_count', 'preload_font_urls');
        }
        if ($media_query) {
            $stylesheet = \sprintf('@media %s { %s }', $media_query, $stylesheet);
        }
        $options['stylesheet_url'] = $import_stylesheet_url;
        $parsed_stylesheet = $this->create_validated_css_document($stylesheet, $options);
        $validation_results = \array_merge($validation_results, $parsed_stylesheet['validation_results']);
        $viewport_rules = $parsed_stylesheet['viewport_rules'];
        $important_count = $parsed_stylesheet['important_count'];
        $preload_font_urls = $parsed_stylesheet['preload_font_urls'];
        if (!empty($parsed_stylesheet['css_document'])) {
            /**
             * CSS Doc.
             *
             * @var CSSDocument $css_document
             */
            $css_document = $parsed_stylesheet['css_document'];
            $this->replace_inside_css_list($css_list, $item, $css_document->getContents());
        } else {
            $css_list->remove($item);
        }
        return \compact('validation_results', 'imported_font_urls', 'viewport_rules', 'important_count', 'preload_font_urls');
    }
    /**
     * Replace an item inside of a CSSList.
     *
     * This is being used instead of `CSSList::splice()` because it uses `array_splice()` which does not work properly
     * if the array keys are not sequentially indexed from 0, which happens when `CSSList::remove()` is employed.
     *
     * @see CSSList::splice()
     * @see CSSList::replace()
     * @see CSSList::remove()
     *
     * @param CSSList                      $css_list  CSS list.
     * @param AtRule|RuleSet|CSSList       $old_item  Old item.
     * @param AtRule[]|RuleSet[]|CSSList[] $new_items New item(s). If empty, the old item is simply removed.
     * @return bool Whether the replacement was successful.
     */
    private function replace_inside_css_list(CSSList $css_list, $old_item, $new_items = [])
    {
        $contents = \array_values($css_list->getContents());
        // Required to obtain the offset instead of the index.
        $offset = \array_search($old_item, $contents, \true);
        if (\false !== $offset) {
            \array_splice($contents, $offset, 1, $new_items);
            $css_list->setContents($contents);
            return \true;
        }
        return \false;
    }
    /**
     * Create validated CSS document.
     *
     * @since 1.0
     *
     * @param string $stylesheet_string Stylesheet.
     * @param array  $options           Options. See definition in \AMP_Style_Sanitizer::process_stylesheet().
     * @return array {
     *    Parsed stylesheet.
     *
     *    @type CSSDocument $css_document       CSS Document.
     *    @type array       $validation_results Validation results, array containing arrays with error and sanitized keys.
     *    @type string      $stylesheet_url     Stylesheet URL, if available.
     *    @type array       $imported_font_urls Imported font URLs.
     *    @type array       $viewport_rules     Extracted viewport rules.
     *    @type int         $important_count    Number of !important qualifiers.
     *    @type string[]    $preload_font_urls  Font URLs to preload.
     * }
     */
    private function create_validated_css_document($stylesheet_string, $options)
    {
        $validation_results = [];
        $imported_font_urls = [];
        $viewport_rules = [];
        $important_count = 0;
        $preload_font_urls = [];
        $css_document = null;
        // Note that there is no known case where an exception can be thrown here since PHP-CSS-Parser is using lenient parsing.
        try {
            // Remove spaces from data URLs, which cause errors and PHP-CSS-Parser can't handle them.
            $stylesheet_string = $this->remove_spaces_from_url_values($stylesheet_string);
            $parser_settings = \Google\Web_Stories_Dependencies\Sabberworm\CSS\Settings::create();
            $css_parser = new \Google\Web_Stories_Dependencies\Sabberworm\CSS\Parser($stylesheet_string, $parser_settings);
            $css_document = $css_parser->parse();
            // @todo If 'utf-8' is not $css_parser->getCharset() then issue warning?
            if (!empty($options['stylesheet_url'])) {
                $this->real_path_urls(\array_filter($css_document->getAllValues(), static function ($value) {
                    return $value instanceof URL;
                }), $options['stylesheet_url']);
            }
            $processed_css_list = $this->process_css_list($css_document, $options);
            $validation_results = \array_merge($validation_results, $processed_css_list['validation_results']);
            $viewport_rules = \array_merge($viewport_rules, $processed_css_list['viewport_rules']);
            $important_count = $processed_css_list['important_count'];
            $imported_font_urls = $processed_css_list['imported_font_urls'];
            $preload_font_urls = \array_merge($preload_font_urls, $processed_css_list['preload_font_urls']);
        } catch (SourceException $exception) {
            $error = ['code' => self::CSS_SYNTAX_PARSE_ERROR, 'message' => $exception->getMessage(), 'type' => \Google\Web_Stories_Dependencies\AMP_Validation_Error_Taxonomy::CSS_ERROR_TYPE, 'spec_name' => $options['spec_name']];
            /*
             * This is not a recoverable error, so sanitized here is just used to give user control
             * over whether to proceed with serving this exception-raising stylesheet in AMP.
             */
            $sanitized = $this->should_sanitize_validation_error($error);
            $validation_results[] = \compact('error', 'sanitized');
        }
        return \compact('validation_results', 'css_document', 'imported_font_urls', 'viewport_rules', 'important_count', 'preload_font_urls');
    }
    /**
     * Parse stylesheet.
     *
     * Sanitizes invalid CSS properties and rules, compresses the CSS to remove whitespace and comments, and parses
     * declaration blocks to allow selectors to later be evaluated for whether they apply to the current document
     * during tree-shaking.
     *
     * @since 1.0
     *
     * @param string $stylesheet_string Stylesheet.
     * @param array  $options           Options. See definition in \AMP_Style_Sanitizer::process_stylesheet().
     * @return array {
     *    Parsed stylesheet.
     *
     *    @type array    $tokens             Stylesheet tokens, where arrays are tuples for declaration blocks.
     *    @type string   $hash               MD5 hash of the parsed stylesheet.
     *    @type array    $validation_results Validation results, array containing arrays with error and sanitized keys.
     *    @type array    $imported_font_urls Imported font stylesheet URLs.
     *    @type float    $parse_time         The time duration it took to parse the stylesheet, in milliseconds.
     *    @type int      $important_count    Number of !important qualifiers.
     *    @type string[] $preload_font_urls  Font URLs to preload.
     * }
     */
    private function parse_stylesheet($stylesheet_string, $options = [])
    {
        $start_time = \microtime(\true);
        $options = \array_merge(['allowed_at_rules' => [], 'property_denylist' => [
            // See <https://www.ampproject.org/docs/design/responsive/style_pages#disallowed-styles>.
            'behavior',
            '-moz-binding',
        ], 'property_allowlist' => [], 'validate_keyframes' => \false, 'stylesheet_url' => null, 'spec_name' => null], $options);
        // Strip the dreaded UTF-8 byte order mark (BOM, \uFEFF). This should ideally get handled by PHP-CSS-Parser <https://github.com/sabberworm/PHP-CSS-Parser/issues/150>.
        $stylesheet_string = \preg_replace('/^\\xEF\\xBB\\xBF/', '', $stylesheet_string);
        // Strip obsolete CDATA sections and HTML comments which were used for old school XHTML.
        $stylesheet_string = \preg_replace('#^\\s*<!--#', '', $stylesheet_string);
        $stylesheet_string = \preg_replace('#^\\s*<!\\[CDATA\\[#', '', $stylesheet_string);
        $stylesheet_string = \preg_replace('#\\]\\]>\\s*$#', '', $stylesheet_string);
        $stylesheet_string = \preg_replace('#-->\\s*$#', '', $stylesheet_string);
        $tokens = [];
        $parsed_stylesheet = $this->create_validated_css_document($stylesheet_string, $options);
        $validation_results = $parsed_stylesheet['validation_results'];
        if (!empty($parsed_stylesheet['css_document'])) {
            $css_document = $parsed_stylesheet['css_document'];
            $output_format = \Google\Web_Stories_Dependencies\Sabberworm\CSS\OutputFormat::createCompact();
            $output_format->setSemicolonAfterLastRule(\false);
            $before_declaration_block = \sprintf('/*%s*/', \chr(1));
            $between_selectors = \sprintf('/*%s*/', \chr(2));
            $after_declaration_block_selectors = \sprintf('/*%s*/', \chr(3));
            $between_properties = \sprintf('/*%s*/', \chr(4));
            $after_declaration_block = \sprintf('/*%s*/', \chr(5));
            $before_at_rule = \sprintf('/*%s*/', \chr(6));
            $after_at_rule = \sprintf('/*%s*/', \chr(7));
            // Add comments to stylesheet if PHP-CSS-Parser has the required extensions for tree shaking.
            if (self::has_required_php_css_parser()) {
                $output_format->set('BeforeDeclarationBlock', $before_declaration_block);
                $output_format->set('SpaceBeforeSelectorSeparator', $between_selectors);
                $output_format->set('AfterDeclarationBlockSelectors', $after_declaration_block_selectors);
                $output_format->set('AfterDeclarationBlock', $after_declaration_block);
                $output_format->set('BeforeAtRuleBlock', $before_at_rule);
                $output_format->set('AfterAtRuleBlock', $after_at_rule);
            }
            $output_format->set('SpaceBetweenRules', $between_properties);
            $stylesheet_string = $css_document->render($output_format);
            $pattern = '#';
            $pattern .= \preg_quote($before_at_rule, '#');
            $pattern .= '|';
            $pattern .= \preg_quote($after_at_rule, '#');
            $pattern .= '|';
            $pattern .= '(' . \preg_quote($before_declaration_block, '#') . ')';
            $pattern .= '(.+?)';
            $pattern .= \preg_quote($after_declaration_block_selectors, '#');
            $pattern .= '(.+?)';
            $pattern .= \preg_quote($after_declaration_block, '#');
            $pattern .= '#s';
            $dynamic_selector_pattern = null;
            if (!empty($this->args['dynamic_element_selectors'])) {
                $dynamic_selector_pattern = \implode('|', \array_map(static function ($selector) {
                    return \preg_quote($selector, '#');
                }, $this->args['dynamic_element_selectors']));
            }
            $split_stylesheet = \preg_split($pattern, $stylesheet_string, -1, \PREG_SPLIT_DELIM_CAPTURE);
            // Ensure all instances of </style> are escaped as <\/style> (such as can occur in SVG data: URLs) to prevent
            // the inline style from prematurely closing style[amp-custom].
            $split_stylesheet = \str_replace('</style>', '<\\/style>', $split_stylesheet, $count);
            $length = \count($split_stylesheet);
            for ($i = 0; $i < $length; $i++) {
                // Skip empty tokens.
                if ('' === $split_stylesheet[$i]) {
                    unset($split_stylesheet[$i]);
                    continue;
                }
                if ($before_declaration_block === $split_stylesheet[$i]) {
                    // Skip keyframe-selector, which is can be: from | to | <percentage>.
                    if (\preg_match('/^((from|to)\\b|-?\\d+(\\.\\d+)?%)/i', $split_stylesheet[$i + 1])) {
                        $tokens[] = \str_replace($between_selectors, '', $split_stylesheet[++$i]) . \str_replace($between_properties, '', $split_stylesheet[++$i]);
                        continue;
                    }
                    $selectors = \explode($between_selectors . ',', $split_stylesheet[++$i]);
                    $declaration = \explode(';' . $between_properties, \trim($split_stylesheet[++$i], '{}'));
                    // @todo The following logic could be made much more robust if PHP-CSS-Parser did parsing of selectors. See <https://github.com/sabberworm/PHP-CSS-Parser/pull/138#issuecomment-418193262> and <https://github.com/ampproject/amp-wp/issues/2102>.
                    $selectors_parsed = [];
                    foreach ($selectors as $selector) {
                        $selectors_parsed[$selector] = [];
                        // Remove :not() and pseudo selectors to eliminate false negatives, such as with `body:not(.title-tagline-hidden) .site-branding-text` (but not after escape character).
                        $reduced_selector = \preg_replace('/(?<!\\\\)::?[a-zA-Z0-9_-]+(\\(.+?\\))?/', '', $selector);
                        // Ignore any selector terms that occur under a dynamic selector.
                        if ($dynamic_selector_pattern) {
                            $reduced_selector = \preg_replace('#((?:' . $dynamic_selector_pattern . ')(?:\\.[a-z0-9_-]+)*)[^a-z0-9_-].*#si', '$1', $reduced_selector . ' ');
                        }
                        /*
                         * Gather attribute names while removing attribute selectors to eliminate false negative,
                         * such as with `.social-navigation a[href*="example.com"]:before`.
                         */
                        $reduced_selector = \preg_replace_callback('/\\[([A-Za-z0-9_:-]+)(\\W?=[^\\]]+)?\\]/', static function ($matches) use($selector, &$selectors_parsed) {
                            $selectors_parsed[$selector][self::SELECTOR_EXTRACTED_ATTRIBUTES][] = $matches[1];
                            return '';
                        }, $reduced_selector);
                        // Extract class names.
                        $reduced_selector = \preg_replace_callback(
                            '/\\.((?:[a-zA-Z0-9_-]+|\\\\.)+)/',
                            // The `\\\\.` will allow any char via escaping, like the colon in `.lg\:w-full`.
                            static function ($matches) use($selector, &$selectors_parsed) {
                                $selectors_parsed[$selector][self::SELECTOR_EXTRACTED_CLASSES][] = \stripslashes($matches[1]);
                                return '';
                            },
                            $reduced_selector
                        );
                        // Extract IDs.
                        $reduced_selector = \preg_replace_callback('/#([a-zA-Z0-9_-]+)/', static function ($matches) use($selector, &$selectors_parsed) {
                            $selectors_parsed[$selector][self::SELECTOR_EXTRACTED_IDS][] = $matches[1];
                            return '';
                        }, $reduced_selector);
                        // Extract tag names.
                        $reduced_selector = \preg_replace_callback('/[a-zA-Z0-9_-]+/', static function ($matches) use($selector, &$selectors_parsed) {
                            $selectors_parsed[$selector][self::SELECTOR_EXTRACTED_TAGS][] = $matches[0];
                            return '';
                        }, $reduced_selector);
                        // At this point, $reduced_selector should contain just the remnants of the selector, primarily combinators.
                        unset($reduced_selector);
                    }
                    $tokens[] = [$selectors_parsed, $declaration];
                } else {
                    $tokens[] = \str_replace($between_properties, '', $split_stylesheet[$i]);
                }
            }
        }
        return \array_merge(\compact('tokens', 'validation_results'), ['imported_font_urls' => $parsed_stylesheet['imported_font_urls'], 'hash' => \md5(\wp_json_encode($tokens)), 'parse_time' => \microtime(\true) - $start_time, 'viewport_rules' => $parsed_stylesheet['viewport_rules'], 'important_count' => $parsed_stylesheet['important_count'], 'preload_font_urls' => $parsed_stylesheet['preload_font_urls']]);
    }
    /**
     * Previous return values from calls to should_sanitize_validation_error().
     *
     * This is used to prevent duplicates from being reported when the sanitization status
     * changes for a validation error in a previously-cached stylesheet.
     *
     * @see AMP_Style_Sanitizer::should_sanitize_validation_error()
     * @var array
     */
    protected $previous_should_sanitize_validation_error_results = [];
    /**
     * Check whether or not sanitization should occur in response to validation error.
     *
     * Supply sources to the error and the current node to data.
     *
     * @since 1.0
     *
     * @param array $validation_error Validation error.
     * @param array $data Data including the node.
     * @return bool Whether to sanitize.
     */
    public function should_sanitize_validation_error($validation_error, $data = [])
    {
        if (!isset($data['node'])) {
            $data['node'] = $this->current_node;
        }
        if (!isset($validation_error['sources'])) {
            $validation_error['sources'] = $this->current_sources;
        }
        /*
         * This is used to prevent duplicates from being reported when the sanitization status
         * changes for a validation error in a previously-cached stylesheet.
         */
        $args = \compact('validation_error', 'data');
        foreach ($this->previous_should_sanitize_validation_error_results as $result) {
            if ($result['args'] === $args) {
                return $result['sanitized'];
            }
        }
        $sanitized = parent::should_sanitize_validation_error($validation_error, $data);
        $this->previous_should_sanitize_validation_error_results[] = \compact('args', 'sanitized');
        return $sanitized;
    }
    /**
     * Remove spaces from CSS URL values which PHP-CSS-Parser doesn't handle.
     *
     * @since 1.0
     *
     * @param string $css CSS.
     * @return string CSS with spaces removed from URLs.
     */
    private function remove_spaces_from_url_values($css)
    {
        return \preg_replace_callback(
            // Match CSS url() values that don't have quoted string values.
            '/\\burl\\(\\s*(?=\\w)(?P<url>[^}]*?\\s*)\\)/',
            static function ($matches) {
                return \preg_replace('/\\s+/', '', $matches[0]);
            },
            $css
        );
    }
    /**
     * Process CSS list.
     *
     * @since 1.0
     *
     * @param CSSList $css_list CSS List.
     * @param array   $options Options.
     * @return array {
     *     Processed CSS list.
     *
     *     @type array    $validation_results Validation results.
     *     @type array    $viewport_rules     Extracted viewport rules.
     *     @type array    $imported_font_urls Imported font URLs.
     *     @type int      $important_count    Number of !important qualifiers.
     *     @type string[] $preload_font_urls  Font URLs to preload.
     * }
     */
    private function process_css_list(CSSList $css_list, $options)
    {
        $validation_results = [];
        $viewport_rules = [];
        $imported_font_urls = [];
        $preload_font_urls = [];
        $important_count = 0;
        foreach ($css_list->getContents() as $css_item) {
            $sanitized = \false;
            if ($css_item instanceof DeclarationBlock && empty($options['validate_keyframes'])) {
                $processed = $this->process_css_declaration_block($css_item, $css_list, $options);
                $important_count += $processed['important_count'];
                $preload_font_urls = \array_merge($preload_font_urls, $processed['preload_font_urls']);
                $validation_results = \array_merge($validation_results, $processed['validation_results']);
            } elseif ($css_item instanceof AtRuleBlockList) {
                if (!\in_array($css_item->atRuleName(), $options['allowed_at_rules'], \true)) {
                    $error = ['code' => self::CSS_SYNTAX_INVALID_AT_RULE, 'at_rule' => $css_item->atRuleName(), 'type' => \Google\Web_Stories_Dependencies\AMP_Validation_Error_Taxonomy::CSS_ERROR_TYPE, 'spec_name' => $options['spec_name']];
                    $sanitized = $this->should_sanitize_validation_error($error);
                    $validation_results[] = \compact('error', 'sanitized');
                }
                if (!$sanitized) {
                    $processed = $this->process_css_list($css_item, $options);
                    $viewport_rules = \array_merge($viewport_rules, $processed['viewport_rules']);
                    $important_count += $processed['important_count'];
                    $preload_font_urls = \array_merge($preload_font_urls, $processed['preload_font_urls']);
                    $validation_results = \array_merge($validation_results, $processed['validation_results']);
                }
            } elseif ($css_item instanceof Import) {
                $imported_stylesheet = $this->splice_imported_stylesheet($css_item, $css_list, $options);
                $imported_font_urls = \array_merge($imported_font_urls, $imported_stylesheet['imported_font_urls']);
                $validation_results = \array_merge($validation_results, $imported_stylesheet['validation_results']);
                $preload_font_urls = \array_merge($preload_font_urls, $imported_stylesheet['preload_font_urls']);
                $viewport_rules = \array_merge($viewport_rules, $imported_stylesheet['viewport_rules']);
                $important_count += $imported_stylesheet['important_count'];
            } elseif ($css_item instanceof AtRuleSet) {
                if (\preg_match('/^(-.+-)?viewport$/', $css_item->atRuleName())) {
                    $output_format = new OutputFormat();
                    foreach ($css_item->getRules() as $rule) {
                        $rule_value = $rule->getValue();
                        if ($rule_value instanceof Value) {
                            $rule_value = $rule_value->render($output_format);
                        }
                        $viewport_rules[$rule->getRule()] = $rule_value;
                    }
                    $css_list->remove($css_item);
                } elseif (!\in_array($css_item->atRuleName(), $options['allowed_at_rules'], \true)) {
                    $error = ['code' => self::CSS_SYNTAX_INVALID_AT_RULE, 'at_rule' => $css_item->atRuleName(), 'type' => \Google\Web_Stories_Dependencies\AMP_Validation_Error_Taxonomy::CSS_ERROR_TYPE, 'spec_name' => $options['spec_name']];
                    $sanitized = $this->should_sanitize_validation_error($error);
                    $validation_results[] = \compact('error', 'sanitized');
                }
                if (!$sanitized) {
                    $processed = $this->process_css_declaration_block($css_item, $css_list, $options);
                    $validation_results = \array_merge($validation_results, $processed['validation_results']);
                    $important_count += $processed['important_count'];
                    $preload_font_urls = \array_merge($preload_font_urls, $processed['preload_font_urls']);
                }
            } elseif ($css_item instanceof KeyFrame) {
                if (!\in_array('keyframes', $options['allowed_at_rules'], \true)) {
                    $error = ['code' => self::CSS_SYNTAX_INVALID_AT_RULE, 'at_rule' => $css_item->atRuleName(), 'type' => \Google\Web_Stories_Dependencies\AMP_Validation_Error_Taxonomy::CSS_ERROR_TYPE, 'spec_name' => $options['spec_name']];
                    $sanitized = $this->should_sanitize_validation_error($error);
                    $validation_results[] = \compact('error', 'sanitized');
                }
                if (!$sanitized) {
                    $processed = $this->process_css_keyframes($css_item, $options);
                    $validation_results = \array_merge($validation_results, $processed['validation_results']);
                    $important_count += $processed['important_count'];
                }
            } elseif ($css_item instanceof AtRule) {
                if ('charset' === $css_item->atRuleName()) {
                    /*
                     * The @charset at-rule is not allowed in style elements, so it is not allowed in AMP.
                     * If the @charset is defined, then it really should have already been acknowledged
                     * by PHP-CSS-Parser when the CSS was parsed in the first place, so at this point
                     * it is irrelevant and can be removed.
                     */
                    $sanitized = \true;
                } else {
                    $error = ['code' => self::CSS_SYNTAX_INVALID_AT_RULE, 'at_rule' => $css_item->atRuleName(), 'type' => \Google\Web_Stories_Dependencies\AMP_Validation_Error_Taxonomy::CSS_ERROR_TYPE, 'spec_name' => $options['spec_name']];
                    $sanitized = $this->should_sanitize_validation_error($error);
                    $validation_results[] = \compact('error', 'sanitized');
                }
            } else {
                $error = ['code' => self::CSS_SYNTAX_INVALID_DECLARATION, 'item' => \get_class($css_item), 'type' => \Google\Web_Stories_Dependencies\AMP_Validation_Error_Taxonomy::CSS_ERROR_TYPE, 'spec_name' => $options['spec_name']];
                $sanitized = $this->should_sanitize_validation_error($error);
                $validation_results[] = \compact('error', 'sanitized');
            }
            if ($sanitized) {
                $css_list->remove($css_item);
            }
        }
        return \compact('validation_results', 'imported_font_urls', 'viewport_rules', 'important_count', 'preload_font_urls');
    }
    /**
     * Convert URLs in to non-relative real-paths.
     *
     * @param URL[]  $urls           URLs.
     * @param string $stylesheet_url Stylesheet URL.
     */
    private function real_path_urls($urls, $stylesheet_url)
    {
        $base_url = \preg_replace(':[^/]+(\\?.*)?(#.*)?$:', '', $stylesheet_url);
        if (empty($base_url)) {
            return;
        }
        foreach ($urls as $url) {
            // URLs cannot have spaces in them, so strip them (especially when spaces get erroneously injected in data: URLs).
            $url_string = $url->getURL()->getString();
            // For data: URLs, all that is needed is to remove spaces so set and continue.
            if ('data:' === \substr($url_string, 0, 5)) {
                continue;
            }
            // If the URL is already absolute, continue since there there is nothing left to do.
            $parsed_url = \wp_parse_url($url_string);
            if (!empty($parsed_url['host']) || empty($parsed_url['path']) || '/' === \substr($parsed_url['path'], 0, 1)) {
                continue;
            }
            $parsed_url = \wp_parse_url($base_url . $url->getURL()->getString());
            // Resolve any relative parent directory paths.
            $path = $this->unrelativize_path($parsed_url['path']);
            if (\is_wp_error($path)) {
                continue;
            }
            $parsed_url['path'] = $path;
            $real_url = $this->reconstruct_url($parsed_url);
            $url->getURL()->setString($real_url);
        }
    }
    /**
     * Process CSS rule set.
     *
     * @since 1.0
     * @link https://www.ampproject.org/docs/design/responsive/style_pages#disallowed-styles
     * @link https://www.ampproject.org/docs/design/responsive/style_pages#restricted-styles
     *
     * @param RuleSet $ruleset  Ruleset.
     * @param CSSList $css_list CSS List.
     * @param array   $options  Options.
     *
     * @return array {
     *     Results.
     *
     *     @type array    $validation_results Validation results.
     *     @type int      $important_count    Number of !important qualifiers.
     *     @type string[] $preload_font_urls  Font URLs to preload.
     * }
     */
    private function process_css_declaration_block(RuleSet $ruleset, CSSList $css_list, $options)
    {
        $validation_results = [];
        $important_count = 0;
        $preload_font_urls = [];
        if ($ruleset instanceof DeclarationBlock) {
            $validation_results = \array_merge($validation_results, $this->ampify_ruleset_selectors($ruleset));
            if (0 === \count($ruleset->getSelectors())) {
                $css_list->remove($ruleset);
                return \compact('validation_results', 'important_count', 'preload_font_urls');
            }
        }
        // Remove disallowed properties.
        if (!empty($options['property_allowlist'])) {
            $properties = $ruleset->getRules();
            foreach ($properties as $property) {
                $vendorless_property_name = \preg_replace('/^-\\w+-/', '', $property->getRule());
                if (!\in_array($vendorless_property_name, $options['property_allowlist'], \true)) {
                    $error = ['code' => self::CSS_SYNTAX_INVALID_PROPERTY, 'css_property_name' => $property->getRule(), 'css_property_value' => $property->getValue(), 'type' => \Google\Web_Stories_Dependencies\AMP_Validation_Error_Taxonomy::CSS_ERROR_TYPE, 'spec_name' => $options['spec_name']];
                    $sanitized = $this->should_sanitize_validation_error($error);
                    if ($sanitized) {
                        $ruleset->removeRule($property->getRule());
                    }
                    $validation_results[] = \compact('error', 'sanitized');
                }
            }
        } else {
            foreach ($options['property_denylist'] as $illegal_property_name) {
                $properties = $ruleset->getRules($illegal_property_name);
                foreach ($properties as $property) {
                    $error = ['code' => self::CSS_SYNTAX_INVALID_PROPERTY_NOLIST, 'css_property_name' => $property->getRule(), 'css_property_value' => (string) $property->getValue(), 'type' => \Google\Web_Stories_Dependencies\AMP_Validation_Error_Taxonomy::CSS_ERROR_TYPE, 'spec_name' => $options['spec_name']];
                    $sanitized = $this->should_sanitize_validation_error($error);
                    if ($sanitized) {
                        $ruleset->removeRule($property->getRule());
                    }
                    $validation_results[] = \compact('error', 'sanitized');
                }
            }
        }
        if ($ruleset instanceof AtRuleSet && 'font-face' === $ruleset->atRuleName()) {
            $preload_font_urls = $this->process_font_face_at_rule($ruleset, $options);
        }
        $transformed = $this->transform_important_qualifiers($ruleset, $css_list, $options);
        $validation_results = \array_merge($validation_results, $transformed['validation_results']);
        $important_count = $transformed['important_count'];
        // Remove the ruleset if it is now empty.
        if (0 === \count($ruleset->getRules())) {
            $css_list->remove($ruleset);
        }
        return \compact('validation_results', 'important_count', 'preload_font_urls');
    }
    /**
     * Process @font-face by making src URLs non-relative and converting data: URLs into file URLs (with educated guessing).
     *
     * @since 1.0
     *
     * @param AtRuleSet $ruleset Ruleset for @font-face.
     * @param array     $options {
     *     Options.
     *
     *     @type string $stylesheet_url Stylesheet URL, if available.
     * }
     * @return string[] Font URLs to preload.
     */
    private function process_font_face_at_rule(AtRuleSet $ruleset, $options)
    {
        $src_properties = $ruleset->getRules('src');
        if (empty($src_properties)) {
            return [];
        }
        $preload_font_urls = [];
        // Obtain the font-family name to guess the filename.
        $font_family = null;
        $font_basename = null;
        $properties = $ruleset->getRules('font-family');
        $property = \end($properties);
        if ($property instanceof Rule) {
            $font_family = \trim($property->getValue(), '"\'');
            // Remove all non-word characters from the font family to serve as the filename.
            $font_basename = \preg_replace('/[^A-Za-z0-9_\\-]/', '', $font_family);
            // Same as sanitize_key() minus case changes.
        }
        // Obtain the stylesheet base URL from which to guess font file locations.
        $stylesheet_base_url = null;
        if (!empty($options['stylesheet_url'])) {
            $stylesheet_base_url = \preg_replace(':[^/]+(\\?.*)?(#.*)?$:', '', $options['stylesheet_url']);
            $stylesheet_base_url = \trailingslashit($stylesheet_base_url);
        }
        // Obtain the font file path (if any) and the first font src type.
        $font_file = '';
        $first_src_type = '';
        // Attempt to transform data: URLs in src properties to be external file URLs.
        foreach ($src_properties as $src_property) {
            $value = $src_property->getValue();
            if (!$value instanceof RuleValueList) {
                continue;
            }
            /*
             * The CSS Parser parses a src such as:
             *
             *    url(data:application/font-woff;...) format('woff'),
             *    url('Genericons.ttf') format('truetype'),
             *    url('Genericons.svg#genericonsregular') format('svg')
             *
             * As a list of components consisting of:
             *
             *    URL,
             *    RuleValueList( CSSFunction, URL ),
             *    RuleValueList( CSSFunction, URL ),
             *    CSSFunction
             *
             * Clearly the components here are not logically grouped. So the first step is to fix the order.
             */
            $sources = [];
            foreach ($value->getListComponents() as $component) {
                if ($component instanceof RuleValueList) {
                    $subcomponents = $component->getListComponents();
                    $subcomponent = \array_shift($subcomponents);
                    if ($subcomponent) {
                        if (empty($sources)) {
                            $sources[] = [$subcomponent];
                        } else {
                            $sources[\count($sources) - 1][] = $subcomponent;
                        }
                    }
                    foreach ($subcomponents as $subcomponent) {
                        $sources[] = [$subcomponent];
                    }
                } elseif (empty($sources)) {
                    $sources[] = [$component];
                } else {
                    $sources[\count($sources) - 1][] = $component;
                }
            }
            /**
             * Source file URL list.
             *
             * @var string[] $source_file_urls
             */
            $source_file_urls = [];
            /**
             * Source data URL collection.
             *
             * @var URL[]    $source_data_url_objects
             */
            $source_data_url_objects = [];
            foreach ($sources as $source) {
                if (\count($source) !== 2) {
                    continue;
                }
                list($url, $format) = $source;
                if (!$url instanceof URL || !$format instanceof CSSFunction || $format->getName() !== 'format' || \count($format->getArguments()) !== 1) {
                    continue;
                }
                list($format_value) = $format->getArguments();
                $format_value = \trim($format_value, '"\'');
                $value = $url->getURL()->getString();
                if ('data:' === \substr($value, 0, 5)) {
                    $source_data_url_objects[$format_value] = $source[0];
                    if (empty($first_src_type)) {
                        $first_src_type = 'inline';
                    }
                } else {
                    $source_file_urls[] = $value;
                    if (empty($first_src_type)) {
                        $first_src_type = 'file';
                        $font_file = $value;
                    }
                }
            }
            // Convert data: URLs into regular URLs, assuming there will be a file present (e.g. woff fonts in core themes).
            foreach ($source_data_url_objects as $format => $data_url) {
                $mime_type = \strtok(\substr($data_url->getURL()->getString(), 5), ';');
                if ($mime_type) {
                    $extension = \preg_replace(':.+/(.+-)?:', '', $mime_type);
                } else {
                    $extension = $format;
                }
                $extension = \sanitize_key($extension);
                $guessed_urls = [];
                // Guess URLs based on any other font sources that are not using data: URLs (e.g. truetype fallback for inline woff2).
                foreach ($source_file_urls as $source_file_url) {
                    $guessed_url = \preg_replace(
                        ':(?<=\\.)\\w+(\\?.*)?(#.*)?$:',
                        // Match the file extension in the URL.
                        $extension,
                        $source_file_url,
                        1,
                        $count
                    );
                    if (1 === $count) {
                        $guessed_urls[] = $guessed_url;
                    }
                }
                /*
                 * Guess some font file URLs based on the font name in a fonts directory based on precedence of Twenty Nineteen.
                 * For example, the NonBreakingSpaceOverride woff2 font file is located at fonts/NonBreakingSpaceOverride.woff2.
                 */
                if ($stylesheet_base_url && $font_basename) {
                    $guessed_urls[] = $stylesheet_base_url . \sprintf('fonts/%s.%s', $font_basename, $extension);
                    $guessed_urls[] = $stylesheet_base_url . \sprintf('fonts/%s.%s', \strtolower($font_basename), $extension);
                }
                // Find the font file that exists, and then replace the data: URL with the external URL for the font.
                foreach ($guessed_urls as $guessed_url) {
                    $path = $this->get_validated_url_file_path($guessed_url, ['woff', 'woff2', 'ttf', 'otf', 'svg']);
                    if (!\is_wp_error($path)) {
                        $data_url->getURL()->setString($guessed_url);
                        if ('inline' === $first_src_type) {
                            $first_src_type = 'file';
                            $font_file = $guessed_url;
                        }
                        continue 2;
                    }
                }
                // As fallback, look for fonts bundled with the AMP plugin.
                $font_filename = \sprintf('%s.%s', \strtolower($font_basename), $extension);
                $bundled_fonts = ['nonbreakingspaceoverride.woff', 'nonbreakingspaceoverride.woff2', 'genericons.woff'];
                if (\in_array($font_filename, $bundled_fonts, \true)) {
                    $font_file = \plugin_dir_url(\AMP__FILE__) . "assets/fonts/{$font_filename}";
                    $data_url->getURL()->setString($font_file);
                    $first_src_type = 'file';
                }
            }
            // End foreach $source_data_url_objects.
        }
        // End foreach $src_properties.
        // Override the 'font-display' property to improve font performance.
        if ($font_family && \in_array($font_family, \array_keys($this->args['font_face_display_overrides']), \true)) {
            $ruleset->removeRule('font-display');
            $font_display_rule = new Rule('font-display');
            $font_display_rule->setValue($this->args['font_face_display_overrides'][$font_family]);
            $ruleset->addRule($font_display_rule);
        }
        // If the font-display is auto, block, or swap then we should automatically add the preload link for the first font file.
        $properties = $ruleset->getRules('font-display');
        $property = \end($properties);
        // Last since the last property wins in CSS.
        /** @var RuleValueList|string|null */
        $property_value = $property instanceof Rule ? $property->getValue() : '';
        if ((!$property instanceof Rule || \in_array($property_value, ['auto', 'block', 'swap'], \true)) && 'file' === $first_src_type && !empty($font_file)) {
            $preload_font_urls[] = $font_file;
        }
        return $preload_font_urls;
    }
    /**
     * Process CSS keyframes.
     *
     * @since 1.0
     * @link https://www.ampproject.org/docs/design/responsive/style_pages#restricted-styles.
     * @link https://github.com/ampproject/amphtml/blob/b685a0780a7f59313666225478b2b79b463bcd0b/validator/validator-main.protoascii#L1002-L1043
     * @todo Tree shaking could be extended to keyframes, to omit a keyframe if it is not referenced by any rule.
     *
     * @param KeyFrame $css_list Ruleset.
     * @param array    $options  Options.
     * @return array {
     *     Results.
     *
     *     @type array $validation_results Validation results.
     *     @type int   $important_count    Number of !important qualifiers.
     * }
     */
    private function process_css_keyframes(KeyFrame $css_list, $options)
    {
        $validation_results = [];
        $important_count = 0;
        foreach ($css_list->getContents() as $rules) {
            if (!$rules instanceof DeclarationBlock) {
                $error = ['code' => self::CSS_SYNTAX_INVALID_DECLARATION, 'item' => \get_class($rules), 'type' => \Google\Web_Stories_Dependencies\AMP_Validation_Error_Taxonomy::CSS_ERROR_TYPE, 'spec_name' => $options['spec_name']];
                $sanitized = $this->should_sanitize_validation_error($error);
                if ($sanitized) {
                    $css_list->remove($rules);
                }
                $validation_results[] = \compact('error', 'sanitized');
                continue;
            }
            $transformed = $this->transform_important_qualifiers($rules, $css_list, $options);
            $validation_results = \array_merge($validation_results, $transformed['validation_results']);
            $important_count += $transformed['important_count'];
            if (!empty($options['property_allowlist'])) {
                $properties = $rules->getRules();
                foreach ($properties as $property) {
                    $vendorless_property_name = \preg_replace('/^-\\w+-/', '', $property->getRule());
                    if (!\in_array($vendorless_property_name, $options['property_allowlist'], \true)) {
                        $error = ['code' => self::CSS_SYNTAX_INVALID_PROPERTY, 'css_property_name' => $property->getRule(), 'css_property_value' => (string) $property->getValue(), 'type' => \Google\Web_Stories_Dependencies\AMP_Validation_Error_Taxonomy::CSS_ERROR_TYPE, 'spec_name' => $options['spec_name']];
                        $sanitized = $this->should_sanitize_validation_error($error);
                        if ($sanitized) {
                            $rules->removeRule($property->getRule());
                        }
                        $validation_results[] = \compact('error', 'sanitized');
                    }
                }
            }
        }
        return \compact('validation_results', 'important_count');
    }
    /**
     * Replace !important qualifiers with more specific rules.
     *
     * @since 1.0
     * @see https://www.npmjs.com/package/replace-important
     * @see https://www.ampproject.org/docs/fundamentals/spec#important
     *
     * @param RuleSet|DeclarationBlock $ruleset  Rule set.
     * @param CSSList                  $css_list CSS List.
     * @param array                    $options  Options.
     * @return array {
     *     Results.
     *
     *     @type array $validation_results Validation results.
     *     @type int   $important_count    Number of !important qualifiers.
     * }
     */
    private function transform_important_qualifiers(RuleSet $ruleset, CSSList $css_list, $options)
    {
        $important_count = 0;
        $validation_results = [];
        // An !important only makes sense for rulesets that have selectors.
        $allow_transformation = $ruleset instanceof DeclarationBlock && !$css_list instanceof KeyFrame;
        $properties = $ruleset->getRules();
        $importants = [];
        foreach ($properties as $property) {
            if (!$property->getIsImportant()) {
                continue;
            }
            if (!$this->args['transform_important_qualifiers']) {
                $important_count++;
            } elseif ($allow_transformation) {
                $importants[] = $property;
                $property->setIsImportant(\false);
                $ruleset->removeRule($property->getRule());
            } else {
                $error = ['code' => self::CSS_SYNTAX_INVALID_IMPORTANT, 'type' => \Google\Web_Stories_Dependencies\AMP_Validation_Error_Taxonomy::CSS_ERROR_TYPE, 'css_property_name' => $property->getRule(), 'css_property_value' => $property->getValue(), 'spec_name' => $options['spec_name']];
                $sanitized = $this->should_sanitize_validation_error($error);
                if ($sanitized) {
                    $property->setIsImportant(\false);
                } else {
                    $important_count++;
                }
                $validation_results[] = \compact('error', 'sanitized');
            }
        }
        if (!$ruleset instanceof DeclarationBlock || !$allow_transformation || empty($importants)) {
            return \compact('validation_results', 'important_count');
        }
        /**
         * Ruleset covering !important styles.
         *
         * @var DeclarationBlock $important_ruleset
         */
        $important_ruleset = clone $ruleset;
        $important_ruleset->setSelectors(\array_map(
            /**
             * Modify selectors to be more specific to roughly match the effect of !important.
             *
             * @link https://github.com/ampproject/ampstart/blob/4c21d69afdd07b4c60cd190937bda09901955829/tools/replace-important/lib/index.js#L88-L109
             *
             * @param Selector $old_selector Original selector.
             * @return Selector The new more-specific selector.
             */
            static function (Selector $old_selector) {
                // Calculate the specificity multiplier for the placeholder.
                $specificity_multiplier = \Google\Web_Stories_Dependencies\AMP_Style_Sanitizer::INLINE_SPECIFICITY_MULTIPLIER + 1 + \floor($old_selector->getSpecificity() / 100);
                if ($old_selector->getSpecificity() % 100 > 0) {
                    $specificity_multiplier++;
                }
                if ($old_selector->getSpecificity() % 10 > 0) {
                    $specificity_multiplier++;
                }
                $new_selector = $old_selector->getSelector();
                if ('#_)' === \substr($new_selector, -3)) {
                    $new_selector = \rtrim($new_selector, ')') . \str_repeat('#_', $specificity_multiplier) . ')';
                } else {
                    $new_selector .= ':not(' . \str_repeat('#_', $specificity_multiplier) . ')';
                    // Here "_" is just a short single-char ID.
                }
                return new Selector($new_selector);
            },
            $ruleset->getSelectors()
        ));
        $important_ruleset->setRules($importants);
        $contents = \array_values($css_list->getContents());
        // Ensure keys are 0-indexed and sequential.
        $offset = \array_search($ruleset, $contents, \true);
        if (\false !== $offset) {
            \array_splice($contents, $offset + 1, 0, [$important_ruleset]);
            $css_list->setContents($contents);
        }
        return \compact('validation_results', 'important_count');
    }
    /**
     * Collect and store all CSS style attributes.
     *
     * Collects the CSS styles from within the HTML contained in this instance's Dom\Document.
     *
     * @since 0.4
     * @since 0.7 Modified to use element passed by XPath query.
     *
     * @note Uses recursion to traverse down the tree of Dom\Document nodes.
     *
     * @param DOMElement $element Element with a style attribute.
     */
    private function collect_inline_styles(\DOMElement $element)
    {
        $attr_node = $element->getAttributeNode('style');
        if (!$attr_node instanceof \DOMAttr) {
            return;
        }
        $value = \trim($attr_node->nodeValue);
        if (empty($value)) {
            $element->removeAttribute('style');
            return;
        }
        // Skip processing stylesheets that contain mustache template variables if the element is inside of a mustache template.
        if (\preg_match('/{{[^}]+?}}/', $value) && 0 !== $this->dom->xpath->query('//template[ @type="amp-mustache" ]//.|//script[ @template="amp-mustache" and @type="text/plain" ]//.', $element)->length) {
            return;
        }
        $class = 'amp-wp-' . \substr(\md5($value), 0, 7);
        $specificity = ':not(' . \str_repeat('#_', self::INLINE_SPECIFICITY_MULTIPLIER) . ')';
        $rule = \sprintf('.%s%s{%s}', $class, $specificity, $value);
        $this->set_current_node($element);
        // And sources when needing to be located.
        // @todo If ValidationExemption::is_px_verified_for_node( $element ) then keep !important.
        // @todo If ValidationExemption::is_amp_unvalidated_for_node( $element ) then keep invalid markup.
        $parsed = $this->get_parsed_stylesheet($rule, ['allowed_at_rules' => [], 'property_allowlist' => $this->style_custom_cdata_spec['css_spec']['declaration'], 'spec_name' => self::STYLE_AMP_CUSTOM_SPEC_NAME]);
        $element->removeAttribute('style');
        $element->setAttribute(self::ORIGINAL_STYLE_ATTRIBUTE_NAME, $value);
        if ($parsed['tokens']) {
            $this->pending_stylesheets[] = ['group' => self::STYLE_AMP_CUSTOM_GROUP_INDEX, 'original_size' => \strlen($rule), 'final_size' => null, 'element' => $element, 'origin' => 'style_attribute', 'sources' => $this->current_sources, 'priority' => $this->get_stylesheet_priority($attr_node), 'tokens' => $parsed['tokens'], 'hash' => $parsed['hash'], 'parse_time' => $parsed['parse_time'], 'shake_time' => null, 'cached' => $parsed['cached'], 'important_count' => $parsed['important_count'], 'kept_error_count' => $parsed['kept_error_count'], 'preload_font_urls' => $parsed['preload_font_urls']];
            if ($element->hasAttribute('class')) {
                $element->setAttribute('class', $element->getAttribute('class') . ' ' . $class);
            } else {
                $element->setAttribute('class', $class);
            }
        }
        $this->set_current_node(null);
    }
    /**
     * Finalize stylesheets for style[amp-custom] and style[amp-keyframes] elements.
     *
     * Concatenate all pending stylesheets, remove unused rules, and add to AMP style elements in document.
     * Combine all amp-keyframe styles and add them to the end of the body.
     *
     * @since 1.0
     * @see https://www.ampproject.org/docs/fundamentals/spec#keyframes-stylesheet
     */
    private function finalize_styles()
    {
        $preload_font_urls = [];
        $stylesheet_groups = [self::STYLE_AMP_CUSTOM_GROUP_INDEX => [
            'source_map_comment' => "\n\n/*# sourceURL=amp-custom.css */",
            'cdata_spec' => $this->style_custom_cdata_spec,
            'included_count' => 0,
            'import_front_matter' => '',
            // Extra @import statements that are prepended when fetch fails and validation error is rejected.
            'important_count' => 0,
            'kept_error_count' => 0,
            'is_excessive_size' => \false,
            'preload_font_urls' => [],
        ], self::STYLE_AMP_KEYFRAMES_GROUP_INDEX => ['source_map_comment' => "\n\n/*# sourceURL=amp-keyframes.css */", 'cdata_spec' => $this->style_keyframes_cdata_spec, 'included_count' => 0, 'import_front_matter' => '', 'important_count' => 0, 'kept_error_count' => 0, 'is_excessive_size' => \false, 'preload_font_urls' => []]];
        $imported_font_urls = [];
        // Divide pending stylesheet between custom and keyframes, and calculate size of each (before tree shaking).
        foreach ($this->pending_stylesheets as $i => $pending_stylesheet) {
            foreach ($pending_stylesheet['tokens'] as $j => $part) {
                if (\is_string($part) && 0 === \strpos($part, '@import')) {
                    $stylesheet_groups[$pending_stylesheet['group']]['import_front_matter'] .= $part;
                    // @todo Not currently relayed in stylesheet data.
                    unset($this->pending_stylesheets[$i]['tokens'][$j]);
                }
            }
            if (!empty($pending_stylesheet['imported_font_urls'])) {
                $imported_font_urls = \array_merge($imported_font_urls, $pending_stylesheet['imported_font_urls']);
            }
        }
        // Process the pending stylesheets.
        foreach (\array_keys($stylesheet_groups) as $group) {
            $stylesheet_groups[$group] = \array_merge($stylesheet_groups[$group], $this->finalize_stylesheet_group($group, $stylesheet_groups[$group]));
        }
        // If we're not working with the document element (e.g. for Customizer rendered partials) then there is nothing left to do.
        if (empty($this->args['use_document_element'])) {
            return;
        }
        // Add the font preloads.
        foreach ($stylesheet_groups as $stylesheet_group) {
            foreach ($stylesheet_group['preload_font_urls'] as $preload_font_url) {
                $this->dom->links->addPreload($preload_font_url, RequestDestination::FONT);
            }
        }
        // Add style[amp-custom] to document.
        if ($stylesheet_groups[self::STYLE_AMP_CUSTOM_GROUP_INDEX]['included_count'] > 0) {
            /*
             * On AMP-first themes when there are new/rejected validation errors present, a parsed stylesheet may include
             * @import rules. These must be moved to the beginning to be honored.
             */
            $css = $stylesheet_groups[self::STYLE_AMP_CUSTOM_GROUP_INDEX]['import_front_matter'];
            $css .= \implode('', $this->get_stylesheets());
            $css .= $stylesheet_groups[self::STYLE_AMP_CUSTOM_GROUP_INDEX]['source_map_comment'];
            // Create the style[amp-custom] element and add it to the <head>.
            $this->amp_custom_style_element = $this->dom->createElement('style');
            $this->amp_custom_style_element->setAttribute('amp-custom', '');
            $this->amp_custom_style_element->appendChild($this->dom->createTextNode($css));
            // When there are kept errors, then mark the element as being AMP-unvalidated. Note that excessive CSS
            // is not a validation error that is arisen when parsing a stylesheet (as that is emitted when finalizing
            // a stylesheet group). Otherwise, if there are !important qualifiers or the amount of CSS is greater than
            // the maximum allowed by AMP, mark the custom style as PX-verified.
            if ($stylesheet_groups[self::STYLE_AMP_CUSTOM_GROUP_INDEX]['kept_error_count'] > 0) {
                ValidationExemption::mark_node_as_amp_unvalidated($this->amp_custom_style_element);
            } elseif ($stylesheet_groups[self::STYLE_AMP_CUSTOM_GROUP_INDEX]['important_count'] > 0 || $stylesheet_groups[self::STYLE_AMP_CUSTOM_GROUP_INDEX]['is_excessive_size']) {
                ValidationExemption::mark_node_as_px_verified($this->amp_custom_style_element);
            }
            $this->dom->head->appendChild($this->amp_custom_style_element);
        }
        /*
         * Add font stylesheets from CDNs which were extracted from @import rules.
         * We can't add crossorigin=anonymous to these since such a CORS request would not be made in the non-AMP version,
         * and so if the service worker cached the opaque response on the non-AMP version then it wouldn't be usable in
         * the AMP version if it was requested with CORS.
         */
        foreach (\array_unique($imported_font_urls) as $imported_font_url) {
            $link = $this->dom->createElement('link');
            $link->setAttribute('rel', 'stylesheet');
            $link->setAttribute('href', $imported_font_url);
            $this->dom->head->appendChild($link);
        }
        // Add style[amp-keyframes] to document.
        if ($stylesheet_groups[self::STYLE_AMP_KEYFRAMES_GROUP_INDEX]['included_count'] > 0) {
            $css = $stylesheet_groups[self::STYLE_AMP_KEYFRAMES_GROUP_INDEX]['import_front_matter'];
            $css .= \implode('', \wp_list_pluck(\array_filter($this->pending_stylesheets, static function ($pending_stylesheet) {
                return $pending_stylesheet['included'] && self::STYLE_AMP_KEYFRAMES_GROUP_INDEX === $pending_stylesheet['group'];
            }), 'serialized'));
            $css .= $stylesheet_groups[self::STYLE_AMP_KEYFRAMES_GROUP_INDEX]['source_map_comment'];
            $style_element = $this->dom->createElement('style');
            $style_element->setAttribute('amp-keyframes', '');
            $style_element->appendChild($this->dom->createTextNode($css));
            $this->dom->body->appendChild($style_element);
        }
        $this->remove_admin_bar_if_css_excluded();
        $this->add_css_budget_to_admin_bar();
    }
    /**
     * Remove admin bar if its CSS was excluded.
     *
     * @since 1.2
     */
    private function remove_admin_bar_if_css_excluded()
    {
        if (!\is_admin_bar_showing()) {
            return;
        }
        $admin_bar_id = 'wpadminbar';
        $admin_bar = $this->dom->getElementById($admin_bar_id);
        if (!$admin_bar || !$admin_bar->parentNode) {
            return;
        }
        $included = \true;
        foreach ($this->pending_stylesheets as &$pending_stylesheet) {
            $is_admin_bar_css = self::STYLE_AMP_CUSTOM_GROUP_INDEX === $pending_stylesheet['group'] && 'admin-bar-css' === $pending_stylesheet['element']->getAttribute('id');
            if ($is_admin_bar_css) {
                $included = $pending_stylesheet['included'];
                break;
            }
        }
        unset($pending_stylesheet);
        if (!$included) {
            // Remove admin-bar class from body element.
            // @todo It would be nice if any style rules which refer to .admin-bar could also be removed, but this would mean retroactively going back over the CSS again and re-shaking it.
            if ($this->dom->body->hasAttribute('class')) {
                $this->dom->body->setAttribute('class', \preg_replace('/(^|\\s)admin-bar(\\s|$)/', ' ', $this->dom->body->getAttribute('class')));
            }
            // Remove admin bar element.
            $comment_text = \sprintf(
                /* translators: %s: CSS selector for admin bar element  */
                \__('Admin bar (%s) was removed to preserve AMP validity due to excessive CSS.', 'amp'),
                '#' . $admin_bar_id
            );
            $admin_bar->parentNode->replaceChild($this->dom->createComment(' ' . $comment_text . ' '), $admin_bar);
        }
    }
    /**
     * Get data to amend to the validate response.
     *
     * @return array {
     *     Validate response data.
     *
     *     @type array $stylesheets Stylesheets.
     * }
     */
    public function get_validate_response_data()
    {
        $stylesheets = [];
        foreach ($this->pending_stylesheets as $pending_stylesheet) {
            $attributes = [];
            foreach ($pending_stylesheet['element']->attributes as $attribute) {
                $attributes[$attribute->nodeName] = $attribute->nodeValue;
            }
            $pending_stylesheet['element'] = ['name' => $pending_stylesheet['element']->nodeName, 'attributes' => $attributes];
            switch ($pending_stylesheet['group']) {
                case self::STYLE_AMP_CUSTOM_GROUP_INDEX:
                    $pending_stylesheet['group'] = 'amp-custom';
                    break;
                case self::STYLE_AMP_KEYFRAMES_SPEC_NAME:
                    $pending_stylesheet['group'] = 'amp-keyframes';
                    break;
            }
            unset($pending_stylesheet['serialized']);
            $stylesheets[] = $pending_stylesheet;
        }
        return \compact('stylesheets');
    }
    /**
     * Update admin bar.
     */
    public function add_css_budget_to_admin_bar()
    {
        if (!\is_admin_bar_showing()) {
            return;
        }
        $validity_li_element = $this->dom->getElementById('wp-admin-bar-amp-validity');
        if (!$validity_li_element instanceof \DOMElement) {
            return;
        }
        /**
         * Cloned <li> element that we can modify to include stylesheet information.
         *
         * @var DOMElement $stylesheets_li_element
         */
        $stylesheets_li_element = $validity_li_element->cloneNode(\true);
        $stylesheets_li_element->setAttribute('id', 'wp-admin-bar-amp-stylesheets');
        $stylesheets_a_element = $stylesheets_li_element->getElementsByTagName('a')->item(0);
        if (!$stylesheets_a_element instanceof \DOMElement) {
            return;
        }
        $stylesheets_a_element->setAttribute('href', $stylesheets_a_element->getAttribute('href') . '#amp_stylesheets');
        while ($stylesheets_a_element->firstChild) {
            $stylesheets_a_element->removeChild($stylesheets_a_element->firstChild);
        }
        $total_size = 0;
        foreach ($this->pending_stylesheets as $pending_stylesheet) {
            if (empty($pending_stylesheet['duplicate'])) {
                $total_size += $pending_stylesheet['final_size'];
            }
        }
        $css_usage_percentage = \ceil($total_size / $this->style_custom_cdata_spec['max_bytes'] * 100);
        $menu_item_text = \__('CSS Usage', 'amp') . ': ';
        $menu_item_text .= $css_usage_percentage . '%';
        $stylesheets_a_element->appendChild($this->dom->createTextNode($menu_item_text));
        if ($css_usage_percentage > 100) {
            $icon = Icon::INVALID;
        } elseif ($css_usage_percentage >= self::CSS_BUDGET_WARNING_PERCENTAGE) {
            $icon = Icon::WARNING;
        }
        if (isset($icon)) {
            $span = $this->dom->createElement('span');
            $span->setAttribute('class', 'ab-icon amp-icon ' . $icon);
            $stylesheets_a_element->appendChild($span);
        }
        $validity_li_element->parentNode->insertBefore($stylesheets_li_element, $validity_li_element->nextSibling);
    }
    /**
     * Convert CSS selectors and remove obsolete selector hacks for IE.
     *
     * @param DeclarationBlock $ruleset Ruleset.
     * @return array Validation results.
     */
    private function ampify_ruleset_selectors($ruleset)
    {
        $selectors = [];
        $results = [];
        $has_changed_selectors = \false;
        $language = \strtolower(\get_bloginfo('language'));
        foreach ($ruleset->getSelectors() as $old_selector) {
            $selector = $old_selector->getSelector();
            // Strip out selectors that contain the disallowed prefix 'i-amphtml-'.
            if (\preg_match('/(^|\\W)i-amphtml-/', $selector)) {
                $error = ['code' => self::CSS_DISALLOWED_SELECTOR, 'type' => \Google\Web_Stories_Dependencies\AMP_Validation_Error_Taxonomy::CSS_ERROR_TYPE, 'css_selector' => $selector];
                $sanitized = $this->should_sanitize_validation_error($error);
                $results[] = \compact('error', 'sanitized');
                if ($sanitized) {
                    $has_changed_selectors = \true;
                    continue;
                }
            }
            // Automatically tree-shake IE6/IE7 hacks for selectors with `* html` and `*+html`.
            if (\preg_match('/^\\*\\s*\\+?\\s*html/', $selector)) {
                $has_changed_selectors = \true;
                continue;
            }
            // Automatically remove selectors with html[lang] that are for another language (and thus are irrelevant). This is safe because amp-bind'ed [lang] is not allowed.
            $is_other_language_root = \preg_match('/^html\\[lang(?P<starts_with>\\^)?=([\'"]?)(?P<lang>.+?)\\2\\]/', \strtolower($selector), $matches) && (empty($matches['starts_with']) ? $language !== $matches['lang'] : \substr($language, 0, \strlen($matches['lang'])) !== $matches['lang']);
            if ($is_other_language_root) {
                $has_changed_selectors = \true;
                continue;
            }
            // Remove selectors with :lang() for another language (and thus irrelevant).
            if (\preg_match('/:lang\\((?P<languages>.+?)\\)/', $selector, $matches)) {
                $has_matching_language = 0;
                $selector_languages = \array_map(static function ($selector_language) {
                    return \trim($selector_language, '"\'');
                }, \preg_split('/\\s*,\\s*/', \strtolower(\trim($matches['languages']))));
                foreach ($selector_languages as $selector_language) {
                    /*
                     * The following logic accounts for the following conditions, where all but the last is a match:
                     *
                     * N: en && fr
                     * Y: en && en
                     * Y: en && en-US
                     * Y: en-US && en
                     * N: en-US && en-UK
                     */
                    if (\substr($language, 0, \strlen($selector_language)) === $selector_language || \substr($selector_language, 0, \strlen($language)) === $language) {
                        $has_matching_language = \true;
                        break;
                    }
                }
                if (!$has_matching_language) {
                    $has_changed_selectors = \true;
                    continue;
                }
            }
            // An element (type) either starts a selector or is preceded by combinator, comma, opening paren, or closing brace.
            $before_type_selector_pattern = '(?<=^|\\(|\\s|>|\\+|~|,|})';
            $after_type_selector_pattern = '(?=$|[^a-zA-Z0-9_-])';
            // Replace focus selectors with :focus-within.
            if ($this->focus_class_name_selector_pattern) {
                $count = 0;
                $selector = \preg_replace_callback($this->focus_class_name_selector_pattern, static function ($matches) {
                    $replacement = ':focus-within';
                    if ('focus' === $matches['class'] && (!empty($matches['beginning']) || !empty($matches['combinator']) && '' === \trim($matches['combinator']))) {
                        /*
                         * If a descendant combinator precedes the focus selector, prefix the pseudo class selector
                         * with a class selector that's known to be common among themes that use the focus selector.
                         * This is to prevent the pseudo class selector being applied to the ancestor selector,
                         * which can cause unintended behavior on the page.
                         */
                        $replacement = '.menu-item-has-children' . $replacement;
                    }
                    // Ensure preceding combinator is preserved.
                    if (!empty($matches['combinator'])) {
                        $replacement = $matches['combinator'] . $replacement;
                    }
                    return $replacement;
                }, $selector, -1, $count);
                if ($count > 0) {
                    $has_changed_selectors = \true;
                }
            }
            // Replace the somewhat-meta [style] attribute selectors with attribute selector using the data attribute the original styles are copied into.
            if ($this->args['transform_important_qualifiers']) {
                $selector = \preg_replace('/(?<=\\[)style(?=([*$~]?=.*?)?])/is', self::ORIGINAL_STYLE_ATTRIBUTE_NAME, $selector, -1, $count);
                if ($count > 0) {
                    $has_changed_selectors = \true;
                }
            }
            /*
             * Loop over each selector mappings. A single HTML tag can map to multiple AMP tags (e.g. img could be amp-img or amp-anim).
             * The $selector_mappings array contains ~6 items, so rest easy your O(n^3) eyes when seeing triple nested loops!
             */
            $edited_selectors = [$selector];
            foreach ($this->selector_mappings as $html_tag => $amp_tags) {
                // Create pattern for determining whether a mapped HTML element is present in this selector.
                $html_pattern = '/' . $before_type_selector_pattern . \preg_quote($html_tag, '/') . $after_type_selector_pattern . '/i';
                /*
                 * Iterate over each selector and perform the tag mapping replacements.
                 * Note that $edited_selectors array contains only item in the normal case.
                 * Note also that the size of $edited_selectors can grow while iterating, hence disabling sniffs.
                 */
                for ($i = 0; $i < \count($edited_selectors); $i++) {
                    // phpcs:ignore Generic.CodeAnalysis.ForLoopWithTestFunctionCall.NotAllowed, Squiz.PHP.DisallowSizeFunctionsInLoops.Found
                    // Skip doing any replacement if the AMP tag is already present, as this indicates the selector was written for AMP already.
                    $amp_tag_pattern = '/' . $before_type_selector_pattern . \implode('|', $amp_tags) . $after_type_selector_pattern . '/i';
                    if (\preg_match($amp_tag_pattern, $edited_selectors[$i], $matches) && \in_array($matches[0], $amp_tags, \true)) {
                        continue;
                    }
                    // Replace the HTML tag with the first first mapped AMP tag.
                    $edited_selector = \preg_replace($html_pattern, $amp_tags[0], $edited_selectors[$i], -1, $count);
                    // If the HTML tag was not found, then short-circuit.
                    if (0 === $count) {
                        continue;
                    }
                    $edited_selectors_from_selector = [$edited_selector];
                    // Replace the HTML tag with the the remaining mapped AMP tags.
                    foreach (\array_slice($amp_tags, 1) as $amp_tag) {
                        // Note: This array contains only a couple items.
                        $edited_selectors_from_selector[] = \preg_replace($html_pattern, $amp_tag, $edited_selectors[$i]);
                    }
                    // Replace the current edited selector with all the new edited selectors resulting from the mapping replacement.
                    \array_splice($edited_selectors, $i, 1, $edited_selectors_from_selector);
                    $has_changed_selectors = \true;
                }
            }
            $selectors = \array_merge($selectors, $edited_selectors);
        }
        if ($has_changed_selectors) {
            $ruleset->setSelectors($selectors);
        }
        return $results;
    }
    /**
     * Given a list of class names, create a regular expression pattern to match them in a selector.
     *
     * @since 1.4
     * @since 2.0 In addition to the class, now includes capture groups for an immediately-preceding combinator or whether the class begins the selector.
     *
     * @param string[] $class_names Class names.
     * @return string Regular expression pattern.
     */
    private static function get_class_name_selector_pattern($class_names)
    {
        $class_pattern = \implode('|', \array_map(static function ($class_name) {
            return \preg_quote($class_name, '/');
        }, (array) $class_names));
        return "/(?:(?<beginning>^\\s*\\.)|(?<combinator>[>+~\\s]*)\\.)(?<class>{$class_pattern})(?=\$|[^a-zA-Z0-9_-])/s";
    }
    /**
     * Finalize a stylesheet group (amp-custom or amp-keyframes).
     *
     * @since 1.2
     *
     * @param int   $group        Group name (either self::STYLE_AMP_CUSTOM_GROUP_INDEX or self::STYLE_AMP_KEYFRAMES_GROUP_INDEX ).
     * @param array $group_config Group config.
     * @return array {
     *     Finalized group info.
     *
     *     @type int      $included_count    Number of included stylesheets in group.
     *     @type bool     $is_excessive_size Whether the total is greater than the max bytes allowed.
     *     @type int      $important_count   Number of !important qualifiers.
     *     @type int      $kept_error_count  Number of validation errors whose markup was kept.
     *     @type string[] $preload_font_urls Font URLs to preload.
     * }
     */
    private function finalize_stylesheet_group($group, $group_config)
    {
        $max_bytes = $group_config['cdata_spec']['max_bytes'] - \strlen($group_config['source_map_comment']);
        $included_count = 0;
        $is_excessive_size = \false;
        $concatenated_size = 0;
        $important_count = 0;
        $kept_error_count = 0;
        $preload_font_urls = [];
        $previously_seen_stylesheet_index = [];
        foreach ($this->pending_stylesheets as $pending_stylesheet_index => &$pending_stylesheet) {
            if ($group !== $pending_stylesheet['group']) {
                continue;
            }
            $start_time = \microtime(\true);
            $shaken_tokens = [];
            foreach ($pending_stylesheet['tokens'] as $token) {
                if (\is_string($token)) {
                    $shaken_tokens[] = [\true, $token];
                    continue;
                }
                list($selectors_parsed, $declaration_block) = $token;
                $used_selector_count = 0;
                $selectors = [];
                foreach ($selectors_parsed as $selector => $parsed_selector) {
                    $should_include = $this->args['skip_tree_shaking'] || (empty($parsed_selector[self::SELECTOR_EXTRACTED_CLASSES]) || $this->has_used_class_name($parsed_selector[self::SELECTOR_EXTRACTED_CLASSES])) && (empty($parsed_selector[self::SELECTOR_EXTRACTED_IDS]) || 0 === \count(\array_filter($parsed_selector[self::SELECTOR_EXTRACTED_IDS], function ($id) {
                        return !$this->dom->getElementById($id);
                    }))) && (empty($parsed_selector[self::SELECTOR_EXTRACTED_TAGS]) || $this->has_used_tag_names($parsed_selector[self::SELECTOR_EXTRACTED_TAGS])) && (empty($parsed_selector[self::SELECTOR_EXTRACTED_ATTRIBUTES]) || $this->has_used_attributes($parsed_selector[self::SELECTOR_EXTRACTED_ATTRIBUTES]));
                    $selectors[$selector] = $should_include;
                    if ($should_include) {
                        $used_selector_count++;
                    }
                }
                $shaken_tokens[] = [0 !== $used_selector_count, $selectors, $declaration_block];
            }
            // Strip empty at-rules after tree shaking.
            $stylesheet_part_count = \count($shaken_tokens);
            for ($i = 0; $i < $stylesheet_part_count; $i++) {
                // Skip anything that isn't an at-rule.
                if (!\is_string($shaken_tokens[$i][1]) || '@' !== \substr($shaken_tokens[$i][1], 0, 1)) {
                    continue;
                }
                // Delete empty at-rules.
                if ('{}' === \substr($shaken_tokens[$i][1], -2)) {
                    $shaken_tokens[$i][0] = \false;
                    continue;
                }
                // Delete at-rules that were emptied due to tree-shaking.
                if ('{' === \substr($shaken_tokens[$i][1], -1)) {
                    $open_braces = 1;
                    for ($j = $i + 1; $j < $stylesheet_part_count; $j++) {
                        if (\is_array($shaken_tokens[$j][1])) {
                            // Is declaration block.
                            if (\true === $shaken_tokens[$j][0]) {
                                // The declaration block has selectors which survived tree shaking, so the contained at-
                                // rule cannot be removed and so we must abort.
                                break;
                            } else {
                                // Continue to the next stylesheet part as this declaration block can be included in the
                                // list of parts that may be part of an at-rule that is now empty and should be removed.
                                continue;
                            }
                        }
                        $is_at_rule = '@' === \substr($shaken_tokens[$j][1], 0, 1);
                        if ($is_at_rule && '{}' === \substr($shaken_tokens[$j][1], -2)) {
                            continue;
                            // The rule opened is empty from the start.
                        }
                        if ($is_at_rule && '{' === \substr($shaken_tokens[$j][1], -1)) {
                            $open_braces++;
                        } elseif ('}' === $shaken_tokens[$j][1]) {
                            $open_braces--;
                        } else {
                            break;
                        }
                        // Splice out the parts that are empty.
                        if (0 === $open_braces) {
                            for ($k = $i; $k <= $j; $k++) {
                                $shaken_tokens[$k][0] = \false;
                            }
                            $i = $j;
                            // Jump the outer loop ahead to skip over what has been already marked as removed.
                            continue 2;
                        }
                    }
                }
            }
            $pending_stylesheet['shaken_tokens'] = $shaken_tokens;
            unset($pending_stylesheet['tokens'], $shaken_tokens);
            // @todo After this point we could unset( $pending_stylesheet['tokens'] ) since they wouldn't be used in the course of generating a page, though they would still be useful for other purposes.
            $pending_stylesheet['serialized'] = \implode('', \array_map(
                static function ($shaken_token) {
                    if (\is_array($shaken_token[1])) {
                        // Construct a declaration block.
                        $selectors = \array_keys(\array_filter($shaken_token[1]));
                        if (empty($selectors)) {
                            return '';
                        } else {
                            return \implode(',', $selectors) . '{' . \implode(';', $shaken_token[2]) . '}';
                        }
                    } else {
                        // Pass through parts other than declaration blocks.
                        return $shaken_token[1];
                    }
                },
                // Include the stylesheet parts that were not marked for exclusion during tree shaking.
                \array_filter($pending_stylesheet['shaken_tokens'], static function ($shaken_token) {
                    return \false !== $shaken_token[0];
                })
            ));
            $pending_stylesheet['included'] = null;
            // To be determined below.
            $pending_stylesheet['final_size'] = \strlen($pending_stylesheet['serialized']);
            // If this stylesheet is a duplicate of something that came before, mark the previous as not included automatically.
            if (isset($previously_seen_stylesheet_index[$pending_stylesheet['hash']])) {
                $this->pending_stylesheets[$previously_seen_stylesheet_index[$pending_stylesheet['hash']]]['included'] = \false;
                $this->pending_stylesheets[$previously_seen_stylesheet_index[$pending_stylesheet['hash']]]['duplicate'] = \true;
            }
            $previously_seen_stylesheet_index[$pending_stylesheet['hash']] = $pending_stylesheet_index;
            $pending_stylesheet['shake_time'] = \microtime(\true) - $start_time;
        }
        // End foreach pending_stylesheets.
        unset($pending_stylesheet);
        // Determine which stylesheets are included based on their priorities.
        $pending_stylesheet_indices = \array_keys($this->pending_stylesheets);
        \usort($pending_stylesheet_indices, function ($a, $b) {
            return $this->pending_stylesheets[$a]['priority'] - $this->pending_stylesheets[$b]['priority'];
        });
        foreach ($pending_stylesheet_indices as $i) {
            if ($group !== $this->pending_stylesheets[$i]['group']) {
                continue;
            }
            // Skip duplicates.
            if (\false === $this->pending_stylesheets[$i]['included']) {
                continue;
            }
            // Skip stylesheets that were completely tree-shaken and mark as included.
            if (0 === $this->pending_stylesheets[$i]['final_size']) {
                $this->pending_stylesheets[$i]['included'] = \true;
                continue;
            }
            $is_stylesheet_excessive = $concatenated_size + $this->pending_stylesheets[$i]['final_size'] > $max_bytes;
            // Report validation error if size is now too big.
            if (!$this->args['allow_excessive_css'] && $is_stylesheet_excessive) {
                $validation_error = ['code' => self::STYLESHEET_TOO_LONG, 'type' => \Google\Web_Stories_Dependencies\AMP_Validation_Error_Taxonomy::CSS_ERROR_TYPE, 'spec_name' => self::STYLE_AMP_KEYFRAMES_GROUP_INDEX === $group ? self::STYLE_AMP_KEYFRAMES_SPEC_NAME : self::STYLE_AMP_CUSTOM_SPEC_NAME];
                if (isset($this->pending_stylesheets[$i]['sources'])) {
                    $validation_error['sources'] = $this->pending_stylesheets[$i]['sources'];
                }
                $data = ['node' => $this->pending_stylesheets[$i]['element']];
                if ($this->should_sanitize_validation_error($validation_error, $data)) {
                    $this->pending_stylesheets[$i]['included'] = \false;
                    continue;
                    // Skip to the next stylesheet.
                }
            }
            if (!isset($this->pending_stylesheets[$i]['included'])) {
                $this->pending_stylesheets[$i]['included'] = \true;
                $included_count++;
                $concatenated_size += $this->pending_stylesheets[$i]['final_size'];
                $preload_font_urls = \array_merge($preload_font_urls, $this->pending_stylesheets[$i]['preload_font_urls']);
                if ($is_stylesheet_excessive) {
                    $is_excessive_size = \true;
                }
                // Note: the following two may be incorrect because the !important property or erroneous rule may have
                // actually been tree-shaken and thus is no longer in the document.
                $important_count += $this->pending_stylesheets[$i]['important_count'];
                $kept_error_count += $this->pending_stylesheets[$i]['kept_error_count'];
            }
        }
        return \compact('included_count', 'is_excessive_size', 'important_count', 'kept_error_count', 'preload_font_urls');
    }
    /**
     * Creates and inserts a meta[name="viewport"] tag if there are @viewport style rules.
     *
     * These rules aren't valid in CSS, but they might be valid in that meta tag.
     * So this adds them to the content attribute of a new meta tag.
     * These are later processed, to merge the content values into a single meta tag.
     *
     * @param DOMElement $element        An element.
     * @param array      $viewport_rules An associative array of $rule_name => $rule_value.
     */
    private function create_meta_viewport(\DOMElement $element, $viewport_rules)
    {
        if (empty($viewport_rules)) {
            return;
        }
        $viewport_meta = $this->dom->createElement('meta');
        $viewport_meta->setAttribute('name', 'viewport');
        $viewport_meta->setAttribute('content', \implode(',', \array_map(static function ($property_name) use($viewport_rules) {
            return $property_name . '=' . $viewport_rules[$property_name];
        }, \array_keys($viewport_rules))));
        // Inject a potential duplicate meta viewport element, to later be merged in AMP_Meta_Sanitizer.
        $element->parentNode->insertBefore($viewport_meta, $element);
    }
}
