<?php
/**
 * Class AMP_Core_Theme_Sanitizer.
 *
 * @package AMP
 * @since 1.0
 */

use AmpProject\Amp;
use AmpProject\AmpWP\Services;
use AmpProject\Dom\Element;
use AmpProject\Extension;
use AmpProject\Html\Attribute;
use AmpProject\Html\Tag;
use AmpProject\Html\Role;
use AmpProject\Layout;

/**
 * Class AMP_Core_Theme_Sanitizer
 *
 * Fixes up common issues in core themes and others.
 *
 * @see AMP_Validation_Error_Taxonomy::accept_core_theme_validation_errors()
 * @since 1.0
 * @internal
 */
class AMP_Core_Theme_Sanitizer extends AMP_Base_Sanitizer {

	/**
	 * Array of flags used to control sanitization.
	 *
	 * @since 1.0
	 * @var array {
	 *      @type string $stylesheet     Stylesheet slug.
	 *      @type string $template       Template slug.
	 *      @type array  $theme_features List of theme features that need to be applied. Features are method names,
	 *      @type bool   $native_img_used Whether to use native img.
	 * }
	 */
	protected $args;

	/**
	 * Array of themes that are supported.
	 *
	 * @var array
	 */
	protected static $supported_themes = [
		'twentytwentyone',
		'twentytwenty',
		'twentynineteen',
		'twentyseventeen',
		'twentysixteen',
		'twentyfifteen',
		'twentyfourteen',
		'twentythirteen',
		'twentytwelve',
		'twentyeleven',
		'twentyten',
	];

	/**
	 * Known modal roles.
	 *
	 * @var array
	 */
	protected static $modal_roles = [
		Role::NAVIGATION,
		Role::MENU,
		Role::SEARCH,
		Role::ALERT,
		Role::FIGURE,
		Role::FORM,
		Role::IMG,
		Role::TOOLBAR,
		Role::TOOLTIP,
	];

	/**
	 * Retrieve the config for features needed by a theme.
	 *
	 * @since 1.0
	 * @since 1.5.0 Converted `theme_features` variable into `get_theme_config` function.
	 *
	 * @param string $theme_slug Theme slug.
	 * @param array  $args       Sanitizer args.
	 * @return array|null Array comprising of the theme config if its slug is found, null if it is not.
	 */
	protected static function get_theme_features_config( $theme_slug, $args = [] ) {
		switch ( $theme_slug ) {
			case 'twentytwentyone':
				$config = [
					'dequeue_scripts'                      => [
						'twenty-twenty-one-responsive-embeds-script',
						'twenty-twenty-one-primary-navigation-script',
					],
					'remove_actions'                       => [
						'wp_print_footer_scripts' => [
							'twenty_twenty_one_skip_link_focus_fix', // Unnecessary since part of the AMP runtime.
						],
						'wp_footer'               => [
							'twentytwentyone_add_ie_class',
							'twenty_twenty_one_supports_js', // AMP is essentially no-js, with any interactivity added explicitly via amp-bind.
							[
								'Twenty_Twenty_One_Dark_Mode',
								'the_switch',
								10,
							],
						],
					],
					'amend_twentytwentyone_styles'         => [],
					'amend_twentytwentyone_sub_menu_toggles' => [],
					'add_twentytwentyone_mobile_modal'     => [],
					'add_twentytwentyone_sub_menu_fix'     => [],
					'add_twentytwentyone_dark_mode_toggle' => [],
					'amend_twentytwentyone_dark_mode_styles' => [],
				];

				return $config;

			// Twenty Twenty.
			case 'twentytwenty':
				$config = [
					'prevent_sanitize_in_customizer_preview' => [
						'//style[ @id = "twentytwenty-style-inline-css" ]',
					],
					'dequeue_scripts'                  => [
						'twentytwenty-js',
					],
					'remove_actions'                   => [
						'wp_head'                 => [
							'twentytwenty_no_js_class', // AMP is essentially no-js, with any interactivity added explicitly via amp-bind.
						],
						'wp_print_footer_scripts' => [
							'twentytwenty_skip_link_focus_fix', // See <https://github.com/WordPress/twentynineteen/pull/47>.
						],
					],
					'add_twentytwenty_modals'          => [],
					'add_twentytwenty_toggles'         => [],
					'add_nav_menu_styles'              => [],
					'add_twentytwenty_masthead_styles' => $args,
					'add_img_display_block_fix'        => $args,
					'add_twentytwenty_custom_logo_fix' => $args,
					'add_twentytwenty_current_page_awareness' => [],
					'show_twentytwenty_desktop_expanded_menu' => [],
				];

				$theme = wp_get_theme( 'twentytwenty' );

				if ( $theme->exists() && version_compare( $theme->get( 'Version' ), '1.0.0', '<=' ) ) {
					$config['add_smooth_scrolling'] = [
						'//a[ starts-with( @href, "#" ) and not( @href = "#" )and not( @href = "#0" ) and not( contains( @class, "do-not-scroll" ) ) and not( contains( @class, "skip-link" ) ) ]',
					];
				}

				return $config;

			// Twenty Nineteen.
			case 'twentynineteen':
				return [
					'dequeue_scripts'              => [
						'twentynineteen-skip-link-focus-fix', // This is part of AMP. See <https://github.com/ampproject/amphtml/issues/18671>.
						'twentynineteen-priority-menu',
						'twentynineteen-touch-navigation', // @todo There could be an AMP implementation of this, similar to what is implemented on ampproject.org.
					],
					'remove_actions'               => [
						'wp_print_footer_scripts' => [
							'twentynineteen_skip_link_focus_fix', // See <https://github.com/WordPress/twentynineteen/pull/47>.
						],
						'wp_nav_menu'             => [
							'twentynineteen_add_ellipses_to_nav',
						],
					],
					'adjust_twentynineteen_images' => [],
					'update_twentynineteen_mobile_main_menu' => [],
					'add_nav_menu_styles'          => [],
				];

			// Twenty Seventeen.
			case 'twentyseventeen':
				return [
					'prevent_sanitize_in_customizer_preview' => [
						'//link[ @id = "twentyseventeen-colors-dark-css" ]',
					],
					// @todo Try to implement belowEntryMetaClass().
					'dequeue_scripts'                     => [
						'twentyseventeen-html5', // Only relevant for IE<9.
						'twentyseventeen-global', // There are somethings not yet implemented in AMP. See todos below.
						'jquery-scrollto', // Implemented via add_smooth_scrolling().
						'twentyseventeen-navigation', // Handled by add_nav_menu_styles, add_nav_menu_toggle, add_nav_sub_menu_buttons.
						'twentyseventeen-skip-link-focus-fix', // Unnecessary since part of the AMP runtime.
					],
					'remove_actions'                      => [
						'wp_head' => [
							'twentyseventeen_javascript_detection', // AMP is essentially no-js, with any interactivity added explicitly via amp-bind.
						],
					],
					'force_fixed_background_support'      => [],
					'add_twentyseventeen_masthead_styles' => [],
					'add_twentyseventeen_sticky_nav_menu' => [],
					'add_has_header_video_body_class'     => [],
					'add_nav_menu_styles'                 => [
						'sub_menu_button_toggle_class' => 'toggled-on',
						'no_js_submenu_visible'        => true,
					],
					'add_smooth_scrolling'                => [
						'//header[@id = "masthead"]//a[ contains( @class, "menu-scroll-down" ) ]',
					],
					'set_twentyseventeen_quotes_icon'     => [],
					'add_twentyseventeen_attachment_image_attributes' => $args,
				];

			// Twenty Sixteen.
			case 'twentysixteen':
				return [
					// @todo Figure out an AMP solution for onResizeARIA().
					// @todo Try to implement belowEntryMetaClass().
					'dequeue_scripts'     => [
						'twentysixteen-script',
						'twentysixteen-html5', // Only relevant for IE<9.
						'twentysixteen-keyboard-image-navigation', // AMP does not yet allow for listening to keydown events.
						'twentysixteen-skip-link-focus-fix', // Unnecessary since part of the AMP runtime.
					],
					'remove_actions'      => [
						'wp_head' => [
							'twentysixteen_javascript_detection', // AMP is essentially no-js, with any interactivity added explicitly via amp-bind.
						],
					],
					'add_nav_menu_styles' => [
						'sub_menu_button_toggle_class' => 'toggled-on',
						'no_js_submenu_visible'        => true,
					],
				];

			// Twenty Fifteen.
			case 'twentyfifteen':
				return [
					// @todo Figure out an AMP solution for onResizeARIA().
					'dequeue_scripts'     => [
						'twentyfifteen-script',
						'twentyfifteen-keyboard-image-navigation', // AMP does not yet allow for listening to keydown events.
						'twentyfifteen-skip-link-focus-fix', // Unnecessary since part of the AMP runtime.
					],
					'remove_actions'      => [
						'wp_head' => [
							'twentyfifteen_javascript_detection', // AMP is essentially no-js, with any interactivity added explicitly via amp-bind.
						],
					],
					'add_nav_menu_styles' => [
						'sub_menu_button_toggle_class' => 'toggle-on',
						'no_js_submenu_visible'        => true,
					],
				];

			// Twenty Fourteen.
			case 'twentyfourteen':
				return [
					// @todo Figure out an AMP solution for onResizeARIA().
					'dequeue_scripts'                    => [
						'twentyfourteen-script',
						'twentyfourteen-keyboard-image-navigation', // AMP does not yet allow for listening to keydown events.
						'jquery-masonry', // Masonry style layout is not supported in AMP.
						'twentyfourteen-slider',
					],
					'add_nav_menu_styles'                => [],
					'add_twentyfourteen_masthead_styles' => [],
					'add_twentyfourteen_slider_carousel' => [],
					'add_twentyfourteen_search'          => [],
				];

			// Twenty Thirteen.
			case 'twentythirteen':
				return [
					'dequeue_scripts'          => [
						'jquery-masonry', // Masonry style layout is not supported in AMP.
						'twentythirteen-script',
					],
					'add_nav_menu_toggle'      => [],
					'add_nav_sub_menu_buttons' => [],
					'add_nav_menu_styles'      => [],
				];

			// Twenty Twelve.
			case 'twentytwelve':
				return [
					'dequeue_scripts'     => [
						'twentytwelve-navigation',
					],
					'add_nav_menu_styles' => [],
				];

			// Twenty Eleven.
			case 'twentyeleven':
				return [
					'prevent_sanitize_in_customizer_preview' => [
						'//style[ @id = "twentyeleven-header-css" ]',
						'//link[ @id = "dark-css" ]',
					],
				];

			// Twenty Ten.
			case 'twentyten':
				return [];

			default:
				return null;
		}
	}

	/**
	 * Get list of supported core themes.
	 *
	 * @since 1.0
	 *
	 * @return string[] Slugs for supported themes.
	 */
	public static function get_supported_themes() {
		return self::$supported_themes;
	}

	/**
	 * Get the acceptable validation errors.
	 *
	 * @since 1.0
	 * @deprecated Now unused because viewport CSS at-rules are extracted from stylesheets into meta[name=viewport] tags.
	 *
	 * @return array Acceptable errors.
	 */
	public static function get_acceptable_errors() {
		_deprecated_function( __METHOD__, '1.5' );
		return [];
	}

	/**
	 * Adds extra theme support arguments on the fly.
	 *
	 * This method is neither a buffering hook nor a sanitization callback and is called manually by
	 * {@see AMP_Theme_Support}. Typically themes will add theme support directly and don't need such
	 * a method. In this case, it is a workaround for adding theme support on behalf of external themes.
	 *
	 * @since 1.1
	 */
	public static function extend_theme_support() {
		$template = get_template();
		if ( ! in_array( $template, self::get_supported_themes(), true ) ) {
			return;
		}

		$args    = self::get_theme_support_args( $template );
		$support = AMP_Theme_Support::get_theme_support_args();
		if ( ! is_array( $support ) ) {
			$support = [];
		}

		add_theme_support( AMP_Theme_Support::SLUG, array_merge( $support, $args ) );
	}

	/**
	 * Returns extra arguments to pass to `add_theme_support()`.
	 *
	 * @since 1.1
	 *
	 * @param string $theme Theme slug.
	 * @return array Arguments to merge with existing theme support arguments.
	 */
	protected static function get_theme_support_args( $theme ) {
		// phpcs:disable WordPress.WP.I18n.TextDomainMismatch
		switch ( $theme ) {
			case 'twentytwelve':
				return [
					'nav_menu_toggle' => [
						'nav_container_xpath'        => '//nav[ @id = "site-navigation" ]//ul',
						'nav_container_toggle_class' => 'toggled-on',
						'menu_button_xpath'          => '//nav[ @id = "site-navigation" ]//button[ contains( @class, "menu-toggle" ) ]',
						'menu_button_toggle_class'   => 'toggled-on',
					],
				];
			case 'twentythirteen':
				return [
					'nav_menu_toggle'   => [
						'nav_container_id'           => 'site-navigation',
						'nav_container_toggle_class' => 'toggled-on',
						'menu_button_xpath'          => '//nav[ @id = "site-navigation" ]//button[ contains( @class, "menu-toggle" ) ]',
					],
					'nav_menu_dropdown' => [
						'sub_menu_button_class'        => 'dropdown-toggle',
						'sub_menu_button_toggle_class' => 'toggle-on',
						'expand_text'                  => __( 'expand child menu', 'amp' ),
						'collapse_text'                => __( 'collapse child menu', 'amp' ),
					],
					'nav_menu_styles'   => [],
				];
			case 'twentyfourteen':
				return [
					'nav_menu_toggle' => [
						'nav_container_id'           => 'primary-navigation',
						'nav_container_toggle_class' => 'toggled-on',
						'menu_button_xpath'          => '//header[ @id = "masthead" ]//button[ contains( @class, "menu-toggle" ) ]',
						'menu_button_toggle_class'   => '',
					],
				];

			case 'twentyfifteen':
				return [
					'nav_menu_toggle'   => [
						'nav_container_id'           => 'secondary',
						'nav_container_toggle_class' => 'toggled-on',
						'menu_button_xpath'          => '//header[ @id = "masthead" ]//button[ contains( @class, "secondary-toggle" ) ]',
						'menu_button_toggle_class'   => 'toggled-on',
					],
					'nav_menu_dropdown' => [
						'sub_menu_button_class'        => 'dropdown-toggle',
						'sub_menu_button_toggle_class' => 'toggle-on',
						'expand_text '                 => __( 'expand child menu', 'twentyfifteen' ),
						'collapse_text'                => __( 'collapse child menu', 'twentyfifteen' ),
					],
				];

			case 'twentysixteen':
				return [
					'nav_menu_toggle'   => [
						'nav_container_id'           => 'site-header-menu',
						'nav_container_toggle_class' => 'toggled-on',
						'menu_button_xpath'          => '//header[@id = "masthead"]//button[ @id = "menu-toggle" ]',
						'menu_button_toggle_class'   => 'toggled-on',
					],
					'nav_menu_dropdown' => [
						'sub_menu_button_class'        => 'dropdown-toggle',
						'sub_menu_button_toggle_class' => 'toggled-on',
						'expand_text '                 => __( 'expand child menu', 'twentysixteen' ),
						'collapse_text'                => __( 'collapse child menu', 'twentysixteen' ),
					],
				];

			case 'twentyseventeen':
				$config = [
					'nav_menu_toggle'   => [
						'nav_container_id'           => 'site-navigation',
						'nav_container_toggle_class' => 'toggled-on',
						'menu_button_xpath'          => '//nav[@id = "site-navigation"]//button[ contains( @class, "menu-toggle" ) ]',
						'menu_button_toggle_class'   => 'toggled-on',
					],
					'nav_menu_dropdown' => [
						'sub_menu_button_class'        => 'dropdown-toggle',
						'sub_menu_button_toggle_class' => 'toggled-on',
						'expand_text '                 => __( 'expand child menu', 'twentyseventeen' ),
						'collapse_text'                => __( 'collapse child menu', 'twentyseventeen' ),
					],
				];

				if ( function_exists( 'twentyseventeen_get_svg' ) ) {
					$config['nav_menu_dropdown']['icon'] = twentyseventeen_get_svg(
						[
							'icon'     => 'angle-down',
							'fallback' => true,
						]
					);
				}

				return $config;
		}
		// phpcs:enable WordPress.WP.I18n.TextDomainMismatch

		return [];
	}

	/**
	 * Get theme config.
	 *
	 * @since 1.0
	 * @codeCoverageIgnore
	 * @deprecated 1.1
	 *
	 * @param string $theme Theme slug.
	 * @return array Class names.
	 */
	protected static function get_theme_config( $theme ) {
		_deprecated_function( __METHOD__, '1.1' );

		$args = self::get_theme_support_args( $theme );

		// This returns arguments in a backward-compatible way.
		return array_merge( $args['nav_menu_toggle'], $args['nav_menu_dropdown'] );
	}

	/**
	 * Find theme features for core theme.
	 *
	 * @since 1.0
	 *
	 * @param array $args   Args.
	 * @param bool  $static Static. that is, whether should run during output buffering.
	 * @return array Theme features.
	 */
	protected static function get_theme_features( $args, $static = false ) {
		$theme_features   = [];
		$theme_candidates = wp_array_slice_assoc( $args, [ 'stylesheet', 'template' ] );
		foreach ( $theme_candidates as $theme_candidate ) {
			if ( in_array( $theme_candidate, self::$supported_themes, true ) ) {
				$theme_features = self::get_theme_features_config( $theme_candidate, $args );
				break;
			}
		}

		// Allow specific theme features to be requested even if the theme is not in core.
		if ( isset( $args['theme_features'] ) ) {
			$theme_features = array_merge( $args['theme_features'], $theme_features );
		}

		$final_theme_features = [];
		foreach ( $theme_features as $theme_feature => $feature_args ) {
			if ( ! method_exists( __CLASS__, $theme_feature ) ) {
				continue;
			}
			try {
				$reflection = new ReflectionMethod( __CLASS__, $theme_feature );
				if ( $reflection->isStatic() === $static ) {
					$final_theme_features[ $theme_feature ] = $feature_args;
				}
			} catch ( Exception $e ) {
				unset( $e );
			}
		}
		return $final_theme_features;
	}

	/**
	 * Add filters to manipulate output during output buffering before the DOM is constructed.
	 *
	 * @since 1.0
	 *
	 * @param array $args Args.
	 */
	public static function add_buffering_hooks( $args = [] ) {
		$theme_features = self::get_theme_features( $args, true );
		foreach ( $theme_features as $theme_feature => $feature_args ) {
			if ( method_exists( __CLASS__, $theme_feature ) ) {
				call_user_func( [ __CLASS__, $theme_feature ], $feature_args );
			}
		}
	}

	/**
	 * Add filter to output the quote icons in front of the article content.
	 *
	 * This is only used in Twenty Seventeen.
	 *
	 * @since 1.0
	 * @link https://github.com/WordPress/wordpress-develop/blob/f4580c122b7d0d2d66d22f806c6fe6e11023c6f0/src/wp-content/themes/twentyseventeen/assets/js/global.js#L105-L108
	 */
	public static function set_twentyseventeen_quotes_icon() {
		add_filter(
			'the_content',
			static function ( $content ) {

				// Why isn't Twenty Seventeen doing this to begin with? Why is it using JS to add the quote icon?
				if ( function_exists( 'twentyseventeen_get_svg' ) && 'quote' === get_post_format() ) {
					$icon    = twentyseventeen_get_svg( [ 'icon' => 'quote-right' ] );
					$content = preg_replace( '#(<blockquote.*?>)#s', '$1' . $icon, $content );
				}

				return $content;
			}
		);
	}

	/**
	 * Add filter to adjust the attachment image attributes to ensure attachment pages have a consistent <amp-img> rendering.
	 *
	 * This is only used in Twenty Seventeen.
	 *
	 * @since 1.0
	 * @link https://github.com/WordPress/wordpress-develop/blob/ddc8f803c6e99118998191fd2ea24124feb53659/src/wp-content/themes/twentyseventeen/functions.php#L545:L554
	 *
	 * @param array $args Args.
	 */
	public static function add_twentyseventeen_attachment_image_attributes( $args = [] ) {
		if ( ! empty( $args['native_img_used'] ) ) {
			return;
		}

		/*
		 * The max-height of the `.custom-logo-link img` is defined as being 80px, unless
		 * there is header media in which case it is 200px. Issues related to vertically-squashed
		 * images can be avoided if we just make sure that the image has this height to begin with.
		 */
		add_filter(
			'get_custom_logo',
			static function ( $html ) {
				$src = wp_get_attachment_image_src( get_theme_mod( 'custom_logo' ), 'full' );
				if ( ! $src ) {
					return $html;
				}

				$img_width  = (int) $src[1];
				$img_height = (int) $src[2];

				// If height is zero, bail.
				if ( 0 === $img_height || 0 === $img_width ) {
					return $html;
				}

				if ( 'blank' === get_header_textcolor() && has_custom_header() ) {
					$height = 200;
				} else {
					$height = 80;
				}
				$width = $height * ( $img_width / $img_height ); // Note that float values are allowed.

				$html = preg_replace( '/(?<=width=")\d+(?=")/', $width, $html );
				$html = preg_replace( '/(?<=height=")\d+(?=")/', $height, $html );
				return $html;
			}
		);
	}

	/**
	 * Fix up core themes to do things in the AMP way.
	 *
	 * @since 1.0
	 */
	public function sanitize() {
		$theme_features = self::get_theme_features( $this->args, false );
		foreach ( $theme_features as $theme_feature => $feature_args ) {
			if ( method_exists( $this, $theme_feature ) ) {
				$this->$theme_feature( $feature_args );
			}
		}
	}

	/**
	 * Adds the data-ampdevmode attribute to the set of specified elements to prevent further sanitization. This is
	 * necessary as certain features in the Customizer require these elements to be present in their unaltered state.
	 *
	 * @param array $xpaths List of XPaths.
	 */
	public function prevent_sanitize_in_customizer_preview( $xpaths = [] ) {
		if ( ! is_customize_preview() ) {
			return;
		}

		// We can't use the `amp_dev_mode_element_xpaths` filter here as AMP_Dev_Mode_Sanitizer has already been
		// executed.
		foreach ( $xpaths as $xpath ) {
			foreach ( $this->dom->xpath->query( $xpath ) as $node ) {
				if ( $node instanceof DOMElement ) {
					$node->setAttribute( AMP_Rule_Spec::DEV_MODE_ATTRIBUTE, '' );
				}
			}
		}
	}

	/**
	 * Dequeue scripts.
	 *
	 * @since 1.0
	 *
	 * @param string[] $handles Handles, where each item value is the script handle.
	 */
	public static function dequeue_scripts( $handles = [] ) {
		add_action(
			'wp_enqueue_scripts',
			static function () use ( $handles ) {
				foreach ( $handles as $handle ) {
					wp_dequeue_script( $handle );
				}
			},
			PHP_INT_MAX
		);
	}

	/**
	 * Remove actions.
	 *
	 * @since 1.0
	 *
	 * @param array $actions Actions, with action name as key and value being callback.
	 */
	public static function remove_actions( $actions = [] ) {
		global $wp_filter;

		foreach ( $actions as $action => $callbacks ) {
			foreach ( $callbacks as $callback ) {
				$priority = has_action( $action, $callback );
				if ( false !== $priority ) {
					remove_action( $action, $callback, $priority );
					continue;
				}

				if ( ! is_array( $callback ) || 3 !== count( $callback ) ) {
					continue;
				}

				list( $class, $method, $priority ) = $callback;

				if ( isset( $wp_filter[ $action ]->callbacks[ $priority ] ) ) {
					foreach ( $wp_filter[ $action ]->callbacks[ $priority ] as $added_callback ) {
						if (
							is_array( $added_callback['function'] )
							&&
							isset( $added_callback['function'][0], $added_callback['function'][1] )
							&&
							is_object( $added_callback['function'][0] )
							&&
							is_string( $added_callback['function'][1] )
							&&
							$method === $added_callback['function'][1]
							&&
							get_class( $added_callback['function'][0] ) === $class
						) {
							remove_action( $action, $added_callback['function'] );
							return;
						}
					}
				}
			}
		}
	}

	/**
	 * Add smooth scrolling from link to target element.
	 *
	 * @since 1.0
	 *
	 * @param string[] $link_xpaths XPath queries to the links that should smooth scroll.
	 */
	public function add_smooth_scrolling( $link_xpaths ) {
		foreach ( $link_xpaths as $link_xpath ) {
			foreach ( $this->dom->xpath->query( $link_xpath ) as $link ) {
				if ( $link instanceof DOMElement && preg_match( '/#(.+)/', $link->getAttribute( Attribute::HREF ), $matches ) ) {
					$link->setAttribute( Attribute::ON, sprintf( 'tap:%s.scrollTo(duration=600)', $matches[1] ) );

					// Prevent browser from jumping immediately to the link target.
					$link->removeAttribute( Attribute::HREF );
					$link->setAttribute( Attribute::TABINDEX, '0' );
					$link->setAttribute( Attribute::ROLE, Role::BUTTON );
				}
			}
		}
	}

	/**
	 * Force SVG support, replacing no-svg class name with svg class name.
	 *
	 * @since 1.0
	 *
	 * @link https://github.com/WordPress/wordpress-develop/blob/1af1f65a21a1a697fb5f33027497f9e5ae638453/src/wp-content/themes/twentyseventeen/assets/js/global.js#L211-L213
	 * @link https://caniuse.com/#feat=svg
	 */
	public function force_svg_support() {
		$class = $this->dom->documentElement->getAttribute( 'class' );

		if ( $class ) {
			$count = 0;
			$class = preg_replace(
				'/(^|\s)no-svg(\s|$)/',
				' svg ',
				$class,
				-1,
				$count
			);

			if ( $count > 0 ) {
				$this->dom->documentElement->setAttribute( 'class', $class );
			}
		}
	}

	/**
	 * Force support for fixed background-attachment.
	 *
	 * @since 1.0
	 *
	 * @link https://github.com/WordPress/wordpress-develop/blob/1af1f65a21a1a697fb5f33027497f9e5ae638453/src/wp-content/themes/twentyseventeen/assets/js/global.js#L215-L217
	 * @link https://caniuse.com/#feat=background-attachment
	 */
	public function force_fixed_background_support() {
		$this->dom->documentElement->setAttribute(
			'class',
			$this->dom->documentElement->getAttribute( 'class' ) . ' background-fixed'
		);
	}

	/**
	 * Add body class when there is a header video.
	 *
	 * @since 1.0
	 * @link https://github.com/WordPress/wordpress-develop/blob/a26c24226c6b131a0ed22c722a836c100d3ba254/src/wp-content/themes/twentyseventeen/assets/js/global.js#L244-L247
	 *
	 * @param array $args Args.
	 */
	public static function add_has_header_video_body_class( $args = [] ) {
		$args = array_merge(
			[
				'class_name' => 'has-header-video',
			],
			$args
		);

		add_filter(
			'body_class',
			static function ( $body_classes ) use ( $args ) {
				if ( has_header_video() ) {
					$body_classes[] = $args['class_name'];
				}
				return $body_classes;
			}
		);
	}

	/**
	 * Get the (common) navigation outer height.
	 *
	 * @todo If the nav menu has many items and it spans multiple rows, this will be too small.
	 * @link https://github.com/WordPress/wordpress-develop/blob/fd5ba80c5c3d9cf62348567073945e246285fbca/src/wp-content/themes/twentyseventeen/assets/js/global.js#L50
	 *
	 * @return int Navigation outer height.
	 */
	protected static function get_twentyseventeen_navigation_outer_height() {
		return 72;
	}

	/**
	 * Add required styles for featured image header in Twenty Twenty.
	 *
	 * @param array $args Args.
	 */
	public static function add_twentytwenty_masthead_styles( $args = [] ) {
		if ( ! empty( $args['native_img_used'] ) ) {
			return;
		}

		// @todo This was introduced in <https://github.com/ampproject/amp-wp/commit/e1c7462> but it doesn't seem to have any effect.
		add_action(
			'wp_enqueue_scripts',
			static function () {
				ob_start();
				?>
				<style>
				.featured-media amp-img {
					position: static;
				}
				</style>
				<?php
				$styles = str_replace( [ '<style>', '</style>' ], '', ob_get_clean() );
				wp_add_inline_style( get_template() . '-style', $styles );
			},
			11
		);
	}

	/**
	 * Fix display of Custom Logo in Twenty Twenty.
	 *
	 * This is required because width:auto on the site-logo amp-img does not preserve the proportional width in the same
	 * way as the same styles applied to an img.
	 *
	 * @since 1.5
	 * @link https://github.com/ampproject/amp-wp/issues/4418
	 * @link https://codepen.io/westonruter/pen/rNVqadv
	 *
	 * @param array $args Args.
	 */
	public static function add_twentytwenty_custom_logo_fix( $args = [] ) {
		if ( ! empty( $args['native_img_used'] ) ) {
			return;
		}

		$method = __METHOD__;
		add_filter(
			'get_custom_logo',
			static function ( $html ) use ( $method ) {
				// Pattern sourced from AMP_Base_Embed_Handler::match_element_attributes().
				$pattern = sprintf(
					'/<img%s/',
					implode(
						'',
						array_map(
							function ( $attr_name ) {
								return sprintf( '(?=[^>]*?%1$s="(?P<%1$s>\d+)")?', preg_quote( $attr_name, '/' ) );
							},
							[ 'width', 'height' ]
						)
					)
				);

				preg_match( $pattern, $html, $matches );

				if ( isset( $matches['width'], $matches['height'] ) ) {
					$width  = (int) $matches['width'];
					$height = (int) $matches['height'];

					// If height is zero, bail.
					if ( 0 === $height || 0 === $width ) {
						return $html;
					}

					$desktop_height = 9; // in rem; see <https://github.com/WordPress/wordpress-develop/blob/ad8d01a7e9e13144d1676b8e6d70c3e81ef703af/src/wp-content/themes/twentytwenty/style.css#L4887>.
					$mobile_height  = 6; // in rem; see <https://github.com/WordPress/wordpress-develop/blob/ad8d01a7e9e13144d1676b8e6d70c3e81ef703af/src/wp-content/themes/twentytwenty/style.css#L1424>.

					$desktop_width = $desktop_height * ( $width / $height );
					$mobile_width  = $mobile_height * ( $width / $height );

					$html .= sprintf(
						'<style data-src="%s">.site-logo amp-img { width: %frem; } @media (min-width: 700px) { .site-logo amp-img { width: %frem; } }</style>',
						esc_attr( $method ),
						$mobile_width,
						$desktop_width
					);

				}
				return $html;
			},
			PHP_INT_MAX
		);
	}

	/**
	 * Add style rule with a selector of higher specificity than just `img` to make `amp-img` have `display:block` rather than `display:inline-block`.
	 *
	 * This is needed to override the AMP core stylesheet which has a more specific selector `.i-amphtml-layout-intrinsic` which
	 * is given a `display: inline-block`; this display value prevents margins from collapsing with surrounding block elements,
	 * resulting in larger margins in AMP than expected.
	 *
	 * @since 1.5
	 * @link https://github.com/ampproject/amp-wp/issues/4419
	 *
	 * @param array $args Args.
	 */
	public static function add_img_display_block_fix( $args = [] ) {
		if ( ! empty( $args['native_img_used'] ) ) {
			return;
		}

		$method = __METHOD__;
		// Note that wp_add_inline_style() is not used because this stylesheet needs to be added _before_ style.css so
		// that any subsequent style rules for images will continue to override.
		add_action(
			'wp_print_styles',
			static function () use ( $method ) {
				printf(
					'<style data-src="%s">%s</style>',
					esc_attr( $method ),
					// The selector is targeting an attribute that can never appear. It is purely present to increase specificity.
					'amp-img:not([_]) { display: block }'
				);
			}
		);
	}

	/**
	 * Add required styles for Twenty Seventeen header.
	 *
	 * This is required since JS is not applying the required styles at runtime.
	 *
	 * @since 1.0
	 */
	public static function add_twentyseventeen_masthead_styles() {
		add_action(
			'wp_enqueue_scripts',
			static function () {
				$is_front_page_layout = ( is_front_page() && 'posts' !== get_option( 'show_on_front' ) ) || ( is_home() && is_front_page() );
				ob_start();
				?>
				<style>
				.navigation-top.site-navigation-fixed {
					display: none;
				}

				/* This is needed by add_smooth_scrolling because it removes the [href] attribute. */
				.menu-scroll-down {
					cursor: pointer;
				}

				<?php if ( $is_front_page_layout && ! has_custom_header() ) : ?>
					/* https://github.com/WordPress/wordpress-develop/blob/fd5ba80c5c3d9cf62348567073945e246285fbca/src/wp-content/themes/twentyseventeen/assets/js/global.js#L92-L94 */
					.site-branding {
						margin-bottom: <?php echo (int) AMP_Core_Theme_Sanitizer::get_twentyseventeen_navigation_outer_height(); ?>px;
					}
				<?php endif; ?>

				@media screen and (min-width: 48em) {
					/* Note that adjustHeaderHeight() is irrelevant with this change */
					<?php if ( ! $is_front_page_layout ) : ?>
						.navigation-top {
							position: static;
						}
					<?php endif; ?>

					/* Initial styles that amp-animations for navigationTopShow and navigationTopHide will override */
					.navigation-top.site-navigation-fixed {
						opacity: 0;
						transform: translateY( -<?php echo (int) AMP_Core_Theme_Sanitizer::get_twentyseventeen_navigation_outer_height(); ?>px );
						display: block;
					}
				}
				</style>
				<?php
				$styles = str_replace( [ '<style>', '</style>' ], '', ob_get_clean() );
				wp_add_inline_style( get_template() . '-style', $styles );
			},
			11
		);
	}

	/**
	 * Add sticky nav menu to Twenty Seventeen.
	 *
	 * This is implemented by cloning the navigation-top element, giving it a fixed position outside of the viewport,
	 * and then showing it at the top of the window as soon as the original nav begins to get scrolled out of view.
	 * In order to improve accessibility, the cloned nav gets aria-hidden=true and all of the links get tabindex=-1
	 * to prevent the keyboard from focusing on elements off the screen; it is not necessary to focus on the elements
	 * in the fixed nav menu because as soon as the original nav menu is focused then the window is scrolled to the
	 * top anyway.
	 *
	 * @since 1.0
	 */
	public function add_twentyseventeen_sticky_nav_menu() {
		/**
		 * Top navigation element.
		 *
		 * @var DOMElement|null $navigation_top
		 */
		$navigation_top = $this->dom->xpath->query( '//header[ @id = "masthead" ]//div[ contains( @class, "navigation-top" ) ]' )->item( 0 );
		if ( ! $navigation_top ) {
			return;
		}

		/**
		 * Cloned top navigation element to put in fixed position.
		 *
		 * @var DOMElement $navigation_top_fixed
		 */
		$navigation_top_fixed = $navigation_top->cloneNode( true );
		$navigation_top_fixed->setAttribute( 'class', $navigation_top_fixed->getAttribute( 'class' ) . ' site-navigation-fixed' );

		$navigation_top_fixed->setAttribute( 'aria-hidden', 'true' );
		foreach ( $navigation_top_fixed->getElementsByTagName( 'a' ) as $link ) {
			/**
			 * Navigation link to add a tab index to.
			 *
			 * @var DOMElement $link
			 */
			$link->setAttribute( Attribute::TABINDEX, '-1' );
		}

		$navigation_top->parentNode->insertBefore( $navigation_top_fixed, $navigation_top->nextSibling );
		foreach ( $this->dom->xpath->query( './/*[ @id ]', $navigation_top_fixed ) as $element ) {
			/**
			 * Navigation element in the fixed navigation bar.
			 *
			 * @var DOMElement $element
			 */
			$element->setAttribute( 'id', $element->getAttribute( 'id' ) . '-fixed' );
		}

		$attributes = [
			'layout'              => 'nodisplay',
			'intersection-ratios' => 1,
			'on'                  => implode(
				';',
				[
					'exit:navigationTopShow.start',
					'enter:navigationTopHide.start',
				]
			),
		];
		if ( is_admin_bar_showing() ) {
			$attributes['viewport-margins'] = '32px 0';
		}
		$position_observer = AMP_DOM_Utils::create_node( $this->dom, 'amp-position-observer', $attributes );
		$navigation_top->appendChild( $position_observer );

		$animations = [
			'navigationTopShow' => [
				'duration'   => 0,
				'fill'       => 'both',
				'animations' => [
					'selector'  => '.navigation-top.site-navigation-fixed',
					'media'     => '(min-width: 48em)',
					'keyframes' => [
						'opacity'   => 1.0,
						'transform' => 'translateY( 0 )',
					],
				],
			],
			'navigationTopHide' => [
				'duration'   => 0,
				'fill'       => 'both',
				'animations' => [
					'selector'  => '.navigation-top.site-navigation-fixed',
					'media'     => '(min-width: 48em)',
					'keyframes' => [
						'opacity'   => 0.0,
						'transform' => sprintf( 'translateY( -%dpx )', self::get_twentyseventeen_navigation_outer_height() ),
					],
				],
			],
		];

		foreach ( $animations as $animation_id => $animation ) {
			$amp_animation   = AMP_DOM_Utils::create_node(
				$this->dom,
				'amp-animation',
				[
					'id'     => $animation_id,
					'layout' => 'nodisplay',
				]
			);
			$position_script = $this->dom->createElement( 'script' );
			$position_script->setAttribute( 'type', 'application/json' );
			$position_script->appendChild( $this->dom->createTextNode( wp_json_encode( $animation ) ) );
			$amp_animation->appendChild( $position_script );
			$this->dom->body->appendChild( $amp_animation );
		}
	}

	/**
	 * Add styles for the nav menu specifically to deal with AMP running in a no-js context.
	 *
	 * @since 1.0
	 *
	 * @param array $args Args.
	 */
	public static function add_nav_menu_styles( $args = [] ) {
		add_action(
			'wp_enqueue_scripts',
			static function () use ( $args ) {
				ob_start();
				?>
				<style>
				<?php if ( ! empty( $args['no_js_submenu_visible'] ) ) : ?>
					/* Override no-js selector in parent theme. */
					<?php
					$selector = is_string( $args['no_js_submenu_visible'] ) ? $args['no_js_submenu_visible'] : '.no-js .main-navigation ul ul';
					?>
					<?php echo esc_html( $selector ); ?> {
						display: none;
					}
				<?php endif; ?>

				<?php if ( ! empty( $args['sub_menu_button_toggle_class'] ) ) : ?>
					/* Use sibling selector and re-use class on button instead of toggling toggle-on class on ul.sub-menu */
					.main-navigation ul .<?php echo esc_html( $args['sub_menu_button_toggle_class'] ); ?> + .sub-menu {
						display: block;
					}
				<?php endif; ?>

				<?php if ( 'twentytwenty' === get_template() ) : ?>
					.cover-modal {
						display: inherit;
					}

					.menu-modal-inner {
						height: 100%;
					}

					.admin-bar .cover-modal {
						/* Use padding to shift down modal because amp-lightbox has top:0 !important. */
						padding-top: 32px;
					}

					@media (max-width: 782px) {
						.admin-bar .cover-modal {
							/* Use padding to shift down modal because amp-lightbox has top:0 !important. */
							padding-top: 46px;
						}
					}

					@media (max-width: 999px) {
						amp-lightbox.cover-modal.show-modal {
							display: unset;
						}
					}

				<?php elseif ( 'twentynineteen' === get_template() ) : ?>
					.amp-twentynineteen-main-navigation {
						max-width: 100vw;
						width: 100vw;
						height: 100vh;
					}
					.admin-bar .amp-twentynineteen-main-navigation {
						top: 46px;
						height: calc(100vh - 46px);
					}

					.amp-twentynineteen-main-navigation .sub-menu > li > a:hover,
					.amp-twentynineteen-main-navigation .sub-menu > li > a:focus,
					.amp-twentynineteen-main-navigation .sub-menu > li > .menu-item-link-return:hover,
					.amp-twentynineteen-main-navigation .sub-menu > li > .menu-item-link-return:focus {
						background: inherit;
					}

					.amp-twentynineteen-main-navigation .sub-menu > li {
						position: static;
						display: flex;
					}

					.amp-twentynineteen-main-navigation .sub-menu > li.menu-item-has-children .submenu-expand {
						position: static;
					}

					.amp-twentynineteen-main-navigation .sub-menu.expanded-true {
						display: table;
						margin-top: 0;
						opacity: 1;
						padding-left: 0;
						left: 0;
						top: 0;
						right: 0;
						bottom: 0;
						position: static;
						z-index: 100000; /* Make sure appears above mobile admin bar */
						width: 100vw;
						height: 100%;
						max-width: 100vw;
					}

					.amp-twentynineteen-main-navigation .sub-menu > li.mobile-parent-nav-menu-item {
						display: block;
					}

					.amp-twentynineteen-main-navigation .submenu-expand .svg-icon {
						<?php if ( is_rtl() ) : ?>
							transform: rotate(90deg);
						<?php else : ?>
							transform: rotate(270deg);
						<?php endif; ?>
					}

					.amp-twentynineteen-main-navigation .sub-menu > li > a,
					.amp-twentynineteen-main-navigation .sub-menu > li > .menu-item-link-return {
						flex-grow: 1;
						max-width: none;
					}

					.amp-twentynineteen-main-navigation .sub-menu > li > .menu-item-link-return .svg-icon {
						<?php if ( is_rtl() ) : ?>
							transform: rotate(180deg);
						<?php endif; ?>
					}

					.main-navigation .main-menu > li.menu-item-has-children > .submenu-expand.display-on-mobile,
					.display-on-mobile {
						display: none;
					}

					@media only screen and (max-width: 768px) {
						.display-on-mobile {
							display: block;
						}

						.main-navigation .main-menu .menu-item-has-children:not(.off-canvas):focus-within > .sub-menu,
						.main-navigation .main-menu .menu-item-has-children:not(.off-canvas):hover > .sub-menu,
						.main-navigation .main-menu .menu-item-has-children:not(.off-canvas):focus > .sub-menu,
						.main-navigation .main-menu > li.menu-item-has-children .submenu-expand.display-on-desktop,
						.display-on-desktop {
							display: none;
						}

						.main-navigation .main-menu > li.menu-item-has-children > .submenu-expand.display-on-mobile svg {
							position: relative;
							top: -2px;
							<?php if ( is_rtl() ) : ?>
								right: -2px;
							<?php else : ?>
								left: -2px;
							<?php endif; ?>
						}

						.main-navigation .main-menu > li.menu-item-has-children > .submenu-expand.display-on-mobile {
							display: inline-block;
							<?php if ( is_rtl() ) : ?>
								margin-left: 0.5rem;
								margin-right: 0.25rem;
							<?php else : ?>
								margin-left: 0.25rem;
								margin-right: 0.5rem;
							<?php endif; ?>
							background: #0073aa;
							color: #FFF;
							border-radius: 50%;
							width: 20px;
							height: 20px;
							vertical-align: middle;
						}
					}
				<?php elseif ( 'twentyseventeen' === get_template() ) : ?>
					/* Show the button*/
					.no-js .menu-toggle {
						display: block;
					}
					.no-js .main-navigation > div > ul {
						display: none;
					}
					.no-js .main-navigation.toggled-on > div > ul {
						display: block;
					}
					@media screen and (min-width: 48em) {
						.no-js .menu-toggle,
						.no-js .dropdown-toggle {
							display: none;
						}
						.no-js .main-navigation ul,
						.no-js .main-navigation ul ul,
						.no-js .main-navigation > div > ul {
							display: block;
						}
					}
				<?php elseif ( 'twentysixteen' === get_template() ) : ?>
					@media screen and (max-width: 56.875em) {
						/* Show the button*/
						.no-js .menu-toggle {
							display: block;
						}
						.no-js .site-header-menu {
							display: none;
						}
						.no-js .site-header-menu.toggled-on {
							display: block;
						}
					}
					@media screen and (min-width: 56.875em) {
						.no-js .main-navigation ul ul {
							display: block;
						}
					}
				<?php elseif ( 'twentyfifteen' === get_template() ) : ?>
					@media screen and (min-width: 59.6875em) {
						/* Attempt to emulate https://github.com/WordPress/wordpress-develop/blob/5e9a39baa7d4368f7d3c36dcbcd53db6317677c9/src/wp-content/themes/twentyfifteen/js/functions.js#L108-L149 */
						#sidebar {
							position: sticky;
							top: -9vh;
							max-height: 109vh;
							overflow-y: auto;
						}
					}

				<?php elseif ( 'twentythirteen' === get_template() ) : ?>
					@media (min-width: 644px) {
						.dropdown-toggle {
							display: none;
						}
					}
					@media (max-width: 643px) {
						.nav-menu .toggle-on + .sub-menu {
							clip: inherit;
							overflow: inherit;
							height: inherit;
							width: inherit;
						}
						/* Override :hover selector rules in theme which would cause submenu to persist open. */
						ul.nav-menu li:hover button:not( .toggle-on ) +  ul,
						.nav-menu ul li:hover button:not( .toggle-on ) +  ul {
							height: 1px;
							width: 1px;
							overflow: hidden;
							clip: rect(1px, 1px, 1px, 1px);
						}
						.menu-item-has-children {
							position: relative;
						}
						.dropdown-toggle {
							-moz-osx-font-smoothing: grayscale;
							-webkit-font-smoothing: antialiased;
							display: inline-block;
							font-size: 16px;
							font-style: normal;
							font-weight: normal;
							font-variant: normal;
							line-height: 1;
							text-align: center;
							text-decoration: inherit;
							text-transform: none;
							vertical-align: top;
							border: 0;
							box-sizing: content-box;
							content: "";
							height: 42px;
							padding: 0;
							background: transparent;
							width: 42px;
							position: absolute;
							top: 3px;
							<?php if ( is_rtl() ) : ?>
								left: 0;
							<?php else : ?>
								right: 0;
							<?php endif; ?>
						}
						.dropdown-toggle:active,
						.dropdown-toggle:focus,
						.dropdown-toggle:hover {
							padding: 0;
							border: 0;
							background: transparent;
						}
						.dropdown-toggle:after {
							color: #333;
							speak: none;
							font-family: "Genericons";
							content: "\f431";
							font-size: 24px;
							line-height: 42px;
							position: relative;
							top: 0;
							width: 42px;
							<?php if ( is_rtl() ) : ?>
								left: 1px;
							<?php else : ?>
								right: 1px;
							<?php endif; ?>
						}
						.dropdown-toggle.toggle-on:after {
							content: "\f432";
						}
						.dropdown-toggle:hover,
						.dropdown-toggle:focus {
							background-color: rgba(51, 51, 51, 0.1);

						.dropdown-toggle:focus {
							outline: 1px solid rgba(51, 51, 51, 0.3);
						}
					}
				<?php endif; ?>
				</style>
				<?php
				$styles = str_replace( [ '<style>', '</style>' ], '', ob_get_clean() );
				wp_add_inline_style( get_template() . '-style', $styles );
			},
			11
		);
	}

	/**
	 * Adjust images in twentynineteen.
	 *
	 * @since 1.1
	 */
	public static function adjust_twentynineteen_images() {

		// Make sure the featured image gets responsive layout.
		add_filter(
			'wp_get_attachment_image_attributes',
			static function ( $attributes ) {
				if ( preg_match( '/(^|\s)(attachment-post-thumbnail)(\s|$)/', $attributes['class'] ) ) {
					$attributes['data-amp-layout'] = 'responsive';
				}
				return $attributes;
			}
		);
	}

	/**
	 * Add styles for Twenty Fourteen masthead.
	 *
	 * @since 1.1
	 */
	public static function add_twentyfourteen_masthead_styles() {
		add_action(
			'wp_enqueue_scripts',
			static function () {
				ob_start();
				?>
				<style>
					/* Styles for featured content */
					.grid #featured-content .post-thumbnail,
					.slider #featured-content .post-thumbnail {
						padding-top: 0; /* Override responsive hack which is handled by AMP layout. */
						overflow: visible;
					}
					.featured-content .post-thumbnail .wp-post-image {
						position: static;
						left: auto;
						top: auto;
					}
					.slider #featured-content .hentry {
						display: block;
					}

					/*
					 * The following are needed because clicking on the :before pseudo element does not trigger a tap event.
					 * So instead of positioning the screen reader text off screen, we just position it to cover cover the
					 * toggle button entirely, with a zero opacity.
					 */
					.search-toggle {
						position: relative;
					}
					.search-toggle > a.screen-reader-text {
						left: 0;
						top: 0;
						right: 0;
						bottom: 0;
						width: auto;
						height: auto;
						clip: unset;
						opacity: 0;
					}

					<?php if ( 'slider' === get_theme_mod( 'featured_content_layout' ) ) : ?>

						/*
						 * Styles for slider carousel.
						 */
						.featured-content-inner > amp-carousel {
							position: relative;
						}
						body.slider amp-carousel > .amp-carousel-button {
							-webkit-font-smoothing: antialiased;
							background-color: black;
							background-image: none;
							border-radius: 0;
							border-color: #fff;
							border-style: solid;
							border-width: 2px 1px 0 0;
							box-sizing: border-box;
							cursor: pointer;
							display: inline-block;
							font: normal 16px/1 Genericons;
							height: 48px;
							left: auto;
							opacity: 1;
							text-align: center;
							text-decoration: inherit;
							top: auto;
							width: 50%;
							transform: none;
						}
						body.slider amp-carousel > .amp-carousel-button:focus {
							outline: white thin dotted;
						}
						body.slider amp-carousel > .amp-carousel-button:hover {
							background-color: #24890d;
							outline: none;
						}
						body.slider amp-carousel > .amp-carousel-button-prev:before {
							color: #fff;
							content: "\f430";
							font-size: 32px;
							line-height: 46px;
						}
						body.slider amp-carousel > .amp-carousel-button-next:before {
							color: #fff;
							content: "\f429";
							font-size: 32px;
							line-height: 46px;
						}
						.featured-content .post-thumbnail .wp-post-image {
							object-fit: cover;
							object-position: top;
						}

						@media screen and (max-width: 672px) {
							.slider-control-paging {
								float: none;
								margin: 0;
							}
							.featured-content .post-thumbnail .wp-post-image {
								height: 55.49132947vw;
							}
							.slider-control-paging li {
								display: inline-block;
								float: none;
							}
						}
						@media screen and (min-width: 673px) {
							body.slider amp-carousel > .amp-carousel-button {
								width: 48px;
								border: 0;
								bottom: 0;
							}
							body.slider amp-carousel > .amp-carousel-button-prev {
								right: 50px;
							}
							body.slider amp-carousel > .amp-carousel-button-next {
								right: 0;
							}
						}

					<?php endif; ?>
				</style>
				<?php
				$css = str_replace( [ '<style>', '</style>' ], '', ob_get_clean() );

				wp_add_inline_style( 'twentyfourteen-style', $css );
			},
			11
		);
	}

	/**
	 * Add amp-carousel for slider in Twenty Fourteen.
	 *
	 * @since 1.1
	 */
	public function add_twentyfourteen_slider_carousel() {
		if ( 'slider' !== get_theme_mod( 'featured_content_layout' ) ) {
			return;
		}

		$featured_content = $this->dom->getElementById( 'featured-content' );
		if ( ! $featured_content ) {
			return;
		}

		$featured_content_inner = $this->dom->xpath->query( './div[ @class = "featured-content-inner" ]', $featured_content )->item( 0 );
		if ( ! $featured_content_inner ) {
			return;
		}

		$selected_slide_default  = 0;
		$selected_slide_state_id = 'twentyFourteenSelectedSlide';

		// Create the slider state.
		$amp_state = $this->dom->createElement( 'amp-state' );
		$amp_state->setAttribute( 'id', $selected_slide_state_id );
		$script = $this->dom->createElement( 'script' );
		$script->setAttribute( 'type', 'application/json' );
		$script->appendChild( $this->dom->createTextNode( wp_json_encode( $selected_slide_default ) ) );
		$amp_state->appendChild( $script );
		$featured_content->appendChild( $amp_state );

		// Create the carousel slider.
		$amp_carousel_desktop_id = 'twentyFourteenSliderDesktop';
		$amp_carousel_mobile_id  = 'twentyFourteenSliderMobile';
		$amp_carousel_attributes = [
			'layout'                             => 'responsive',
			'on'                                 => "slideChange:AMP.setState( { $selected_slide_state_id: event.index } )",
			'width'                              => '100',
			'type'                               => 'slides',
			'loop'                               => '',
			Amp::BIND_DATA_ATTR_PREFIX . 'slide' => $selected_slide_state_id,
		];
		$amp_carousel_desktop    = AMP_DOM_Utils::create_node(
			$this->dom,
			'amp-carousel',
			array_merge(
				$amp_carousel_attributes,
				[
					'id'     => $amp_carousel_desktop_id,
					'media'  => '(min-width: 672px)',
					'height' => '55.49132947', // Value comes from <https://github.com/WordPress/wordpress-develop/blob/fc2a8f0e11316d066a686995b8578d82cd5546cf/src/wp-content/themes/twentyfourteen/style.css#L3024>.
				]
			)
		);
		$amp_carousel_mobile     = AMP_DOM_Utils::create_node(
			$this->dom,
			'amp-carousel',
			array_merge(
				$amp_carousel_attributes,
				[
					'id'     => $amp_carousel_mobile_id,
					'media'  => '(max-width: 672px)',
					'height' => '73',
				]
			)
		);

		while ( $featured_content_inner->firstChild ) {
			$node = $featured_content_inner->removeChild( $featured_content_inner->firstChild );
			$amp_carousel_desktop->appendChild( $node );
			$amp_carousel_mobile->appendChild( $node->cloneNode( true ) );
		}
		$featured_content_inner->appendChild( $amp_carousel_desktop );
		$featured_content_inner->appendChild( $amp_carousel_mobile );

		// Create the selector.
		$amp_selector = $this->dom->createElement( 'amp-selector' );
		$amp_selector->setAttribute( 'layout', 'container' );
		$slider_control_nav = $this->dom->createElement( 'ol' );
		$slider_control_nav->setAttribute( 'class', 'slider-control-nav slider-control-paging' );
		$count = $amp_carousel_desktop->getElementsByTagName( 'article' )->length;
		for ( $i = 0; $i < $count; $i++ ) {
			$li = $this->dom->createElement( 'li' );
			$a  = $this->dom->createElement( 'a' );
			if ( $selected_slide_default === $i ) {
				$li->setAttribute( 'selected', '' );
				$a->setAttribute( 'class', 'slider-active' );
			}
			$a->setAttribute( Amp::BIND_DATA_ATTR_PREFIX . 'class', "$selected_slide_state_id == $i ? 'slider-active' : ''" );
			$a->setAttribute( Attribute::ROLE, Role::BUTTON );
			$a->setAttribute( Attribute::ON, "tap:AMP.setState( { $selected_slide_state_id: $i } )" );
			$li->setAttribute( 'option', (string) $i );
			$a->appendChild( $this->dom->createTextNode( $i + 1 ) );
			$li->appendChild( $a );
			$slider_control_nav->appendChild( $li );
		}
		$amp_selector->appendChild( $slider_control_nav );
		$featured_content->appendChild( $amp_selector );
	}

	/**
	 * Use AMP-based solutions for toggling search bar in Twenty Fourteen.
	 *
	 * @link https://github.com/WordPress/wordpress-develop/blob/fc2a8f0e11316d066a686995b8578d82cd5546cf/src/wp-content/themes/twentyfourteen/js/functions.js#L69-L87
	 */
	public function add_twentyfourteen_search() {
		$search_toggle_div  = $this->dom->xpath->query( '//div[ contains( @class, "search-toggle" ) ]' )->item( 0 );
		$search_toggle_link = $this->dom->xpath->query( './a', $search_toggle_div )->item( 0 );
		$search_container   = $this->dom->getElementById( 'search-container' );
		if (
			! $search_toggle_div instanceof DOMElement
			|| ! $search_toggle_link instanceof DOMElement
			|| ! $search_container instanceof DOMElement
		) {
			return;
		}

		// Create the <amp-state> element that contains whether the search bar is shown.
		$amp_state       = $this->dom->createElement( 'amp-state' );
		$hidden_state_id = 'twentyfourteenSearchHidden';
		$hidden          = true;
		$amp_state->setAttribute( 'id', $hidden_state_id );
		$script = $this->dom->createElement( 'script' );
		$script->setAttribute( 'type', 'application/json' );
		$script->appendChild( $this->dom->createTextNode( wp_json_encode( $hidden ) ) );
		$amp_state->appendChild( $script );
		$search_container->appendChild( $amp_state );

		// Update AMP state to show the search bar and focus on search input when tapping on the search button.
		$search_input_id = 'twentyfourteen_search_input';
		$search_input_el = $this->dom->xpath->query( './/input[ @name = "s" ]', $search_container )->item( 0 );
		$search_toggle_link->removeAttribute( 'href' );
		$on = "tap:AMP.setState( { $hidden_state_id: ! $hidden_state_id } )";
		if ( $search_input_el instanceof DOMElement ) {
			$search_input_el->setAttribute( 'id', $search_input_id );
			$on .= ",$search_input_id.focus()";
		}
		$search_toggle_link->setAttribute( Attribute::ON, $on );
		$search_toggle_link->setAttribute( Attribute::TABINDEX, '0' );
		$search_toggle_link->setAttribute( Attribute::ROLE, Role::BUTTON );

		// Set visibility and aria-expanded based of the link based on whether the search bar is expanded.
		$search_toggle_link->setAttribute( 'aria-expanded', wp_json_encode( $hidden ) );
		$search_toggle_link->setAttribute( Amp::BIND_DATA_ATTR_PREFIX . 'aria-expanded', "$hidden_state_id ? 'false' : 'true'" );
		$search_toggle_div->setAttribute( Amp::BIND_DATA_ATTR_PREFIX . 'class', "$hidden_state_id ? 'search-toggle' : 'search-toggle active'" );
		$search_container->setAttribute( Amp::BIND_DATA_ATTR_PREFIX . 'class', "$hidden_state_id ? 'search-box-wrapper hide' : 'search-box-wrapper'" );
	}

	/**
	 * Wrap a modal node tree in an <amp-lightbox> element.
	 *
	 * @param array $args {
	 *     Associative array of arguments.
	 *
	 *     @type string   $modal_id            ID to use for the modal and its associated buttons.
	 *     @type string   $modal_content_xpath XPath to query the contents of the modal.
	 *     @type string[] $open_button_xpath   Array of XPaths to query the buttons that open the modal.
	 *     @type string[] $close_button_xpath  Array of XPaths to query the buttons that close the modal. These should be contained within the modal.
	 *     @type string   $animate_in          Optional. What animation to use for showing the modal. Valid options are: 'fade-in', 'fly-in-bottom', 'fly-in-top'. Defaults to 'fade-in'.
	 *     @type bool     $scrollable          Optional. Whether the inner content of the modal should be scrollable. Defaults to true.
	 * }
	 */
	public function wrap_modal_in_lightbox( $args = [] ) {
		if ( ! isset( $args['modal_id'], $args['modal_content_xpath'], $args['open_button_xpath'], $args['close_button_xpath'] ) ) {
			return;
		}

		$modal_id           = $args['modal_id'];
		$modal_content_node = $this->dom->xpath->query( $args['modal_content_xpath'] )->item( 0 );

		if ( ! is_string( $modal_id ) || ! $modal_content_node instanceof DOMElement ) {
			return;
		}

		$body_id = $this->dom->getElementId( $this->dom->body, 'body' );

		$open_xpaths  = $args['open_button_xpath'];
		$close_xpaths = $args['close_button_xpath'];

		$modal_actions = [
			"{$modal_id}.open"  => $open_xpaths,
			// Although we add the 'show-modal' class here, we don't remove it again, as it will
			// _first_ remove the correct positioning and only _then_ start the fade-out animation.
			// See: https://youtu.be/aooq-liRtMs .
			"{$modal_id}.toggleClass(class=show-modal,force=true)" => $open_xpaths,
			"{$body_id}.toggleClass(class=showing-modal,force=true)" => $open_xpaths,
			"{$modal_id}.close" => $close_xpaths,
			"{$body_id}.toggleClass(class=showing-modal,force=false)" => $close_xpaths,
		];

		// As we have the toggle targets, we need to go backwards from their and find all
		// nodes that are meant to toggle these targets.
		// The triple loop below is generally a double loop (modals x toggles), however
		// we need the third loop as we cannot guarantee that each xpath will only ever
		// retrieve a single result.
		foreach ( $modal_actions as $modal_action => $toggle_xpaths ) {
			foreach ( $toggle_xpaths as $toggle_xpath ) {
				foreach ( $this->dom->xpath->query( $toggle_xpath ) as $toggle_node ) {
					if ( $toggle_node instanceof DOMElement ) {
						AMP_DOM_Utils::add_amp_action( $toggle_node, 'tap', $modal_action );
					}
				}
			}
		}

		// Make sure lightboxes are marked as inactive and not expanded when they are closed via the Escape key.
		$state_string = str_replace( '-', '_', $modal_id );
		AMP_DOM_Utils::add_amp_action( $modal_content_node, 'lightboxOpen', "{$modal_id}.toggleClass(class=active,force=true)" );
		AMP_DOM_Utils::add_amp_action( $modal_content_node, 'lightboxOpen', "AMP.setState({{$state_string}:true})" );
		AMP_DOM_Utils::add_amp_action( $modal_content_node, 'lightboxClose', "{$modal_id}.toggleClass(class=active,force=false)" );
		AMP_DOM_Utils::add_amp_action( $modal_content_node, 'lightboxClose', "AMP.setState({{$state_string}:false})" );

		// Create an <amp-lightbox> element that will contain the modal.
		$amp_lightbox = $this->dom->createElement( 'amp-lightbox' );
		$amp_lightbox->setAttribute( 'id', $modal_id );
		$amp_lightbox->setAttribute( 'layout', 'nodisplay' );
		$amp_lightbox->setAttribute( 'animate-in', isset( $args['animate_in'] ) ? $args['animate_in'] : 'fade-in' );
		$amp_lightbox->setAttribute( 'scrollable', isset( $args['scrollable'] ) ? $args['scrollable'] : true );

		$amp_lightbox_inner_content = $this->dom->xpath->query( ".//*[ @class and contains( concat( ' ', normalize-space( @class ), ' ' ), ' modal-inner ' ) ]", $modal_content_node )->item( 0 );
		foreach ( [ $amp_lightbox, $amp_lightbox_inner_content ] as $event_element ) {
			/**
			 * Event element to add accessibility attributes to.
			 *
			 * @var DOMElement $event_element
			 */
			$event_element->setAttribute( Attribute::ROLE, $this->guess_modal_role( $modal_content_node ) );
			// Setting tabindex to -1 (not reachable) as keyboard focus is handled through toggles.
			$event_element->setAttribute( Attribute::TABINDEX, -1 );
		}

		$parent_node = $modal_content_node->parentNode;
		$parent_node->replaceChild( $amp_lightbox, $modal_content_node );

		$strip_wrapper_levels = isset( $args['strip_wrapper_levels'] ) ? $args['strip_wrapper_levels'] : 0;

		while ( $strip_wrapper_levels > 0 ) {
			$children = [];
			foreach ( $modal_content_node->childNodes as $child_node ) {
				if ( $child_node instanceof DOMElement ) {
					$children[] = $child_node;
				}
			}

			if ( count( $children ) > 1 ) {
				break;
			}

			// Add class(es) and action(s) of removed wrapper to lightbox to avoid breaking CSS selectors.
			AMP_DOM_Utils::copy_attributes( [ 'class', 'on', 'data-toggle-target' ], $modal_content_node, $amp_lightbox );

			$modal_content_node = $modal_content_node->removeChild( $children[0] );

			$strip_wrapper_levels--;
		}

		$amp_lightbox->appendChild( $modal_content_node );
	}

	/**
	 * Add generic modal interactivity compat for the Twenty Twenty theme.
	 *
	 * Modals implemented in JS will be transformed into <amp-lightbox> equivalents,
	 * with the tap actions being attached to their associated toggles.
	 */
	public function add_twentytwenty_modals() {
		$modals = $this->dom->xpath->query( "//*[ @class and contains( concat( ' ', normalize-space( @class ), ' ' ), ' cover-modal ' ) ]" );

		if ( false === $modals || 0 === $modals->length ) {
			return;
		}

		foreach ( $modals as $modal ) {
			/**
			 * Modal element to transform.
			 *
			 * @var DOMElement $modal
			 */

			if ( ! $modal->hasAttribute( 'data-modal-target-string' ) ) {
				return;
			}

			$modal_target = $modal->getAttribute( 'data-modal-target-string' );
			$toggles      = $this->dom->xpath->query( "//*[ @data-toggle-target = '{$modal_target}' ]" );

			$open_button_xpaths  = [];
			$close_button_xpaths = [];
			foreach ( $toggles as $toggle ) {
				/**
				 * Toggle element to transform.
				 *
				 * @var DOMElement $toggle
				 */

				$within_modal = false;
				$parent       = $toggle->parentNode;
				while ( $parent ) {
					if ( $parent === $modal ) {
						$within_modal = true;
						break;
					}
					$parent = $parent->parentNode;
				}

				if ( $within_modal ) {
					$close_button_xpaths[] = $toggle->getNodePath();
				} else {
					$open_button_xpaths[] = $toggle->getNodePath();
				}
			}

			$modal_id = $this->dom->getElementId( $modal );

			// Add the lightbox itself as a close button xpath as well.
			// With twentytwenty compat, the lightbox fills the entire screen, and only an inner wrapper will contain
			// the actionable elements in the modal. Therefore, the lightbox represents the "background".
			$close_button_xpaths[] = "//*[ @id = '{$modal_id}' ]";

			// Ensure anchor links also close the modal the same as the the Close Menu button.
			$close_button_xpaths[] = sprintf( "//*[ @id = '{$modal_id}' ]//a[ @href and contains( @href, '#' ) ]" );

			// Then, add the inner element of the lightbox as an open button xpath.
			// This is done to prevent the above close action from closing the modal when an inner element is clicked.
			// Workaround found here: https://stackoverflow.com/a/45971501 .
			$open_button_xpaths[] = "//*[ @id = '{$modal_id}' ]//*[ @class and contains( concat( ' ', normalize-space( @class ), ' ' ), ' modal-inner ' ) ]";

			$this->wrap_modal_in_lightbox(
				[
					'modal_id'             => $modal_id,
					'modal_content_xpath'  => $modal->getNodePath(),
					'open_button_xpath'    => $open_button_xpaths,
					'close_button_xpath'   => $close_button_xpaths,
					'strip_wrapper_levels' => 1,
				]
			);
		}
	}

	/**
	 * Add generic toggle interactivity compat for the Twentytwenty theme.
	 *
	 * Toggles implemented in JS will be transformed into <amp-bind> equivalents,
	 * with <amp-state> components storing the CSS classes to set.
	 */
	public function add_twentytwenty_toggles() {
		$toggles = $this->dom->xpath->query( '//*[ @data-toggle-target ]' );
		$body_id = $this->dom->getElementId( $this->dom->body, 'body' );

		if ( false === $toggles || 0 === $toggles->length ) {
			return;
		}

		/**
		 * Toggle to transform.
		 *
		 * @var DOMElement $toggle
		 */
		foreach ( $toggles as $toggle ) {
			$toggle_target = $toggle->getAttribute( 'data-toggle-target' );
			$toggle_id     = $this->dom->getElementId( $toggle );

			if ( 'next' === $toggle_target ) {
				$target_node = $toggle->nextSibling;
			} else {
				$target_xpath = $this->xpath_from_css_selector( $toggle_target );
				if ( null === $target_xpath ) {
					continue;
				}

				$target_nodes = $this->dom->xpath->query( $target_xpath, $toggle );
				if ( false === $target_nodes || 0 === count( $target_nodes ) ) {
					continue;
				}
				$target_node = $target_nodes->item( 0 );
			}

			if ( ! $target_node instanceof DOMElement ) {
				continue;
			}

			// Get the class to toggle, if specified.
			$toggle_class = $toggle->hasAttribute( 'data-class-to-toggle' ) ? $toggle->getAttribute( 'data-class-to-toggle' ) : 'active';

			$is_sub_menu     = AMP_DOM_Utils::has_class( $target_node, 'sub-menu' );
			$new_target_node = $is_sub_menu ? $this->get_closest_submenu( $toggle ) : $target_node;
			$new_target_id   = $this->dom->getElementId( $new_target_node );

			$state_string = str_replace( '-', '_', $new_target_id );

			// Toggle the target of the clicked toggle.
			AMP_DOM_Utils::add_amp_action( $toggle, 'tap', "{$new_target_id}.toggleClass(class='{$toggle_class}')" );
			// Set the central state of the toggle's target.
			AMP_DOM_Utils::add_amp_action( $toggle, 'tap', "AMP.setState({{$state_string}: !{$state_string}})" );
			// Adapt the aria-expanded attribute according to the central state.
			$toggle->setAttribute( 'data-amp-bind-aria-expanded', "{$state_string} ? 'true' : 'false'" );

			// If the toggle target is 'next' or a sub-menu, only give the clicked toggle the active class.
			if ( 'next' === $toggle_target || AMP_DOM_Utils::has_class( $target_node, 'sub-menu' ) ) {
				AMP_DOM_Utils::add_amp_action( $toggle, 'tap', "{$toggle_id}.toggleClass(class='active')" );
			} else {
				// If not, toggle all toggles with this toggle target.
				$target_toggles = $this->dom->xpath->query( "//*[ @data-toggle-target = '{$toggle_target}' ]" );
				foreach ( $target_toggles as $target_toggle ) {
					if ( AMP_DOM_Utils::has_class( $target_toggle, 'close-nav-toggle' ) ) {
						// Skip adding the 'active' class on the "Close" button in the primary nav menu.
						continue;
					}
					$target_toggle_id = $this->dom->getElementId( $target_toggle );
					AMP_DOM_Utils::add_amp_action( $toggle, 'tap', "{$target_toggle_id}.toggleClass(class='active')" );
				}
			}

			// Toggle body class.
			if ( $toggle->hasAttribute( 'data-toggle-body-class' ) ) {
				$body_class = $toggle->getAttribute( 'data-toggle-body-class' );
				AMP_DOM_Utils::add_amp_action( $toggle, 'tap', "{$body_id}.toggleClass(class='{$body_class}')" );
			}

			if ( $toggle->hasAttribute( 'data-set-focus' ) ) {
				$focus_selector = $toggle->getAttribute( 'data-set-focus' );

				if ( ! empty( $focus_selector ) ) {
					$focus_xpath   = $this->xpath_from_css_selector( $focus_selector );
					$focus_element = $this->dom->xpath->query( $focus_xpath )->item( 0 );

					if ( $focus_element instanceof DOMElement ) {
						$focus_element_id = $this->dom->getElementId( $focus_element );
						AMP_DOM_Utils::add_amp_action( $toggle, 'tap', "{$focus_element_id}.focus" );
					}
				}
			}
		}
	}

	/**
	 * Amend the Twenty Twenty-One dark mode stylesheet to only apply the relevant rules when the user has enables
	 * dark mode support from the customizer options.
	 */
	public static function amend_twentytwentyone_dark_mode_styles() {
		add_action(
			'wp_enqueue_scripts',
			static function () {
				$theme_style_handle     = 'twenty-twenty-one-style';
				$dark_mode_style_handle = 'tt1-dark-mode';

				// Bail if the dark mode stylesheet is not enqueued or the theme stylesheet isn't registered.
				if (
					! wp_style_is( $dark_mode_style_handle, 'enqueued' )
					||
					! wp_style_is( $theme_style_handle, 'registered' )
				) {
					return; // @codeCoverageIgnore
				}

				wp_dequeue_style( $dark_mode_style_handle );

				$dark_mode_css_file = get_theme_file_path(
					sprintf( 'assets/css/style-dark-mode%s.css', is_rtl() ? '-rtl' : '' )
				);

				if ( ! file_exists( $dark_mode_css_file ) ) {
					return; // @codeCoverageIgnore
				}

				$styles = file_get_contents( $dark_mode_css_file ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents

				// Allow for rules to override the light theme related rules.
				$styles = str_replace( '.respect-color-scheme-preference.is-dark-theme body', '.respect-color-scheme-preference body.is-dark-theme', $styles );

				wp_add_inline_style( $theme_style_handle, $styles );
			},
			11
		);
	}

	/**
	 * Amend Twenty Twenty-One Styles.
	 */
	public static function amend_twentytwentyone_styles() {
		add_action(
			'wp_enqueue_scripts',
			static function () {
				$style_handle = 'twenty-twenty-one-style';

				// Bail if the stylesheet is not enqueued.
				if ( ! wp_style_is( $style_handle ) ) {
					return; // @codeCoverageIgnore
				}

				$css_file = get_theme_file_path(
					sprintf( 'style%s.css', is_rtl() ? '-rtl' : '' )
				);

				if ( ! file_exists( $css_file ) ) {
					return; // @codeCoverageIgnore
				}

				/** @var _WP_Dependency $dependency */
				$dependency = wp_styles()->registered[ $style_handle ];

				// Set the registered handle as an alias for other stylesheets to depend on.
				$dependency->src = false;

				$styles = file_get_contents( $css_file ); //phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents

				// AMP provides a global API "AMP.toggleTheme()" to support dark theme by toggling a BODY element class.
				// However, for dark mode in the Twenty Twenty-One theme, the class needs to be toggled on HTML and BODY elements because the CSS variables are set at the root level.
				// In AMP we cannot toggle HTML classes. So here we are changing the location where CSS variables are defined from `:root` to the BODY element.
				if ( get_theme_mod( 'respect_user_color_preference', false ) ) {
					$styles = str_replace( ':root {', 'body {', $styles );
				}

				// Append extra rules needed for nav menus according to changes made to the document during sanitization.
				$styles .= '
					/* Trap keyboard navigation within mobile menu when it\'s open */
					@media only screen and (max-width: 481px) {
						.primary-navigation-open #page {
							visibility: hidden;
						}

						.primary-navigation-open .menu-button-container {
							visibility: visible;
						}
					}

					@media (min-width: 482px) {
						/* Show the sub-menu on hover of menu item */
						.primary-menu-container > .menu-wrapper > .menu-item-has-children:hover > .sub-menu {
							display: block;
						}

						/* Hide the plus icon on hover of menu item */
						.primary-menu-container > .menu-wrapper > .menu-item-has-children:hover > .sub-menu-toggle > .icon-plus {
							display: none;
						}

						/* Show the minus icon on hover of menu item */
						.primary-menu-container > .menu-wrapper > .menu-item-has-children:hover > .sub-menu-toggle > .icon-minus {
							display: flex;
						}
					}
				';

				/*
				 * In Twenty Twenty-One, when a button is used to resize AMP elements, they can appear transparent when hovered over.
				 * To resolve this issue, the theme's background color is used as the background color for the button
				 * when it is in the hovered state.
				 */
				$styles .= '
					button[overflow]:hover {
						background-color: var(--global--color-background);
					}
				';

				// Ideally wp_add_inline_style() would accept a $position argument like wp_add_inline_script() does, in
				// which case the following could be replaced with wp_add_inline_style( $style_handle, $new_styles, 'before' ).
				// But this is not supported, so we have to resort to manipulating the underlying after array.
				if ( ! isset( $dependency->extra['after'] ) ) {
					$dependency->extra['after'] = [];
				}
				array_unshift( $dependency->extra['after'], $styles );
			},
			11
		);
	}

	/**
	 * Make the dark mode toggle in the Twenty Twenty-One theme AMP compatible.
	 */
	public function add_twentytwentyone_dark_mode_toggle() {
		// First check if dark mode is enabled.
		$should_respect_color_scheme = get_theme_mod( 'respect_user_color_preference', false );

		if ( ! $should_respect_color_scheme ) {
			return;
		}

		// Create button element for dark mode toggle.
		// Dark mode toggle button html and js has been omitted due to removing `the_switch` function.
		$button = AMP_DOM_Utils::create_node(
			$this->dom,
			Tag::BUTTON,
			[
				Attribute::ID     => 'dark-mode-toggler',
				Attribute::CLASS_ => 'fixed-bottom',
			]
		);

		/* translators: %s: On/Off */
		$dark_mode_label = __( 'Dark Mode: %s', 'twentytwentyone' ); // phpcs:ignore WordPress.WP.I18n.TextDomainMismatch
		$dark_mode_off   = sprintf( $dark_mode_label, __( 'Off', 'twentytwentyone' ) ); // phpcs:ignore WordPress.WP.I18n.TextDomainMismatch
		$dark_mode_on    = sprintf( $dark_mode_label, __( 'On', 'twentytwentyone' ) ); // phpcs:ignore WordPress.WP.I18n.TextDomainMismatch

		// Create span tag to show `On` for dark mode.
		$button_text_on = $this->dom->createElement( Tag::SPAN );
		$button_text_on->setAttribute( Attribute::CLASS_, 'dark-mode-button-on' );
		$button_text_on->nodeValue = $dark_mode_on;

		// Create span tag to show `Off` for dark mode.
		$button_text_off = $this->dom->createElement( Tag::SPAN );
		$button_text_off->setAttribute( Attribute::CLASS_, 'dark-mode-button-off' );
		$button_text_off->nodeValue = $dark_mode_off;

		// Add button_text_{On, Off} to button.
		$button->appendChild( $button_text_on );
		$button->appendChild( $button_text_off );

		// Add button to body.
		$this->dom->body->appendChild( $button );

		$style = $this->dom->createElement( Tag::STYLE );
		$style->setAttribute( Attribute::ID, 'amp-twentytwentyone-dark-mode-toggle-styles' );
		$style->textContent = sprintf( // We need to add these styles to show On and Off to the user.
			'
				.no-js #dark-mode-toggler {
					display: block;
				}
				#dark-mode-toggler > span {
					margin-%s: 5px;
				}
				.dark-mode-button-on {
					display: none;
				}
				body.is-dark-theme .dark-mode-button-on {
					display: inline-block;
				}
				body.is-dark-theme .dark-mode-button-off {
					display: none;
				}
			',
			is_rtl() ? 'right' : 'left'
		);
		$this->dom->head->appendChild( $style );

		$toggle_class = 'is-dark-theme';

		// Add data-prefers-dark-mode-class in body to use toggleTheme component.
		$this->dom->body->setAttribute( 'data-prefers-dark-mode-class', $toggle_class );

		AMP_DOM_Utils::add_amp_action( $button, 'tap', 'AMP.toggleTheme()' );
	}

	/**
	 * Make the mobile menu for the Twenty Twenty-One theme AMP compatible.
	 */
	public function add_twentytwentyone_mobile_modal() {
		$menu_toggle = $this->dom->getElementById( 'primary-mobile-menu' );

		if ( ! $menu_toggle ) {
			return;
		}

		$state_string = 'mobile_menu_toggled';
		$body_id      = $this->dom->getElementId( $this->dom->body, 'body' );

		AMP_DOM_Utils::add_amp_action( $menu_toggle, 'tap', "AMP.setState({{$state_string}: !{$state_string}})" );
		AMP_DOM_Utils::add_amp_action( $menu_toggle, 'tap', "{$body_id}.toggleClass(class=primary-navigation-open)" );
		AMP_DOM_Utils::add_amp_action( $menu_toggle, 'tap', "{$body_id}.toggleClass(class=lock-scrolling)" );
		$menu_toggle->setAttribute( 'data-amp-bind-aria-expanded', "{$state_string} ? 'true' : 'false'" );

		// Close the mobile modal when clicking in-page anchor links in the menu.
		foreach ( $this->dom->xpath->query( '//*[ @id = "site-navigation" ]//a[ @href and contains( @href, "#" ) ]' ) as $link ) {
			/** @var DOMElement $link */
			AMP_DOM_Utils::add_amp_action( $link, 'tap', "AMP.setState({{$state_string}: false})" );
			AMP_DOM_Utils::add_amp_action( $link, 'tap', "{$body_id}.toggleClass(class=primary-navigation-open,force=false)" );
			AMP_DOM_Utils::add_amp_action( $link, 'tap', "{$body_id}.toggleClass(class=lock-scrolling,force=false)" );

			// Ensure target is scrolled into view. Note that in-page anchor links currently do not work in the non-AMP
			// version. Normally scrollTo shouldn't be necessary but it appears necessary due to scroll locking.
			$target = preg_replace( '/.*#/', '', $link->getAttribute( 'href' ) );
			if ( $target && $this->dom->getElementById( $target ) ) {
				AMP_DOM_Utils::add_amp_action( $link, 'tap', "{$target}.scrollTo" );
			}
		}
	}

	/**
	 * Make the sub-menu functionality for the Twenty Twenty-One theme AMP compatible.
	 *
	 * Note: Hover functionality is accomplished through CSS.
	 *
	 * @see amend_twentytwentyone_styles()
	 */
	public function add_twentytwentyone_sub_menu_fix() {
		$menu_toggles = $this->dom->xpath->query( '//nav//button[ @class and contains( concat( " ", normalize-space( @class ), " " ), " sub-menu-toggle " ) ]' );

		if ( 0 === $menu_toggles->length ) {
			return;
		}

		$menu_toggle_ids = substr_replace( range( 1, $menu_toggles->length ), 'toggle_', 0, 0 );

		// Sub-menus to be closed when the user clicks on the body.
		$toggles_to_disable_for_body = [];

		foreach ( $menu_toggle_ids as $key => $menu_toggle_id ) {
			/** @var DOMElement $menu_toggle */
			$menu_toggle = $menu_toggles->item( $key );

			$menu_toggle->setAttribute( 'data-amp-bind-aria-expanded', "{$menu_toggle_id} ? 'true' : 'false'" );

			// Sub-menus to be closed when this one is to be opened.
			$toggles_to_disable            = '';
			$toggles_to_disable_for_body[] = "{$menu_toggle_id}:false";

			foreach ( $menu_toggle_ids as $other_menu_toggle_id ) {
				if ( $menu_toggle_id === $other_menu_toggle_id ) {
					continue;
				}

				$toggles_to_disable .= ",{$other_menu_toggle_id}:false";
			}

			AMP_DOM_Utils::add_amp_action( $menu_toggle, 'tap', "AMP.setState({{$menu_toggle_id}:!{$menu_toggle_id}{$toggles_to_disable}})" );
		}

		$state_vars = implode( ',', $toggles_to_disable_for_body );
		AMP_DOM_Utils::add_amp_action( $this->dom->body, 'tap', "AMP.setState({{$state_vars}})" );
		$this->dom->body->setAttribute( 'role', 'document' );
		$this->dom->body->setAttribute( 'tabindex', '-1' );
	}

	/**
	 * Sanitize the sub-menus in the Twenty Twenty-One theme.
	 */
	public function amend_twentytwentyone_sub_menu_toggles() {
		$menu_toggles = $this->dom->xpath->query( '//button[ @onclick = "twentytwentyoneExpandSubMenu(this)" ]' );

		// Remove the `onclick` attribute for sub-menu toggles in the primary and secondary menus.
		foreach ( $menu_toggles as $menu_toggle ) {
			/** @var DOMElement $menu_toggle */
			$menu_toggle->removeAttribute( 'onclick' );
		}
	}

	/**
	 * Show "Desktop Expanded Menu" in AMP mode.
	 * Removes 'no-js' class from menu element.
	 *
	 * @return void
	 */
	public function show_twentytwenty_desktop_expanded_menu() {

		$xpath         = "//*[@class='header-navigation-wrapper']/div[ @class and contains( concat( ' ', normalize-space( @class ), ' ' ), ' header-toggles hide-no-js ' ) ]";
		$expanded_menu = $this->dom->xpath->query( $xpath )->item( 0 );

		if ( $expanded_menu instanceof DOMElement ) {
			$class = $expanded_menu->getAttribute( Attribute::CLASS_ );
			$expanded_menu->setAttribute(
				Attribute::CLASS_,
				str_replace( 'hide-no-js', '', $class )
			);
		}
	}

	/**
	 * Get the closest sub-menu within a menu item.
	 *
	 * @param DOMElement $element Element to get the closest sub-menu of.
	 * @return DOMElement Requested sub-menu element, or the starting element
	 *                    if none found.
	 */
	protected function get_closest_submenu( DOMElement $element ) {
		$menu_item = $element;

		while ( ! AMP_DOM_Utils::has_class( $menu_item, 'menu-item' ) ) {
			$menu_item = $menu_item->parentNode;
			if ( ! $menu_item ) {
				return $element;
			}
		}

		$sub_menu = $this->dom->xpath->query( ".//*[ @class and contains( concat( ' ', normalize-space( @class ), ' ' ), ' sub-menu ' ) ]", $menu_item )->item( 0 );

		if ( ! $sub_menu instanceof DOMElement ) {
			return $element;
		}

		return $sub_menu;
	}

	/**
	 * Automatically open the submenus related to the current page in the menu modal.
	 */
	public function add_twentytwenty_current_page_awareness() {
		$page_ancestors = $this->dom->xpath->query( "//li[ @class and contains( concat( ' ', normalize-space( @class ), ' ' ), ' current_page_ancestor ' ) ]" );
		foreach ( $page_ancestors as $page_ancestor ) {
			$toggle   = $this->dom->xpath->query( "./div/button[ @class and contains( concat( ' ', normalize-space( @class ), ' ' ), ' sub-menu-toggle ' ) ]", $page_ancestor )->item( 0 );
			$children = $this->dom->xpath->query( "./ul[ @class and contains( concat( ' ', normalize-space( @class ), ' ' ), ' children ' ) ]", $page_ancestor )->item( 0 );
			foreach ( [ $toggle, $children ] as $element ) {
				if ( ! $element instanceof DOMElement ) {
					continue;
				}

				$classes   = $element->hasAttribute( 'class' ) ? explode( ' ', $element->getAttribute( 'class' ) ) : [];
				$classes[] = 'active';
				$element->setAttribute( 'class', implode( ' ', array_unique( $classes ) ) );
			}
		}
	}

	/**
	 * Provides a "best guess" as to what XPath would mirror a given CSS
	 * selector.
	 *
	 * This is a very simplistic conversion and will only work for very basic
	 * CSS selectors.
	 *
	 * @param string $css_selector CSS selector to convert.
	 * @return string|null XPath that closely mirrors the provided CSS selector,
	 *                             or null if an error occurred.
	 * @since 1.4.0
	 */
	protected function xpath_from_css_selector( $css_selector ) {
		// Start with basic clean-up.
		$css_selector = trim( $css_selector );
		$css_selector = preg_replace( '/\s+/', ' ', $css_selector );

		$xpath             = '';
		$direct_descendant = false;
		$token             = strtok( $css_selector, ' ' );

		while ( false !== $token ) {
			$matches = [];

			// Direct descendant.
			if ( preg_match( '/^>$/', $token, $matches ) ) {
				$direct_descendant = true;
				$token             = strtok( ' ' );
				continue;
			}

			// Single ID.
			if ( preg_match( '/^#(?<id>[a-zA-Z0-9-_]*)$/', $token, $matches ) ) {
				$descendant        = $direct_descendant ? '/' : '//';
				$xpath            .= "{$descendant}*[ @id = '{$matches['id']}' ]";
				$direct_descendant = false;
				$token             = strtok( ' ' );
				continue;
			}

			// Single class.
			if ( preg_match( '/^\.(?<class>[a-zA-Z0-9-_]*)$/', $token, $matches ) ) {
				$descendant        = $direct_descendant ? '/' : '//';
				$xpath            .= "{$descendant}*[ @class and contains( concat( ' ', normalize-space( @class ), ' ' ), ' {$matches['class']} ' ) ]";
				$direct_descendant = false;
				$token             = strtok( ' ' );
				continue;
			}

			// Element.
			if ( preg_match( '/^(?<element>[^.][a-zA-Z0-9-_]*)$/', $token, $matches ) ) {
				$descendant        = $direct_descendant ? '/' : '//';
				$xpath            .= "{$descendant}{$matches['element']}";
				$direct_descendant = false;
				$token             = strtok( ' ' );
				continue;
			}

			$token = strtok( ' ' );
		}

		return $xpath;
	}

	/**
	 * Try to guess the role of a modal based on its classes.
	 *
	 * @param DOMElement $modal Modal to guess the role for.
	 * @return string Role that was guessed.
	 */
	protected function guess_modal_role( DOMElement $modal ) {
		// No classes to base our guess on, so keep it generic.
		if ( ! $modal->hasAttribute( Attribute::CLASS_ ) ) {
			return Role::DIALOG;
		}

		$classes = preg_split( '/\s+/', trim( $modal->getAttribute( Attribute::CLASS_ ) ) );

		foreach ( self::$modal_roles as $role ) {
			if ( in_array( $role, $classes, true ) ) {
				return $role;
			}
		}

		// None of the roles we are looking for match any of the classes.
		return Role::DIALOG;
	}

	/**
	 * Evaluates the given XPath expression and give first Element if exist.
	 *
	 * @param string  $expression   The XPath expression to execute.
	 * @param Element $context_node The optional context node can be specified for doing relative XPath queries.
	 *                              By default, the queries are relative to the root element.
	 *
	 * @return Element|null Return Element if exists otherwise null.
	 */
	protected function get_first_element( $expression, $context_node = null ) {
		/** @var DOMNodeList $dom_node_list */
		$dom_node_list = $this->dom->xpath->query( $expression, $context_node );

		$dom_node = $dom_node_list->item( 0 );
		return $dom_node instanceof Element ? $dom_node : null;
	}

	/**
	 * Update main navigation menu for "twentynineteen" theme.
	 *
	 * @return void
	 */
	public function update_twentynineteen_mobile_main_menu() {

		$xpaths = [
			'main_menu'               => '//nav[ @id = "site-navigation" ]//ul[ @class = "main-menu"]',
			'top_menu_items'          => './li[ contains( @class, "menu-item" ) and contains( @class, "menu-item-has-children" ) ]',
			'expand_button'           => './button[ @class = "submenu-expand" ]',
			'submenu'                 => './ul[ @class = "sub-menu" ]',
			'close_button_in_submenu' => './li[ contains( @class, "mobile-parent-nav-menu-item" ) ]//button[ contains( @class, "menu-item-link-return" ) ]',
		];

		$main_menu = $this->get_first_element( $xpaths['main_menu'] );
		if ( empty( $main_menu ) ) {
			return;
		}

		$menu_items = $this->dom->xpath->query( $xpaths['top_menu_items'], $main_menu );

		/** @var Element $menu_item */
		foreach ( $menu_items as $menu_item ) {
			$expand_button = $this->get_first_element( $xpaths['expand_button'], $menu_item );
			$sub_menu      = $this->get_first_element( $xpaths['submenu'], $menu_item );

			if ( empty( $expand_button ) || empty( $sub_menu ) ) {
				continue;
			}

			// AMP Sidebar.
			$amp_sidebar = AMP_DOM_Utils::create_node(
				$this->dom,
				Extension::SIDEBAR,
				[
					Attribute::LAYOUT => Layout::NODISPLAY,
					Attribute::CLASS_ => 'amp-twentynineteen-main-navigation',
					Attribute::SIDE   => is_rtl() ? 'left' : 'right',
				]
			);

			$sidebar_id = $this->dom->getElementId( $amp_sidebar );

			// AMP nested menu.
			$amp_nested_menu = AMP_DOM_Utils::create_node(
				$this->dom,
				Extension::NESTED_MENU,
				[
					Attribute::LAYOUT => Layout::FILL,
				]
			);

			// Clone the sub-menu and expand button for AMP navigation.
			/** @var Element $amp_sub_menu */
			$amp_sub_menu = $sub_menu->cloneNode( true );

			/** @var Element $amp_expand_button */
			$amp_expand_button = $expand_button->cloneNode( true );

			$sub_menu->setAttribute( Attribute::CLASS_, $sub_menu->getAttribute( Attribute::CLASS_ ) . ' display-on-desktop' );
			$expand_button->setAttribute( Attribute::CLASS_, $expand_button->getAttribute( Attribute::CLASS_ ) . ' display-on-desktop' );

			$menu_item->appendChild( $amp_expand_button );
			$menu_item->appendChild( $amp_sub_menu );
			$this->update_twentynineteen_main_nested_menu( $amp_sub_menu );

			$amp_sub_menu->setAttribute( Attribute::CLASS_, $amp_sub_menu->getAttribute( Attribute::CLASS_ ) . ' expanded-true display-on-mobile' );
			$amp_expand_button->setAttribute( Attribute::CLASS_, $amp_expand_button->getAttribute( Attribute::CLASS_ ) . ' display-on-mobile' );

			// Handle buttons.
			$amp_expand_button->addAmpAction( 'tap', "$sidebar_id.open" );
			$back_button = $this->get_first_element( $xpaths['close_button_in_submenu'], $amp_sub_menu );
			if ( ! empty( $back_button ) ) {
				$back_button->addAmpAction( 'tap', "$sidebar_id.close" );
			}

			$amp_nested_menu->appendChild( $amp_sub_menu );
			$amp_sidebar->appendChild( $amp_nested_menu );
			$menu_item->appendChild( $amp_sidebar );
		}
	}

	/**
	 * Update the markup of the nested menu to AMP compatible markup.
	 *
	 * @param Element $sub_menu Element of sub-menu from main navigation.
	 *
	 * @return void
	 */
	public function update_twentynineteen_main_nested_menu( Element $sub_menu ) {

		$xpaths = [
			'menu_items'    => './li[ contains( @class, "menu-item" ) and contains( @class, "menu-item-has-children" ) ]',
			'expand_button' => './button[ @class = "submenu-expand" ]',
			'back_button'   => './li[ contains( @class, "mobile-parent-nav-menu-item" ) ]//button[ contains( @class, "menu-item-link-return" ) ]',
			'submenu'       => './ul[ @class = "sub-menu" ]',
		];

		$menu_items = $this->dom->xpath->query( $xpaths['menu_items'], $sub_menu );
		if ( 0 === $menu_items->length ) {
			return;
		}

		/** @var Element $menu_item */
		foreach ( $menu_items as $menu_item ) {
			$nested_sub_menu = $this->get_first_element( $xpaths['submenu'], $menu_item );

			if ( empty( $nested_sub_menu ) ) {
				continue;
			}

			$this->update_twentynineteen_main_nested_menu( $nested_sub_menu );

			$back_button = $this->get_first_element( $xpaths['back_button'], $nested_sub_menu );
			if ( ! empty( $back_button ) ) {
				$back_button->setAttribute( Attribute::AMP_NESTED_SUBMENU_CLOSE, '' );
			}

			$open_button = $this->get_first_element( $xpaths['expand_button'], $menu_item );
			if ( ! empty( $open_button ) ) {
				$open_button->setAttribute( Attribute::AMP_NESTED_SUBMENU_OPEN, '' );
			}

			$amp_nested_menu_div = AMP_DOM_Utils::create_node(
				$this->dom,
				Tag::DIV,
				[
					Attribute::AMP_NESTED_SUBMENU => '',
				]
			);

			$nested_sub_menu->setAttribute( Attribute::CLASS_, $nested_sub_menu->getAttribute( Attribute::CLASS_ ) . ' expanded-true' );
			$amp_nested_menu_div->appendChild( $nested_sub_menu );
			$menu_item->appendChild( $amp_nested_menu_div );
		}
	}
}
