Commit realizado el 12:13:52 08-04-2024

This commit is contained in:
Pagina Web Monito
2024-04-08 12:13:55 -04:00
commit 0c33094de9
7815 changed files with 1365694 additions and 0 deletions

View File

@@ -0,0 +1 @@
{"ampRuntimeVersion":"012307052224000","ampCssUrl":"https://cdn.ampproject.org/rtv/012307052224000/v0.css","canaryPercentage":"0.005","diversions":["002307150128000","022307052224000","032307150128000","042307212240000","052307052224000","112307150128000"],"ltsRuntimeVersion":"012306202201000","ltsCssUrl":"https://cdn.ampproject.org/rtv/012306202201000/v0.css"}

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,272 @@
<?php
namespace Google\Web_Stories_Dependencies\AmpProject;
use Google\Web_Stories_Dependencies\AmpProject\Dom\Document;
use Google\Web_Stories_Dependencies\AmpProject\Dom\Element;
use Google\Web_Stories_Dependencies\AmpProject\Html\Attribute;
use Google\Web_Stories_Dependencies\AmpProject\Html\Tag;
use DOMNode;
/**
* Central helper functionality for all Amp-related PHP code.
*
* @package ampproject/amp-toolbox
*/
final class Amp
{
/**
* Attribute prefix for AMP-bind data attributes.
*
* @var string
*/
const BIND_DATA_ATTR_PREFIX = 'data-amp-bind-';
/**
* List of AMP attribute tags that can be appended to the <html> element.
*
* The *_ALT version represent a Unicode variation of the lightning emoji.
* @see https://github.com/ampproject/amphtml/issues/25990
*
* @var string[]
*/
const TAGS = [Attribute::AMP, Attribute::AMP_EMOJI, Attribute::AMP_EMOJI_ALT, Attribute::AMP4ADS, Attribute::AMP4ADS_EMOJI, Attribute::AMP4ADS_EMOJI_ALT, Attribute::AMP4EMAIL, Attribute::AMP4EMAIL_EMOJI, Attribute::AMP4EMAIL_EMOJI_ALT];
/**
* Host and scheme of the AMP cache.
*
* @var string
*/
const CACHE_HOST = 'https://cdn.ampproject.org';
/**
* URL of the AMP cache.
*
* @var string
*/
const CACHE_ROOT_URL = self::CACHE_HOST . '/';
/**
* List of valid AMP HTML formats.
*
* @var string[]
*/
const FORMATS = [Format::AMP, Format::AMP4ADS, Format::AMP4EMAIL];
/**
* List of dynamic components.
*
* This list should be kept in sync with the list of dynamic components at:
*
* @see https://github.com/ampproject/amphtml/blob/292dc66b8c0bb078bbe3a1bca960e8f494f7fc8f/spec/amp-cache-guidelines.md#guidelines-adding-a-new-cache-to-the-amp-ecosystem
*
* @var array[]
*/
const DYNAMIC_COMPONENTS = [Attribute::CUSTOM_ELEMENT => [Extension::GEO], Attribute::CUSTOM_TEMPLATE => []];
/**
* Array of custom element names that delay rendering.
*
* @var string[]
*/
const RENDER_DELAYING_EXTENSIONS = [Extension::DYNAMIC_CSS_CLASSES, Extension::EXPERIMENT, Extension::STORY];
/**
* Standard boilerplate CSS stylesheet.
*
* @var string
*/
const BOILERPLATE_CSS = 'body{-webkit-animation:-amp-start 8s steps(1,end) 0s 1 normal both;-moz-animation:-amp-start 8s steps(1,end) 0s 1 normal both;-ms-animation:-amp-start 8s steps(1,end) 0s 1 normal both;animation:-amp-start 8s steps(1,end) 0s 1 normal both}@-webkit-keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}@-moz-keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}@-ms-keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}@-o-keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}@keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}';
// phpcs:ignore Generic.Files.LineLength.TooLong
/**
* Boilerplate CSS stylesheet for the <noscript> tag.
*
* @var string
*/
const BOILERPLATE_NOSCRIPT_CSS = 'body{-webkit-animation:none;-moz-animation:none;-ms-animation:none;animation:none}';
// phpcs:ignore Generic.Files.LineLength.TooLong
/**
* Boilerplate CSS stylesheet for Amp4Ads & Amp4Email.
*
* @var string
*/
const AMP4ADS_AND_AMP4EMAIL_BOILERPLATE_CSS = 'body{visibility:hidden}';
/**
* AMP runtime tag name.
*
* @var string
*/
const RUNTIME = 'amp-runtime';
// AMP classes reserved for internal use.
const LAYOUT_ATTRIBUTE = 'i-amphtml-layout';
const NO_BOILERPLATE_ATTRIBUTE = 'i-amphtml-no-boilerplate';
const LAYOUT_CLASS_PREFIX = 'i-amphtml-layout-';
const LAYOUT_SIZE_DEFINED_CLASS = 'i-amphtml-layout-size-defined';
const SIZER_ELEMENT = 'i-amphtml-sizer';
const INTRINSIC_SIZER_ELEMENT = 'i-amphtml-intrinsic-sizer';
const LAYOUT_AWAITING_SIZE_CLASS = 'i-amphtml-layout-awaiting-size';
/**
* Slot used by AMP for all service elements, like "i-amphtml-sizer" elements and similar.
*
* @var string
*/
const SERVICE_SLOT = 'i-amphtml-svc';
/**
* Check if a given node is the AMP runtime script.
*
* The AMP runtime script node is of the form '<script async src="https://cdn.ampproject.org...v0.js"></script>'.
*
* @param DOMNode $node Node to check.
* @return bool Whether the given node is the AMP runtime script.
*/
public static function isRuntimeScript(DOMNode $node)
{
if (!$node instanceof Element || !self::isAsyncScript($node) || self::isExtension($node)) {
return \false;
}
$src = $node->getAttribute(Attribute::SRC);
if (\strpos($src, self::CACHE_ROOT_URL) !== 0) {
return \false;
}
if (\substr($src, -6) !== '/v0.js' && \substr($src, -7) !== '/v0.mjs' && \substr($src, -14) !== '/amp4ads-v0.js' && \substr($src, -15) !== '/amp4ads-v0.mjs') {
return \false;
}
return \true;
}
/**
* Check if a given node is the AMP viewer script.
*
* The AMP viewer script node is of the form '<script async
* src="https://cdn.ampproject.org/v0/amp-viewer-integration-...js>"</script>'.
*
* @param DOMNode $node Node to check.
* @return bool Whether the given node is the AMP runtime script.
*/
public static function isViewerScript(DOMNode $node)
{
if (!$node instanceof Element || !self::isAsyncScript($node) || self::isExtension($node)) {
return \false;
}
$src = $node->getAttribute(Attribute::SRC);
if (\strpos($src, self::CACHE_HOST . '/v0/amp-viewer-integration-') !== 0) {
return \false;
}
if (\substr($src, -3) !== '.js') {
return \false;
}
return \true;
}
/**
* Check if a given node is an AMP extension.
*
* @param DOMNode $node Node to check.
* @return bool Whether the given node is the AMP runtime script.
*/
public static function isExtension(DOMNode $node)
{
return !empty(self::getExtensionName($node));
}
/**
* Get the name of the extension.
*
* Returns an empty string if the name of the extension could not be retrieved.
*
* @param DOMNode $node Node to get the name of.
* @return string Name of the custom node or template. Empty string if none found.
*/
public static function getExtensionName(DOMNode $node)
{
if (!$node instanceof Element || $node->tagName !== Tag::SCRIPT) {
return '';
}
if ($node->hasAttribute(Attribute::CUSTOM_ELEMENT)) {
return $node->getAttribute(Attribute::CUSTOM_ELEMENT);
}
if ($node->hasAttribute(Attribute::CUSTOM_TEMPLATE)) {
return $node->getAttribute(Attribute::CUSTOM_TEMPLATE);
}
if ($node->hasAttribute(Attribute::HOST_SERVICE)) {
return $node->getAttribute(Attribute::HOST_SERVICE);
}
return '';
}
/**
* Check whether a given node is a script for a render-delaying extension.
*
* @param DOMNode $node Node to check.
* @return bool Whether the node is a script for a render-delaying extension.
*/
public static function isRenderDelayingExtension(DOMNode $node)
{
$extensionName = self::getExtensionName($node);
if (empty($extensionName)) {
return \false;
}
return \in_array($extensionName, self::RENDER_DELAYING_EXTENSIONS, \true);
}
/**
* Check whether a given DOM node is an AMP custom element.
*
* @param DOMNode $node DOM node to check.
* @return bool Whether the checked DOM node is an AMP custom element.
*/
public static function isCustomElement(DOMNode $node)
{
return $node instanceof Element && \strpos($node->tagName, Extension::PREFIX) === 0;
}
/**
* Check whether the given document is an AMP story.
*
* @param Document $document Document of the page to check within.
* @return bool Whether the provided document is an AMP story.
*/
public static function isAmpStory(Document $document)
{
foreach ($document->head->childNodes as $node) {
if ($node instanceof Element && $node->tagName === Tag::SCRIPT && $node->getAttribute(Attribute::CUSTOM_ELEMENT) === Extension::STORY) {
return \true;
}
}
return \false;
}
/**
* Check whether a given node is an AMP template.
*
* @param DOMNode $node Node to check.
* @return bool Whether the node is an AMP template.
*/
public static function isTemplate(DOMNode $node)
{
if (!$node instanceof Element) {
return \false;
}
if ($node->tagName === Tag::TEMPLATE) {
return \true;
}
if ($node->tagName === Tag::SCRIPT && $node->hasAttribute(Attribute::TEMPLATE) && $node->getAttribute(Attribute::TEMPLATE) === Extension::MUSTACHE) {
return \true;
}
return \false;
}
/**
* Check whether a given node is an async <script> element.
*
* @param DOMNode $node Node to check.
* @return bool Whether the given node is an async <script> element.
*/
private static function isAsyncScript(DOMNode $node)
{
if (!$node instanceof Element || $node->tagName !== Tag::SCRIPT) {
return \false;
}
if (!$node->hasAttribute(Attribute::SRC) || !$node->hasAttribute(Attribute::ASYNC)) {
return \false;
}
return \true;
}
/**
* Check whether a given node is an AMP iframe.
*
* @param DOMNode $node Node to check.
* @return bool Whether the node is an AMP iframe.
*/
public static function isAmpIframe(DOMNode $node)
{
if (!$node instanceof Element) {
return \false;
}
return $node->tagName === Extension::IFRAME || $node->tagName === Extension::VIDEO_IFRAME;
}
}

View File

@@ -0,0 +1,61 @@
<?php
namespace Google\Web_Stories_Dependencies\AmpProject\Cli;
use Google\Web_Stories_Dependencies\AmpProject\Exception\Cli\InvalidCommand;
/**
* Executable that assembles all of the commands.
*
* @package ampproject/amp-toolbox
*/
final class AmpExecutable extends Executable
{
/**
* Array of command classes to register.
*
* @var string[]
*/
const COMMAND_CLASSES = [Command\Optimize::class, Command\Validate::class];
/**
* Array of command object instances.
*
* @var Command[]
*/
private $commandInstances = [];
/**
* Register options and arguments on the given $options object.
*
* @param Options $options Options instance to register the commands with.
* @return void
*/
protected function setup(Options $options)
{
foreach (self::COMMAND_CLASSES as $commandClass) {
/** @var Command $command */
$command = new $commandClass($this);
$command->register($options);
$this->commandInstances[$command->getName()] = $command;
}
}
/**
* Your main program.
*
* Arguments and options have been parsed when this is run.
*
* @param Options $options Options instance to register the commands with.
* @return void
*/
protected function main(Options $options)
{
$commandName = $options->getCommand();
if (empty($commandName)) {
echo $this->options->help();
exit(1);
}
if (!\array_key_exists($commandName, $this->commandInstances)) {
throw InvalidCommand::forUnregisteredCommand($commandName);
}
$command = $this->commandInstances[$commandName];
$command->process($options);
}
}

View File

@@ -0,0 +1,143 @@
<?php
namespace Google\Web_Stories_Dependencies\AmpProject\Cli;
use Google\Web_Stories_Dependencies\AmpProject\Exception\Cli\InvalidColor;
/**
* This file is adapted from the splitbrain\php-cli library, which is authored by Andreas Gohr <andi@splitbrain.org> and
* licensed under the MIT license.
*
* Source: https://github.com/splitbrain/php-cli/blob/8c2c001b1b55d194402cf18aad2757049ac6d575/src/Colors.php
*/
/**
* Handles color output on (Unix) terminals.
*
* @package ampproject/amp-toolbox
*/
class Colors
{
const C_BLACK = 'black';
const C_BLUE = 'blue';
const C_BROWN = 'brown';
const C_CYAN = 'cyan';
const C_DARKGRAY = 'darkgray';
const C_GREEN = 'green';
const C_LIGHTBLUE = 'lightblue';
const C_LIGHTCYAN = 'lightcyan';
const C_LIGHTGRAY = 'lightgray';
const C_LIGHTGREEN = 'lightgreen';
const C_LIGHTPURPLE = 'lightpurple';
const C_LIGHTRED = 'lightred';
const C_PURPLE = 'purple';
const C_RED = 'red';
const C_RESET = 'reset';
const C_WHITE = 'white';
const C_YELLOW = 'yellow';
/**
* Associative array of known color names.
*
* @var array<string>
*/
const KNOWN_COLORS = [self::C_RESET => "\x1b[0m", self::C_BLACK => "\x1b[0;30m", self::C_DARKGRAY => "\x1b[1;30m", self::C_BLUE => "\x1b[0;34m", self::C_LIGHTBLUE => "\x1b[1;34m", self::C_GREEN => "\x1b[0;32m", self::C_LIGHTGREEN => "\x1b[1;32m", self::C_CYAN => "\x1b[0;36m", self::C_LIGHTCYAN => "\x1b[1;36m", self::C_RED => "\x1b[0;31m", self::C_LIGHTRED => "\x1b[1;31m", self::C_PURPLE => "\x1b[0;35m", self::C_LIGHTPURPLE => "\x1b[1;35m", self::C_BROWN => "\x1b[0;33m", self::C_YELLOW => "\x1b[1;33m", self::C_LIGHTGRAY => "\x1b[0;37m", self::C_WHITE => "\x1b[1;37m"];
/**
* Whether colors should be used.
*
* @var bool
*/
protected $enabled = \true;
/**
* Constructor.
*
* Tries to disable colors for non-terminals.
*/
public function __construct()
{
$this->enabled = \getenv('TERM') || \function_exists('posix_isatty') && \posix_isatty(\STDOUT);
}
/**
* Enable color output.
*/
public function enable()
{
$this->enabled = \true;
}
/**
* Disable color output.
*/
public function disable()
{
$this->enabled = \false;
}
/**
* Check whether color support is enabled.
*
* @return bool
*/
public function isEnabled()
{
return $this->enabled;
}
/**
* Convenience function to print a line in a given color.
*
* @param string $line The line to print. A new line is added automatically.
* @param string $color One of the available color names.
* @param resource $channel Optional. File descriptor to write to. Defaults to STDOUT.
* @throws InvalidColor If the requested color code is not known.
*/
public function line($line, $color, $channel = \STDOUT)
{
$this->set($color);
\fwrite($channel, \rtrim($line) . "\n");
$this->reset();
}
/**
* Returns the given text wrapped in the appropriate color and reset code
*
* @param string $text String to wrap.
* @param string $color One of the available color names.
* @return string The wrapped string.
* @throws InvalidColor If the requested color code is not known.
*/
public function wrap($text, $color)
{
return $this->getColorCode($color) . $text . $this->getColorCode(self::C_RESET);
}
/**
* Gets the appropriate terminal code for the given color.
*
* @param string $color One of the available color names.
* @return string Color code.
* @throws InvalidColor If the requested color code is not known.
*/
public function getColorCode($color)
{
if (!\array_key_exists($color, self::KNOWN_COLORS)) {
throw InvalidColor::forUnknownColor($color);
}
if (!$this->enabled) {
return '';
}
return self::KNOWN_COLORS[$color];
}
/**
* Set the given color for consecutive output.
*
* @param string $color One of the supported color names.
* @param resource $channel Optional. File descriptor to write to. Defaults to STDOUT.
* @throws InvalidColor If the requested color code is not known.
*/
public function set($color, $channel = \STDOUT)
{
\fwrite($channel, $this->getColorCode($color));
}
/**
* Reset the terminal color.
*
* @param resource $channel Optional. File descriptor to write to. Defaults to STDOUT.
*/
public function reset($channel = \STDOUT)
{
$this->set(self::C_RESET, $channel);
}
}

View File

@@ -0,0 +1,58 @@
<?php
namespace Google\Web_Stories_Dependencies\AmpProject\Cli;
/**
* A command that is registered with the amp executable.
*
* @package AmpProject\Cli
*/
abstract class Command
{
/**
* Name of the command.
*
* This needs to be overridden in extending commands.
*
* @var string
*/
const NAME = '<unknown>';
/**
* Instance of the CLI executable that the command belongs to.
*
* @var Executable
*/
protected $cli;
/**
* Instantiate the command.
*
* @param Executable $cli Instance of the CLI executable that the command belongs to.
*/
public function __construct(Executable $cli)
{
$this->cli = $cli;
}
/**
* Get the name of the command.
*
* @return string Name of the command.
*/
public function getName()
{
return static::NAME;
}
/**
* Register the command.
*
* @param Options $options Options instance to register the command with.
*/
public abstract function register(Options $options);
/**
* Process the command.
*
* Arguments and options have been parsed when this is run.
*
* @param Options $options Options instance to process the command with.
*/
public abstract function process(Options $options);
}

View File

@@ -0,0 +1,63 @@
<?php
namespace Google\Web_Stories_Dependencies\AmpProject\Cli\Command;
use Google\Web_Stories_Dependencies\AmpProject\Cli\Command;
use Google\Web_Stories_Dependencies\AmpProject\Cli\Options;
use Google\Web_Stories_Dependencies\AmpProject\Exception\Cli\InvalidArgument;
use Google\Web_Stories_Dependencies\AmpProject\Optimizer\ErrorCollection;
use Google\Web_Stories_Dependencies\AmpProject\Optimizer\TransformationEngine;
/**
* Optimize AMP HTML markup and return optimized markup.
*
* @package ampproject/amp-toolbox
*/
final class Optimize extends Command
{
/**
* Name of the command.
*
* @var string
*/
const NAME = 'optimize';
/**
* Help text of the command.
*
* @var string
*/
const HELP_TEXT = 'Optimize AMP HTML markup and return optimized markup.';
/**
* Register the command.
*
* @param Options $options Options instance to register the command with.
*/
public function register(Options $options)
{
$options->registerCommand(self::NAME, self::HELP_TEXT);
$options->registerArgument('file', "File with unoptimized AMP markup. Use '-' for STDIN.", \true, self::NAME);
}
/**
* Process the command.
*
* Arguments and options have been parsed when this is run.
*
* @param Options $options Options instance to process the command with.
*
* @throws InvalidArgument If the provided file is not readable.
*/
public function process(Options $options)
{
list($file) = $options->getArguments();
if ($file !== '-' && (!\is_file($file) || !\is_readable($file))) {
throw InvalidArgument::forUnreadableFile($file);
}
if ($file === '-') {
$file = 'php://stdin';
}
$html = \file_get_contents($file);
$optimizer = new TransformationEngine();
$errors = new ErrorCollection();
$optimizedHtml = $optimizer->optimizeHtml($html, $errors);
echo $optimizedHtml . \PHP_EOL;
}
}

View File

@@ -0,0 +1,77 @@
<?php
namespace Google\Web_Stories_Dependencies\AmpProject\Cli\Command;
use Google\Web_Stories_Dependencies\AmpProject\Cli\Colors;
use Google\Web_Stories_Dependencies\AmpProject\Cli\Command;
use Google\Web_Stories_Dependencies\AmpProject\Cli\Options;
use Google\Web_Stories_Dependencies\AmpProject\Cli\TableFormatter;
use Google\Web_Stories_Dependencies\AmpProject\Exception\Cli\InvalidArgument;
use Google\Web_Stories_Dependencies\AmpProject\Validator\ValidationEngine;
use Google\Web_Stories_Dependencies\AmpProject\Validator\ValidationStatus;
/**
* Validate AMP HTML markup and return validation errors.
*
* @package ampproject/amp-toolbox
*/
final class Validate extends Command
{
/**
* Name of the command.
*
* @var string
*/
const NAME = 'validate';
/**
* Help text of the command.
*
* @var string
*/
const HELP_TEXT = 'Validate AMP HTML markup and return validation errors.';
/**
* Register the command.
*
* @param Options $options Options instance to register the command with.
*/
public function register(Options $options)
{
$options->registerCommand(self::NAME, self::HELP_TEXT);
$options->registerArgument('file', "File with AMP markup to validate. Use '-' for STDIN.", \true, self::NAME);
}
/**
* Process the command.
*
* Arguments and options have been parsed when this is run.
*
* @param Options $options Options instance to process the command with.
*
* @throws InvalidArgument If the provided file is not readable.
*/
public function process(Options $options)
{
list($file) = $options->getArguments();
if ($file !== '-' && (!\is_file($file) || !\is_readable($file))) {
throw InvalidArgument::forUnreadableFile($file);
}
if ($file === '-') {
$file = 'php://stdin';
}
$html = \file_get_contents($file);
$validator = new ValidationEngine();
$result = $validator->validateHtml($html);
foreach ($result->getErrors() as $error) {
echo \sprintf("%d:%d [%s] %s (%s)\n", $error->getLine(), $error->getColumn(), $error->getSeverity(), $error->getCode(), \implode(', ', $error->getParams()));
}
switch ($result->getStatus()->asInt()) {
case ValidationStatus::PASS:
$this->cli->success('Validation SUCCEEDED.');
exit(0);
case ValidationStatus::FAIL:
$this->cli->error('Validation FAILED!');
exit(1);
case ValidationStatus::UNKNOWN:
$this->cli->critical('Validation produced an UNKNOWN state!');
exit(128);
}
}
}

View File

@@ -0,0 +1,336 @@
<?php
namespace Google\Web_Stories_Dependencies\AmpProject\Cli;
use Google\Web_Stories_Dependencies\AmpProject\Exception\AmpCliException;
use Google\Web_Stories_Dependencies\AmpProject\Exception\Cli\InvalidSapi;
use Exception;
/**
* This file is adapted from the splitbrain\php-cli library, which is authored by Andreas Gohr <andi@splitbrain.org> and
* licensed under the MIT license.
*
* Source: https://github.com/splitbrain/php-cli/blob/fb4f888866d090b10e3e68292d197ca274cea626/src/CLI.php
*/
/**
* Your commandline script should inherit from this class and implement the abstract methods.
*
* @package ampproject/amp-toolbox
*/
abstract class Executable
{
/**
* Instance of the Colors helper object.
*
* @var Colors
*/
public $colors;
/**
* The executable script itself.
*
* @var string
*/
protected $bin;
/**
* Instance of the options parser to use.
*
* @var Options
*/
protected $options;
/**
* PSR-3 compatible log levels and their prefix, color, output channel.
*
* @var array<array>
*/
protected $loglevels = [LogLevel::DEBUG => ['', Colors::C_RESET, \STDOUT], LogLevel::INFO => [' ', Colors::C_CYAN, \STDOUT], LogLevel::NOTICE => ['☛ ', Colors::C_CYAN, \STDOUT], LogLevel::SUCCESS => ['✓ ', Colors::C_GREEN, \STDOUT], LogLevel::WARNING => ['⚠ ', Colors::C_BROWN, \STDERR], LogLevel::ERROR => ['✗ ', Colors::C_RED, \STDERR], LogLevel::CRITICAL => ['☠ ', Colors::C_LIGHTRED, \STDERR], LogLevel::ALERT => ['✖ ', Colors::C_LIGHTRED, \STDERR], LogLevel::EMERGENCY => ['✘ ', Colors::C_LIGHTRED, \STDERR]];
/**
* Default log level.
*
* @var string
*/
protected $loglevel = 'info';
/**
* Constructor.
*
* Initialize the arguments, set up helper classes and set up the CLI environment.
*
* @param bool $autocatch Optional. Whether exceptions should be caught and handled automatically. Defaults
* to true.
* @param Options|null $options Optional. Instance of the Options object to use. Defaults to null to instantiate a
* new one.
* @param Colors|null $colors Optional. Instance of the Colors object to use. Defaults to null to instantiate a
* new one.
*/
public function __construct($autocatch = \true, Options $options = null, Colors $colors = null)
{
if ($autocatch) {
\set_exception_handler([$this, 'fatal']);
}
$this->colors = $colors instanceof Colors ? $colors : new Colors();
$this->options = $options instanceof Options ? $options : new Options($this->colors);
}
/**
* Execute the CLI program.
*
* Executes the setup() routine, adds default options, initiate the options parsing and argument checking
* and finally executes main() - Each part is split into their own protected function below, so behaviour
* can easily be overwritten.
*
* @param bool $exitOnCompletion Optional. Whether to exit on completion. Defaults to true.
* @throws InvalidSapi If a SAPI other than 'cli' is detected.
*/
public function run($exitOnCompletion = \true)
{
$sapi = \php_sapi_name();
if ('cli' !== $sapi) {
throw InvalidSapi::forSapi($sapi);
}
$this->setup($this->options);
$this->registerDefaultOptions();
$this->parseOptions();
$this->handleDefaultOptions();
$this->setupLogging();
$this->checkArguments();
$this->execute();
if ($exitOnCompletion) {
exit(0);
}
}
/**
* Exits the program on a fatal error.
*
* @param Exception|string $error Either an exception or an error message.
* @param array $context Optional. Associative array of contextual information. Defaults to an empty
* array.
*/
public function fatal($error, array $context = [])
{
$code = 0;
if ($error instanceof Exception) {
$this->debug(\get_class($error) . ' caught in ' . $error->getFile() . ':' . $error->getLine());
$this->debug($error->getTraceAsString());
$code = $error->getCode();
$error = $error->getMessage();
}
if (!$code) {
$code = AmpCliException::E_ANY;
}
$this->critical($error, $context);
exit($code);
}
/**
* System is unusable.
*
* @param string $message Log message.
* @param array $context Optional. Contextual information. Defaults to an empty array.
* @return void
*/
public function emergency($message, array $context = [])
{
$this->log(LogLevel::EMERGENCY, $message, $context);
}
/**
* Action must be taken immediately.
*
* Example: Entire website down, database unavailable, etc. This should trigger the SMS alerts and wake you up.
*
* @param string $message Log message.
* @param array $context Optional. Contextual information. Defaults to an empty array.
* @return void
*/
public function alert($message, array $context = [])
{
$this->log(LogLevel::ALERT, $message, $context);
}
/**
* Critical conditions.
*
* Example: Application component unavailable, unexpected exception.
*
* @param string $message Log message.
* @param array $context Optional. Contextual information. Defaults to an empty array.
* @return void
*/
public function critical($message, array $context = [])
{
$this->log(LogLevel::CRITICAL, $message, $context);
}
/**
* Runtime errors that do not require immediate action but should typically be logged and monitored.
*
* @param string $message Log message.
* @param array $context Optional. Contextual information. Defaults to an empty array.
* @return void
*/
public function error($message, array $context = [])
{
$this->log(LogLevel::ERROR, $message, $context);
}
/**
* Exceptional occurrences that are not errors.
*
* Example: Use of deprecated APIs, poor use of an API, undesirable things that are not necessarily wrong.
*
* @param string $message Log message.
* @param array $context Optional. Contextual information. Defaults to an empty array.
* @return void
*/
public function warning($message, array $context = [])
{
$this->log(LogLevel::WARNING, $message, $context);
}
/**
* Normal, positive outcome.
*
* @param string $string Log message.
* @param array $context Optional. Contextual information. Defaults to an empty array.
* @return void
*/
public function success($string, array $context = [])
{
$this->log(LogLevel::SUCCESS, $string, $context);
}
/**
* Normal but significant events.
*
* @param string $message Log message.
* @param array $context Optional. Contextual information. Defaults to an empty array.
* @return void
*/
public function notice($message, array $context = [])
{
$this->log(LogLevel::NOTICE, $message, $context);
}
/**
* Interesting events.
*
* Example: User logs in, SQL logs.
*
* @param string $message Log message.
* @param array $context Optional. Contextual information. Defaults to an empty array.
* @return void
*/
public function info($message, array $context = [])
{
$this->log(LogLevel::INFO, $message, $context);
}
/**
* Detailed debug information.
*
* @param string $message Log message.
* @param array $context Optional. Contextual information. Defaults to an empty array.
* @return void
*/
public function debug($message, array $context = [])
{
$this->log(LogLevel::DEBUG, $message, $context);
}
/**
* Log a message of a given log level to the logs.
*
* @param string $level Log level to use.
* @param string $message Log message.
* @param array $context Optional. Contextual information. Defaults to an empty array.
* @return void
*/
public function log($level, $message, array $context = [])
{
if (!LogLevel::matches($level, $this->options->getOption('loglevel', $this->loglevel))) {
return;
}
list($prefix, $color, $channel) = $this->loglevels[$level];
if (!$this->colors->isEnabled()) {
$prefix = '';
}
$message = $this->interpolate($message, $context);
$this->colors->line($prefix . $message, $color, $channel);
}
/**
* Interpolates context values into the message placeholders.
*
* @param string $message Message to interpolate.
* @param array $context Optional. Contextual information. Defaults to an empty array.
* @return string Interpolated string.
*/
protected function interpolate($message, array $context = [])
{
// Build a replacement array with braces around the context keys.
$replace = [];
foreach ($context as $key => $val) {
// Check that the value can be cast to string.
if (!\is_array($val) && (!\is_object($val) || \method_exists($val, '__toString'))) {
$replace['{' . $key . '}'] = $val;
}
}
// Interpolate replacement values into the message and return.
return \strtr($message, $replace);
}
/**
* Add the default help, color and log options.
*/
protected function registerDefaultOptions()
{
$this->options->registerOption('help', 'Display this help screen and exit immediately.', 'h');
$this->options->registerOption('no-colors', 'Do not use any colors in output. Useful when piping output to other tools or files.');
$this->options->registerOption('loglevel', "Minimum level of messages to display. Default is {$this->colors->wrap($this->loglevel, Colors::C_CYAN)}." . ' Valid levels are: debug, info, notice, success, warning, error, critical, alert, emergency.', null, 'level');
}
/**
* Handle the default options.
*/
protected function handleDefaultOptions()
{
if ($this->options->getOption('no-colors')) {
$this->colors->disable();
}
if ($this->options->getOption('help')) {
echo $this->options->help();
exit(0);
}
}
/**
* Handle the logging options.
*/
protected function setupLogging()
{
$this->loglevel = $this->options->getOption('loglevel', $this->loglevel);
if (!\in_array($this->loglevel, LogLevel::ORDER)) {
$this->fatal('Unknown log level');
}
}
/**
* Wrapper around the option parsing.
*/
protected function parseOptions()
{
$this->options->parseOptions();
}
/**
* Wrapper around the argument checking.
*/
protected function checkArguments()
{
$this->options->checkArguments();
}
/**
* Wrapper around main.
*/
protected function execute()
{
$this->main($this->options);
}
/**
* Register options and arguments on the given $options object.
*
* @param Options $options Options instance to register the commands with.
* @return void
*/
protected abstract function setup(Options $options);
/**
* Main program routine.
*
* Arguments and options have been parsed when this is run.
*
* @param Options $options Options instance to register the commands with.
* @return void
*/
protected abstract function main(Options $options);
}

View File

@@ -0,0 +1,94 @@
<?php
namespace Google\Web_Stories_Dependencies\AmpProject\Cli;
use Google\Web_Stories_Dependencies\AmpProject\Exception\AmpCliException;
use Google\Web_Stories_Dependencies\AmpProject\Exception\Cli\InvalidSapi;
use Exception;
/**
* Abstract class with the individual log levels.
*
* @package ampproject/amp-toolbox
*/
abstract class LogLevel
{
/**
* Detailed debug information.
*
* @var string
*/
const DEBUG = 'debug';
/**
* Interesting events.
*
* Example: User logs in, SQL logs.
*
* @var string
*/
const INFO = 'info';
/**
* Normal but significant events.
*
* @var string
*/
const NOTICE = 'notice';
/**
* Normal, positive outcome.
*
* @var string
*/
const SUCCESS = 'success';
/**
* Exceptional occurrences that are not errors.
*
* Example: Use of deprecated APIs, poor use of an API, undesirable things that are not necessarily wrong.
*
* @var string
*/
const WARNING = 'warning';
/**
* Runtime errors that do not require immediate action but should typically be logged and monitored.
*
* @var string
*/
const ERROR = 'error';
/**
* Critical conditions.
*
* Example: Application component unavailable, unexpected exception.
*
* @var string
*/
const CRITICAL = 'critical';
/**
* Action must be taken immediately.
*
* Example: Entire website down, database unavailable, etc. This should trigger the SMS alerts and wake you up.
*
* @var string
*/
const ALERT = 'alert';
/**
* System is unusable.
*
* @var string
*/
const EMERGENCY = 'emergency';
/**
* Ordering to use for log levels.
*
* @var string[]
*/
const ORDER = [self::DEBUG, self::INFO, self::NOTICE, self::SUCCESS, self::WARNING, self::ERROR, self::CRITICAL, self::ALERT, self::EMERGENCY];
/**
* Test whether a given log level matches the currently set threshold.
*
* @param string $logLevel Log level to check.
* @param string $threshold Log level threshold to check against.
* @return bool Whether the provided log level matches the threshold.
*/
public static function matches($logLevel, $threshold)
{
return \array_search($logLevel, self::ORDER, \true) >= \array_search($threshold, self::ORDER, \true);
}
}

View File

@@ -0,0 +1,455 @@
<?php
namespace Google\Web_Stories_Dependencies\AmpProject\Cli;
use Google\Web_Stories_Dependencies\AmpProject\Exception\Cli\InvalidArgument;
use Google\Web_Stories_Dependencies\AmpProject\Exception\Cli\InvalidCommand;
use Google\Web_Stories_Dependencies\AmpProject\Exception\Cli\InvalidOption;
use Google\Web_Stories_Dependencies\AmpProject\Exception\Cli\MissingArgument;
/**
* This file is adapted from the splitbrain\php-cli library, which is authored by Andreas Gohr <andi@splitbrain.org> and
* licensed under the MIT license.
*
* Source: https://github.com/splitbrain/php-cli/blob/8c2c001b1b55d194402cf18aad2757049ac6d575/src/Options.php
*/
/**
* Parses command line options passed to the CLI script. Allows CLI scripts to easily register all accepted options and
* commands and even generates a help text from this setup.
*
* @package ampproject/amp-toolbox
*/
class Options
{
/**
* List of options to parse.
*
* @var array
*/
protected $setup;
/**
* Storage for parsed options.
*
* @var array
*/
protected $options = [];
/**
* Currently parsed command if any.
*
* @var string
*/
protected $command = '';
/**
* Passed non-option arguments.
*
* @var array
*/
protected $arguments = [];
/**
* Name of the executed script.
*
* @var string
*/
protected $bin;
/**
* Instance of the Colors helper object.
*
* @var Colors
*/
protected $colors;
/**
* Newline used for spacing help texts.
*
* @var string
*/
protected $newline = "\n";
/**
* Constructor.
*
* @param Colors $colors Optional. Configured color object.
* @throws InvalidArgument When arguments can't be read.
*/
public function __construct(Colors $colors = null)
{
$this->colors = $colors instanceof Colors ? $colors : new Colors();
$this->setup = ['' => ['options' => [], 'arguments' => [], 'help' => '', 'commandHelp' => 'This tool accepts a command as first parameter as outlined below:']];
// Default command.
$this->arguments = $this->readPHPArgv();
$this->bin = \basename(\array_shift($this->arguments));
$this->options = [];
}
/**
* Gets the name of the binary that was executed.
*
* @return string Name of the binary that was executed.
*/
public function getBin()
{
return $this->bin;
}
/**
* Sets the help text for the tool itself.
*
* @param string $help Help text to set.
*/
public function setHelp($help)
{
$this->setup['']['help'] = $help;
}
/**
* Sets the help text for the tools commands itself.
*
* @param string $help Help text to set.
*/
public function setCommandHelp($help)
{
$this->setup['']['commandHelp'] = $help;
}
/**
* Use a more compact help screen with less new lines.
*
* @param bool $set Optional. Whether to set compact help or not. Defaults to true.
*/
public function useCompactHelp($set = \true)
{
$this->newline = $set ? '' : "\n";
}
/**
* Register the names of arguments for help generation and number checking.
*
* This has to be called in the order arguments are expected.
*
* @param string $name Name of the argument.
* @param string $help Help text.
* @param bool $required Optional. Whether this argument is required. Defaults to true.
* @param string $command Optional. Command this argument applies to. Empty string (default) for global arguments.
* @throws InvalidCommand If the referenced command is not registered.
*/
public function registerArgument($name, $help, $required = \true, $command = '')
{
if (!isset($this->setup[$command])) {
throw InvalidCommand::forUnregisteredCommand($command);
}
$this->setup[$command]['arguments'][] = ['name' => $name, 'help' => $help, 'required' => $required];
}
/**
* Register a sub command.
*
* Sub commands have their own options and use their own function (not main()).
*
* @param string $name Name of the command to register.
* @param string $help Help text of the command.
* @throws InvalidCommand If the referenced command is already registered.
*/
public function registerCommand($name, $help)
{
if (isset($this->setup[$name])) {
throw InvalidCommand::forAlreadyRegisteredCommand($name);
}
$this->setup[$name] = ['options' => [], 'arguments' => [], 'help' => $help];
}
/**
* Register an option for option parsing and help generation.
*
* @param string $long Multi character option (specified with --).
* @param string $help Help text for this option.
* @param string|null $short Optional. One character option (specified with -). Disable with null (default).
* @param bool|string $needsArgument Optional. Whether this option requires an argument. Use a boolean value, or
* provide a string to require a specific argument by name. Defaults to false.
* @param string $command Optional. Name of the command this option applies to. Use an empty string for
* none (default).
* @throws InvalidCommand If the referenced command is not registered.
* @throws InvalidArgument If the short option is too long.
*/
public function registerOption($long, $help, $short = null, $needsArgument = \false, $command = '')
{
if (!isset($this->setup[$command])) {
throw InvalidCommand::forUnregisteredCommand($command);
}
$this->setup[$command]['options'][$long] = ['needsArgument' => $needsArgument, 'help' => $help, 'short' => $short];
if ($short) {
if (\strlen($short) > 1) {
throw InvalidArgument::forMultiCharacterShortOption();
}
$this->setup[$command]['short'][$short] = $long;
}
}
/**
* Checks the actual number of arguments against the required number.
*
* This is run from CLI automatically and usually does not need to be called directly.
*
* @throws MissingArgument If not enough arguments were provided.
*/
public function checkArguments()
{
$argumentCount = \count($this->arguments);
$required = 0;
foreach ($this->setup[$this->command]['arguments'] as $argument) {
if (!$argument['required']) {
break;
}
// Last required arguments seen.
$required++;
}
if ($required > $argumentCount) {
throw MissingArgument::forNotEnoughArguments();
}
}
/**
* Parses the given arguments for known options and command.
*
* The given $arguments array should NOT contain the executed file as first item anymore! The $arguments
* array is stripped from any options and possible command. All found options can be accessed via the
* getOptions() function.
*
* Note that command options will overwrite any global options with the same name.
*
* This is run from CLI automatically and usually does not need to be called directly.
*
* @throws InvalidOption If an unknown option was provided.
* @throws MissingArgument If an argument is missing.
*/
public function parseOptions()
{
$nonOptions = [];
$argumentCount = \count($this->arguments);
for ($index = 0; $index < $argumentCount; $index++) {
$argument = $this->arguments[$index];
// The special element '--' means explicit end of options. Treat the rest of the arguments as non-options
// and end the loop.
if ($argument == '--') {
$nonOptions = \array_merge($nonOptions, \array_slice($this->arguments, $index + 1));
break;
}
// '-' is stdin - a normal argument.
if ($argument == '-') {
$nonOptions = \array_merge($nonOptions, \array_slice($this->arguments, $index));
break;
}
// First non-option.
if ($argument[0] != '-') {
$nonOptions = \array_merge($nonOptions, \array_slice($this->arguments, $index));
break;
}
// Long option.
if (\strlen($argument) > 1 && $argument[1] === '-') {
$argument = \explode('=', \substr($argument, 2), 2);
$option = \array_shift($argument);
$value = \array_shift($argument);
if (!isset($this->setup[$this->command]['options'][$option])) {
throw InvalidOption::forUnknownOption($option);
}
// Argument required?
if ($this->setup[$this->command]['options'][$option]['needsArgument']) {
if (\is_null($value) && $index + 1 < $argumentCount && !\preg_match('/^--?[\\w]/', $this->arguments[$index + 1])) {
$value = $this->arguments[++$index];
}
if (\is_null($value)) {
throw MissingArgument::forNoArgument($option);
}
$this->options[$option] = $value;
} else {
$this->options[$option] = \true;
}
continue;
}
// Short option.
$option = \substr($argument, 1);
if (!isset($this->setup[$this->command]['short'][$option])) {
throw InvalidOption::forUnknownOption($option);
} else {
$option = $this->setup[$this->command]['short'][$option];
// Store it under long name.
}
// Argument required?
if ($this->setup[$this->command]['options'][$option]['needsArgument']) {
$value = null;
if ($index + 1 < $argumentCount && !\preg_match('/^--?[\\w]/', $this->arguments[$index + 1])) {
$value = $this->arguments[++$index];
}
if (\is_null($value)) {
throw MissingArgument::forNoArgument($option);
}
$this->options[$option] = $value;
} else {
$this->options[$option] = \true;
}
}
// Parsing is now done, update arguments array.
$this->arguments = $nonOptions;
// If not done yet, check if first argument is a command and re-execute argument parsing if it is.
if (!$this->command && $this->arguments && isset($this->setup[$this->arguments[0]])) {
// It is a command!
$this->command = \array_shift($this->arguments);
$this->parseOptions();
// Second pass.
}
}
/**
* Get the value of the given option.
*
* Please note that all options are accessed by their long option names regardless of how they were
* specified on commandline.
*
* Can only be used after parseOptions() has been run.
*
* @param string $option Option to get.
* @param bool|string $default Optional. Default value to return if the option is not set. Defaults to false.
* @return bool|string Value of the option.
*/
public function getOption($option, $default = \false)
{
if (isset($this->options[$option])) {
return $this->options[$option];
}
return $default;
}
/**
* Get all options.
*
* Please note that all options are accessed by their long option names regardless of how they were
* specified on commandline.
*
* Can only be used after parseOptions() has been run.
*
* @return string[] Associative array of all options.
*/
public function getOptions()
{
return $this->options;
}
/**
* Return the found command, if any.
*
* @return string Name of the command that was found.
*/
public function getCommand()
{
return $this->command;
}
/**
* Get all the arguments passed to the script.
*
* This will not contain any recognized options or the script name itself.
*
* @return array Associative array of arguments.
*/
public function getArguments()
{
return $this->arguments;
}
/**
* Builds a help screen from the available options.
*
* You may want to call it from -h or on error.
*
* @return string Help screen text.
*/
public function help()
{
$tableFormatter = new TableFormatter($this->colors);
$text = '';
$hasCommands = \count($this->setup) > 1;
$commandHelp = $this->setup['']['commandHelp'];
foreach ($this->setup as $command => $config) {
$hasOptions = (bool) $this->setup[$command]['options'];
$hasArguments = (bool) $this->setup[$command]['arguments'];
// Usage or command syntax line.
if (!$command) {
$text .= $this->colors->wrap('USAGE:', Colors::C_BROWN);
$text .= "\n";
$text .= ' ' . $this->bin;
$indentation = 2;
} else {
$text .= $this->newline;
$text .= $this->colors->wrap(' ' . $command, Colors::C_PURPLE);
$indentation = 4;
}
if ($hasOptions) {
$text .= ' ' . $this->colors->wrap('<OPTIONS>', Colors::C_GREEN);
}
if (!$command && $hasCommands) {
$text .= ' ' . $this->colors->wrap('<COMMAND> ...', Colors::C_PURPLE);
}
foreach ($this->setup[$command]['arguments'] as $argument) {
$output = $this->colors->wrap('<' . $argument['name'] . '>', Colors::C_CYAN);
if (!$argument['required']) {
$output = '[' . $output . ']';
}
$text .= ' ' . $output;
}
$text .= $this->newline;
// Usage or command intro.
if ($this->setup[$command]['help']) {
$text .= "\n";
$text .= $tableFormatter->format([$indentation, '*'], ['', $this->setup[$command]['help'] . $this->newline]);
}
// Option description.
if ($hasOptions) {
if (!$command) {
$text .= "\n";
$text .= $this->colors->wrap('OPTIONS:', Colors::C_BROWN);
}
$text .= "\n";
foreach ($this->setup[$command]['options'] as $long => $option) {
$name = '';
if ($option['short']) {
$name .= '-' . $option['short'];
if ($option['needsArgument']) {
$name .= ' <' . $option['needsArgument'] . '>';
}
$name .= ', ';
}
$name .= "--{$long}";
if ($option['needsArgument']) {
$name .= ' <' . $option['needsArgument'] . '>';
}
$text .= $tableFormatter->format([$indentation, '30%', '*'], ['', $name, $option['help']], ['', 'green', '']);
$text .= $this->newline;
}
}
// Argument description.
if ($hasArguments) {
if (!$command) {
$text .= "\n";
$text .= $this->colors->wrap('ARGUMENTS:', Colors::C_BROWN);
}
$text .= $this->newline;
foreach ($this->setup[$command]['arguments'] as $argument) {
$name = '<' . $argument['name'] . '>';
$text .= $tableFormatter->format([$indentation, '30%', '*'], ['', $name, $argument['help']], ['', 'cyan', '']);
}
}
// Headline and intro for following command documentation.
if (!$command && $hasCommands) {
$text .= "\n";
$text .= $this->colors->wrap('COMMANDS:', Colors::C_BROWN);
$text .= "\n";
$text .= $tableFormatter->format([$indentation, '*'], ['', $commandHelp]);
$text .= $this->newline;
}
}
return $text;
}
/**
* Safely read the $argv PHP array across different PHP configurations.
* Will take care of register_globals and register_argc_argv ini directives.
*
* @return array The $argv PHP array.
* @throws InvalidArgument If the $argv array could not be read.
*/
private function readPHPArgv()
{
global $argv;
if (\is_array($argv)) {
return $argv;
}
if (\is_array($_SERVER) && \array_key_exists('argv', $_SERVER) && \is_array($_SERVER['argv'])) {
return $_SERVER['argv'];
}
if (\array_key_exists('HTTP_SERVER_VARS', $GLOBALS) && \is_array($GLOBALS['HTTP_SERVER_VARS']) && \array_key_exists('argv', $GLOBALS['HTTP_SERVER_VARS']) && \is_array($GLOBALS['HTTP_SERVER_VARS']['argv'])) {
return $GLOBALS['HTTP_SERVER_VARS']['argv'];
}
throw InvalidArgument::forUnreadableArguments();
}
}

View File

@@ -0,0 +1,497 @@
<?php
namespace Google\Web_Stories_Dependencies\AmpProject\Cli;
use Google\Web_Stories_Dependencies\AmpProject\Exception\Cli\InvalidColumnFormat;
/**
* This file is adapted from the splitbrain\php-cli library, which is authored by Andreas Gohr <andi@splitbrain.org> and
* licensed under the MIT license.
*
* Source: https://github.com/splitbrain/php-cli/blob/8c2c001b1b55d194402cf18aad2757049ac6d575/src/TableFormatter.php
*/
/**
* Output text in multiple columns.
*
* @package ampproject/amp-toolbox
*/
class TableFormatter
{
/**
* Border between columns.
*
* @var string
*/
protected $border = ' ';
/**
* Padding around the border.
*
* @var int
*/
protected $padding = 0;
/**
* The terminal width in characters.
*
* Falls back to 74 characters if it cannot be detected.
*
* @var int
*/
protected $maxWidth = 74;
/**
* Instance of the Colors helper object.
*
* @var Colors
*/
protected $colors;
/**
* Width of each column size based on the content length.
*
* @var array
*/
protected $tableColumnWidths = [];
/**
* Maximum length of the table content.
*
* @var int
*/
protected $maxColumnWidth = 0;
/**
* Whether to wrap the table with borders.
*
* @var bool
*/
protected $isBorderedTable = \false;
/**
* TableFormatter constructor.
*
* @param Colors|null $colors Optional. Instance of the Colors helper object.
*/
public function __construct(Colors $colors = null)
{
// Try to get terminal width.
$width = $this->getTerminalWidth();
if ($width) {
$this->maxWidth = $width - 1;
}
$this->colors = $colors instanceof Colors ? $colors : new Colors();
}
/**
* The currently set border.
*
* Defaults to ' '.
*
* @return string
*/
public function getBorder()
{
return $this->border;
}
/**
* Set the border.
*
* The border is set between each column. Its width is added to the column widths.
*
* @param string $border Border to set.
*/
public function setBorder($border)
{
$this->border = $border;
}
/**
* Set the padding.
*
* The padding around the border is added to the column widths.
*
* @param int $padding Padding to set.
*/
public function setPadding($padding)
{
$this->padding = $padding;
}
/**
* Width of the terminal in characters.
*
* Initially auto-detected, with a fallback of 74 characters.
*
* @return int
*/
public function getMaxWidth()
{
return $this->maxWidth;
}
/**
* Set the width of the terminal to assume (in characters).
*
* @param int $maxWidth Terminal width in characters.
*/
public function setMaxWidth($maxWidth)
{
$this->maxWidth = $maxWidth;
}
/**
* Displays text in multiple word wrapped columns.
*
* @param array<int|string> $columns List of column widths (in characters, percent or '*').
* @param array<string> $texts List of texts for each column.
* @param array<string> $colors Optional. A list of color names to use for each column. Use empty string within
* the array for default. Defaults to an empty array.
* @return string Adapted text.
*/
public function format($columns, $texts, $colors = [])
{
$columns = $this->calculateColumnWidths($columns);
$wrapped = [];
$maxLength = 0;
foreach ($columns as $column => $width) {
$wrapped[$column] = \explode("\n", $this->wordwrap($texts[$column], $width, "\n", \true));
$length = \count($wrapped[$column]);
if ($length > $maxLength) {
$maxLength = $length;
}
}
$last = \count($columns) - 1;
$output = '';
for ($index = 0; $index < $maxLength; $index++) {
foreach ($columns as $column => $width) {
if ($this->isBorderedTable && $column === 0) {
$output .= $this->border . \str_repeat(' ', $this->padding);
$width = $width - \strlen($this->border) - $this->padding;
}
if (isset($wrapped[$column][$index])) {
$value = $wrapped[$column][$index];
} else {
$value = '';
}
if ($this->isBorderedTable && $column === $last && $width > $this->tableColumnWidths[$last]) {
$width = $this->tableColumnWidths[$last];
}
$chunk = $this->pad($value, $width);
if (isset($colors[$column]) && $colors[$column]) {
$chunk = $this->colors->wrap($chunk, $colors[$column]);
}
$output .= $chunk;
// Add border in-between columns.
if ($column != $last) {
$output .= \str_repeat(' ', $this->padding) . $this->border . \str_repeat(' ', $this->padding);
}
if ($this->isBorderedTable && $column === $last) {
$output .= \str_repeat(' ', $this->padding) . $this->border;
}
}
$output .= "\n";
}
return $output;
}
/**
* Tries to figure out the width of the terminal.
*
* @return int Terminal width, 0 if unknown.
*/
protected function getTerminalWidth()
{
// From environment.
if (isset($_SERVER['COLUMNS'])) {
return (int) $_SERVER['COLUMNS'];
}
// Via tput.
$process = \proc_open('tput cols', [1 => ['pipe', 'w'], 2 => ['pipe', 'w']], $pipes);
$width = (int) \stream_get_contents($pipes[1]);
\proc_close($process);
return $width;
}
/**
* Takes an array with dynamic column width and calculates the correct widths.
*
* Column width can be given as fixed char widths, percentages and a single * width can be given
* for taking the remaining available space. When mixing percentages and fixed widths, percentages
* refer to the remaining space after allocating the fixed width.
*
* @param array $columns Columns to calculate the widths for.
* @return int[] Array of calculated column widths.
* @throws InvalidColumnFormat If the column format is not valid.
*/
protected function calculateColumnWidths($columns)
{
$index = 0;
$border = $this->strlen($this->border);
$fixed = (\count($columns) - 1) * $border;
// Borders are used already.
$fluid = -1;
// First pass for format check and fixed columns.
foreach ($columns as $index => $column) {
// Handle fixed columns.
if ((string) \intval($column) === (string) $column) {
$fixed += $column;
continue;
}
// Check if other columns are using proper units.
if (\substr($column, -1) === '%') {
continue;
}
if ($column === '*') {
// Only one fluid.
if ($fluid < 0) {
$fluid = $index;
continue;
} else {
throw InvalidColumnFormat::forMultipleFluidColumns();
}
}
throw InvalidColumnFormat::forUnknownColumnFormat($column);
}
$allocated = $fixed;
$remain = $this->maxWidth - $allocated;
// Second pass to handle percentages.
foreach ($columns as $index => $column) {
if (\substr($column, -1) !== '%') {
continue;
}
$percent = \floatval($column);
$real = (int) \floor($percent * $remain / 100);
$columns[$index] = $real;
$allocated += $real;
}
$remain = $this->maxWidth - $allocated;
if ($remain < 0) {
throw InvalidColumnFormat::forExceededMaxWidth();
}
// Assign remaining space.
if ($fluid < 0) {
$columns[$index] += $remain;
// Add to last column.
} else {
$columns[$fluid] = $remain;
}
return $columns;
}
/**
* Pad the given string to the correct length.
*
* @param string $string String to pad.
* @param int $length Length to pad the string to.
* @return string Padded string.
*/
protected function pad($string, $length)
{
$strlen = $this->strlen($string);
if ($strlen > $length) {
return $string;
}
$pad = $length - $strlen;
return $string . \str_pad('', $pad, ' ');
}
/**
* Measures character length in UTF-8 when possible.
*
* @param string $string String to measure the character length of.
* @return int Count of characters.
*/
protected function strlen($string)
{
// Don't count color codes.
$string = \preg_replace("/\x1b\\[\\d+(;\\d+)?m/", '', $string);
if (\function_exists('mb_strlen')) {
return \mb_strlen($string, 'utf-8');
}
return \strlen($string);
}
/**
* Extract a substring in UTF-8 if possible.
* @param string $string String to extract a substring out of.
* @param int $start Optional. Starting index to extract from. Defaults to 0.
* @param int|null $length Optional. Length to extract. Set to null to use the remainder of the string (default).
* @return string Extracted substring.
*/
protected function substr($string, $start = 0, $length = null)
{
if (\function_exists('mb_substr')) {
return \mb_substr($string, $start, $length);
}
// mb_substr() treats $length differently than substr().
if ($length) {
return \substr($string, $start, $length);
}
return \substr($string, $start);
}
/**
* Wrap words of a string into a requested width.
*
* @param string $string String to wrap.
* @param int $width Optional. Width to warp the string into. Defaults to 75.
* @param string $break Optional. Character to use for wrapping. Defaults to a newline character. Defaults to the
* newline character.
* @param bool $cut Optional. Whether to cut longer words to enforce the width. Defaults to false.
* @return string Word-wrapped string.
* @link http://stackoverflow.com/a/4988494
*/
protected function wordwrap($string, $width = 75, $break = "\n", $cut = \false)
{
if (!\is_int($width) || $width < 0) {
$width = 75;
}
if (!\is_string($break) || empty($break)) {
$break = "\n";
}
$lines = \explode($break, $string);
foreach ($lines as &$line) {
$line = \rtrim($line);
if ($this->strlen($line) <= $width) {
continue;
}
$words = \explode(' ', $line);
$line = '';
$actual = '';
foreach ($words as $word) {
if ($this->strlen($actual . $word) <= $width) {
$actual .= $word . ' ';
} else {
if ($actual != '') {
$line .= \rtrim($actual) . $break;
}
$actual = $word;
if ($cut) {
while ($this->strlen($actual) > $width) {
$line .= $this->substr($actual, 0, $width) . $break;
$actual = $this->substr($actual, $width);
}
}
$actual .= ' ';
}
}
$line .= \trim($actual);
}
return \implode($break, $lines);
}
/**
* Format the rows in a bordered table.
*
* @param array<array<string>> $rows List of texts for each column.
* @param array<string> $headers Optional. List of texts used in the table header.
*
* @return string A borered table containing the given rows.
*/
public function formatTable($rows, $headers = [])
{
$this->setBorder('|');
$this->setPadding(1);
$this->setIsBorderedTable(\true);
if (!empty($headers)) {
$this->calculateTableColumnWidths($headers);
}
foreach ($rows as $row) {
$this->calculateTableColumnWidths($row);
}
$numberOfColumns = \count($this->tableColumnWidths);
$columns = \array_map(function ($width, $index) {
// Add extra padding to the first and last columns.
if ($index === 0 || $index === \count($this->tableColumnWidths) - 1) {
$width = $width + \strlen($this->border) + $this->padding;
}
return $width;
}, $this->tableColumnWidths, \array_keys($this->tableColumnWidths));
// For a three column table, we'll have have "| " at start and " |" at the end,
// and in-between two " | ". So in total "| " + " | " + " | " + " |" = 10 chars.
$borderCharWidth = \strlen($this->border);
$totalBorderWidth = 2 * ($borderCharWidth + $this->padding) + ($numberOfColumns - 1) * ($borderCharWidth + $this->padding * 2);
$estimatedColumnWidth = $numberOfColumns * $this->maxColumnWidth;
$estimatedTotalWidth = $totalBorderWidth + $estimatedColumnWidth;
if ($estimatedTotalWidth > $this->maxWidth) {
$maxWidthWithoutBorders = $this->maxWidth - $totalBorderWidth;
$avrg = \floor($maxWidthWithoutBorders / $numberOfColumns);
$resizedWidths = [];
$extraWidth = 0;
foreach ($this->tableColumnWidths as $width) {
if ($width > $avrg) {
$resizedWidths[] = $width;
} else {
$extraWidth = $extraWidth + ($avrg - $width);
}
}
if (!empty($resizedWidths) && $extraWidth) {
$avrgExtraWidth = \floor($extraWidth / \count($resizedWidths));
foreach ($this->tableColumnWidths as $i => &$width) {
if (\in_array($width, $resizedWidths, \true)) {
$width = $avrg + $avrgExtraWidth;
\array_shift($resizedWidths);
if (empty($resizedWidths)) {
$width = 0;
// Zero it so not in sum.
$width = $maxWidthWithoutBorders - \array_sum($this->tableColumnWidths);
}
}
if ($i === 0 || $i === $numberOfColumns - 1) {
$width = $width + \strlen($this->border) + $this->padding;
}
$columns[$i] = \intval($width);
}
}
}
$horizontalBorder = $this->getTableHorizontalBorder($rows[0], $columns);
$table = $horizontalBorder . "\n";
if (!empty($headers)) {
$table .= $this->getTableRow($headers, $columns);
$table .= $horizontalBorder . "\n";
}
foreach ($rows as $row) {
$table .= $this->getTableRow($row, $columns);
}
$table .= $horizontalBorder;
return $table;
}
/**
* Whether the table is wrapped with borders or not.
*
* @param bool $isBorderedTable Whether the table is wrapped with borders or not.
*/
public function setIsBorderedTable($isBorderedTable)
{
$this->isBorderedTable = $isBorderedTable;
}
/**
* Calculate table column widths based on the column content length.
*
* @param array<string> $row List of texts for each column.
*/
protected function calculateTableColumnWidths($row)
{
foreach ($row as $i => $rowContent) {
$width = \strlen($rowContent);
if ($width > $this->maxColumnWidth) {
$this->maxColumnWidth = $width;
}
if (!isset($this->tableColumnWidths[$i]) || $width > $this->tableColumnWidths[$i]) {
$this->tableColumnWidths[$i] = $width;
}
}
}
/**
* Get the table row.
*
* @param array<string> $row List of texts for each column.
* @param array<int> $columns List of maximum column widths.
*
* @return string Table row.
*/
protected function getTableRow($row, $columns)
{
return \trim($this->format($columns, $row)) . "\n";
}
/**
* Get the table horizontal border.
*
* @param array<string> $row List of texts for each column.
* @param array<int> $columns List of maximum column widths.
*
* @return string Table border.
*/
protected function getTableHorizontalBorder($row, $columns)
{
$tableRow = $this->getTableRow($row, $columns);
$tableRow = \explode("\n", $tableRow);
$firstRow = \array_shift($tableRow);
$firstRow = \trim($firstRow);
$borderChar = \preg_quote($this->border, '/');
$border = \preg_replace("/[^{$borderChar}]/", '-', $firstRow);
$border = \preg_replace("/[{$borderChar}]/", '+', $border);
return $border;
}
}

View File

@@ -0,0 +1,18 @@
<?php
namespace Google\Web_Stories_Dependencies\AmpProject;
/**
* Compatibility fix that can be registered.
*
* @package ampproject/amp-toolbox
*/
interface CompatibilityFix
{
/**
* Register the compatibility fix.
*
* @return void
*/
public static function register();
}

View File

@@ -0,0 +1,51 @@
<?php
namespace Google\Web_Stories_Dependencies\AmpProject\CompatibilityFix;
use Google\Web_Stories_Dependencies\AmpProject\CompatibilityFix;
/**
* Backwards compatibility fix for classes that were moved.
*
* @package ampproject/amp-toolbox
*/
final class MovedClasses implements CompatibilityFix
{
/**
* Mapping of aliases to be registered.
*
* @var array<string, string> Associative array of class alias mappings.
*/
const ALIASES = [
// v0.9.0 - moved HTML-based utility into a separate `Html` sub-namespace.
'Google\\Web_Stories_Dependencies\\AmpProject\\AtRule' => 'Google\\Web_Stories_Dependencies\\AmpProject\\Html\\AtRule',
'Google\\Web_Stories_Dependencies\\AmpProject\\Attribute' => 'Google\\Web_Stories_Dependencies\\AmpProject\\Html\\Attribute',
'Google\\Web_Stories_Dependencies\\AmpProject\\LengthUnit' => 'Google\\Web_Stories_Dependencies\\AmpProject\\Html\\LengthUnit',
'Google\\Web_Stories_Dependencies\\AmpProject\\RequestDestination' => 'Google\\Web_Stories_Dependencies\\AmpProject\\Html\\RequestDestination',
'Google\\Web_Stories_Dependencies\\AmpProject\\Role' => 'Google\\Web_Stories_Dependencies\\AmpProject\\Html\\Role',
'Google\\Web_Stories_Dependencies\\AmpProject\\Tag' => 'Google\\Web_Stories_Dependencies\\AmpProject\\Html\\Tag',
// v0.9.0 - extracted `Encoding` out of `Dom\Document`, as it is turned into AMP value object.
'Google\\Web_Stories_Dependencies\\AmpProject\\Dom\\Document\\Encoding' => 'Google\\Web_Stories_Dependencies\\AmpProject\\Encoding',
];
/**
* Register the compatibility fix.
*
* @return void
*/
public static function register()
{
\spl_autoload_register(__CLASS__ . '::autoloader');
}
/**
* Autoloader to register.
*
* @param string $oldClassName Old class name that was requested to be autoloaded.
* @return void
*/
public static function autoloader($oldClassName)
{
if (!\array_key_exists($oldClassName, self::ALIASES)) {
return;
}
\class_alias(self::ALIASES[$oldClassName], $oldClassName, \true);
}
}

View File

@@ -0,0 +1,159 @@
<?php
namespace Google\Web_Stories_Dependencies\AmpProject;
/**
* Flexible unit of measure for CSS dimensions.
*
* Adapted from the `amp.validator.CssLength` class found in `validator.js` from the `ampproject/amphtml` project on
* GitHub.
*
* @version 1911070201440
* @link https://github.com/ampproject/amphtml/blob/1911070201440/validator/engine/validator.js#L3351
*
* @package ampproject/amp-toolbox
*/
final class CssLength
{
// Special attribute values.
const AUTO = 'auto';
const FLUID = 'fluid';
/**
* Whether the value or unit is invalid. Note that passing an empty value as `$attr_value` is considered valid.
*
* @var bool
*/
protected $isValid = \false;
/**
* Whether the attribute value is set.
*
* @var bool
*/
protected $isDefined = \false;
/**
* Whether the attribute value is 'auto'. This is a special value that indicates that the value gets derived from
* the context. In practice that's only ever the case for a width.
*
* @var bool
*/
protected $isAuto = \false;
/**
* Whether the attribute value is 'fluid'.
*
* @var bool
*/
protected $isFluid = \false;
/**
* The numeric value.
*
* @var float
*/
protected $numeral = 0;
/**
* The unit, 'px' being the default in case it's absent.
*
* @var string
*/
protected $unit = 'px';
/**
* Value of attribute.
*
* @var string
*/
protected $attrValue;
/**
* Instantiate a CssLength object.
*
* @param string|null $attrValue Attribute value to be parsed.
*/
public function __construct($attrValue)
{
if (null === $attrValue) {
$this->isValid = \true;
return;
}
$this->attrValue = $attrValue;
$this->isDefined = \true;
}
/**
* Validate the attribute value.
*
* @param bool $allowAuto Whether or not to allow the 'auto' value as a value.
* @param bool $allowFluid Whether or not to allow the 'fluid' value as a value.
*/
public function validate($allowAuto, $allowFluid)
{
if ($this->isValid()) {
return;
}
if (self::AUTO === $this->attrValue) {
$this->isAuto = \true;
$this->isValid = $allowAuto;
return;
}
if (self::FLUID === $this->attrValue) {
$this->isFluid = \true;
$this->isValid = $allowFluid;
}
$pattern = '/^(?<numeral>\\d+(?:\\.\\d+)?)(?<unit>px|em|rem|vh|vw|vmin|vmax)?$/';
if (\preg_match($pattern, $this->attrValue, $match)) {
$this->isValid = \true;
$this->numeral = isset($match['numeral']) ? (float) $match['numeral'] : $this->numeral;
$this->unit = isset($match['unit']) ? $match['unit'] : $this->unit;
}
}
/**
* Whether or not the attribute value is valid.
*
* @return bool
*/
public function isValid()
{
return $this->isValid;
}
/**
* Whether the attribute value is set.
*
* @return bool
*/
public function isDefined()
{
return $this->isDefined;
}
/**
* Whether the attribute value is 'fluid'.
*
* @return bool
*/
public function isFluid()
{
return $this->isFluid;
}
/**
* Whether the attribute value is 'auto'.
*
* @return bool
*/
public function isAuto()
{
return $this->isAuto;
}
/**
* The unit of the attribute.
*
* @return string
*/
public function getUnit()
{
return $this->unit;
}
/**
* The numeral of the attribute.
*
* @return float
*/
public function getNumeral()
{
return $this->numeral;
}
}

View File

@@ -0,0 +1,75 @@
<?php
namespace Google\Web_Stories_Dependencies\AmpProject;
use Google\Web_Stories_Dependencies\AmpProject\Dom\Document;
use Google\Web_Stories_Dependencies\AmpProject\Dom\Element;
use DOMNode;
/**
* Helper functionality to deal with AMP dev-mode.
*
* @link https://github.com/ampproject/amphtml/issues/20974
*
* @package ampproject/amp-toolbox
*/
final class DevMode
{
/**
* Attribute name for AMP dev mode.
*
* @var string
*/
const DEV_MODE_ATTRIBUTE = 'data-ampdevmode';
/**
* Check whether the provided document is in dev mode.
*
* @param Document $document Document for which to check whether dev mode is active.
* @return bool Whether the document is in dev mode.
*/
public static function isActiveForDocument(Document $document)
{
return $document->documentElement->hasAttribute(self::DEV_MODE_ATTRIBUTE);
}
/**
* Check whether a node is exempt from validation during dev mode.
*
* @param DOMNode $node Node to check.
* @return bool Whether the node should be exempt during dev mode.
*/
public static function hasExemptionForNode(DOMNode $node)
{
if (!$node instanceof Element) {
return \false;
}
$document = self::getDocument($node);
if ($node === $document->documentElement) {
return $document->hasInitialAmpDevMode();
}
return $node->hasAttribute(self::DEV_MODE_ATTRIBUTE);
}
/**
* Check whether a certain node should be exempt from validation.
*
* @param DOMNode $node Node to check.
* @return bool Whether the node should be exempt from validation.
*/
public static function isExemptFromValidation(DOMNode $node)
{
$document = self::getDocument($node);
return self::isActiveForDocument($document) && self::hasExemptionForNode($node);
}
/**
* Get the document from the specified node.
*
* @param DOMNode $node The Node from which the document should be retrieved.
* @return Document
*/
private static function getDocument(DOMNode $node)
{
$document = $node->ownerDocument;
if (!$document instanceof Document) {
$document = Document::fromNode($node);
}
return $document;
}
}

View File

@@ -0,0 +1,966 @@
<?php
namespace Google\Web_Stories_Dependencies\AmpProject\Dom;
use Google\Web_Stories_Dependencies\AmpProject\DevMode;
use Google\Web_Stories_Dependencies\AmpProject\Dom\Document\AfterLoadFilter;
use Google\Web_Stories_Dependencies\AmpProject\Dom\Document\AfterSaveFilter;
use Google\Web_Stories_Dependencies\AmpProject\Dom\Document\BeforeLoadFilter;
use Google\Web_Stories_Dependencies\AmpProject\Dom\Document\BeforeSaveFilter;
use Google\Web_Stories_Dependencies\AmpProject\Dom\Document\Filter;
use Google\Web_Stories_Dependencies\AmpProject\Dom\Document\Option;
use Google\Web_Stories_Dependencies\AmpProject\Encoding;
use Google\Web_Stories_Dependencies\AmpProject\Exception\FailedToRetrieveRequiredDomElement;
use Google\Web_Stories_Dependencies\AmpProject\Exception\InvalidDocumentFilter;
use Google\Web_Stories_Dependencies\AmpProject\Exception\MaxCssByteCountExceeded;
use Google\Web_Stories_Dependencies\AmpProject\Html\Attribute;
use Google\Web_Stories_Dependencies\AmpProject\Html\Tag;
use Google\Web_Stories_Dependencies\AmpProject\Optimizer\CssRule;
use Google\Web_Stories_Dependencies\AmpProject\Validator\Spec\CssRuleset\AmpNoTransformed;
use Google\Web_Stories_Dependencies\AmpProject\Validator\Spec\SpecRule;
use DOMComment;
use DOMDocument;
use DOMElement;
use DOMNode;
use DOMNodeList;
use DOMText;
use DOMXPath;
use ReflectionClass;
use ReflectionException;
use ReflectionNamedType;
/**
* Abstract away some of the difficulties of working with PHP's DOMDocument.
*
* @property DOMXPath $xpath XPath query object for this document.
* @property Element $html The document's <html> element.
* @property Element $head The document's <head> element.
* @property Element $body The document's <body> element.
* @property Element|null $charset The document's charset meta element.
* @property Element|null $viewport The document's viewport meta element.
* @property DOMNodeList $ampElements The document's <amp-*> elements.
* @property Element $ampCustomStyle The document's <style amp-custom> element.
* @property int $ampCustomStyleByteCount Count of bytes of CSS in the <style amp-custom> tag.
* @property int $inlineStyleByteCount Count of bytes of CSS in all of the inline style attributes.
* @property LinkManager $links Link manager to manage <link> tags in the <head>.
*
* @package ampproject/amp-toolbox
*/
final class Document extends DOMDocument
{
/**
* Default document type to use.
*
* @var string
*/
const DEFAULT_DOCTYPE = '<!DOCTYPE html>';
/**
* Regular expression to match the HTML doctype.
*
* @var string
*/
const HTML_DOCTYPE_REGEX_PATTERN = '#<!doctype\\s+html[^>]+?>#si';
/*
* Regular expressions to fetch the individual structural tags.
* These patterns were optimized to avoid extreme backtracking on large documents.
*/
const HTML_STRUCTURE_DOCTYPE_PATTERN = '/^(?<doctype>[^<]*(?>\\s*<!--.*?-->\\s*)*<!doctype(?>\\s+[^>]+)?>)/is';
const HTML_STRUCTURE_HTML_START_TAG = '/^(?<html_start>[^<]*(?>\\s*<!--.*?-->\\s*)*<html(?>\\s+[^>]*)?>)/is';
const HTML_STRUCTURE_HTML_END_TAG = '/(?<html_end><\\/html(?>\\s+[^>]*)?>.*)$/is';
const HTML_STRUCTURE_HEAD_START_TAG = '/^[^<]*(?><!--.*?-->\\s*)*(?><head(?>\\s+[^>]*)?>)/is';
const HTML_STRUCTURE_BODY_START_TAG = '/^[^<]*(?><!--.*-->\\s*)*(?><body(?>\\s+[^>]*)?>)/is';
const HTML_STRUCTURE_BODY_END_TAG = '/(?><\\/body(?>\\s+[^>]*)?>.*)$/is';
const HTML_STRUCTURE_HEAD_TAG = '/^(?>[^<]*(?><head(?>\\s+[^>]*)?>).*?<\\/head(?>\\s+[^>]*)?>)/is';
// Regex pattern used for removing Internet Explorer conditional comments.
const HTML_IE_CONDITIONAL_COMMENTS_PATTERN = '/<!--(?>\\[if\\s|<!\\[endif)(?>[^>]+(?<!--)>)*(?>[^>]+(?<=--)>)/i';
/**
* Error message to use when the __get() is triggered for an unknown property.
*
* @var string
*/
const PROPERTY_GETTER_ERROR_MESSAGE = 'Undefined property: AmpProject\\Dom\\Document::';
// Attribute to use as a placeholder to move the emoji AMP symbol (⚡) over to DOM.
const EMOJI_AMP_ATTRIBUTE_PLACEHOLDER = 'emoji-amp';
/**
* XPath query to retrieve all <amp-*> tags, relative to the <body> node.
*
* @var string
*/
const XPATH_AMP_ELEMENTS_QUERY = ".//*[starts-with(name(), 'amp-')]";
/**
* XPath query to retrieve the <style amp-custom> tag, relative to the <head> node.
*
* @var string
*/
const XPATH_AMP_CUSTOM_STYLE_QUERY = './/style[@amp-custom]';
/**
* XPath query to fetch the inline style attributes, relative to the <body> node.
*
* @var string
*/
const XPATH_INLINE_STYLE_ATTRIBUTES_QUERY = './/@style';
/**
* Associative array for lazily-created, cached properties for the document.
*
* @var array
*/
private $properties = [];
/**
* Associative array of options to configure the behavior of the DOM document abstraction.
*
* @see Option::DEFAULTS For a list of available options.
*
* @var Options
*/
private $options;
/**
* Whether `data-ampdevmode` was initially set on the the document element.
*
* @var bool
*/
private $hasInitialAmpDevMode = \false;
/**
* The original encoding of how the Dom\Document was created.
*
* This is stored to do an automatic conversion to UTF-8, which is a requirement for AMP.
*
* @var Encoding
*/
private $originalEncoding;
/**
* The maximum number of bytes of CSS that is enforced.
*
* A negative number will disable the byte count limit.
*
* @var int
*/
private $cssMaxByteCountEnforced = -1;
/**
* List of document filter class names.
*
* @var string[]
*/
private $filterClasses = [];
/**
* List of document filter class instances.
*
* @var Filter[]
*/
private $filters = [];
/**
* Unique ID manager for the Document instance.
*
* @var UniqueIdManager
*/
private $uniqueIdManager;
/**
* Creates a new AmpProject\Dom\Document object
*
* @link https://php.net/manual/domdocument.construct.php
*
* @param string $version Optional. The version number of the document as part of the XML declaration.
* @param string $encoding Optional. The encoding of the document as part of the XML declaration.
*/
public function __construct($version = '', $encoding = null)
{
$this->originalEncoding = new Encoding($encoding);
parent::__construct($version ?: '1.0', Encoding::AMP);
$this->registerNodeClass(DOMElement::class, Element::class);
$this->options = new Options(Option::DEFAULTS);
$this->uniqueIdManager = new UniqueIdManager();
$this->registerFilters([Filter\DetectInvalidByteSequence::class, Filter\SvgSourceAttributeEncoding::class, Filter\AmpEmojiAttribute::class, Filter\AmpBindAttributes::class, Filter\SelfClosingTags::class, Filter\SelfClosingSVGElements::class, Filter\NoscriptElements::class, Filter\DeduplicateTag::class, Filter\ConvertHeadProfileToLink::class, Filter\MustacheScriptTemplates::class, Filter\DoctypeNode::class, Filter\NormalizeHtmlAttributes::class, Filter\DocumentEncoding::class, Filter\HttpEquivCharset::class, Filter\LibxmlCompatibility::class, Filter\ProtectEsiTags::class, Filter\NormalizeHtmlEntities::class]);
}
/**
* Named constructor to provide convenient way of transforming HTML into DOM.
*
* Due to slow automatic encoding detection, it is recommended to provide an explicit
* charset either via a <meta charset> tag or via $options.
*
* @param string $html HTML to turn into a DOM.
* @param array|string $options Optional. Array of options to configure the document. Used as encoding if a string
* is passed. Defaults to an empty array.
* @return Document|false DOM generated from provided HTML, or false if the transformation failed.
*/
public static function fromHtml($html, $options = [])
{
// Assume options are the encoding if a string is passed, for BC reasons.
if (\is_string($options)) {
$options = [Option::ENCODING => $options];
}
$encoding = isset($options[Option::ENCODING]) ? $options[Option::ENCODING] : null;
$dom = new self('', $encoding);
if (!$dom->loadHTML($html, $options)) {
return \false;
}
return $dom;
}
/**
* Named constructor to provide convenient way of transforming a HTML fragment into DOM.
*
* The difference to Document::fromHtml() is that fragments are not normalized as to their structure.
*
* Due to slow automatic encoding detection, it is recommended to pass in an explicit
* charset via $options.
*
* @param string $html HTML to turn into a DOM.
* @param array|string $options Optional. Array of options to configure the document. Used as encoding if a string
* is passed. Defaults to an empty array.
* @return Document|false DOM generated from provided HTML, or false if the transformation failed.
*/
public static function fromHtmlFragment($html, $options = [])
{
// Assume options are the encoding if a string is passed, for BC reasons.
if (\is_string($options)) {
$options = [Option::ENCODING => $options];
}
$encoding = isset($options[Option::ENCODING]) ? $options[Option::ENCODING] : null;
$dom = new self('', $encoding);
if (!$dom->loadHTMLFragment($html, $options)) {
return \false;
}
return $dom;
}
/**
* Named constructor to provide convenient way of retrieving the DOM from a node.
*
* @param DOMNode $node Node to retrieve the DOM from. This is being modified by reference (!).
* @return Document DOM generated from provided HTML, or false if the transformation failed.
*/
public static function fromNode(DOMNode &$node)
{
/**
* Document of the node.
*
* If the node->ownerDocument returns null, the node is the document.
*
* @var DOMDocument
*/
$root = $node->ownerDocument === null ? $node : $node->ownerDocument;
if ($root instanceof self) {
return $root;
}
$dom = new self();
// We replace the $node by reference, to make sure the next lines of code will
// work as expected with the new document.
// Otherwise $dom and $node would refer to two different DOMDocuments.
$node = $dom->importNode($node, \true);
$dom->appendChild($node);
$dom->hasInitialAmpDevMode = $dom->documentElement->hasAttribute(DevMode::DEV_MODE_ATTRIBUTE);
return $dom;
}
/**
* Reset the internal optimizations of the Document object.
*
* This might be needed if you are doing an operation that causes the cached
* nodes and XPath objects to point to the wrong document.
*
* @return self Reset version of the Document object.
*/
private function reset()
{
// Drop references to old DOM document.
unset($this->properties['xpath'], $this->properties['head'], $this->properties['body']);
// Reference of the document itself doesn't change here, but might need to change in the future.
return $this;
}
/**
* Load HTML from a string.
*
* @link https://php.net/manual/domdocument.loadhtml.php
*
* @param string $source The HTML string.
* @param array|int|string $options Optional. Array of options to configure the document. Used as additional Libxml
* parameters if an int or string is passed. Defaults to an empty array.
* @return bool true on success or false on failure.
*/
public function loadHTML($source, $options = [])
{
$source = $this->normalizeDocumentStructure($source);
$success = $this->loadHTMLFragment($source, $options);
if ($success) {
$this->insertMissingCharset();
// Do some further clean-up.
$this->moveInvalidHeadNodesToBody();
$this->movePostBodyNodesToBody();
}
return $success;
}
/**
* Load a HTML fragment from a string.
*
* @param string $source The HTML fragment string.
* @param array|int|string $options Optional. Array of options to configure the document. Used as additional Libxml
* parameters if an int or string is passed. Defaults to an empty array.
* @return bool true on success or false on failure.
*/
public function loadHTMLFragment($source, $options = [])
{
// Assume options are the additional libxml flags if a string or int is passed, for BC reasons.
if (\is_string($options)) {
$options = (int) $options;
}
if (\is_int($options)) {
$options = [Option::LIBXML_FLAGS => $options];
}
$this->options = $this->options->merge($options);
$this->reset();
foreach ($this->filterClasses as $filterClass) {
$filter = null;
try {
$filter = $this->instantiateFilter($filterClass);
$this->filters[] = $filter;
} catch (ReflectionException $exception) {
// A filter cannot properly be instantiated. Let's just skip loading it for now.
continue;
}
if (!$filter instanceof Filter) {
throw InvalidDocumentFilter::forFilter($filter);
}
if ($filter instanceof BeforeLoadFilter) {
$source = $filter->beforeLoad($source);
}
}
$success = parent::loadHTML($source, $this->options[Option::LIBXML_FLAGS]);
if ($success) {
foreach ($this->filters as $filter) {
if ($filter instanceof AfterLoadFilter) {
$filter->afterLoad($this);
}
}
$this->hasInitialAmpDevMode = $this->documentElement->hasAttribute(DevMode::DEV_MODE_ATTRIBUTE);
}
return $success;
}
/**
* Dumps the internal document into a string using HTML formatting.
*
* @link https://php.net/manual/domdocument.savehtml.php
*
* @param DOMNode|null $node Optional. Parameter to output a subset of the document.
* @return string The HTML, or false if an error occurred.
*/
#[\ReturnTypeWillChange]
public function saveHTML(DOMNode $node = null)
{
return $this->saveHTMLFragment($node);
}
/**
* Dumps the internal document fragment into a string using HTML formatting.
*
* @param DOMNode|null $node Optional. Parameter to output a subset of the document.
* @return string The HTML fragment, or false if an error occurred.
*/
public function saveHTMLFragment(DOMNode $node = null)
{
$filtersInReverse = \array_reverse($this->filters);
foreach ($filtersInReverse as $filter) {
if ($filter instanceof BeforeSaveFilter) {
$filter->beforeSave($this);
}
}
if (null === $node || \PHP_VERSION_ID >= 70300) {
$html = parent::saveHTML($node);
} else {
$html = $this->extractNodeViaFragmentBoundaries($node);
}
foreach ($filtersInReverse as $filter) {
if ($filter instanceof AfterSaveFilter) {
$html = $filter->afterSave($html);
}
}
return $html;
}
/**
* Get the current options of the Document instance.
*
* @return Options
*/
public function getOptions()
{
return $this->options;
}
/**
* Add the required utf-8 meta charset tag if it is still missing.
*/
private function insertMissingCharset()
{
// Bail if a charset tag is already present.
if ($this->xpath->query('.//meta[ @charset ]')->item(0)) {
return;
}
$charset = $this->createElement(Tag::META);
$charset->setAttribute(Attribute::CHARSET, Encoding::AMP);
$this->head->insertBefore($charset, $this->head->firstChild);
}
/**
* Extract a node's HTML via fragment boundaries.
*
* Temporarily adds fragment boundary comments in order to locate the desired node to extract from
* the given HTML document. This is required because libxml seems to only preserve whitespace when
* serializing when calling DOMDocument::saveHTML() on the entire document. If you pass the element
* to DOMDocument::saveHTML() then formatting whitespace gets added unexpectedly. This is seen to
* be fixed in PHP 7.3, but for older versions of PHP the following workaround is needed.
*
* @param DOMNode $node Node to extract the HTML for.
* @return string Extracted HTML string.
*/
private function extractNodeViaFragmentBoundaries(DOMNode $node)
{
$boundary = $this->uniqueIdManager->getUniqueId('fragment_boundary');
$startBoundary = $boundary . ':start';
$endBoundary = $boundary . ':end';
$commentStart = $this->createComment($startBoundary);
$commentEnd = $this->createComment($endBoundary);
$node->parentNode->insertBefore($commentStart, $node);
$node->parentNode->insertBefore($commentEnd, $node->nextSibling);
$pattern = '/^.*?' . \preg_quote("<!--{$startBoundary}-->", '/') . '(.*)' . \preg_quote("<!--{$endBoundary}-->", '/') . '.*?\\s*$/s';
$html = \preg_replace($pattern, '$1', parent::saveHTML());
$node->parentNode->removeChild($commentStart);
$node->parentNode->removeChild($commentEnd);
return $html;
}
/**
* Normalize the document structure.
*
* This makes sure the document adheres to the general structure that AMP requires:
* ```
* <!DOCTYPE html>
* <html>
* <head>
* <meta charset="utf-8">
* </head>
* <body>
* </body>
* </html>
* ```
*
* @param string $content Content to normalize the structure of.
* @return string Normalized content.
*/
private function normalizeDocumentStructure($content)
{
$matches = [];
$doctype = self::DEFAULT_DOCTYPE;
$htmlStart = '<html>';
$htmlEnd = '</html>';
// Strip IE conditional comments, which are supported by IE 5-9 only (which AMP doesn't support).
$content = \preg_replace(self::HTML_IE_CONDITIONAL_COMMENTS_PATTERN, '', $content);
// Detect and strip <!doctype> tags.
if (\preg_match(self::HTML_STRUCTURE_DOCTYPE_PATTERN, $content, $matches)) {
$doctype = $matches['doctype'];
$content = \preg_replace(self::HTML_STRUCTURE_DOCTYPE_PATTERN, '', $content, 1);
}
// Detect and strip <html> tags.
if (\preg_match(self::HTML_STRUCTURE_HTML_START_TAG, $content, $matches)) {
$htmlStart = $matches['html_start'];
$content = \preg_replace(self::HTML_STRUCTURE_HTML_START_TAG, '', $content, 1);
\preg_match(self::HTML_STRUCTURE_HTML_END_TAG, $content, $matches);
$htmlEnd = isset($matches['html_end']) ? $matches['html_end'] : $htmlEnd;
$content = \preg_replace(self::HTML_STRUCTURE_HTML_END_TAG, '', $content, 1);
}
// Detect <head> and <body> tags and add as needed.
if (!\preg_match(self::HTML_STRUCTURE_HEAD_START_TAG, $content, $matches)) {
if (!\preg_match(self::HTML_STRUCTURE_BODY_START_TAG, $content, $matches)) {
// Both <head> and <body> missing.
$content = "<head></head><body>{$content}</body>";
} else {
// Only <head> missing.
$content = "<head></head>{$content}";
}
} elseif (!\preg_match(self::HTML_STRUCTURE_BODY_END_TAG, $content, $matches)) {
// Only <body> missing.
// @todo This is an expensive regex operation, look into further optimization.
$content = \preg_replace(self::HTML_STRUCTURE_HEAD_TAG, '$0<body>', $content, 1, $count);
// Closing </head> tag is missing.
if (!$count) {
$content = $content . '</head><body>';
}
$content .= '</body>';
}
$content = "{$htmlStart}{$content}{$htmlEnd}";
// Reinsert a standard doctype (while preserving any potentially leading comments).
$doctype = \preg_replace(self::HTML_DOCTYPE_REGEX_PATTERN, self::DEFAULT_DOCTYPE, $doctype);
$content = "{$doctype}{$content}";
return $content;
}
/**
* Normalize the structure of the document if it was already provided as a DOM.
*
* Warning: This method may not use any magic getters for html, head, or body.
*/
public function normalizeDomStructure()
{
if (!$this->documentElement) {
$this->appendChild($this->createElement(Tag::HTML));
}
if (Tag::HTML !== $this->documentElement->nodeName) {
$nextSibling = $this->documentElement->nextSibling;
/**
* The old document element that we need to remove and replace as we cannot just move it around.
*
* @var Element
*/
$oldDocumentElement = $this->removeChild($this->documentElement);
$html = $this->createElement(Tag::HTML);
$this->insertBefore($html, $nextSibling);
if ($oldDocumentElement->nodeName === Tag::HEAD) {
$head = $oldDocumentElement;
} else {
$head = $this->getElementsByTagName(Tag::HEAD)->item(0);
if (!$head) {
$head = $this->createElement(Tag::HEAD);
}
}
if (!$head instanceof Element) {
throw FailedToRetrieveRequiredDomElement::forHeadElement($head);
}
$this->properties['head'] = $head;
$html->appendChild($head);
if ($oldDocumentElement->nodeName === Tag::BODY) {
$body = $oldDocumentElement;
} else {
$body = $this->getElementsByTagName(Tag::BODY)->item(0);
if (!$body) {
$body = $this->createElement(Tag::BODY);
}
}
if (!$body instanceof Element) {
throw FailedToRetrieveRequiredDomElement::forBodyElement($body);
}
$this->properties['body'] = $body;
$html->appendChild($body);
if ($oldDocumentElement !== $body && $oldDocumentElement !== $this->head) {
$body->appendChild($oldDocumentElement);
}
} else {
$head = $this->getElementsByTagName(Tag::HEAD)->item(0);
if (!$head) {
$this->properties['head'] = $this->createElement(Tag::HEAD);
$this->documentElement->insertBefore($this->properties['head'], $this->documentElement->firstChild);
}
$body = $this->getElementsByTagName(Tag::BODY)->item(0);
if (!$body) {
$this->properties['body'] = $this->createElement(Tag::BODY);
$this->documentElement->appendChild($this->properties['body']);
}
}
$this->moveInvalidHeadNodesToBody();
$this->movePostBodyNodesToBody();
}
/**
* Move invalid head nodes back to the body.
*
* Warning: This method may not use any magic getters for html, head, or body.
*/
private function moveInvalidHeadNodesToBody()
{
// Walking backwards makes it easier to move elements in the expected order.
$node = $this->properties['head']->lastChild;
while ($node) {
$nextSibling = $node->previousSibling;
if (!$this->isValidHeadNode($node)) {
$this->properties['body']->insertBefore($this->properties['head']->removeChild($node), $this->properties['body']->firstChild);
}
$node = $nextSibling;
}
}
/**
* Move any nodes appearing after </body> or </html> to be appended to the <body>.
*
* This accounts for markup that is output at shutdown, such markup from Query Monitor. Not only is elements after
* the </body> not valid in AMP, but trailing elements after </html> will get wrapped in additional <html> elements.
* While comment nodes would be allowed in AMP, everything is moved regardless so that source stack comments will
* retain their relative position with the element nodes they annotate.
*
* Warning: This method may not use any magic getters for html, head, or body.
*/
private function movePostBodyNodesToBody()
{
// Move nodes (likely comments) from after the </body>.
while ($this->properties['body']->nextSibling) {
$this->properties['body']->appendChild($this->properties['body']->nextSibling);
}
// Move nodes from after the </html>.
while ($this->documentElement->nextSibling) {
$nextSibling = $this->documentElement->nextSibling;
if ($nextSibling instanceof Element && Tag::HTML === $nextSibling->nodeName) {
// Handle trailing elements getting wrapped in implicit duplicate <html>.
while ($nextSibling->firstChild) {
$this->properties['body']->appendChild($nextSibling->firstChild);
}
$nextSibling->parentNode->removeChild($nextSibling);
// Discard now-empty implicit <html>.
} else {
$this->properties['body']->appendChild($this->documentElement->nextSibling);
}
}
}
/**
* Determine whether a node can be in the head.
*
* Warning: This method may not use any magic getters for html, head, or body.
*
* @link https://github.com/ampproject/amphtml/blob/445d6e3be8a5063e2738c6f90fdcd57f2b6208be/validator/engine/htmlparser.js#L83-L100
* @link https://www.w3.org/TR/html5/document-metadata.html
*
* @param DOMNode $node Node.
* @return bool Whether valid head node.
*/
public function isValidHeadNode(DOMNode $node)
{
return $node instanceof Element && \in_array($node->nodeName, Tag::ELEMENTS_ALLOWED_IN_HEAD, \true) || $node instanceof DOMText && \preg_match('/^\\s*$/', $node->nodeValue) || $node instanceof DOMComment;
}
/**
* Get the ID for an element.
*
* If the element does not have an ID, create one first.
*
* @param Element $element Element to get the ID for.
* @param string $prefix Optional. The prefix to use (should not have a trailing dash). Defaults to 'i-amp-id'.
* @return string ID to use.
*/
public function getElementId(Element $element, $prefix = 'i-amp')
{
if ($element->hasAttribute(Attribute::ID)) {
return $element->getAttribute(Attribute::ID);
}
$id = $this->uniqueIdManager->getUniqueId($prefix);
while ($this->getElementById($id) instanceof Element) {
$id = $this->uniqueIdManager->getUniqueId($prefix);
}
$element->setAttribute(Attribute::ID, $id);
return $id;
}
/**
* Determine whether `data-ampdevmode` was initially set on the document element.
*
* @return bool
*/
public function hasInitialAmpDevMode()
{
return $this->hasInitialAmpDevMode;
}
/**
* Add style(s) to the <style amp-custom> tag.
*
* @param string $style Style to add.
* @throws MaxCssByteCountExceeded If the allowed max byte count is exceeded.
*/
public function addAmpCustomStyle($style)
{
$style = \trim($style, CssRule::CSS_TRIM_CHARACTERS);
$existingStyle = (string) $this->ampCustomStyle->textContent;
// Inject new styles before any potential source map annotation comment like: /*# sourceURL=amp-custom.css */.
// If not present, then just put it at the end of the stylesheet. This isn't strictly required, but putting the
// source map comments at the end is the convention.
$newStyle = \preg_replace(':(?=\\s+/\\*#[^*]+?\\*/\\s*$|$):s', $style, $existingStyle, 1);
$newByteCount = \strlen($newStyle);
if ($this->getRemainingCustomCssSpace() < $newByteCount - $this->ampCustomStyleByteCount) {
throw MaxCssByteCountExceeded::forAmpCustom($newStyle);
}
$this->ampCustomStyle->textContent = $newStyle;
$this->properties['ampCustomStyleByteCount'] = $newByteCount;
}
/**
* Add the given number of bytes ot the total inline style byte count.
*
* @param int $byteCount Bytes to add.
*/
public function addInlineStyleByteCount($byteCount)
{
$this->inlineStyleByteCount += $byteCount;
}
/**
* Get the remaining number bytes allowed for custom CSS.
*
* @return int
*/
public function getRemainingCustomCssSpace()
{
if ($this->cssMaxByteCountEnforced < 0) {
// No CSS byte count limit is being enforced, so return the next best thing to +∞.
return \PHP_INT_MAX;
}
return \max(0, $this->cssMaxByteCountEnforced - (int) $this->ampCustomStyleByteCount - (int) $this->inlineStyleByteCount);
}
/**
* Get the array of allowed keys of lazily-created, cached properties.
* The array index is the key and the array value is the key's default value.
*
* @return array Array of allowed keys.
*/
protected function getAllowedKeys()
{
return ['xpath', Tag::HTML, Tag::HEAD, Tag::BODY, Attribute::CHARSET, Attribute::VIEWPORT, 'ampElements', 'ampCustomStyle', 'ampCustomStyleByteCount', 'inlineStyleByteCount', 'links'];
}
/**
* Magic getter to implement lazily-created, cached properties for the document.
*
* @param string $name Name of the property to get.
* @return mixed Value of the property, or null if unknown property was requested.
*/
public function __get($name)
{
switch ($name) {
case 'xpath':
$this->properties['xpath'] = new DOMXPath($this);
return $this->properties['xpath'];
case Tag::HTML:
$html = $this->getElementsByTagName(Tag::HTML)->item(0);
if ($html === null) {
// Document was assembled manually and bypassed normalisation.
$this->normalizeDomStructure();
$html = $this->getElementsByTagName(Tag::HTML)->item(0);
}
if (!$html instanceof Element) {
throw FailedToRetrieveRequiredDomElement::forHtmlElement($html);
}
$this->properties['html'] = $html;
return $this->properties['html'];
case Tag::HEAD:
$head = $this->getElementsByTagName(Tag::HEAD)->item(0);
if ($head === null) {
// Document was assembled manually and bypassed normalisation.
$this->normalizeDomStructure();
$head = $this->getElementsByTagName(Tag::HEAD)->item(0);
}
if (!$head instanceof Element) {
throw FailedToRetrieveRequiredDomElement::forHeadElement($head);
}
$this->properties['head'] = $head;
return $this->properties['head'];
case Tag::BODY:
$body = $this->getElementsByTagName(Tag::BODY)->item(0);
if ($body === null) {
// Document was assembled manually and bypassed normalisation.
$this->normalizeDomStructure();
$body = $this->getElementsByTagName(Tag::BODY)->item(0);
}
if (!$body instanceof Element) {
throw FailedToRetrieveRequiredDomElement::forBodyElement($body);
}
$this->properties['body'] = $body;
return $this->properties['body'];
case Attribute::CHARSET:
// This is not cached as it could potentially be requested too early, before the viewport was added, and
// the cache would then store null without rechecking later on after the viewport has been added.
for ($node = $this->head->firstChild; $node !== null; $node = $node->nextSibling) {
if ($node instanceof Element && $node->tagName === Tag::META && $node->getAttribute(Attribute::NAME) === Attribute::CHARSET) {
return $node;
}
}
return null;
case Attribute::VIEWPORT:
// This is not cached as it could potentially be requested too early, before the viewport was added, and
// the cache would then store null without rechecking later on after the viewport has been added.
for ($node = $this->head->firstChild; $node !== null; $node = $node->nextSibling) {
if ($node instanceof Element && $node->tagName === Tag::META && $node->getAttribute(Attribute::NAME) === Attribute::VIEWPORT) {
return $node;
}
}
return null;
case 'ampElements':
// This is not cached as we clone some elements during SSR transformations to avoid ending up with
// partially transformed, broken elements.
return $this->xpath->query(self::XPATH_AMP_ELEMENTS_QUERY, $this->body) ?: new DOMNodeList();
case 'ampCustomStyle':
$ampCustomStyle = $this->xpath->query(self::XPATH_AMP_CUSTOM_STYLE_QUERY, $this->head)->item(0);
if (!$ampCustomStyle instanceof Element) {
$ampCustomStyle = $this->createElement(Tag::STYLE);
$ampCustomStyle->appendChild($this->createAttribute(Attribute::AMP_CUSTOM));
$this->head->appendChild($ampCustomStyle);
}
$this->properties['ampCustomStyle'] = $ampCustomStyle;
return $this->properties['ampCustomStyle'];
case 'ampCustomStyleByteCount':
if (!isset($this->properties['ampCustomStyle'])) {
$ampCustomStyle = $this->xpath->query(self::XPATH_AMP_CUSTOM_STYLE_QUERY, $this->head)->item(0);
if (!$ampCustomStyle instanceof Element) {
return 0;
}
$this->properties['ampCustomStyle'] = $ampCustomStyle;
}
if (!isset($this->properties['ampCustomStyleByteCount'])) {
$this->properties['ampCustomStyleByteCount'] = \strlen($this->properties['ampCustomStyle']->textContent);
}
return $this->properties['ampCustomStyleByteCount'];
case 'inlineStyleByteCount':
if (!isset($this->properties['inlineStyleByteCount'])) {
$this->properties['inlineStyleByteCount'] = 0;
$attributes = $this->xpath->query(self::XPATH_INLINE_STYLE_ATTRIBUTES_QUERY, $this->documentElement);
foreach ($attributes as $attribute) {
$this->properties['inlineStyleByteCount'] += \strlen($attribute->textContent);
}
}
return $this->properties['inlineStyleByteCount'];
case 'links':
if (!isset($this->properties['links'])) {
$this->properties['links'] = new LinkManager($this);
}
return $this->properties['links'];
}
// Mimic regular PHP behavior for missing notices.
\trigger_error(self::PROPERTY_GETTER_ERROR_MESSAGE . $name, \E_USER_NOTICE);
return null;
}
/**
* Magic setter to implement lazily-created, cached properties for the document.
*
* @param string $name Name of the property to set.
* @param mixed $value Value of the property.
*/
public function __set($name, $value)
{
if (!\in_array($name, $this->getAllowedKeys(), \true)) {
// Mimic regular PHP behavior for missing notices.
\trigger_error(self::PROPERTY_GETTER_ERROR_MESSAGE . $name, \E_USER_NOTICE);
return;
}
$this->properties[$name] = $value;
}
/**
* Magic callback for lazily-created, cached properties for the document.
*
* @param string $name Name of the property to set.
*/
public function __isset($name)
{
if (!\in_array($name, $this->getAllowedKeys(), \true)) {
// Mimic regular PHP behavior for missing notices.
\trigger_error(self::PROPERTY_GETTER_ERROR_MESSAGE . $name, \E_USER_NOTICE);
return \false;
}
return isset($this->properties[$name]);
}
/**
* Make sure we properly reinitialize on clone.
*
* @return void
*/
public function __clone()
{
$this->reset();
}
/**
* Create new element node.
*
* @link https://php.net/manual/domdocument.createelement.php
*
* This override only serves to provide the correct object type-hint for our extended Dom/Element class.
*
* @param string $name The tag name of the element.
* @param string $value Optional. The value of the element. By default, an empty element will be created.
* You can also set the value later with Element->nodeValue.
* @return Element|false A new instance of class Element or false if an error occurred.
*/
public function createElement($name, $value = '')
{
$element = parent::createElement($name, $value);
if (!$element instanceof Element) {
return \false;
}
return $element;
}
/**
* Create new element node.
*
* @link https://php.net/manual/domdocument.createelement.php
*
* This override only serves to provide the correct object type-hint for our extended Dom/Element class.
*
* @param string $name The tag name of the element.
* @param array $attributes Attributes to add to the newly created element.
* @param string $value Optional. The value of the element. By default, an empty element will be created.
* You can also set the value later with Element->nodeValue.
* @return Element|false A new instance of class Element or false if an error occurred.
*/
public function createElementWithAttributes($name, $attributes, $value = '')
{
$element = parent::createElement($name, $value);
if (!$element instanceof Element) {
return \false;
}
$element->setAttributes($attributes);
return $element;
}
/**
* Check whether the CSS maximum byte count is enforced.
*
* @return bool Whether the CSS maximum byte count is enforced.
*/
public function isCssMaxByteCountEnforced()
{
return $this->cssMaxByteCountEnforced >= 0;
}
/**
* Enforce a maximum number of bytes for the CSS.
*
* @param int|null $maxByteCount Maximum number of bytes to limit the CSS to. A negative number disables the limit.
* If null then the max bytes from AmpNoTransformed is used.
*/
public function enforceCssMaxByteCount($maxByteCount = null)
{
if ($maxByteCount === null) {
// No need to instantiate the spec here, we can just directly reference the needed constant.
$maxByteCount = AmpNoTransformed::SPEC[SpecRule::MAX_BYTES];
}
$this->cssMaxByteCountEnforced = $maxByteCount;
}
/**
* Register filters to pre- or post-process the document content.
*
* @param string[] $filterClasses Array of FQCNs of document filter classes.
*/
public function registerFilters($filterClasses)
{
foreach ($filterClasses as $filterClass) {
$this->filterClasses[] = $filterClass;
}
}
/**
* Instantiate a filter from its class while providing the needed dependencies.
*
* @param string $filterClass Class of the filter to instantiate.
* @return Filter Filter object instance.
* @throws ReflectionException If the constructor could not be reflected upon.
*/
private function instantiateFilter($filterClass)
{
$constructor = (new ReflectionClass($filterClass))->getConstructor();
$parameters = $constructor === null ? [] : $constructor->getParameters();
$dependencies = [];
foreach ($parameters as $parameter) {
$dependencyType = null;
// The use of `ReflectionParameter::getClass()` is deprecated in PHP 8, and is superseded
// by `ReflectionParameter::getType()`. See https://github.com/php/php-src/pull/5209.
if (\PHP_VERSION_ID >= 70100) {
if ($parameter->getType()) {
/** @var ReflectionNamedType $returnType */
$returnType = $parameter->getType();
$dependencyType = new ReflectionClass($returnType->getName());
}
} else {
$dependencyType = $parameter->getClass();
}
if ($dependencyType === null) {
// No type provided, so we pass `null` in the hopes that the argument is optional.
$dependencies[] = null;
continue;
}
if (\is_a($dependencyType->name, Encoding::class, \true)) {
$dependencies[] = $this->originalEncoding;
continue;
}
if (\is_a($dependencyType->name, Options::class, \true)) {
$dependencies[] = $this->options;
continue;
}
if (\is_a($dependencyType->name, UniqueIdManager::class, \true)) {
$dependencies[] = $this->uniqueIdManager;
continue;
}
// Unknown dependency type, so we pass `null` in the hopes that the argument is optional.
$dependencies[] = null;
}
return new $filterClass(...$dependencies);
}
}

View File

@@ -0,0 +1,19 @@
<?php
namespace Google\Web_Stories_Dependencies\AmpProject\Dom\Document;
use Google\Web_Stories_Dependencies\AmpProject\Dom\Document;
/**
* Filter the Dom\Document after it was loaded.
*
* @package ampproject/amp-toolbox
*/
interface AfterLoadFilter extends Filter
{
/**
* Process the Document after the html loaded into the Dom\Document.
*
* @param Document $document Document to be processed.
*/
public function afterLoad(Document $document);
}

View File

@@ -0,0 +1,19 @@
<?php
namespace Google\Web_Stories_Dependencies\AmpProject\Dom\Document;
/**
* Filter the HTML after it is saved from the Dom\Document.
*
* @package ampproject/amp-toolbox
*/
interface AfterSaveFilter extends Filter
{
/**
* Process the Dom\Document after being saved from Dom\Document.
*
* @param string $html String of HTML markup to be preprocessed.
* @return string Preprocessed string of HTML markup.
*/
public function afterSave($html);
}

View File

@@ -0,0 +1,19 @@
<?php
namespace Google\Web_Stories_Dependencies\AmpProject\Dom\Document;
/**
* Filter the HTML before it is loaded into the Dom\Document.
*
* @package ampproject/amp-toolbox
*/
interface BeforeLoadFilter extends Filter
{
/**
* Preprocess the HTML to be loaded into the Dom\Document.
*
* @param string $html String of HTML markup to be preprocessed.
* @return string Preprocessed string of HTML markup.
*/
public function beforeLoad($html);
}

View File

@@ -0,0 +1,19 @@
<?php
namespace Google\Web_Stories_Dependencies\AmpProject\Dom\Document;
use Google\Web_Stories_Dependencies\AmpProject\Dom\Document;
/**
* Filter the Dom\Document before it is saved.
*
* @package ampproject/amp-toolbox
*/
interface BeforeSaveFilter extends Filter
{
/**
* Preprocess the DOM to be saved into HTML.
*
* @param Document $document Document to be preprocessed before saving it into HTML.
*/
public function beforeSave(Document $document);
}

View File

@@ -0,0 +1,14 @@
<?php
namespace Google\Web_Stories_Dependencies\AmpProject\Dom\Document;
/**
* Filter to process the document.
*
* This is only a marker interface and needs to be extended by the specific filter types that defines methods.
*
* @package ampproject/amp-toolbox
*/
interface Filter
{
}

View File

@@ -0,0 +1,182 @@
<?php
namespace Google\Web_Stories_Dependencies\AmpProject\Dom\Document\Filter;
use Google\Web_Stories_Dependencies\AmpProject\Amp;
use Google\Web_Stories_Dependencies\AmpProject\Dom\Document\AfterSaveFilter;
use Google\Web_Stories_Dependencies\AmpProject\Dom\Document\BeforeLoadFilter;
use Google\Web_Stories_Dependencies\AmpProject\Dom\Document\Option;
use Google\Web_Stories_Dependencies\AmpProject\Dom\Options;
/**
* Amp bind attributes filter.
*
* @package ampproject/amp-toolbox
*/
final class AmpBindAttributes implements BeforeLoadFilter, AfterSaveFilter
{
/**
* Pattern for HTML attribute accounting for binding attr name in data attribute syntax, boolean attribute,
* single/double-quoted attribute value, and unquoted attribute values.
*
* @var string
*/
const AMP_BIND_DATA_ATTRIBUTE_ATTR_PATTERN = '#^\\s+(?P<name>(?:' . Amp::BIND_DATA_ATTR_PREFIX . ')?[a-zA-Z0-9_\\-]+)' . '(?P<value>=(?>"[^"]*+"|\'[^\']*+\'|[^\'"\\s]+))?#';
/**
* Match all start tags that contain a binding attribute in data attribute syntax.
*
* @var string
*/
const AMP_BIND_DATA_START_PATTERN = '#<' . '(?P<name>[a-zA-Z0-9_\\-]+)' . '(?P<attrs>\\s+' . '(?>' . '(?>' . '(?![a-zA-Z0-9_\\-\\s]*' . Amp::BIND_DATA_ATTR_PREFIX . '[a-zA-Z0-9_\\-]+="[^"]*+"|\'[^\']*+\')' . '[^>"\']+|"[^"]*+"|\'[^\']*+\'' . ')*+' . '(?>[a-zA-Z0-9_\\-\\s]*' . Amp::BIND_DATA_ATTR_PREFIX . '[a-zA-Z0-9_\\-]+' . ')' . ')+' . '(?>[^>"\']+|"[^"]*+"|\'[^\']*+\')*+' . ')>#is';
/**
* Pattern for HTML attribute accounting for binding attr name in square brackets syntax, boolean attribute,
* single/double-quoted attribute value, and unquoted attribute values.
*
* @var string
*/
const AMP_BIND_SQUARE_BRACKETS_ATTR_PATTERN = '#^\\s+(?P<name>\\[?[a-zA-Z0-9_\\-]+\\]?)' . '(?P<value>=(?>"[^"]*+"|\'[^\']*+\'|[^\'"\\s]+))?#';
/**
* Match all start tags that contain a binding attribute in square brackets syntax.
*
* @var string
*/
const AMP_BIND_SQUARE_START_PATTERN = '#<' . '(?P<name>[a-zA-Z0-9_\\-]+)' . '(?P<attrs>\\s+' . '(?>[^>"\'\\[\\]]+|"[^"]*+"|\'[^\']*+\')*+' . '\\[[a-zA-Z0-9_\\-]+\\]' . '(?>[^>"\']+|"[^"]*+"|\'[^\']*+\')*+' . ')>#s';
/**
* Options instance to use.
*
* @var Options
*/
private $options;
/**
* Store the names of the amp-bind attributes that were converted so that we can restore them later on.
*
* @var array<string>
*/
private $convertedAmpBindAttributes = [];
/**
* AmpBindAttributes constructor.
*
* @param Options $options Options instance to use.
*/
public function __construct(Options $options)
{
$this->options = $options;
}
/**
* Replace AMP binding attributes with something that libxml can parse (as HTML5 data-* attributes).
*
* This is necessary because attributes in square brackets are not understood in PHP and
* get dropped with an error raised:
* > Warning: DOMDocument::loadHTML(): error parsing attribute name
*
* @link https://www.ampproject.org/docs/reference/components/amp-bind
*
* @param string $html HTML containing amp-bind attributes.
* @return string HTML with AMP binding attributes replaced with HTML5 data-* attributes.
*/
public function beforeLoad($html)
{
/**
* Replace callback.
*
* @param array $tagMatches Tag matches.
* @return string Replacement.
*/
$replaceCallback = function ($tagMatches) {
$oldAttrs = $this->maybeStripSelfClosingSlash($tagMatches['attrs']);
$newAttrs = '';
$offset = 0;
while (\preg_match(self::AMP_BIND_SQUARE_BRACKETS_ATTR_PATTERN, \substr($oldAttrs, $offset), $attrMatches)) {
$offset += \strlen($attrMatches[0]);
if ('[' === $attrMatches['name'][0]) {
$attrName = \trim($attrMatches['name'], '[]');
$newAttrs .= ' ' . Amp::BIND_DATA_ATTR_PREFIX . $attrName;
if (isset($attrMatches['value'])) {
$newAttrs .= $attrMatches['value'];
}
$this->convertedAmpBindAttributes[] = $attrName;
} else {
$newAttrs .= $attrMatches[0];
}
}
// Bail on parse error which occurs when the regex isn't able to consume the entire $newAttrs string.
if (\strlen($oldAttrs) !== $offset) {
return $tagMatches[0];
}
return '<' . $tagMatches['name'] . $newAttrs . '>';
};
$result = \preg_replace_callback(self::AMP_BIND_SQUARE_START_PATTERN, $replaceCallback, $html);
if (!\is_string($result)) {
return $html;
}
return $result;
}
/**
* Convert AMP bind-attributes back to their original syntax.
*
* This is not guaranteed to produce the exact same result as the initial markup, as it is more of a best guess.
* It can end up replacing the wrong attributes if the initial markup had inconsistent styling, mixing both syntaxes
* for the same attribute. In either case, it will always produce working markup, so this is not that big of a deal.
*
* @see convertAmpBindAttributes() Reciprocal function.
* @link https://www.ampproject.org/docs/reference/components/amp-bind
*
* @param string $html HTML with amp-bind attributes converted.
* @return string HTML with amp-bind attributes restored.
*/
public function afterSave($html)
{
if ($this->options[Option::AMP_BIND_SYNTAX] === Option::AMP_BIND_SYNTAX_DATA_ATTRIBUTE) {
// All amp-bind attributes should remain in their converted data attribute form.
return $html;
}
if ($this->options[Option::AMP_BIND_SYNTAX] === Option::AMP_BIND_SYNTAX_AUTO && empty($this->convertedAmpBindAttributes)) {
// Only previously converted amp-bind attributes should be restored, but none were converted.
return $html;
}
/**
* Replace callback.
*
* @param array $tagMatches Tag matches.
* @return string Replacement.
*/
$replaceCallback = function ($tagMatches) {
$oldAttrs = $this->maybeStripSelfClosingSlash($tagMatches['attrs']);
$newAttrs = '';
$offset = 0;
while (\preg_match(self::AMP_BIND_DATA_ATTRIBUTE_ATTR_PATTERN, \substr($oldAttrs, $offset), $attrMatches)) {
$offset += \strlen($attrMatches[0]);
$attrName = \substr($attrMatches['name'], \strlen(Amp::BIND_DATA_ATTR_PREFIX));
if ($this->options[Option::AMP_BIND_SYNTAX] === Option::AMP_BIND_SYNTAX_SQUARE_BRACKETS || \in_array($attrName, $this->convertedAmpBindAttributes, \true)) {
$attrValue = isset($attrMatches['value']) ? $attrMatches['value'] : '=""';
$newAttrs .= " [{$attrName}]{$attrValue}";
} else {
$newAttrs .= $attrMatches[0];
}
}
// Bail on parse error which occurs when the regex isn't able to consume the entire $newAttrs string.
if (\strlen($oldAttrs) !== $offset) {
return $tagMatches[0];
}
return '<' . $tagMatches['name'] . $newAttrs . '>';
};
$result = \preg_replace_callback(self::AMP_BIND_DATA_START_PATTERN, $replaceCallback, $html);
if (!\is_string($result)) {
return $html;
}
return $result;
}
/**
* Strip the self-closing slash as long as it is not an attribute value, like for the href attribute.
*
* @param string $attributes Attributes to strip the self-closing slash of.
* @return string Adapted attributes.
*/
private function maybeStripSelfClosingSlash($attributes)
{
$result = \preg_replace('#(?<!=)/$#', '', $attributes);
if (!\is_string($result)) {
return \rtrim($attributes);
}
return \rtrim($result);
}
}

View File

@@ -0,0 +1,79 @@
<?php
namespace Google\Web_Stories_Dependencies\AmpProject\Dom\Document\Filter;
use Google\Web_Stories_Dependencies\AmpProject\Html\Attribute;
use Google\Web_Stories_Dependencies\AmpProject\Dom\Document;
use Google\Web_Stories_Dependencies\AmpProject\Dom\Document\AfterSaveFilter;
use Google\Web_Stories_Dependencies\AmpProject\Dom\Document\BeforeLoadFilter;
/**
* Filter for the emoji AMP symbol (⚡).
*
* @package ampproject/amp-toolbox
*/
final class AmpEmojiAttribute implements BeforeLoadFilter, AfterSaveFilter
{
/**
* Pattern to match an AMP emoji together with its variant (amp4ads, amp4email, ...).
*
* @var string
*/
const AMP_EMOJI_ATTRIBUTE_PATTERN = '/<html\\s([^>]*?(?:' . Attribute::AMP_EMOJI_ALT . '|' . Attribute::AMP_EMOJI . ')(4(?:ads|email))?[^>]*?)>/i';
/**
* Store the emoji that was used to represent the AMP attribute.
*
* There are a few variations, so we want to keep track of this.
*
* @see https://github.com/ampproject/amphtml/issues/25990
*
* @var string
*/
private $usedAmpEmoji;
/**
* Covert the emoji AMP symbol (⚡) into pure text.
*
* The emoji symbol gets stripped by DOMDocument::loadHTML().
*
* @param string $html Source HTML string to convert the emoji AMP symbol in.
* @return string Adapted source HTML string.
*/
public function beforeLoad($html)
{
$this->usedAmpEmoji = '';
$result = \preg_replace_callback(self::AMP_EMOJI_ATTRIBUTE_PATTERN, function ($matches) {
// Split into individual attributes.
$attributes = \array_map('trim', \array_filter(\preg_split('#(\\s+[^"\'\\s=]+(?:=(?:"[^"]+"|\'[^\']+\'|[^"\'\\s]+))?)#', $matches[1], -1, \PREG_SPLIT_DELIM_CAPTURE)));
foreach ($attributes as $index => $attribute) {
$attributeMatches = [];
if (\preg_match('/^(' . Attribute::AMP_EMOJI_ALT . '|' . Attribute::AMP_EMOJI . ')(4(?:ads|email))?$/i', $attribute, $attributeMatches)) {
$this->usedAmpEmoji = $attributeMatches[1];
$variant = !empty($attributeMatches[2]) ? $attributeMatches[2] : '';
$attributes[$index] = Document::EMOJI_AMP_ATTRIBUTE_PLACEHOLDER . "=\"{$variant}\"";
break;
}
}
return '<html ' . \implode(' ', $attributes) . '>';
}, $html, 1);
if (!\is_string($result)) {
return $html;
}
return $result;
}
/**
* Restore the emoji AMP symbol (⚡) from its pure text placeholder.
*
* @param string $html HTML string to restore the AMP emoji symbol in.
* @return string Adapted HTML string.
*/
public function afterSave($html)
{
if (empty($this->usedAmpEmoji)) {
return $html;
}
$result = \preg_replace('/(<html\\s[^>]*?)' . \preg_quote(Document::EMOJI_AMP_ATTRIBUTE_PLACEHOLDER, '/') . '="([^"]*)"/i', '\\1' . $this->usedAmpEmoji . '\\2', $html, 1);
if (!\is_string($result)) {
return $html;
}
return $result;
}
}

View File

@@ -0,0 +1,38 @@
<?php
namespace Google\Web_Stories_Dependencies\AmpProject\Dom\Document\Filter;
use Google\Web_Stories_Dependencies\AmpProject\Html\Attribute;
use Google\Web_Stories_Dependencies\AmpProject\Dom\Document;
use Google\Web_Stories_Dependencies\AmpProject\Dom\Document\AfterLoadFilter;
use Google\Web_Stories_Dependencies\AmpProject\Html\Tag;
/**
* Filter to convert a possible head[profile] attribute to link[rel=profile].
*
* @package ampproject/amp-toolbox
*/
final class ConvertHeadProfileToLink implements AfterLoadFilter
{
/**
* Converts a possible head[profile] attribute to link[rel=profile].
*
* The head[profile] attribute is only valid in HTML4, not HTML5.
* So if it exists and isn't empty, add it to the <head> as a link[rel=profile] and strip the attribute.
*
* @param Document $document Document to be processed.
*/
public function afterLoad(Document $document)
{
if (!$document->head->hasAttribute(Attribute::PROFILE)) {
return;
}
$profile = $document->head->getAttribute(Attribute::PROFILE);
if ($profile) {
$link = $document->createElement(Tag::LINK);
$link->setAttribute(Attribute::REL, Attribute::PROFILE);
$link->setAttribute(Attribute::HREF, $profile);
$document->head->appendChild($link);
}
$document->head->removeAttribute(Attribute::PROFILE);
}
}

View File

@@ -0,0 +1,69 @@
<?php
namespace Google\Web_Stories_Dependencies\AmpProject\Dom\Document\Filter;
use Google\Web_Stories_Dependencies\AmpProject\Dom\Document;
use Google\Web_Stories_Dependencies\AmpProject\Dom\Document\AfterLoadFilter;
use Google\Web_Stories_Dependencies\AmpProject\Dom\Element;
use Google\Web_Stories_Dependencies\AmpProject\Html\Tag;
use DOMAttr;
/**
* Filter for deduplicating head and body tags.
*
* @package ampproject/amp-toolbox
*/
final class DeduplicateTag implements AfterLoadFilter
{
/**
* Deduplicate head and body tags.
*
* This keeps the first tag as the main tag and moves over all child nodes and attribute nodes from any subsequent
* same tags over to remove them.
*
* @param Document $document Document to be processed.
*/
public function afterLoad(Document $document)
{
$tagNames = [Tag::HEAD, Tag::BODY];
foreach ($tagNames as $tagName) {
$tags = $document->getElementsByTagName($tagName);
/**
* Main tag to keep.
*
* @var Element|null $mainTag
*/
$mainTag = $tags->item(0);
if (null === $mainTag) {
continue;
}
while ($tags->length > 1) {
/**
* Tag to remove.
*
* @var Element $tagToRemove
*/
$tagToRemove = $tags->item(1);
foreach ($tagToRemove->childNodes as $childNode) {
$mainTag->appendChild($childNode->parentNode->removeChild($childNode));
}
while ($tagToRemove->hasAttributes()) {
/**
* Attribute node to move over to the main tag.
*
* @var DOMAttr $attribute
*/
$attribute = $tagToRemove->attributes->item(0);
$tagToRemove->removeAttributeNode($attribute);
// @TODO This doesn't deal properly with attributes present on both tags. Maybe overkill to add?
// We could move over the copy_attributes from AMP_DOM_Utils to do this.
$mainTag->setAttributeNode($attribute);
}
$tagToRemove->parentNode->removeChild($tagToRemove);
}
// Avoid doing the above query again if possible.
if (\in_array($tagName, [Tag::HEAD, Tag::BODY], \true)) {
$document->{$tagName} = $mainTag;
}
}
}
}

View File

@@ -0,0 +1,46 @@
<?php
namespace Google\Web_Stories_Dependencies\AmpProject\Dom\Document\Filter;
use Google\Web_Stories_Dependencies\AmpProject\Dom\Document\BeforeLoadFilter;
use Google\Web_Stories_Dependencies\AmpProject\Dom\Document\Option;
use Google\Web_Stories_Dependencies\AmpProject\Dom\Options;
use Google\Web_Stories_Dependencies\AmpProject\Exception\InvalidByteSequence;
/**
* Filter for checking if the markup contains invalid byte sequences.
*
* If invalid byte sequences are passed to `DOMDocument`, it fails silently and produces Mojibake.
*
* @package ampproject/amp-toolbox
*/
final class DetectInvalidByteSequence implements BeforeLoadFilter
{
/**
* Options instance to use.
*
* @var Options
*/
private $options;
/**
* DetectInvalidByteSequence constructor.
*
* @param Options $options Options instance to use.
*/
public function __construct(Options $options)
{
$this->options = $options;
}
/**
* Check if the markup contains invalid byte sequences.
*
* @param string $html String of HTML markup to be preprocessed.
* @return string Preprocessed string of HTML markup.
*/
public function beforeLoad($html)
{
if ($this->options[Option::CHECK_ENCODING] && \function_exists('mb_check_encoding') && !\mb_check_encoding($html)) {
throw InvalidByteSequence::forHtml();
}
return $html;
}
}

View File

@@ -0,0 +1,81 @@
<?php
namespace Google\Web_Stories_Dependencies\AmpProject\Dom\Document\Filter;
use Google\Web_Stories_Dependencies\AmpProject\Dom\Document\AfterSaveFilter;
use Google\Web_Stories_Dependencies\AmpProject\Dom\Document\BeforeLoadFilter;
/**
* Filter to secure and restore the doctype node.
*
* @package ampproject/amp-toolbox
*/
final class DoctypeNode implements BeforeLoadFilter, AfterSaveFilter
{
/**
* Regex pattern used for securing the doctype node if it is not the first one.
*
* @var string
*/
const HTML_SECURE_DOCTYPE_IF_NOT_FIRST_PATTERN = '/(^[^<]*(?>\\s*<!--[^>]*>\\s*)+<)(!)(doctype)(\\s+[^>]+?)(>)/i';
/**
* Regex replacement template for securing the doctype node.
*
* @var string
*/
const HTML_SECURE_DOCTYPE_REPLACEMENT_TEMPLATE = '\\1!--amp-\\3\\4-->';
/**
* Regex pattern used for restoring the doctype node.
*
* @var string
*/
const HTML_RESTORE_DOCTYPE_PATTERN = '/(^[^<]*(?>\\s*<!--[^>]*>\\s*)*<)(!--amp-)(doctype)(\\s+[^>]+?)(-->)/i';
/**
* Regex replacement template for restoring the doctype node.
*
* @var string
*/
const HTML_RESTORE_DOCTYPE_REPLACEMENT_TEMPLATE = '\\1!\\3\\4>';
/**
* Whether we had secured a doctype that needs restoring or not.
*
* This is an int as it receives the $count from the preg_replace().
*
* @var int
*/
private $securedDoctype = 0;
/**
* Secure the original doctype node.
*
* We need to keep elements around that were prepended to the doctype, like comment node used for source-tracking.
* As DOM_Document prepends a new doctype node and removes the old one if the first element is not the doctype, we
* need to ensure the original one is not stripped (by changing its node type) and restore it later on.
*
* @param string $html HTML string to adapt.
* @return string Adapted HTML string.
*/
public function beforeLoad($html)
{
$result = \preg_replace(self::HTML_SECURE_DOCTYPE_IF_NOT_FIRST_PATTERN, self::HTML_SECURE_DOCTYPE_REPLACEMENT_TEMPLATE, $html, 1, $this->securedDoctype);
if (!\is_string($result)) {
return $html;
}
return $result;
}
/**
* Restore the original doctype node.
*
* @param string $html HTML string to adapt.
* @return string Adapted HTML string.
*/
public function afterSave($html)
{
if (!$this->securedDoctype) {
return $html;
}
$result = \preg_replace(self::HTML_RESTORE_DOCTYPE_PATTERN, self::HTML_RESTORE_DOCTYPE_REPLACEMENT_TEMPLATE, $html, 1);
if (!\is_string($result)) {
return $html;
}
return $result;
}
}

View File

@@ -0,0 +1,186 @@
<?php
namespace Google\Web_Stories_Dependencies\AmpProject\Dom\Document\Filter;
use Google\Web_Stories_Dependencies\AmpProject\Html\Attribute;
use Google\Web_Stories_Dependencies\AmpProject\Dom\Document\BeforeLoadFilter;
use Google\Web_Stories_Dependencies\AmpProject\Encoding;
use Google\Web_Stories_Dependencies\AmpProject\Html\Tag;
/**
* Filter for document encoding.
*
* @package ampproject/amp-toolbox
*/
final class DocumentEncoding implements BeforeLoadFilter
{
/**
* Regex pattern to find a tag without an attribute.
*
* @var string
*/
const HTML_FIND_TAG_WITHOUT_ATTRIBUTE_PATTERN = '/<%1$s[^>]*?>[^<]*(?><\\/%1$s>)?/i';
/**
* Regex pattern to find a tag with an attribute.
*
* @var string
*/
const HTML_FIND_TAG_WITH_ATTRIBUTE_PATTERN = '/<%1$s [^>]*?\\s*%2$s\\s*=[^>]*?>[^<]*(?><\\/%1$s>)?/i';
/**
* Regex pattern to extract an attribute value out of an attribute string.
*
* @var string
*/
const HTML_EXTRACT_ATTRIBUTE_VALUE_PATTERN = '/%s=(?>([\'"])(?<full>.*)?\\1|(?<partial>[^ \'";]+))/';
/**
* Delimiter used in regular expressions.
*
* @var string
*/
const HTML_FIND_TAG_DELIMITER = '/';
/**
* Original encoding that was used for the document.
*
* @var Encoding
*/
private $originalEncoding;
/**
* DocumentEncoding constructor.
*
* @param Encoding $originalEncoding Original encoding that was used for the document.
*/
public function __construct(Encoding $originalEncoding)
{
$this->originalEncoding = $originalEncoding;
}
/**
* Detect the encoding of the document.
*
* @param string $html Content of which to detect the encoding.
* @return string Preprocessed string of HTML markup.
*/
public function beforeLoad($html)
{
$encoding = (string) $this->originalEncoding;
// Check for HTML 4 http-equiv meta tags.
foreach ($this->findTags($html, Tag::META, Attribute::HTTP_EQUIV) as $potentialHttpEquivTag) {
$encoding = $this->extractValue($potentialHttpEquivTag, Attribute::CHARSET);
if (\false !== $encoding) {
$httpEquivTag = $potentialHttpEquivTag;
}
}
// Strip all charset tags.
if (isset($httpEquivTag)) {
$html = \str_replace($httpEquivTag, '', $html);
}
// Check for HTML 5 charset meta tag. This overrides the HTML 4 charset.
$charsetTag = $this->findTag($html, Tag::META, Attribute::CHARSET);
if ($charsetTag) {
$encoding = $this->extractValue($charsetTag, Attribute::CHARSET);
// Strip the encoding if it is not the required one.
if (\strtolower($encoding) !== Encoding::AMP) {
$html = \str_replace($charsetTag, '', $html);
}
}
$this->originalEncoding = new Encoding($encoding);
if (!$this->originalEncoding->equals(Encoding::AMP)) {
$html = $this->adaptEncoding($html);
}
return $html;
}
/**
* Adapt the encoding of the content.
*
* @param string $source Source content to adapt the encoding of.
* @return string Adapted content.
*/
private function adaptEncoding($source)
{
// No encoding was provided, so we need to guess.
if (\function_exists('mb_detect_encoding') && $this->originalEncoding->equals(Encoding::UNKNOWN)) {
$this->originalEncoding = new Encoding($this->detectEncoding($source));
}
// Guessing the encoding seems to have failed, so we assume UTF-8 instead.
// In my testing, this was not possible as long as one ISO-8859-x is in the detection order.
if ($this->originalEncoding === null) {
$this->originalEncoding = new Encoding(Encoding::AMP);
// @codeCoverageIgnore
}
$this->originalEncoding->sanitize();
// Sanitization failed, so we do a last effort to auto-detect.
if (\function_exists('mb_detect_encoding') && $this->originalEncoding->equals(Encoding::UNKNOWN)) {
$detectedEncoding = $this->detectEncoding($source);
if ($detectedEncoding !== \false) {
$this->originalEncoding = new Encoding($detectedEncoding);
}
}
$target = \false;
if (!$this->originalEncoding->equals(Encoding::AMP)) {
$target = \function_exists('mb_convert_encoding') ? \mb_convert_encoding($source, Encoding::AMP, (string) $this->originalEncoding) : \false;
}
return \false !== $target ? $target : $source;
}
/**
* Find a given tag with a given attribute.
*
* If multiple tags match, this method will only return the first one.
*
* @param string $content Content in which to find the tag.
* @param string $element Element of the tag.
* @param string $attribute Attribute that the tag contains.
* @return string[] The requested tags. Returns an empty array if none found.
*/
private function findTags($content, $element, $attribute = null)
{
$matches = [];
$pattern = empty($attribute) ? \sprintf(self::HTML_FIND_TAG_WITHOUT_ATTRIBUTE_PATTERN, \preg_quote($element, self::HTML_FIND_TAG_DELIMITER)) : \sprintf(self::HTML_FIND_TAG_WITH_ATTRIBUTE_PATTERN, \preg_quote($element, self::HTML_FIND_TAG_DELIMITER), \preg_quote($attribute, self::HTML_FIND_TAG_DELIMITER));
if (\preg_match($pattern, $content, $matches)) {
return $matches;
}
return [];
}
/**
* Find a given tag with a given attribute.
*
* If multiple tags match, this method will only return the first one.
*
* @param string $content Content in which to find the tag.
* @param string $element Element of the tag.
* @param string $attribute Attribute that the tag contains.
* @return string|false The requested tag, or false if not found.
*/
private function findTag($content, $element, $attribute = null)
{
$matches = $this->findTags($content, $element, $attribute);
if (empty($matches)) {
return \false;
}
return $matches[0];
}
/**
* Extract an attribute value from an HTML tag.
*
* @param string $tag Tag from which to extract the attribute.
* @param string $attribute Attribute of which to extract the value.
* @return string|false Extracted attribute value, false if not found.
*/
private function extractValue($tag, $attribute)
{
$matches = [];
$pattern = \sprintf(self::HTML_EXTRACT_ATTRIBUTE_VALUE_PATTERN, \preg_quote($attribute, self::HTML_FIND_TAG_DELIMITER));
if (\preg_match($pattern, $tag, $matches)) {
return empty($matches['full']) ? $matches['partial'] : $matches['full'];
}
return \false;
}
/**
* Detect character encoding.
*
* @param string $source Source content to detect the encoding of.
* @return string The character encoding of the source.
*/
private function detectEncoding($source)
{
$detectionOrder = \PHP_VERSION_ID >= 80100 ? Encoding::DETECTION_ORDER_PHP81 : Encoding::DETECTION_ORDER;
return \mb_detect_encoding($source, $detectionOrder, \true);
}
}

View File

@@ -0,0 +1,158 @@
<?php
namespace Google\Web_Stories_Dependencies\AmpProject\Dom\Document\Filter;
use Google\Web_Stories_Dependencies\AmpProject\Html\Attribute;
use Google\Web_Stories_Dependencies\AmpProject\Dom\Document;
use Google\Web_Stories_Dependencies\AmpProject\Dom\Document\AfterLoadFilter;
use Google\Web_Stories_Dependencies\AmpProject\Dom\Document\AfterSaveFilter;
use Google\Web_Stories_Dependencies\AmpProject\Dom\Document\BeforeLoadFilter;
use Google\Web_Stories_Dependencies\AmpProject\Dom\Document\BeforeSaveFilter;
use Google\Web_Stories_Dependencies\AmpProject\Dom\Element;
use Google\Web_Stories_Dependencies\AmpProject\Html\Tag;
use DOMComment;
/**
* Filter for http-equiv charset.
*
* @package ampproject/amp-toolbox
*/
final class HttpEquivCharset implements BeforeLoadFilter, AfterLoadFilter, BeforeSaveFilter, AfterSaveFilter
{
/**
* Value of the content field of the meta tag for the http-equiv compatibility charset.
*
* @var string
*/
const HTML_HTTP_EQUIV_CONTENT_VALUE = 'text/html; charset=utf-8';
/**
* Type of the meta tag for the http-equiv compatibility charset.
*
* @var string
*/
const HTML_HTTP_EQUIV_VALUE = 'content-type';
/**
* Charset compatibility tag for making DOMDocument behave.
*
* See: http://php.net/manual/en/domdocument.loadhtml.php#78243.
*
* @var string
*/
const HTTP_EQUIV_META_TAG = '<meta http-equiv="content-type" content="text/html; charset=utf-8">';
/**
* Regex pattern for adding an http-equiv charset for compatibility by anchoring it to the <head> tag.
*
* The opening tag pattern contains a comment to make sure we don't match a <head> tag within a comment.
*
* @var string
*/
const HTML_GET_HEAD_OPENING_TAG_PATTERN = '/(?><!--.*?-->\\s*)*<head(?>\\s+[^>]*)?>/is';
/**
* Regex replacement template for adding an http-equiv charset for compatibility by anchoring it to the <head> tag.
*
* @var string
*/
const HTML_GET_HEAD_OPENING_TAG_REPLACEMENT = '$0' . self::HTTP_EQUIV_META_TAG;
/**
* Regex pattern for adding an http-equiv charset for compatibility by anchoring it to the <html> tag.
*
* The opening tag pattern contains a comment to make sure we don't match a <html> tag within a comment.
*
* @var string
*/
const HTML_GET_HTML_OPENING_TAG_PATTERN = '/(?><!--.*?-->\\s*)*<html(?>\\s+[^>]*)?>/is';
/**
* Regex replacement template for adding an http-equiv charset for compatibility by anchoring it to the <html> tag.
*
* @var string
*/
const HTML_GET_HTML_OPENING_TAG_REPLACEMENT = '$0<head>' . self::HTTP_EQUIV_META_TAG . '</head>';
/**
* Regex pattern for matching an existing or added http-equiv charset.
*/
const HTML_GET_HTTP_EQUIV_TAG_PATTERN = '#<meta http-equiv=([\'"])content-type\\1 ' . 'content=([\'"])text/html; ' . 'charset=utf-8\\2>#i';
/**
* Temporary http-equiv charset node added to a document before saving.
*
* @var Element|null
*/
private $temporaryCharset;
/**
* Add a http-equiv charset meta tag to the document's <head> node.
*
* This is needed to make the DOMDocument behave as it should in terms of encoding.
* See: http://php.net/manual/en/domdocument.loadhtml.php#78243.
*
* @param string $html HTML string to add the http-equiv charset to.
* @return string Adapted string of HTML.
*/
public function beforeLoad($html)
{
$count = 0;
// We try first to detect an existing <head> node.
$result = \preg_replace(self::HTML_GET_HEAD_OPENING_TAG_PATTERN, self::HTML_GET_HEAD_OPENING_TAG_REPLACEMENT, $html, 1, $count);
if (\is_string($result)) {
$html = $result;
}
// If no <head> was found, we look for the <html> tag instead.
if ($count < 1) {
$result = \preg_replace(self::HTML_GET_HTML_OPENING_TAG_PATTERN, self::HTML_GET_HTML_OPENING_TAG_REPLACEMENT, $html, 1, $count);
if (\is_string($result)) {
$html = $result;
}
}
// Finally, we just prepend the head with the required http-equiv charset.
if ($count < 1) {
$html = '<head>' . self::HTTP_EQUIV_META_TAG . '</head>' . $html;
}
return $html;
}
/**
* Remove http-equiv charset again.
*
* @param Document $document Document to be processed.
*/
public function afterLoad(Document $document)
{
$meta = $document->head->firstChild;
// We might have leading comments we need to preserve here.
while ($meta instanceof DOMComment) {
$meta = $meta->nextSibling;
}
if ($meta instanceof Element && Tag::META === $meta->tagName && self::HTML_HTTP_EQUIV_VALUE === $meta->getAttribute(Attribute::HTTP_EQUIV) && self::HTML_HTTP_EQUIV_CONTENT_VALUE === $meta->getAttribute(Attribute::CONTENT)) {
$document->head->removeChild($meta);
}
}
/**
* Add a temporary http-equiv charset to the document before saving.
*
* @param Document $document Document to be preprocessed before saving it into HTML.
*/
public function beforeSave(Document $document)
{
// Force-add http-equiv charset to make DOMDocument behave as it should.
// See: http://php.net/manual/en/domdocument.loadhtml.php#78243.
$this->temporaryCharset = $document->createElement(Tag::META);
$this->temporaryCharset->setAttribute(Attribute::HTTP_EQUIV, self::HTML_HTTP_EQUIV_VALUE);
$this->temporaryCharset->setAttribute(Attribute::CONTENT, self::HTML_HTTP_EQUIV_CONTENT_VALUE);
$document->head->insertBefore($this->temporaryCharset, $document->head->firstChild);
}
/**
* Remove the temporary http-equiv charset again.
*
* It is also removed from the DOM again in case saveHTML() is used multiple times.
*
* @param string $html String of HTML markup to be preprocessed.
* @return string Preprocessed string of HTML markup.
*/
public function afterSave($html)
{
if ($this->temporaryCharset instanceof Element) {
$this->temporaryCharset->parentNode->removeChild($this->temporaryCharset);
}
$result = \preg_replace(self::HTML_GET_HTTP_EQUIV_TAG_PATTERN, '', $html, 1);
if (!\is_string($result)) {
return $html;
}
return $result;
}
}

View File

@@ -0,0 +1,74 @@
<?php
namespace Google\Web_Stories_Dependencies\AmpProject\Dom\Document\Filter;
use Google\Web_Stories_Dependencies\AmpProject\Dom\Document;
use Google\Web_Stories_Dependencies\AmpProject\Dom\Document\AfterLoadFilter;
use Google\Web_Stories_Dependencies\AmpProject\Dom\Document\BeforeLoadFilter;
use Google\Web_Stories_Dependencies\AmpProject\Dom\Document\Option;
use Google\Web_Stories_Dependencies\AmpProject\Dom\Options;
/**
* Filter for adapting the Libxml error behavior and options.
*
* @package ampproject/amp-toolbox
*/
final class LibxmlCompatibility implements BeforeLoadFilter, AfterLoadFilter
{
/**
* Options instance to use.
*
* @var Options
*/
private $options;
/**
* Store the previous state fo the libxml "use internal errors" setting.
*
* @var bool
*/
private $libxmlPreviousState;
/**
* LibxmlCompatibility constructor.
*
* @param Options $options Options instance to use.
*/
public function __construct(Options $options)
{
$this->options = $options;
}
/**
* Preprocess the HTML to be loaded into the Dom\Document.
*
* @param string $html String of HTML markup to be preprocessed.
* @return string Preprocessed string of HTML markup.
*/
public function beforeLoad($html)
{
$this->libxmlPreviousState = \libxml_use_internal_errors(\true);
$this->options[Option::LIBXML_FLAGS] |= \LIBXML_COMPACT;
/*
* LIBXML_HTML_NODEFDTD is only available for libxml 2.7.8+.
* This should be the case for PHP 5.4+, but some systems seem to compile against a custom libxml version that
* is lower than expected.
*/
if (\defined('LIBXML_HTML_NODEFDTD')) {
$this->options[Option::LIBXML_FLAGS] |= \constant('LIBXML_HTML_NODEFDTD');
}
/**
* This flag prevents removing the closing tags used in inline JavaScript variables.
*/
if (\defined('LIBXML_SCHEMA_CREATE')) {
$this->options[Option::LIBXML_FLAGS] |= \constant('LIBXML_SCHEMA_CREATE');
}
return $html;
}
/**
* Process the Document after the html loaded into the Dom\Document.
*
* @param Document $document Document to be processed.
*/
public function afterLoad(Document $document)
{
\libxml_clear_errors();
\libxml_use_internal_errors($this->libxmlPreviousState);
}
}

View File

@@ -0,0 +1,171 @@
<?php
namespace Google\Web_Stories_Dependencies\AmpProject\Dom\Document\Filter;
use Google\Web_Stories_Dependencies\AmpProject\Dom\Document;
use Google\Web_Stories_Dependencies\AmpProject\Dom\Document\AfterLoadFilter;
use Google\Web_Stories_Dependencies\AmpProject\Dom\Document\AfterSaveFilter;
use Google\Web_Stories_Dependencies\AmpProject\Dom\Document\BeforeLoadFilter;
use Google\Web_Stories_Dependencies\AmpProject\Dom\Document\BeforeSaveFilter;
use Google\Web_Stories_Dependencies\AmpProject\Html\Tag;
/**
* Filter to handle the script[template="amp-mustache"].
*
* @package ampproject/amp-toolbox
*/
final class MustacheScriptTemplates implements BeforeLoadFilter, AfterLoadFilter, BeforeSaveFilter, AfterSaveFilter
{
/**
* Xpath query to fetch the elements containing Mustache templates (both <template type=amp-mustache> and
* <script type=text/plain template=amp-mustache>).
*
* @var string
*/
const XPATH_MUSTACHE_TEMPLATE_ELEMENTS_QUERY = './/self::template[ @type = "amp-mustache" ]' . '|//self::script[ @type = "text/plain" ' . 'and @template = "amp-mustache" ]';
/**
* Xpath query to fetch the attributes that are being URL-encoded by saveHTML().
*
* @var string
*/
const XPATH_URL_ENCODED_ATTRIBUTES_QUERY = './/*/@src|.//*/@href|.//*/@action';
/**
* Store whether mustache template tags were replaced and need to be restored.
*
* @see replaceMustacheTemplateTokens()
*
* @var bool
*/
private $mustacheTagsReplaced = \false;
/**
* Secures instances of script[template="amp-mustache"] by renaming element to tmp-script, as a workaround to a
* libxml parsing issue.
*
* This script can have closing tags of its children table and td stripped.
* So this changes its name from script to tmp-script to avoid this.
*
* @link https://github.com/ampproject/amp-wp/issues/4254
*
* @param string $html To replace the tag name that contains the mustache templates.
* @return string The HTML, with the tag name of the mustache templates replaced.
*/
public function beforeLoad($html)
{
$result = \preg_replace('#<script(\\s[^>]*?template=(["\']?)amp-mustache\\2[^>]*)>(.*?)</script\\s*?>#is', '<tmp-script$1>$3</tmp-script>', $html);
if (!\is_string($result)) {
return $html;
}
return $result;
}
/**
* Restores the tag names of script[template="amp-mustache"] elements that were replaced earlier.
*
* @param Document $document Document to be processed.
*/
public function afterLoad(Document $document)
{
$tmp_script_elements = \iterator_to_array($document->getElementsByTagName('tmp-script'));
foreach ($tmp_script_elements as $tmp_script_element) {
$script = $document->createElement(Tag::SCRIPT);
foreach ($tmp_script_element->attributes as $attr) {
$script->setAttribute($attr->nodeName, $attr->nodeValue);
}
while ($tmp_script_element->firstChild) {
$script->appendChild($tmp_script_element->firstChild);
}
$tmp_script_element->parentNode->replaceChild($script, $tmp_script_element);
}
}
/**
* Replace Mustache template tokens to safeguard them from turning into HTML entities.
*
* Prevents amp-mustache syntax from getting URL-encoded in attributes when saveHTML is done.
* While this is applying to the entire document, it only really matters inside of <template>
* elements, since URL-encoding of curly braces in href attributes would not normally matter.
* But when this is done inside of a <template> then it breaks Mustache. Since Mustache
* is logic-less and curly braces are not unsafe for HTML, we can do a global replacement.
* The replacement is done on the entire HTML document instead of just inside of the <template>
* elements since it is faster and wouldn't change the outcome.
*
* @param Document $document Document to be processed.
*/
public function beforeSave(Document $document)
{
$templates = $document->xpath->query(self::XPATH_MUSTACHE_TEMPLATE_ELEMENTS_QUERY, $document->body);
if (0 === $templates->length) {
return;
}
$mustacheTagPlaceholders = $this->getMustacheTagPlaceholders();
foreach ($templates as $template) {
foreach ($document->xpath->query(self::XPATH_URL_ENCODED_ATTRIBUTES_QUERY, $template) as $attribute) {
$value = \preg_replace_callback($this->getMustacheTagPattern(), static function ($matches) use($mustacheTagPlaceholders) {
return $mustacheTagPlaceholders[\trim($matches[0])];
}, $attribute->nodeValue, -1, $count);
if ($count) {
// Note we cannot do `$attribute->nodeValue = $value` because the PHP DOM will try to parse any
// entities. In the case of a URL value like '/foo/?bar=1&baz=2' the result is a warning for an
// unterminated entity reference "baz". When the attribute value is updated via setAttribute() this
// same problem does not occur, so that is why the following is used.
$attribute->parentNode->setAttribute($attribute->nodeName, $value);
$this->mustacheTagsReplaced = \true;
}
}
}
}
/**
* Restore Mustache template tokens that were previously replaced.
*
* @param string $html HTML string to adapt.
* @return string Adapted HTML string.
*/
public function afterSave($html)
{
if (!$this->mustacheTagsReplaced) {
return $html;
}
$mustacheTagPlaceholders = $this->getMustacheTagPlaceholders();
return \str_replace($mustacheTagPlaceholders, \array_keys($mustacheTagPlaceholders), $html);
}
/**
* Get amp-mustache tag/placeholder mappings.
*
* @return string[] Mapping of mustache tag token to its placeholder.
* @see \wpdb::placeholder_escape()
*/
private function getMustacheTagPlaceholders()
{
static $placeholders = null;
if (null === $placeholders) {
$placeholders = [];
// Note: The order of these tokens is important, as it determines the order of the replacements.
$tokens = ['{{{', '}}}', '{{#', '{{^', '{{/', '{{', '}}'];
foreach ($tokens as $token) {
$placeholders[$token] = '_amp_mustache_' . \md5(\uniqid($token));
}
}
return $placeholders;
}
/**
* Get a regular expression that matches all amp-mustache tags while consuming whitespace.
*
* Removing whitespace is needed to avoid DOMDocument turning whitespace into entities, like %20 for spaces.
*
* @return string Regex pattern to match amp-mustache tags with whitespace.
*/
private function getMustacheTagPattern()
{
static $tagPattern = null;
if (null === $tagPattern) {
$delimiter = ':';
$tags = [];
foreach (\array_keys($this->getMustacheTagPlaceholders()) as $token) {
if ('{' === $token[0]) {
$tags[] = \preg_quote($token, $delimiter) . '\\s*';
} else {
$tags[] = '\\s*' . \preg_quote($token, $delimiter);
}
}
$tagPattern = $delimiter . \implode('|', $tags) . $delimiter;
}
return $tagPattern;
}
}

View File

@@ -0,0 +1,41 @@
<?php
namespace Google\Web_Stories_Dependencies\AmpProject\Dom\Document\Filter;
use Google\Web_Stories_Dependencies\AmpProject\Dom\Document;
use Google\Web_Stories_Dependencies\AmpProject\Dom\Document\AfterLoadFilter;
use DOMAttr;
/**
* Normalizes HTML attributes to be HTML5 compatible.
*
* @package ampproject/amp-toolbox
*/
final class NormalizeHtmlAttributes implements AfterLoadFilter
{
/**
* Normalizes HTML attributes to be HTML5 compatible.
*
* Conditionally removes html[xmlns], and converts html[xml:lang] to html[lang].
*
* @param Document $document Document to be processed.
*/
public function afterLoad(Document $document)
{
if (!$document->html->hasAttributes()) {
return;
}
$xmlns = $document->html->attributes->getNamedItem('xmlns');
if ($xmlns instanceof DOMAttr && 'http://www.w3.org/1999/xhtml' === $xmlns->nodeValue) {
$document->html->removeAttributeNode($xmlns);
}
$xml_lang = $document->html->attributes->getNamedItem('xml:lang');
if ($xml_lang instanceof DOMAttr) {
$lang_node = $document->html->attributes->getNamedItem('lang');
if ((!$lang_node || !$lang_node->nodeValue) && $xml_lang->nodeValue) {
// Move the html[xml:lang] value to html[lang].
$document->html->setAttribute('lang', $xml_lang->nodeValue);
}
$document->html->removeAttributeNode($xml_lang);
}
}
}

View File

@@ -0,0 +1,71 @@
<?php
namespace Google\Web_Stories_Dependencies\AmpProject\Dom\Document\Filter;
use Google\Web_Stories_Dependencies\AmpProject\Dom\Document\BeforeLoadFilter;
use Google\Web_Stories_Dependencies\AmpProject\Dom\Document\Option;
use Google\Web_Stories_Dependencies\AmpProject\Dom\Options;
use Google\Web_Stories_Dependencies\AmpProject\Exception\InvalidOptionValue;
/**
* Handles the html entities present in the html and prevents them from double encoding.
*
* @package ampproject/amp-toolbox
*/
final class NormalizeHtmlEntities implements BeforeLoadFilter
{
const VALID_NORMALIZE_OPTION_VALUES = [Option::NORMALIZE_HTML_ENTITIES_AUTO, Option::NORMALIZE_HTML_ENTITIES_ALWAYS, Option::NORMALIZE_HTML_ENTITIES_NEVER];
/**
* Options instance to use.
*
* @var Options
*/
private $options;
/**
* Whether to use the NormalizeHtmlEntities filter or not.
*
* Accepted values are 'auto', 'always' and 'never'.
*
* @var string
*/
private $normalizeHtmlEntities;
/**
* NormalizeHtmlEntities constructor.
*
* @param Options $options Options instance to use.
*
* @throws InvalidOptionValue If invalid value is set to normalize_html_entities option.
*/
public function __construct(Options $options)
{
$this->options = $options;
$this->normalizeHtmlEntities = $options[Option::NORMALIZE_HTML_ENTITIES];
if (!\in_array($this->normalizeHtmlEntities, self::VALID_NORMALIZE_OPTION_VALUES, \true)) {
throw InvalidOptionValue::forValue(Option::NORMALIZE_HTML_ENTITIES, self::VALID_NORMALIZE_OPTION_VALUES, $this->normalizeHtmlEntities);
}
}
/**
* Preprocess the HTML to be loaded into the Dom\Document.
*
* @param string $html String of HTML markup to be preprocessed.
* @return string Preprocessed string of HTML markup.
*/
public function beforeLoad($html)
{
if ($this->normalizeHtmlEntities === Option::NORMALIZE_HTML_ENTITIES_NEVER || $this->normalizeHtmlEntities === Option::NORMALIZE_HTML_ENTITIES_AUTO && !$this->hasHtmlEntities($html)) {
return $html;
}
return \html_entity_decode($html, $this->options[Option::NORMALIZE_HTML_ENTITIES_FLAGS], $this->options[Option::ENCODING]);
}
/**
* Detect the presence of html entities in the html.
*
* @param string $html The html in which this method will to detect the entities.
* @return bool Whether the html contains entities or not.
*/
protected function hasHtmlEntities($html)
{
// TODO: Discuss other popular entities to look for, especially for languages
// with different punctuation symbols.
return \preg_match('/&comma;|&period;|&excl;|&quest;/', $html);
}
}

View File

@@ -0,0 +1,107 @@
<?php
namespace Google\Web_Stories_Dependencies\AmpProject\Dom\Document\Filter;
use Google\Web_Stories_Dependencies\AmpProject\Dom\Document;
use Google\Web_Stories_Dependencies\AmpProject\Dom\Document\AfterLoadFilter;
use Google\Web_Stories_Dependencies\AmpProject\Dom\Document\BeforeLoadFilter;
use Google\Web_Stories_Dependencies\AmpProject\Dom\UniqueIdManager;
use Google\Web_Stories_Dependencies\AmpProject\Html\Tag;
/**
* Handle the noscript elements with placeholders.
*
* @package ampproject/amp-toolbox
*/
final class NoscriptElements implements BeforeLoadFilter, AfterLoadFilter
{
/**
* UniqueIdManager instance to use.
*
* @var UniqueIdManager
*/
private $uniqueIdManager;
/**
* NoscriptElements constructor.
*
* @param UniqueIdManager $uniqueIdManager UniqueIdManager instance to use.
*/
public function __construct(UniqueIdManager $uniqueIdManager)
{
$this->uniqueIdManager = $uniqueIdManager;
}
/**
* Store the <noscript> markup that was extracted to preserve it during parsing.
*
* The array keys are the element IDs for placeholder <meta> tags.
*
* @var string[]
*/
private $noscriptPlaceholderComments = [];
/**
* Maybe replace noscript elements with placeholders.
*
* This is done because libxml<2.8 might parse them incorrectly.
* When appearing in the head element, a noscript can cause the head to close prematurely
* and the noscript gets moved to the body and anything after it which was in the head.
* See <https://stackoverflow.com/questions/39013102/why-does-noscript-move-into-body-tag-instead-of-head-tag>.
* This is limited to only running in the head element because this is where the problem lies,
* and it is important for the AMP_Script_Sanitizer to be able to access the noscript elements
* in the body otherwise.
*
* @param string $html HTML string to adapt.
* @return string Adapted HTML string.
*/
public function beforeLoad($html)
{
if (\version_compare(\LIBXML_DOTTED_VERSION, '2.8', '<')) {
$result = \preg_replace_callback('#^.+?(?=<body)#is', function ($headMatches) {
return \preg_replace_callback('#<noscript[^>]*>.*?</noscript>#si', function ($noscriptMatches) {
$id = $this->uniqueIdManager->getUniqueId('noscript');
$this->noscriptPlaceholderComments[$id] = $noscriptMatches[0];
return \sprintf('<meta class="noscript-placeholder" id="%s">', $id);
}, $headMatches[0]);
}, $html);
if (\is_string($result)) {
$html = $result;
}
}
return $html;
}
/**
* Maybe restore noscript elements with placeholders.
*
* This is done because libxml<2.8 might parse them incorrectly.
* When appearing in the head element, a noscript can cause the head to close prematurely
* and the noscript gets moved to the body and anything after it which was in the head.
* See <https://stackoverflow.com/questions/39013102/why-does-noscript-move-into-body-tag-instead-of-head-tag>.
* This is limited to only running in the head element because this is where the problem lies,
* and it is important for the AMP_Script_Sanitizer to be able to access the noscript elements
* in the body otherwise.
*
* @param Document $document Document to be processed.
*/
public function afterLoad(Document $document)
{
foreach ($this->noscriptPlaceholderComments as $id => $noscriptHtmlFragment) {
$placeholderElement = $document->getElementById($id);
if (!$placeholderElement || !$placeholderElement->parentNode) {
continue;
}
$noscriptFragmentDocument = $document::fromHtmlFragment($noscriptHtmlFragment);
if (!$noscriptFragmentDocument) {
continue;
}
$exportBody = $noscriptFragmentDocument->getElementsByTagName(Tag::BODY)->item(0);
if (!$exportBody) {
continue;
}
$importFragment = $document->createDocumentFragment();
while ($exportBody->firstChild) {
$importNode = $exportBody->removeChild($exportBody->firstChild);
$importNode = $document->importNode($importNode, \true);
$importFragment->appendChild($importNode);
}
$placeholderElement->parentNode->replaceChild($importFragment, $placeholderElement);
}
}
}

View File

@@ -0,0 +1,54 @@
<?php
namespace Google\Web_Stories_Dependencies\AmpProject\Dom\Document\Filter;
use Google\Web_Stories_Dependencies\AmpProject\Dom\Document\AfterSaveFilter;
use Google\Web_Stories_Dependencies\AmpProject\Dom\Document\BeforeLoadFilter;
/**
* Protect the esi tags from being broken.
*
* @package ampproject/amp-toolbox
*/
final class ProtectEsiTags implements BeforeLoadFilter, AfterSaveFilter
{
/**
* List of self-closing ESI tags.
*
* @link https://www.w3.org/TR/esi-lang/
*
* @var string[]
*/
const SELF_CLOSING_TAGS = ['esi:include', 'esi:comment'];
/**
* Preprocess the HTML to be loaded into the Dom\Document.
*
* @param string $html String of HTML markup to be preprocessed.
* @return string Preprocessed string of HTML markup.
*/
public function beforeLoad($html)
{
$patterns = ['#<(' . \implode('|', self::SELF_CLOSING_TAGS) . ')([^>]*?)(?>\\s*(?<!\\\\)/)?>(?!</\\1>)#', '/(<esi:include.+?)(src)=/', '/(<\\/?)esi:/'];
$replacements = ['<$1$2></$1>', '$1esi-src=', '$1esi-'];
$result = \preg_replace($patterns, $replacements, $html);
if (!\is_string($result)) {
return $html;
}
return $result;
}
/**
* Process the Dom\Document after being saved from Dom\Document.
*
* @param string $html String of HTML markup to be preprocessed.
* @return string Preprocessed string of HTML markup.
*/
public function afterSave($html)
{
$patterns = ['/(<\\/?)esi-/', '/(<esi:include.+?)(esi-src)=/', '#></(' . \implode('|', self::SELF_CLOSING_TAGS) . ')>#i'];
$replacements = ['$1esi:', '$1src=', '/>'];
$result = \preg_replace($patterns, $replacements, $html);
if (!\is_string($result)) {
return $html;
}
return $result;
}
}

View File

@@ -0,0 +1,57 @@
<?php
namespace Google\Web_Stories_Dependencies\AmpProject\Dom\Document\Filter;
use Google\Web_Stories_Dependencies\AmpProject\Dom\Document\AfterSaveFilter;
use Google\Web_Stories_Dependencies\AmpProject\Dom\Document\BeforeLoadFilter;
use Google\Web_Stories_Dependencies\AmpProject\Html\Tag;
/**
* Filter to secure and restore self-closing SVG related elements.
*
* @package ampproject/amp-toolbox
*/
final class SelfClosingSVGElements implements BeforeLoadFilter, AfterSaveFilter
{
/**
* SVG elements that are self-closing.
*
* @var string[]
*/
const SELF_CLOSING_TAGS = [Tag::CIRCLE, Tag::G, Tag::PATH];
/**
* Force all self-closing tags to have closing tags.
*
* @param string $html HTML string to adapt.
* @return string Adapted HTML string.
*/
public function beforeLoad($html)
{
static $regexPattern = null;
if (null === $regexPattern) {
$regexPattern = '#<(' . \implode('|', self::SELF_CLOSING_TAGS) . ')((?>\\s*[^/>]*))/?>(?!.*</\\1>)#is';
}
$result = \preg_replace($regexPattern, '<$1$2></$1>', $html);
if (!\is_string($result)) {
return $html;
}
return $result;
}
/**
* Restore all self-closing tags again.
*
* @param string $html HTML string to adapt.
* @return string Adapted HTML string.
*/
public function afterSave($html)
{
static $regexPattern = null;
if (null === $regexPattern) {
$regexPattern = '#<(' . \implode('|', self::SELF_CLOSING_TAGS) . ')((?>\\s*[^>]*))>(?><\\/\\1>)#i';
}
$result = \preg_replace($regexPattern, '<$1$2 />', $html);
if (!\is_string($result)) {
return $html;
}
return $result;
}
}

View File

@@ -0,0 +1,66 @@
<?php
namespace Google\Web_Stories_Dependencies\AmpProject\Dom\Document\Filter;
use Google\Web_Stories_Dependencies\AmpProject\Dom\Document\AfterSaveFilter;
use Google\Web_Stories_Dependencies\AmpProject\Dom\Document\BeforeLoadFilter;
use Google\Web_Stories_Dependencies\AmpProject\Html\Tag;
/**
* Filter to secure and restore self-closing tags.
*
* @package ampproject/amp-toolbox
*/
final class SelfClosingTags implements BeforeLoadFilter, AfterSaveFilter
{
/**
* Whether the self-closing tags were transformed and need to be restored.
*
* This avoids duplicating this effort (maybe corrupting the DOM) on multiple calls to saveHTML().
*
* @var bool
*/
private $selfClosingTagsTransformed = \false;
/**
* Force all self-closing tags to have closing tags.
*
* This is needed because DOMDocument isn't fully aware of these.
*
* @param string $html HTML string to adapt.
* @return string Adapted HTML string.
*/
public function beforeLoad($html)
{
static $regexPattern = null;
if (null === $regexPattern) {
$regexPattern = '#<(' . \implode('|', Tag::SELF_CLOSING_TAGS) . ')([^>]*?)(?>\\s*(?<!\\\\)/)?>(?!</\\1>)#';
}
$this->selfClosingTagsTransformed = \true;
$result = \preg_replace($regexPattern, '<$1$2></$1>', $html);
if (!\is_string($result)) {
return $html;
}
return $result;
}
/**
* Restore all self-closing tags again.
*
* @param string $html HTML string to adapt.
* @return string Adapted HTML string.
*/
public function afterSave($html)
{
static $regexPattern = null;
if (!$this->selfClosingTagsTransformed) {
return $html;
}
if (null === $regexPattern) {
$regexPattern = '#</(' . \implode('|', Tag::SELF_CLOSING_TAGS) . ')>#i';
}
$this->selfClosingTagsTransformed = \false;
$result = \preg_replace($regexPattern, '', $html);
if (!\is_string($result)) {
return $html;
}
return $result;
}
}

View File

@@ -0,0 +1,66 @@
<?php
namespace Google\Web_Stories_Dependencies\AmpProject\Dom\Document\Filter;
use Google\Web_Stories_Dependencies\AmpProject\Dom\Document\AfterSaveFilter;
/**
* Filter for fixing the mangled encoding of src attributes with SVG data.
*
* @package ampproject/amp-toolbox
*/
final class SvgSourceAttributeEncoding implements AfterSaveFilter
{
/**
* Regex pattern to match an SVG sizer element.
*
* @var string
*/
const I_AMPHTML_SIZER_REGEX_PATTERN = '/(?<before_src><i-amphtml-sizer\\s+[^>]*>\\s*<img\\s+[^>]*?\\s+src=([\'"]))' . '(?<src>.*?)' . '(?<after_src>\\2><\\/i-amphtml-sizer>)/i';
/**
* Regex pattern for extracting fields to adapt out of a src attribute.
*
* @var string
*/
const SRC_SVG_REGEX_PATTERN = '/^\\s*(?<type>[^<]+)(?<value><svg[^>]+>)\\s*$/i';
/**
* Process SVG sizers to ensure they match the required format to validate against AMP.
*
* @param string $html HTML output string to tweak.
* @return string Tweaked HTML output string.
*/
public function afterSave($html)
{
$result = \preg_replace_callback(self::I_AMPHTML_SIZER_REGEX_PATTERN, [$this, 'adaptSizer'], $html);
if (!\is_string($result)) {
return $html;
}
return $result;
}
/**
* Adapt the sizer element so that it validates against the AMP spec.
*
* @param array $matches Matches that the regular expression collected.
* @return string Adapted string to use as replacement.
*/
private function adaptSizer($matches)
{
$src = $matches['src'];
$src = \htmlspecialchars_decode($src, \ENT_NOQUOTES);
$src = \preg_replace_callback(self::SRC_SVG_REGEX_PATTERN, [$this, 'adaptSvg'], $src);
if (!\is_string($src)) {
// The regex replace failed, so revert to the initial src.
$src = $matches['src'];
}
return $matches['before_src'] . $src . $matches['after_src'];
}
/**
* Adapt the SVG syntax within the sizer element so that it validates against the AMP spec.
*
* @param array $matches Matches that the regular expression collected.
* @return string Adapted string to use as replacement.
*/
private function adaptSvg($matches)
{
return $matches['type'] . \urldecode($matches['value']);
}
}

View File

@@ -0,0 +1,92 @@
<?php
namespace Google\Web_Stories_Dependencies\AmpProject\Dom\Document;
/**
* Option constants that can be used to configure a Dom\Document instance.
*
* @package ampproject/amp-toolbox
*/
interface Option
{
/**
* Option to configure the preferred amp-bind syntax.
*
* @var string
*/
const AMP_BIND_SYNTAX = 'amp_bind_syntax';
/**
* Option to provide the encoding of the document.
*
* @var string
*/
const ENCODING = 'encoding';
/**
* Option to provide additional libxml flags to configure parsing of the document.
*
* @var string
*/
const LIBXML_FLAGS = 'libxml_flags';
/**
* Option to check encoding in order to detect invalid byte sequences.
*
* @var string
*/
const CHECK_ENCODING = 'check_encoding';
/**
* Option to use the NormalizeHtmlEntities filter.
*
* Accepted values are 'auto', 'always' and 'never'.
*
* @var string
*/
const NORMALIZE_HTML_ENTITIES = 'normalize_html_entities';
/**
* Flags option for html entities.
*
* @var string
*/
const NORMALIZE_HTML_ENTITIES_FLAGS = 'normalize_html_entities_flags';
/**
* Associative array of known options and their respective default value.
*
* @var array
*/
const DEFAULTS = [self::AMP_BIND_SYNTAX => self::AMP_BIND_SYNTAX_AUTO, self::ENCODING => null, self::LIBXML_FLAGS => 0, self::CHECK_ENCODING => \false, self::NORMALIZE_HTML_ENTITIES => self::NORMALIZE_HTML_ENTITIES_AUTO, self::NORMALIZE_HTML_ENTITIES_FLAGS => \ENT_HTML5];
/**
* Possible value 'auto' for the 'amp_bind_syntax' option.
*
* @var string
*/
const AMP_BIND_SYNTAX_AUTO = 'auto';
/**
* Possible value 'data_attribute' for the 'amp_bind_syntax' option.
*
* @var string
*/
const AMP_BIND_SYNTAX_DATA_ATTRIBUTE = 'data_attribute';
/**
* Possible value 'square_brackets' for the 'amp_bind_syntax' option.
*
* @var string
*/
const AMP_BIND_SYNTAX_SQUARE_BRACKETS = 'square_brackets';
/**
* Possible value 'auto' for the 'normalize_html_entities' option.
*
* @var string
*/
const NORMALIZE_HTML_ENTITIES_AUTO = 'auto';
/**
* Possible value 'always' for the 'normalize_html_entities' option.
*
* @var string
*/
const NORMALIZE_HTML_ENTITIES_ALWAYS = 'always';
/**
* Possible value 'never' for the 'normalize_html_entities' option.
*
* @var string
*/
const NORMALIZE_HTML_ENTITIES_NEVER = 'never';
}

View File

@@ -0,0 +1,270 @@
<?php
namespace Google\Web_Stories_Dependencies\AmpProject\Dom;
use Google\Web_Stories_Dependencies\AmpProject\Html\Attribute;
use Google\Web_Stories_Dependencies\AmpProject\Exception\MaxCssByteCountExceeded;
use Google\Web_Stories_Dependencies\AmpProject\Optimizer\CssRule;
use DOMAttr;
use DOMElement;
/**
* Abstract away some convenience logic for handling DOMElement objects.
*
* @property Document $ownerDocument The ownerDocument for these elements should always be a Dom\Document.
* @property int $inlineStyleByteCount Number of bytes that are consumed by the inline style attribute.
*
* @package ampproject/amp-toolbox
*/
final class Element extends DOMElement
{
/**
* Regular expression pattern to match events and actions within an 'on' attribute.
*
* @var string
*/
const AMP_EVENT_ACTIONS_REGEX_PATTERN = '/((?<event>[^:;]+):(?<actions>(?:[^;,\\(]+(?:\\([^\\)]+\\))?,?)+))+?/';
/**
* Regular expression pattern to match individual actions within an event.
*
* @var string
*/
const AMP_ACTION_REGEX_PATTERN = '/(?<action>[^(),\\s]+(?:\\([^\\)]+\\))?)+/';
/**
* Error message to use when the __get() is triggered for an unknown property.
*
* @var string
*/
const PROPERTY_GETTER_ERROR_MESSAGE = 'Undefined property: AmpProject\\Dom\\Element::';
/**
* Associative array for lazily-created, cached properties for the document.
*
* @var array
*/
private $properties = [];
/**
* Add CSS styles to the element as an inline style attribute.
*
* @param string $style CSS style(s) to add to the inline style attribute.
* @param bool $prepend Optional. Whether to prepend the new style to existing styles or not. Defaults to false.
* @return DOMAttr|false The new or modified DOMAttr or false if an error occurred.
* @throws MaxCssByteCountExceeded If the allowed max byte count is exceeded.
*/
public function addInlineStyle($style, $prepend = \false)
{
$style = \trim($style, CssRule::CSS_TRIM_CHARACTERS);
$existingStyle = (string) \trim($this->getAttribute(Attribute::STYLE));
if (!empty($existingStyle)) {
$existingStyle = \rtrim($existingStyle, ';') . ';';
}
$newStyle = $prepend ? $style . ';' . $existingStyle : $existingStyle . $style;
return $this->setAttribute(Attribute::STYLE, $newStyle);
}
/**
* Sets or modifies an attribute.
*
* @link https://php.net/manual/en/domelement.setattribute.php
* @param string $name The name of the attribute.
* @param string $value The value of the attribute.
* @return DOMAttr|false The new or modified DOMAttr or false if an error occurred.
* @throws MaxCssByteCountExceeded If the allowed max byte count is exceeded.
*/
public function setAttribute($name, $value)
{
// Make sure $value is always a string and not null.
$value = \strval($value);
if ($name === Attribute::STYLE && $this->ownerDocument->isCssMaxByteCountEnforced()) {
$newByteCount = \strlen($value);
if ($this->ownerDocument->getRemainingCustomCssSpace() < $newByteCount - $this->inlineStyleByteCount) {
throw MaxCssByteCountExceeded::forInlineStyle($this, $value);
}
$this->ownerDocument->addInlineStyleByteCount($newByteCount - $this->inlineStyleByteCount);
$this->inlineStyleByteCount = $newByteCount;
return parent::setAttribute(Attribute::STYLE, $value);
}
return parent::setAttribute($name, $value);
}
/**
* Adds a boolean attribute without value.
*
* @param string $name The name of the attribute.
* @return DOMAttr|false The new or modified DOMAttr or false if an error occurred.
* @throws MaxCssByteCountExceeded If the allowed max byte count is exceeded.
*/
public function addBooleanAttribute($name)
{
$attribute = new DOMAttr($name);
$result = $this->setAttributeNode($attribute);
if (!$result instanceof DOMAttr) {
return \false;
}
return $result;
}
/**
* Copy one or more attributes from this element to another element.
*
* @param array|string $attributes Attribute name or array of attribute names to copy.
* @param Element $target Target Dom\Element to copy the attributes to.
* @param string $defaultSeparator Default separator to use for multiple values if the attribute is not known.
*/
public function copyAttributes($attributes, Element $target, $defaultSeparator = ',')
{
foreach ((array) $attributes as $attribute) {
if ($this->hasAttribute($attribute)) {
$values = $this->getAttribute($attribute);
if ($target->hasAttribute($attribute)) {
switch ($attribute) {
case Attribute::ON:
$values = self::mergeAmpActions($target->getAttribute($attribute), $values);
break;
case Attribute::CLASS_:
$values = $target->getAttribute($attribute) . ' ' . $values;
break;
default:
$values = $target->getAttribute($attribute) . $defaultSeparator . $values;
}
}
$target->setAttribute($attribute, $values);
}
}
}
/**
* Register an AMP action to an event.
*
* If the element already contains one or more events or actions, the method
* will assemble them in a smart way.
*
* @param string $event Event to trigger the action on.
* @param string $action Action to add.
*/
public function addAmpAction($event, $action)
{
$eventActionString = "{$event}:{$action}";
if (!$this->hasAttribute(Attribute::ON)) {
// There's no "on" attribute yet, so just add it and be done.
$this->setAttribute(Attribute::ON, $eventActionString);
return;
}
$this->setAttribute(Attribute::ON, self::mergeAmpActions($this->getAttribute(Attribute::ON), $eventActionString));
}
/**
* Merge two sets of AMP events & actions.
*
* @param string $first First event/action string.
* @param string $second First event/action string.
* @return string Merged event/action string.
*/
public static function mergeAmpActions($first, $second)
{
$events = [];
foreach ([$first, $second] as $eventActionString) {
$matches = [];
$results = \preg_match_all(self::AMP_EVENT_ACTIONS_REGEX_PATTERN, $eventActionString, $matches);
if (!$results || !isset($matches['event'])) {
continue;
}
foreach ($matches['event'] as $index => $event) {
$events[$event][] = $matches['actions'][$index];
}
}
$valueStrings = [];
foreach ($events as $event => $actionStringsArray) {
$actionsArray = [];
\array_walk($actionStringsArray, static function ($actions) use(&$actionsArray) {
$matches = [];
$results = \preg_match_all(self::AMP_ACTION_REGEX_PATTERN, $actions, $matches);
if (!$results || !isset($matches['action'])) {
$actionsArray[] = $actions;
return;
}
$actionsArray = \array_merge($actionsArray, $matches['action']);
});
$actions = \implode(',', \array_unique(\array_filter($actionsArray)));
$valueStrings[] = "{$event}:{$actions}";
}
return \implode(';', $valueStrings);
}
/**
* Extract this element's HTML attributes and return as an associative array.
*
* @return string[] The attributes for the passed node, or an empty array if it has no attributes.
*/
public function getAttributesAsAssocArray()
{
$attributes = [];
if (!$this->hasAttributes()) {
return $attributes;
}
foreach ($this->attributes as $attribute) {
$attributes[$attribute->nodeName] = $attribute->nodeValue;
}
return $attributes;
}
/**
* Add one or more HTML element attributes to this element.
*
* @param string[] $attributes One or more attributes for the node's HTML element.
*/
public function setAttributes($attributes)
{
foreach ($attributes as $name => $value) {
try {
$this->setAttribute($name, $value);
} catch (MaxCssByteCountExceeded $e) {
/*
* Catch a "Invalid Character Error" when libxml is able to parse attributes with invalid characters,
* but it throws error when attempting to set them via DOM methods. For example, '...this' can be parsed
* as an attribute but it will throw an exception when attempting to setAttribute().
*/
continue;
}
}
}
/**
* Magic getter to implement lazily-created, cached properties for the element.
*
* @param string $name Name of the property to get.
* @return mixed Value of the property, or null if unknown property was requested.
*/
public function __get($name)
{
switch ($name) {
case 'inlineStyleByteCount':
if (!isset($this->properties['inlineStyleByteCount'])) {
$this->properties['inlineStyleByteCount'] = \strlen((string) $this->getAttribute(Attribute::STYLE));
}
return $this->properties['inlineStyleByteCount'];
}
// Mimic regular PHP behavior for missing notices.
\trigger_error(self::PROPERTY_GETTER_ERROR_MESSAGE . $name, \E_USER_NOTICE);
return null;
}
/**
* Magic setter to implement lazily-created, cached properties for the element.
*
* @param string $name Name of the property to set.
* @param mixed $value Value of the property.
*/
public function __set($name, $value)
{
if ($name !== 'inlineStyleByteCount') {
// Mimic regular PHP behavior for missing notices.
\trigger_error(self::PROPERTY_GETTER_ERROR_MESSAGE . $name, \E_USER_NOTICE);
return;
}
$this->properties[$name] = $value;
}
/**
* Magic callback for lazily-created, cached properties for the element.
*
* @param string $name Name of the property to set.
*/
public function __isset($name)
{
if ($name !== 'inlineStyleByteCount') {
// Mimic regular PHP behavior for missing notices.
\trigger_error(self::PROPERTY_GETTER_ERROR_MESSAGE . $name, \E_USER_NOTICE);
return \false;
}
return isset($this->properties[$name]);
}
}

View File

@@ -0,0 +1,73 @@
<?php
namespace Google\Web_Stories_Dependencies\AmpProject\Dom;
use Google\Web_Stories_Dependencies\AmpProject\Str;
use DOMAttr;
/**
* Dump an element with its attributes.
*
* This is meant for quick identification of an element and does not dump the child element or other inner content
* from that element.
*
* @package ampproject/amp-toolbox
*/
final class ElementDump
{
/**
* Element to dump.
*
* @var Element
*/
private $element;
/**
* Maximum length to truncate attributes and textContent to.
*
* Defaults to 120.
*
* @var int
*/
private $truncate;
/**
* Instantiate an ElementDump object.
*
* The object is meant to be cast to a string to do its magic.
*
* @param Element $element Element to dump.
* @param int $truncate Optional. Maximum length to truncate attributes and textContent to. Defaults to 120.
*/
public function __construct(Element $element, $truncate = 120)
{
$this->element = $element;
$this->truncate = $truncate;
}
/**
* Dump the provided element into a string.
*
* @return string Dump of the element.
*/
public function __toString()
{
$attributes = $this->maybeTruncate(\array_reduce(\iterator_to_array($this->element->attributes, \true), static function ($text, DOMAttr $attribute) {
return $text . " {$attribute->nodeName}=\"{$attribute->value}\"";
}, ''));
$textContent = $this->maybeTruncate($this->element->textContent);
return \sprintf('<%1$s%2$s>%3$s</%1$s>', $this->element->tagName, $attributes, $textContent);
}
/**
* Truncate the provided text if needed.
*
* @param string $text Text to truncate.
* @return string Potentially truncated text.
*/
private function maybeTruncate($text)
{
if ($this->truncate <= 0) {
return $text;
}
if (Str::length($text) > $this->truncate) {
return Str::substring($text, 0, $this->truncate - 1) . '…';
}
return $text;
}
}

View File

@@ -0,0 +1,252 @@
<?php
namespace Google\Web_Stories_Dependencies\AmpProject\Dom;
use Google\Web_Stories_Dependencies\AmpProject\Html\Attribute;
use Google\Web_Stories_Dependencies\AmpProject\Exception\FailedToCreateLink;
use Google\Web_Stories_Dependencies\AmpProject\Html\RequestDestination;
use Google\Web_Stories_Dependencies\AmpProject\Html\Tag;
use DOMNode;
/**
* Link manager class that is used to manage the <link> tags within a document's <head>.
*
* These can be used for example to give the browser hints about how to prioritize resources.
*
* @package ampproject/amp-toolbox
*/
final class LinkManager
{
/**
* List of relations currently managed by the link manager.
*
* @var array<string>
*/
const MANAGED_RELATIONS = [Attribute::REL_DNS_PREFETCH, Attribute::REL_MODULEPRELOAD, Attribute::REL_PRECONNECT, Attribute::REL_PREFETCH, Attribute::REL_PRELOAD, Attribute::REL_PRERENDER];
/**
* Document to manage the links for.
*
* @var Document
*/
private $document;
/**
* Reference node to attach the resource hint to.
*
* @var DOMNode|null
*/
private $referenceNode;
/**
* Collection of links already attached to the document.
*
* The key of the array is a concatenation of the HREF and the REL attributes.
*
* @var Element[]
*/
private $links = [];
/**
* LinkManager constructor.
*
* @param Document $document Document to manage the links for.
*/
public function __construct(Document $document)
{
$this->document = $document;
$this->detectExistingLinks();
}
private function detectExistingLinks()
{
$node = $this->document->head->firstChild;
while ($node) {
$nextSibling = $node->nextSibling;
if (!$node instanceof Element || $node->tagName !== Tag::LINK) {
$node = $nextSibling;
continue;
}
$key = $this->getKey($node);
if ($key !== '') {
$this->links[$this->getKey($node)] = $node;
}
$node = $nextSibling;
}
}
/**
* Get the key to use for storing the element in the links cache.
*
* @param Element $element Element to get the key for.
* @return string Key to use. Returns an empty string for invalid elements.
*/
private function getKey(Element $element)
{
$href = $element->getAttribute(Attribute::HREF);
$rel = $element->getAttribute(Attribute::REL);
if (empty($href) || !\in_array($rel, self::MANAGED_RELATIONS, \true)) {
return '';
}
return "{$href}{$rel}";
}
/**
* Add a dns-prefetch resource hint.
*
* @see https://www.w3.org/TR/resource-hints/#dns-prefetch
*
* @param string $href Origin to prefetch the DNS for.
*/
public function addDnsPrefetch($href)
{
$this->add(Attribute::REL_DNS_PREFETCH, $href);
}
/**
* Add a modulepreload declarative fetch primitive.
*
* @see https://html.spec.whatwg.org/multipage/links.html#link-type-modulepreload
*
* @param string $href Modular resource to preload.
* @param string|null $type Optional. Type of the resource. Defaults to not specified, which equals 'script'.
* @param bool|string $crossorigin Optional. Whether and how to configure CORS. Accepts a boolean for adding a
* boolean crossorigin flag, or a string to set a specific crossorigin strategy.
* Allowed values are 'anonymous' and 'use-credentials'. Defaults to true.
*/
public function addModulePreload($href, $type = null, $crossorigin = \true)
{
$attributes = [];
if ($type !== null) {
$attributes = [Attribute::AS_ => $type];
}
if ($crossorigin !== \false) {
$attributes[Attribute::CROSSORIGIN] = \is_string($crossorigin) ? $crossorigin : null;
}
$this->add(Attribute::REL_MODULEPRELOAD, $href, $attributes);
}
/**
* Add a preconnect resource hint.
*
* @see https://www.w3.org/TR/resource-hints/#dfn-preconnect
*
* @param string $href Origin to preconnect to.
* @param bool|string $crossorigin Optional. Whether and how to configure CORS. Accepts a boolean for adding a
* boolean crossorigin flag, or a string to set a specific crossorigin strategy.
* Allowed values are 'anonymous' and 'use-credentials'. Defaults to true.
*/
public function addPreconnect($href, $crossorigin = \true)
{
$this->add(Attribute::REL_PRECONNECT, $href, $crossorigin !== \false ? [Attribute::CROSSORIGIN => \is_string($crossorigin) ? $crossorigin : null] : []);
// Use dns-prefetch as fallback for browser that don't support preconnect.
// See https://web.dev/preconnect-and-dns-prefetch/#resolve-domain-name-early-with-reldns-prefetch.
$this->addDnsPrefetch($href);
}
/**
* Add a prefetch resource hint.
*
* @see https://www.w3.org/TR/resource-hints/#prefetch
*
* @param string $href URL to the resource to prefetch.
* @param string $type Optional. Type of the resource. Defaults to type 'image'.
* @param bool|string $crossorigin Optional. Whether and how to configure CORS. Accepts a boolean for adding a
* boolean crossorigin flag, or a string to set a specific crossorigin strategy.
* Allowed values are 'anonymous' and 'use-credentials'. Defaults to true.
*/
public function addPrefetch($href, $type = RequestDestination::IMAGE, $crossorigin = \true)
{
// TODO: Should we enforce a valid $type here?
$attributes = [Attribute::AS_ => $type];
if ($crossorigin !== \false) {
$attributes[Attribute::CROSSORIGIN] = \is_string($crossorigin) ? $crossorigin : null;
}
$this->add(Attribute::REL_PREFETCH, $href, $attributes);
}
/**
* Add a preload declarative fetch primitive.
*
* @see https://www.w3.org/TR/preload/
*
* @param string $href Resource to preload.
* @param string $type Optional. Type of the resource. Defaults to type 'image'.
* @param string|null $media Optional. Media query to add to the preload. Defaults to none.
* @param bool|string $crossorigin Optional. Whether and how to configure CORS. Accepts a boolean for adding a
* boolean crossorigin flag, or a string to set a specific crossorigin strategy.
* Allowed values are 'anonymous' and 'use-credentials'. Defaults to true.
*/
public function addPreload($href, $type = RequestDestination::IMAGE, $media = null, $crossorigin = \true)
{
// TODO: Should we enforce a valid $type here?
$attributes = [Attribute::AS_ => $type];
if (!empty($media)) {
$attributes[Attribute::MEDIA] = $media;
}
if ($crossorigin !== \false) {
$attributes[Attribute::CROSSORIGIN] = \is_string($crossorigin) ? $crossorigin : null;
}
$this->add(Attribute::REL_PRELOAD, $href, $attributes);
}
/**
* Add a prerender resource hint.
*
* @see https://www.w3.org/TR/resource-hints/#prerender
*
* @param string $href URL of the page to prerender.
*/
public function addPrerender($href)
{
$this->add(Attribute::REL_PRERENDER, $href);
}
/**
* Add a link to the document.
*
* @param string $rel A 'rel' string.
* @param string $href URL to link to.
* @param string[] $attributes Associative array of attributes and their values.
*/
public function add($rel, $href, $attributes = [])
{
$link = $this->document->createElement(Tag::LINK);
$link->setAttribute(Attribute::REL, $rel);
$link->setAttribute(Attribute::HREF, $href);
foreach ($attributes as $attribute => $value) {
$link->setAttribute($attribute, $value);
}
$this->remove($rel, $href);
if (!isset($this->referenceNode)) {
$this->referenceNode = $this->document->viewport;
}
if ($this->referenceNode) {
$link = $this->document->head->insertBefore($link, $this->referenceNode->nextSibling);
} else {
$link = $this->document->head->appendChild($link);
}
if (!$link instanceof Element) {
throw FailedToCreateLink::forLink($link);
}
$this->links[$this->getKey($link)] = $link;
$this->referenceNode = $link;
}
/**
* Get a specific link from the link manager.
*
* @param string $rel Relation to fetch.
* @param string $href Reference to fetch.
* @return Element|null Requested link as a Dom\Element, or null if not found.
*/
public function get($rel, $href)
{
$key = "{$href}{$rel}";
if (!\array_key_exists($key, $this->links)) {
return null;
}
return $this->links[$key];
}
/**
* Remove a specific link from the document.
*
* @param string $rel Relation of the link to remove.
* @param string $href Reference of the link to remove.
*/
public function remove($rel, $href)
{
$key = "{$href}{$rel}";
if (!\array_key_exists($key, $this->links)) {
return;
}
$link = $this->links[$key];
$link->parentNode->removeChild($link);
unset($this->links[$key]);
}
}

View File

@@ -0,0 +1,51 @@
<?php
namespace Google\Web_Stories_Dependencies\AmpProject\Dom;
use DOMNode;
/**
* Walk a hierarchical tree of nodes in a sequential manner.
*
* @package ampproject/amp-toolbox
*/
final class NodeWalker
{
/**
* Depth-first walk through the DOM tree.
*
* @param DOMNode $node Node to start walking from.
* @return DOMNode|null Next node, or null if none found.
*/
public static function nextNode(DOMNode $node)
{
// Walk downwards if there are children.
if ($node->firstChild) {
return $node->firstChild;
}
// Return direct sibling or walk upwards until we find a node with a sibling.
while ($node) {
if ($node->nextSibling) {
return $node->nextSibling;
}
$node = $node->parentNode;
}
// Out of nodes, so we're done.
return null;
}
/**
* Skip the subtree that is descending from the provided node.
*
* @param DOMNode $node Node to skip the subtree of.
* @return DOMNode|null The appropriate "next" node that will skip the current subtree, null if none found.
*/
public static function skipNodeAndChildren(DOMNode $node)
{
if ($node->nextSibling) {
return $node->nextSibling;
}
if ($node->parentNode === null) {
return null;
}
return self::skipNodeAndChildren($node->parentNode);
}
}

View File

@@ -0,0 +1,91 @@
<?php
namespace Google\Web_Stories_Dependencies\AmpProject\Dom;
use ArrayAccess;
/**
* Configuration options for DOM document.
*
* @package ampproject/amp-toolbox
*/
final class Options implements ArrayAccess
{
/**
* Associative array of options to configure the behavior of the DOM document abstraction.
*
* @var array
*/
private $options;
/**
* Creates a new AmpProject\Dom\Options object
*
* @param array $options Associative array of configuration options.
*/
public function __construct($options)
{
$this->options = $options;
}
/**
* Sets a value at a specified offset.
*
* @param string $option The option name.
* @param mixed $value Option value.
*/
#[\ReturnTypeWillChange]
public function offsetSet($option, $value)
{
$this->options[$option] = $value;
}
/**
* Determines whether an option exists.
*
* @param string $option Option name.
* @return bool True if the option exists, false otherwise.
*/
#[\ReturnTypeWillChange]
public function offsetExists($option)
{
return isset($this->options[$option]);
}
/**
* Unsets a specified option.
*
* @param string $option Option name.
*/
#[\ReturnTypeWillChange]
public function offsetUnset($option)
{
unset($this->options[$option]);
}
/**
* Retrieves a value at a specified option.
*
* @param string $option Option name.
* @return mixed If set, the value of the option, null otherwise.
*/
#[\ReturnTypeWillChange]
public function offsetGet($option)
{
return isset($this->options[$option]) ? $this->options[$option] : null;
}
/**
* Merge new options with the existing ones.
*
* @param array $options Associative array of new options.
* @return Options Cloned version of the Options object.
*/
public function merge($options)
{
$clonedOptions = clone $this;
$clonedOptions->options = \array_merge($this->options, $options);
return $clonedOptions;
}
/**
* Get the options in associative array.
* @return array Associative array of options.
*/
public function toArray()
{
return $this->options;
}
}

View File

@@ -0,0 +1,39 @@
<?php
namespace Google\Web_Stories_Dependencies\AmpProject\Dom;
/**
* Unique ID manager.
*
* @package ampproject/amp-toolbox
*/
final class UniqueIdManager
{
/**
* Store the current index by prefix.
*
* This is used to generate unique-per-prefix IDs.
*
* @var int[]
*/
private $indexCounter = [];
/**
* Get auto-incremented ID unique to this class's instantiation.
*
* @param string $prefix Prefix.
* @return string ID.
*/
public function getUniqueId($prefix = '')
{
if (\array_key_exists($prefix, $this->indexCounter)) {
++$this->indexCounter[$prefix];
} else {
$this->indexCounter[$prefix] = 0;
}
$uniqueId = (string) $this->indexCounter[$prefix];
if ($prefix) {
$uniqueId = "{$prefix}-{$uniqueId}";
}
return $uniqueId;
}
}

View File

@@ -0,0 +1,126 @@
<?php
namespace Google\Web_Stories_Dependencies\AmpProject;
/**
* Encoding constants that are used to control Dom\Document encoding.
*
* @package ampproject/amp-toolbox
*/
final class Encoding
{
/**
* UTF-8 encoding, which is the fallback.
*
* @var string
*/
const UTF8 = 'utf-8';
/**
* AMP requires the HTML markup to be encoded in UTF-8.
*
* @var string
*/
const AMP = self::UTF8;
/**
* Encoding detection order in case we have to guess.
*
* This list of encoding detection order is just a wild guess and might need fine-tuning over time.
* If the charset was not provided explicitly, we can really only guess, as the detection can
* never be 100% accurate and reliable.
*
* @var string
*/
const DETECTION_ORDER = 'JIS, UTF-8, EUC-JP, eucJP-win, ISO-2022-JP, ISO-8859-15, ISO-8859-1, ASCII';
/**
* Encoding detection order for PHP 8.1.
*
* In PHP 8.1, mb_detect_encoding gives different result than the lower versions. This alternative detection order
* list fixes this issue.
*/
const DETECTION_ORDER_PHP81 = 'UTF-8, EUC-JP, eucJP-win, ISO-8859-15, JIS, ISO-2022-JP, ISO-8859-1, ASCII';
/**
* Associative array of encoding mappings.
*
* Translates HTML charsets into encodings PHP can understand.
*
* @var string[]
*/
const MAPPINGS = [
// Assume ISO-8859-1 for some charsets.
'latin-1' => 'ISO-8859-1',
// US-ASCII is one of the most popular ASCII names and used as HTML charset,
// but mb_list_encodings contains only "ASCII".
'us-ascii' => 'ascii',
];
/**
* Encoding identifier to use for an unknown encoding.
*
* "auto" is recognized by mb_convert_encoding() as a special value.
*
* @var string
*/
const UNKNOWN = 'auto';
/**
* Current value of the encoding.
*
* @var string
*/
private $encoding;
/**
* Encoding constructor.
*
* @param mixed $encoding Value of the encoding.
*/
public function __construct($encoding)
{
if (!\is_string($encoding)) {
$encoding = self::UNKNOWN;
}
$this->encoding = $encoding;
}
/**
* Check whether the encoding equals a provided encoding.
*
* @param Encoding|string $encoding Encoding to check against.
* @return bool Whether the encodings are the same.
*/
public function equals($encoding)
{
return \strtolower($this->encoding) === \strtolower((string) $encoding);
}
/**
* Sanitize the encoding that was detected.
*
* If sanitization fails, it will return 'auto', letting the conversion
* logic try to figure it out itself.
*/
public function sanitize()
{
$this->encoding = \strtolower($this->encoding);
if ($this->encoding === self::UTF8) {
return;
}
if (!\function_exists('mb_list_encodings')) {
return;
}
static $knownEncodings = null;
if (null === $knownEncodings) {
$knownEncodings = \array_map('strtolower', \mb_list_encodings());
}
if (\array_key_exists($this->encoding, self::MAPPINGS)) {
$this->encoding = self::MAPPINGS[$this->encoding];
}
if (!\in_array($this->encoding, $knownEncodings, \true)) {
$this->encoding = self::UNKNOWN;
}
}
/**
* Return the value of the encoding as a string.
*
* @return string
*/
public function __toString()
{
return (string) $this->encoding;
}
}

View File

@@ -0,0 +1,54 @@
<?php
namespace Google\Web_Stories_Dependencies\AmpProject\Exception;
/**
* Marker interface to distinguish exceptions for the CLI.
*
* @package ampproject/amp-toolbox
*/
interface AmpCliException extends AmpException
{
/**
* No error code specified.
*
* @var int
*/
const E_ANY = -1;
/**
* Command is not valid.
*
* @var int
*/
const E_INVALID_CMD = 6;
/**
* Could not read or parse arguments.
*
* @var int
*/
const E_ARG_READ = 5;
/**
* Option requires an argument.
*
* @var int
*/
const E_OPT_ABIGUOUS = 4;
/**
* Argument not allowed for option.
*
* @var int
*/
const E_OPT_ARG_DENIED = 3;
/**
* Option ambiguous.
*
* @var int
*/
const E_OPT_ARG_REQUIRED = 2;
/**
* Option unknown.
*
* @var int
*/
const E_UNKNOWN_OPT = 1;
}

View File

@@ -0,0 +1,12 @@
<?php
namespace Google\Web_Stories_Dependencies\AmpProject\Exception;
/**
* Marker interface to enable consumers to catch all exceptions for this particular library.
*
* @package ampproject/amp-toolbox
*/
interface AmpException
{
}

View File

@@ -0,0 +1,45 @@
<?php
namespace Google\Web_Stories_Dependencies\AmpProject\Exception\Cli;
use Google\Web_Stories_Dependencies\AmpProject\Exception\AmpCliException;
use InvalidArgumentException;
/**
* Exception thrown when an invalid argument was provided to the CLI.
*
* @package ampproject/amp-toolbox
*/
final class InvalidArgument extends InvalidArgumentException implements AmpCliException
{
/**
* Instantiate an InvalidArgument exception when arguments could not be read.
*
* @return self
*/
public static function forUnreadableArguments()
{
$message = 'Could not read command arguments. Is register_argc_argv off?';
return new self($message, AmpCliException::E_ARG_READ);
}
/**
* Instantiate an InvalidArgument exception when a short option is too long.
*
* @return self
*/
public static function forMultiCharacterShortOption()
{
$message = 'Short options should be exactly one ASCII character.';
return new self($message, AmpCliException::E_OPT_ARG_DENIED);
}
/**
* Instantiate an InvalidArgument exception for file that could not be read.
*
* @param string $file File that could not be read.
* @return self
*/
public static function forUnreadableFile($file)
{
$message = "Could not read file: '{$file}'.";
return new self($message, AmpCliException::E_OPT_ARG_DENIED);
}
}

View File

@@ -0,0 +1,25 @@
<?php
namespace Google\Web_Stories_Dependencies\AmpProject\Exception\Cli;
use Google\Web_Stories_Dependencies\AmpProject\Exception\AmpCliException;
use OutOfBoundsException;
/**
* Exception thrown when an invalid color was provided to the CLI.
*
* @package ampproject/amp-toolbox
*/
final class InvalidColor extends OutOfBoundsException implements AmpCliException
{
/**
* Instantiate an InvalidColor exception for an unknown color that was passed to the CLI.
*
* @param string $color Unknown color that was passed to the CLI.
* @return self
*/
public static function forUnknownColor($color)
{
$message = "Unknown color: '{$color}'.";
return new self($message, AmpCliException::E_ANY);
}
}

View File

@@ -0,0 +1,45 @@
<?php
namespace Google\Web_Stories_Dependencies\AmpProject\Exception\Cli;
use Google\Web_Stories_Dependencies\AmpProject\Exception\AmpCliException;
use OutOfBoundsException;
/**
* Exception thrown when an invalid option was provided to the CLI.
*
* @package ampproject/amp-toolbox
*/
final class InvalidColumnFormat extends OutOfBoundsException implements AmpCliException
{
/**
* Instantiate an InvalidColumn exception for multiple fluid columns.
*
* @return self
*/
public static function forMultipleFluidColumns()
{
$message = 'Only one fluid column allowed.';
return new self($message, AmpCliException::E_ANY);
}
/**
* Instantiate an InvalidColumn exception for an unknown column format.
*
* @param string $column Unknown column format.
* @return self
*/
public static function forUnknownColumnFormat($column)
{
$message = "Unknown column format: '{$column}'.";
return new self($message, AmpCliException::E_ANY);
}
/**
* Instantiate an InvalidColumn exception for an unknown column format.
*
* @return self
*/
public static function forExceededMaxWidth()
{
$message = 'Total of requested column widths exceeds available space.';
return new self($message, AmpCliException::E_ANY);
}
}

View File

@@ -0,0 +1,36 @@
<?php
namespace Google\Web_Stories_Dependencies\AmpProject\Exception\Cli;
use Google\Web_Stories_Dependencies\AmpProject\Exception\AmpCliException;
use InvalidArgumentException;
/**
* Exception thrown when an invalid command was provided to the CLI.
*
* @package ampproject/amp-toolbox
*/
final class InvalidCommand extends InvalidArgumentException implements AmpCliException
{
/**
* Instantiate an InvalidCommand exception for an unregistered command that is being referenced.
*
* @param string $command Unregistered command that is being referenced.
* @return self
*/
public static function forUnregisteredCommand($command)
{
$message = "Command not registered: '{$command}'.";
return new self($message, AmpCliException::E_INVALID_CMD);
}
/**
* Instantiate an InvalidCommand exception for an already registered command that is to be re-registered.
*
* @param string $command Already registered command that is supposed to be registered.
* @return self
*/
public static function forAlreadyRegisteredCommand($command)
{
$message = "Command already registered: '{$command}'.";
return new self($message, AmpCliException::E_INVALID_CMD);
}
}

View File

@@ -0,0 +1,25 @@
<?php
namespace Google\Web_Stories_Dependencies\AmpProject\Exception\Cli;
use Google\Web_Stories_Dependencies\AmpProject\Exception\AmpCliException;
use OutOfBoundsException;
/**
* Exception thrown when an invalid option was provided to the CLI.
*
* @package ampproject/amp-toolbox
*/
final class InvalidOption extends OutOfBoundsException implements AmpCliException
{
/**
* Instantiate an InvalidOption exception for an unknown option that was passed to the CLI.
*
* @param string $option Unknown option that was passed to the CLI.
* @return self
*/
public static function forUnknownOption($option)
{
$message = "Unknown option: '{$option}'.";
return new self($message, AmpCliException::E_UNKNOWN_OPT);
}
}

View File

@@ -0,0 +1,25 @@
<?php
namespace Google\Web_Stories_Dependencies\AmpProject\Exception\Cli;
use Google\Web_Stories_Dependencies\AmpProject\Exception\AmpCliException;
use OutOfBoundsException;
/**
* Exception thrown when an invalid option was provided to the CLI.
*
* @package ampproject/amp-toolbox
*/
final class InvalidSapi extends OutOfBoundsException implements AmpCliException
{
/**
* Instantiate an InvalidSapi exception for a SAPI other than 'cli'.
*
* @param string $sapi Invalid SAPI that was detected.
* @return self
*/
public static function forSapi($sapi)
{
$message = "This has to be run from the command line (detected SAPI '{$sapi}').";
return new self($message, AmpCliException::E_ANY);
}
}

View File

@@ -0,0 +1,36 @@
<?php
namespace Google\Web_Stories_Dependencies\AmpProject\Exception\Cli;
use Google\Web_Stories_Dependencies\AmpProject\Exception\AmpCliException;
use DomainException;
/**
* Exception thrown when an invalid argument was provided to the CLI.
*
* @package ampproject/amp-toolbox
*/
final class MissingArgument extends DomainException implements AmpCliException
{
/**
* Instantiate a MissingArgument exception for an argument that is required but missing.
*
* @param string $option Option for which the argument is missing.
*
* @return self
*/
public static function forNoArgument($option)
{
$message = "Option '{$option}' requires an argument.";
return new self($message, AmpCliException::E_OPT_ARG_REQUIRED);
}
/**
* Instantiate a MissingArgument exception for when too few arguments were passed.
*
* @return self
*/
public static function forNotEnoughArguments()
{
$message = 'Not enough arguments provided.';
return new self($message, AmpCliException::E_OPT_ARG_REQUIRED);
}
}

View File

@@ -0,0 +1,12 @@
<?php
namespace Google\Web_Stories_Dependencies\AmpProject\Exception;
/**
* Marker interface to enable consumers to catch all exceptions for failed remote requests.
*
* @package ampproject/amp-toolbox
*/
interface FailedRemoteRequest extends AmpException
{
}

View File

@@ -0,0 +1,25 @@
<?php
namespace Google\Web_Stories_Dependencies\AmpProject\Exception;
use RuntimeException;
/**
* Exception thrown when a link could not be created.
*
* @package ampproject/amp-toolbox
*/
final class FailedToCreateLink extends RuntimeException implements AmpException
{
/**
* Instantiate a FailedToCreateLink exception for a link that could not be created.
*
* @param mixed $link Link that was not as expected.
* @return self
*/
public static function forLink($link)
{
$type = \is_object($link) ? \get_class($link) : \gettype($link);
$message = "Failed to create a link via the link manager. " . "Expected to produce an 'AmpProject\\Dom\\Element', got '{$type}' instead.";
return new self($message);
}
}

View File

@@ -0,0 +1,25 @@
<?php
namespace Google\Web_Stories_Dependencies\AmpProject\Exception;
use RuntimeException;
/**
* Exception thrown when a cached remote response could not be retrieved.
*
* @package ampproject/amp-toolbox
*/
final class FailedToGetCachedResponse extends RuntimeException implements FailedRemoteRequest
{
/**
* Instantiate a FailedToGetCachedResponse exception for a URL if the cached response data could not be
* retrieved.
*
* @param string $url URL that failed to be fetched.
* @return self
*/
public static function withUrl($url)
{
$message = "Failed to retrieve the cached response for the URL '{$url}'.";
return new self($message);
}
}

View File

@@ -0,0 +1,79 @@
<?php
namespace Google\Web_Stories_Dependencies\AmpProject\Exception;
use Exception;
use RuntimeException;
/**
* Exception thrown when a remote request failed.
*
* @package ampproject/amp-toolbox
*/
final class FailedToGetFromRemoteUrl extends RuntimeException implements FailedRemoteRequest
{
/**
* Status code of the failed request.
*
* This is not always set.
*
* @var int|null
*/
private $statusCode;
/**
* Instantiate a FailedToGetFromRemoteUrl exception for a URL if an HTTP status code is available.
*
* @param string $url URL that failed to be fetched.
* @param int $status HTTP Status that was returned.
* @return self
*/
public static function withHttpStatus($url, $status)
{
$message = "Failed to fetch the contents from the URL '{$url}' as it returned HTTP status {$status}.";
$exception = new self($message);
$exception->statusCode = $status;
return $exception;
}
/**
* Instantiate a FailedToGetFromRemoteUrl exception for a URL if an HTTP status code is not available.
*
* @param string $url URL that failed to be fetched.
* @return self
*/
public static function withoutHttpStatus($url)
{
$message = "Failed to fetch the contents from the URL '{$url}'.";
return new self($message);
}
/**
* Instantiate a FailedToGetFromRemoteUrl exception for a URL if an exception was thrown.
*
* @param string $url URL that failed to be fetched.
* @param Exception $exception Exception that was thrown.
* @return self
*/
public static function withException($url, Exception $exception)
{
$message = "Failed to fetch the contents from the URL '{$url}': {$exception->getMessage()}.";
return new self($message, 0, $exception);
}
/**
* Check whether the status code is set for this exception.
*
* @return bool
*/
public function hasStatusCode()
{
return isset($this->statusCode);
}
/**
* Get the HTTP status code associated with this exception.
*
* Returns -1 if no status code was provided.
*
* @return int
*/
public function getStatusCode()
{
return $this->hasStatusCode() ? $this->statusCode : -1;
}
}

View File

@@ -0,0 +1,28 @@
<?php
namespace Google\Web_Stories_Dependencies\AmpProject\Exception;
use Google\Web_Stories_Dependencies\AmpProject\Str;
use InvalidArgumentException;
/**
* Exception thrown when an HTML document could not be parsed.
*
* @package ampproject/amp-toolbox
*/
final class FailedToParseHtml extends InvalidArgumentException implements AmpException
{
/**
* Instantiate a FailedToParseHtml exception for a HTML that could not be parsed.
*
* @param string $html HTML that failed to be parsed.
* @return self
*/
public static function forHtml($html)
{
if (Str::length($html) > 80) {
$html = Str::substring($html, 0, 77) . '...';
}
$message = "Failed to parse the provided HTML document ({$html}).";
return new self($message);
}
}

View File

@@ -0,0 +1,24 @@
<?php
namespace Google\Web_Stories_Dependencies\AmpProject\Exception;
use InvalidArgumentException;
/**
* Exception thrown when a URL could not be parsed.
*
* @package ampproject/amp-toolbox
*/
final class FailedToParseUrl extends InvalidArgumentException implements AmpException
{
/**
* Instantiate a FailedToParseUrl exception for a URL that could not be parsed.
*
* @param string $url URL that failed to be parsed.
* @return self
*/
public static function forUrl($url)
{
$message = "Failed to parse the URL '{$url}'.";
return new self($message);
}
}

View File

@@ -0,0 +1,49 @@
<?php
namespace Google\Web_Stories_Dependencies\AmpProject\Exception;
use LogicException;
/**
* Exception thrown when a required DOM element could not be retrieved from the document.
*
* @package ampproject/amp-toolbox
*/
final class FailedToRetrieveRequiredDomElement extends LogicException implements AmpException
{
/**
* Instantiate a FailedToRetrieveRequiredDomElement exception for the <html> DOM element.
*
* @param mixed $retrievedElement What was returned when trying to retrieve the element.
* @return FailedToRetrieveRequiredDomElement
*/
public static function forHtmlElement($retrievedElement)
{
$type = \is_object($retrievedElement) ? \get_class($retrievedElement) : \gettype($retrievedElement);
$message = "Failed to retrieve required <html> DOM element, got '{$type}' instead.";
return new self($message);
}
/**
* Instantiate a FailedToRetrieveRequiredDomElement exception for the <head> DOM element.
*
* @param mixed $retrievedElement What was returned when trying to retrieve the element.
* @return FailedToRetrieveRequiredDomElement
*/
public static function forHeadElement($retrievedElement)
{
$type = \is_object($retrievedElement) ? \get_class($retrievedElement) : \gettype($retrievedElement);
$message = "Failed to retrieve required <head> DOM element, got '{$type}' instead.";
return new self($message);
}
/**
* Instantiate a FailedToRetrieveRequiredDomElement exception for the <body> DOM element.
*
* @param mixed $retrievedElement What was returned when trying to retrieve the element.
* @return FailedToRetrieveRequiredDomElement
*/
public static function forBodyElement($retrievedElement)
{
$type = \is_object($retrievedElement) ? \get_class($retrievedElement) : \gettype($retrievedElement);
$message = "Failed to retrieve required <body> DOM element, got '{$type}' instead.";
return new self($message);
}
}

View File

@@ -0,0 +1,24 @@
<?php
namespace Google\Web_Stories_Dependencies\AmpProject\Exception;
use OutOfRangeException;
/**
* Exception thrown when an invalid attribute name is requested from the validator spec.
*
* @package ampproject/amp-toolbox
*/
final class InvalidAttributeName extends OutOfRangeException implements AmpException
{
/**
* Instantiate an InvalidAttributeName exception for an attribute that is not found within name index.
*
* @param string $attribute Name of the attribute that was requested.
* @return self
*/
public static function forAttribute($attribute)
{
$message = "Invalid attribute '{$attribute}' was requested from the validator spec.";
return new self($message);
}
}

View File

@@ -0,0 +1,23 @@
<?php
namespace Google\Web_Stories_Dependencies\AmpProject\Exception;
use InvalidArgumentException;
/**
* Exception thrown when a HTML contains invalid byte sequences.
*
* @package ampproject/amp-toolbox
*/
final class InvalidByteSequence extends InvalidArgumentException implements AmpException
{
/**
* Instantiate a InvalidByteSequence exception for a HTML with invalid byte sequences.
*
* @return self
*/
public static function forHtml()
{
$message = 'Provided HTML contains invalid byte sequences. ' . 'This is usually fixed by replacing string manipulation functions ' . 'with their `mb_*` multibyte counterparts.';
return new self($message);
}
}

View File

@@ -0,0 +1,24 @@
<?php
namespace Google\Web_Stories_Dependencies\AmpProject\Exception;
use OutOfRangeException;
/**
* Exception thrown when an invalid CSS ruleset name is requested from the validator spec.
*
* @package ampproject/amp-toolbox
*/
final class InvalidCssRulesetName extends OutOfRangeException implements AmpException
{
/**
* Instantiate an InvalidCssRulesetName exception for a CSS ruleset that is not found within the CSS rulesets index.
*
* @param string $cssRulesetName CSS ruleset name that was requested.
* @return self
*/
public static function forCssRulesetName($cssRulesetName)
{
$message = "Invalid CSS ruleset name '{$cssRulesetName}' was requested from the validator spec.";
return new self($message);
}
}

View File

@@ -0,0 +1,24 @@
<?php
namespace Google\Web_Stories_Dependencies\AmpProject\Exception;
use OutOfRangeException;
/**
* Exception thrown when an invalid declaration name is requested from the validator spec.
*
* @package ampproject/amp-toolbox
*/
final class InvalidDeclarationName extends OutOfRangeException implements AmpException
{
/**
* Instantiate an InvalidDeclarationName exception for an declaration that is not found within name index.
*
* @param string $declaration Name of the declaration that was requested.
* @return self
*/
public static function forDeclaration($declaration)
{
$message = "Invalid declaration '{$declaration}' was requested from the validator spec.";
return new self($message);
}
}

View File

@@ -0,0 +1,25 @@
<?php
namespace Google\Web_Stories_Dependencies\AmpProject\Exception;
use OutOfRangeException;
/**
* Exception thrown when an invalid document ruleset name is requested from the validator spec.
*
* @package ampproject/amp-toolbox
*/
final class InvalidDocRulesetName extends OutOfRangeException implements AmpException
{
/**
* Instantiate an InvalidDocRulesetName exception for a document ruleset that is not found within the document
* rulesets index.
*
* @param string $docRulesetName document ruleset name that was requested.
* @return self
*/
public static function forDocRulesetName($docRulesetName)
{
$message = "Invalid document ruleset name '{$docRulesetName}' was requested from the validator spec.";
return new self($message);
}
}

View File

@@ -0,0 +1,25 @@
<?php
namespace Google\Web_Stories_Dependencies\AmpProject\Exception;
use InvalidArgumentException;
/**
* Exception thrown when an invalid tag ID is requested from the validator spec.
*
* @package ampproject/amp-toolbox
*/
final class InvalidDocumentFilter extends InvalidArgumentException implements AmpException
{
/**
* Instantiate an InvalidDocumentFilter exception for a class that was not a valid filter.
*
* @param mixed $filter Filter that was registered.
* @return self
*/
public static function forFilter($filter)
{
$type = \is_object($filter) ? \get_class($filter) : \gettype($filter);
$message = \is_string($filter) ? "Invalid document filter '{$filter}' was registered with the AmpProject\\Dom\\Document class." : "Invalid document filter of type '{$type}' was registered with the AmpProject\\Dom\\Document class, '\n . 'expected AmpProject\\Dom\\Document\\Filter.";
return new self($message);
}
}

View File

@@ -0,0 +1,24 @@
<?php
namespace Google\Web_Stories_Dependencies\AmpProject\Exception;
use OutOfRangeException;
/**
* Exception thrown when an invalid error code is requested from the validator spec.
*
* @package ampproject/amp-toolbox
*/
final class InvalidErrorCode extends OutOfRangeException implements AmpException
{
/**
* Instantiate an InvalidErrorCode exception for an unknown error code.
*
* @param string $errorCode Error code that was requested.
* @return self
*/
public static function forErrorCode($errorCode)
{
$message = "Invalid error code '{$errorCode}' was requested from the validator spec.";
return new self($message);
}
}

View File

@@ -0,0 +1,24 @@
<?php
namespace Google\Web_Stories_Dependencies\AmpProject\Exception;
use OutOfRangeException;
/**
* Exception thrown when an invalid extension is requested from the validator spec.
*
* @package ampproject/amp-toolbox
*/
final class InvalidExtension extends OutOfRangeException implements AmpException
{
/**
* Instantiate an InvalidExtension exception for an extension that is not found within the extension spec index.
*
* @param string $extension Spec name that was requested.
* @return self
*/
public static function forExtension($extension)
{
$message = "Invalid extension '{$extension}' was requested from the validator spec.";
return new self($message);
}
}

View File

@@ -0,0 +1,24 @@
<?php
namespace Google\Web_Stories_Dependencies\AmpProject\Exception;
use OutOfRangeException;
/**
* Exception thrown when an invalid format is requested from the validator spec.
*
* @package ampproject/amp-toolbox
*/
final class InvalidFormat extends OutOfRangeException implements AmpException
{
/**
* Instantiate an InvalidFormat exception when an invalid AMP format is being requested.
*
* @param string $format Format that was requested.
* @return self
*/
public static function forFormat($format)
{
$message = "Invalid format '{$format}' was requested from the validator spec.";
return new self($message);
}
}

View File

@@ -0,0 +1,49 @@
<?php
namespace Google\Web_Stories_Dependencies\AmpProject\Exception;
use OutOfRangeException;
/**
* Exception thrown when an invalid list name is requested from the validator spec.
*
* @package ampproject/amp-toolbox
*/
final class InvalidListName extends OutOfRangeException implements AmpException
{
/**
* Instantiate an InvalidListName exception for a attribute that is not found within the attribute
* list name index.
*
* @param string $attributeList Name of the attribute list that was requested.
* @return self
*/
public static function forAttributeList($attributeList)
{
$message = "Invalid attribute list '{$attributeList}' was requested from the validator spec.";
return new self($message);
}
/**
* Instantiate an InvalidListName exception for a declaration that is not found within the declaration
* list name index.
*
* @param string $declarationList Name of the declaration list that was requested.
* @return self
*/
public static function forDeclarationList($declarationList)
{
$message = "Invalid declaration list '{$declarationList}' was requested from the validator spec.";
return new self($message);
}
/**
* Instantiate an InvalidListName exception for a descendant tag that is not found within the descendant tag
* list name index.
*
* @param string $descendantTagList Name of the descendant tag list that was requested.
* @return self
*/
public static function forDescendantTagList($descendantTagList)
{
$message = "Invalid descendant tag list '{$descendantTagList}' was requested from the validator spec.";
return new self($message);
}
}

View File

@@ -0,0 +1,27 @@
<?php
namespace Google\Web_Stories_Dependencies\AmpProject\Exception;
use DomainException;
/**
* Exception thrown when an invalid option value was provided.
*
* @package ampproject/amp-toolbox
*/
final class InvalidOptionValue extends DomainException implements AmpException
{
/**
* Instantiate an InvalidOptionValue exception for an invalid option value.
*
* @param string $option Name of the option.
* @param array<string> $accepted Array of acceptable values.
* @param string $actual Value that was actually provided.
* @return self
*/
public static function forValue($option, $accepted, $actual)
{
$acceptedString = \implode(', ', $accepted);
$message = "The value for the option '{$option}' expected the value to be one of " . "[{$acceptedString}], got '{$actual}' instead.";
return new self($message);
}
}

View File

@@ -0,0 +1,24 @@
<?php
namespace Google\Web_Stories_Dependencies\AmpProject\Exception;
use OutOfRangeException;
/**
* Exception thrown when an invalid spec name is requested from the validator spec.
*
* @package ampproject/amp-toolbox
*/
final class InvalidSpecName extends OutOfRangeException implements AmpException
{
/**
* Instantiate an InvalidSpecName exception for a spec that is not found within the spec name index.
*
* @param string $specName Spec name that was requested.
* @return self
*/
public static function forSpecName($specName)
{
$message = "Invalid spec name '{$specName}' was requested from the validator spec.";
return new self($message);
}
}

View File

@@ -0,0 +1,24 @@
<?php
namespace Google\Web_Stories_Dependencies\AmpProject\Exception;
use OutOfRangeException;
/**
* Exception thrown when an invalid spec rule name is requested from the validator spec.
*
* @package ampproject/amp-toolbox
*/
final class InvalidSpecRuleName extends OutOfRangeException implements AmpException
{
/**
* Instantiate an InvalidSpecRuleName exception for a spec rule that is not found within the spec index.
*
* @param string $specRuleName Spec rule name that was requested.
* @return self
*/
public static function forSpecRuleName($specRuleName)
{
$message = "Invalid spec rule name '{$specRuleName}' was requested from the validator spec.";
return new self($message);
}
}

View File

@@ -0,0 +1,24 @@
<?php
namespace Google\Web_Stories_Dependencies\AmpProject\Exception;
use OutOfRangeException;
/**
* Exception thrown when an invalid tag ID is requested from the validator spec.
*
* @package ampproject/amp-toolbox
*/
final class InvalidTagId extends OutOfRangeException implements AmpException
{
/**
* Instantiate an InvalidTagId exception for a tag that is not found within the tag name index.
*
* @param string $tagId Spec name that was requested.
* @return self
*/
public static function forTagId($tagId)
{
$message = "Invalid tag ID '{$tagId}' was requested from the validator tag.";
return new self($message);
}
}

View File

@@ -0,0 +1,24 @@
<?php
namespace Google\Web_Stories_Dependencies\AmpProject\Exception;
use OutOfRangeException;
/**
* Exception thrown when an invalid tag name is requested from the validator spec.
*
* @package ampproject/amp-toolbox
*/
final class InvalidTagName extends OutOfRangeException implements AmpException
{
/**
* Instantiate an InvalidTagName exception for a tag that is not found within the tag name index.
*
* @param string $tagName Tag name that was requested.
* @return self
*/
public static function forTagName($tagName)
{
$message = "Invalid tag name '{$tagName}' was requested from the validator spec.";
return new self($message);
}
}

View File

@@ -0,0 +1,38 @@
<?php
namespace Google\Web_Stories_Dependencies\AmpProject\Exception;
use Google\Web_Stories_Dependencies\AmpProject\Dom\Element;
use Google\Web_Stories_Dependencies\AmpProject\Dom\ElementDump;
use OverflowException;
/**
* Exception thrown when the maximum CSS byte count has been exceeded.
*
* @package ampproject/amp-toolbox
*/
final class MaxCssByteCountExceeded extends OverflowException implements AmpException
{
/**
* Instantiate a MaxCssByteCountExceeded exception for an inline style that exceeds the maximum byte count.
*
* @param Element $element Element that was supposed to receive the inline style.
* @param string $style Inline style that was supposed to be added.
* @return self
*/
public static function forInlineStyle(Element $element, $style)
{
$message = "Maximum allowed CSS byte count exceeded for inline style '{$style}': " . new ElementDump($element);
return new self($message);
}
/**
* Instantiate a MaxCssByteCountExceeded exception for an amp-custom style that exceeds the maximum byte count.
*
* @param string $style Amp-custom style that was supposed to be added.
* @return self
*/
public static function forAmpCustom($style)
{
$message = "Maximum allowed CSS byte count exceeded for amp-custom style '{$style}'";
return new self($message);
}
}

View File

@@ -0,0 +1,191 @@
<?php
namespace Google\Web_Stories_Dependencies\AmpProject;
/**
* Interface with constants for AMP extensions.
*
* @package ampproject/amp-toolbox
*/
interface Extension
{
const ACCESS = 'amp-access';
const ACCESS_LATERPAY = 'amp-access-laterpay';
const ACCESS_POOOL = 'amp-access-poool';
const ACCESS_SCROLL = 'amp-access-scroll';
const ACCORDION = 'amp-accordion';
const ACTION_MACRO = 'amp-action-macro';
const AD = 'amp-ad';
const ADDTHIS = 'amp-addthis';
const AD_CUSTOM = 'amp-ad-custom';
const AD_EXIT = 'amp-ad-exit';
const ANALYTICS = 'amp-analytics';
const ANIM = 'amp-anim';
const ANIMATION = 'amp-animation';
const APESTER_MEDIA = 'amp-apester-media';
const APP_BANNER = 'amp-app-banner';
const AUDIO = 'amp-audio';
const AUTOCOMPLETE = 'amp-autocomplete';
const AUTO_ADS = 'amp-auto-ads';
const BASE_CAROUSEL = 'amp-base-carousel';
const BEOPINION = 'amp-beopinion';
const BIND = 'amp-bind';
const BIND_MACRO = 'amp-bind-macro';
const BODYMOVIN_ANIMATION = 'amp-bodymovin-animation';
const BRID_PLAYER = 'amp-brid-player';
const BRIGHTCOVE = 'amp-brightcove';
const BYSIDE_CONTENT = 'amp-byside-content';
const CACHE_URL = 'amp-cache-url';
const CALL_TRACKING = 'amp-call-tracking';
const CAROUSEL = 'amp-carousel';
const CONNATIX_PLAYER = 'amp-connatix-player';
const CONSENT = 'amp-consent';
const DAILYMOTION = 'amp-dailymotion';
const DATE_COUNTDOWN = 'amp-date-countdown';
const DATE_DISPLAY = 'amp-date-display';
const DATE_PICKER = 'amp-date-picker';
const DELIGHT_PLAYER = 'amp-delight-player';
const DYNAMIC_CSS_CLASSES = 'amp-dynamic-css-classes';
const EMBED = 'amp-embed';
const EMBEDLY_CARD = 'amp-embedly-card';
const EMBEDLY_KEY = 'amp-embedly-key';
const EXPERIMENT = 'amp-experiment';
const FACEBOOK = 'amp-facebook';
const FACEBOOK_COMMENTS = 'amp-facebook-comments';
const FACEBOOK_LIKE = 'amp-facebook-like';
const FACEBOOK_PAGE = 'amp-facebook-page';
const FIT_TEXT = 'amp-fit-text';
const FONT = 'amp-font';
const FORM = 'amp-form';
const FX_COLLECTION = 'amp-fx-collection';
const FX_FLYING_CARPET = 'amp-flying-carpet';
const GEO = 'amp-geo';
const GFYCAT = 'amp-gfycat';
const GIST = 'amp-gist';
const GOOGLE_ASSISTANT_ASSISTJS = 'amp-google-assistant-assistjs';
const GOOGLE_ASSISTANT_ASSISTJS_CONFIG = 'amp-google-assistant-assistjs-config';
const GOOGLE_ASSISTANT_INLINE_SUGGESTION_BAR = 'amp-google-assistant-inline-suggestion-bar';
const GOOGLE_ASSISTANT_VOICE_BAR = 'amp-google-assistant-voice-bar';
const GOOGLE_ASSISTANT_VOICE_BUTTON = 'amp-google-assistant-voice-button';
const GOOGLE_DOCUMENT_EMBED = 'amp-google-document-embed';
const GOOGLE_READ_ALOUD_PLAYER = 'amp-google-read-aloud-player';
const GWD_ANIMATION = 'amp-gwd-animation';
const HULU = 'amp-hulu';
const IFRAME = 'amp-iframe';
const IFRAMELY = 'amp-iframely';
const IMAGE_LIGHTBOX = 'amp-image-lightbox';
const IMAGE_SLIDER = 'amp-image-slider';
const IMA_VIDEO = 'amp-ima-video';
const IMG = 'amp-img';
const IMGUR = 'amp-imgur';
const INPUTMASK = 'amp-inputmask';
const INLINE_GALLERY = 'amp-inline-gallery';
const INLINE_GALLERY_PAGINATION = 'amp-inline-gallery-pagination';
const INLINE_GALLERY_THUMBNAILS = 'amp-inline-gallery-thumbnails';
const INSTAGRAM = 'amp-instagram';
const INSTALL_SERVICEWORKER = 'amp-install-serviceworker';
const IZLESENE = 'amp-izlesene';
const JWPLAYER = 'amp-jwplayer';
const KALTURA_PLAYER = 'amp-kaltura-player';
const LAYOUT = 'amp-layout';
const LIGHTBOX = 'amp-lightbox';
const LIGHTBOX_GALLERY = 'amp-lightbox-gallery';
const LINK_REWRITER = 'amp-link-rewriter';
const LIST_ = 'amp-list';
const LIST_LOAD_MORE = 'amp-list-load-more';
const LIVE_LIST = 'amp-live-list';
const MATHML = 'amp-mathml';
const MEGAPHONE = 'amp-megaphone';
const MEGA_MENU = 'amp-mega-menu';
const MINUTE_MEDIA_PLAYER = 'amp-minute-media-player';
const MOWPLAYER = 'amp-mowplayer';
const MRAID = 'amp-mraid';
const MUSTACHE = 'amp-mustache';
const NESTED_MENU = 'amp-nested-menu';
const NEXT_PAGE = 'amp-next-page';
const NEXXTV_PLAYER = 'amp-nexxtv-player';
const O2_PLAYER = 'amp-o2-player';
const ONETAP_GOOGLE = 'amp-onetap-google';
const OOYALA_PLAYER = 'amp-ooyala-player';
const ORIENTATION_OBSERVER = 'amp-orientation-observer';
const PAN_ZOOM = 'amp-pan-zoom';
const PINTEREST = 'amp-pinterest';
const PIXEL = 'amp-pixel';
const PLAYBUZZ = 'amp-playbuzz';
const POSITION_OBSERVER = 'amp-position-observer';
const POWR_PLAYER = 'amp-powr-player';
const REACH_PLAYER = 'amp-reach-player';
const RECAPTCHA_INPUT = 'amp-recaptcha-input';
const REDBULL_PLAYER = 'amp-redbull-player';
const REDDIT = 'amp-reddit';
const RENDER = 'amp-render';
const RIDDLE_QUIZ = 'amp-riddle-quiz';
const SCRIPT = 'amp-script';
const SELECTOR = 'amp-selector';
const SIDEBAR = 'amp-sidebar';
const SKIMLINKS = 'amp-skimlinks';
const SLIDES = 'amp-slides';
const SMARTLINKS = 'amp-smartlinks';
const SOCIAL_SHARE = 'amp-social-share';
const SOUNDCLOUD = 'amp-soundcloud';
const SPRINGBOARD_PLAYER = 'amp-springboard-player';
const STATE = 'amp-state';
const STICKY_AD = 'amp-sticky-ad';
const STORY = 'amp-story';
const STORY_360 = 'amp-story-360';
const STORY_ANIMATION = 'amp-story-animation';
const STORY_AUTO_ADS = 'amp-story-auto-ads';
const STORY_AUTO_ANALYTICS = 'amp-story-auto-analytics';
const STORY_BOOKEND = 'amp-story-bookend';
const STORY_CAPTIONS = 'amp-story-captions';
const STORY_CONSENT = 'amp-story-consent';
const STORY_CTA_LAYER = 'amp-story-cta-layer';
const STORY_GRID_LAYER = 'amp-story-grid-layer';
const STORY_INTERACTIVE = 'amp-story-interactive';
const STORY_INTERACTIVE_BINARY_POLL = 'amp-story-interactive-binary-poll';
const STORY_INTERACTIVE_IMG_POLL = 'amp-story-interactive-img-poll';
const STORY_INTERACTIVE_IMG_QUIZ = 'amp-story-interactive-img-quiz';
const STORY_INTERACTIVE_POLL = 'amp-story-interactive-poll';
const STORY_INTERACTIVE_QUIZ = 'amp-story-interactive-quiz';
const STORY_INTERACTIVE_RESULTS = 'amp-story-interactive-results';
const STORY_PAGE = 'amp-story-page';
const STORY_PAGE_ATTACHMENT = 'amp-story-page-attachment';
const STORY_PAGE_OUTLINK = 'amp-story-page-outlink';
const STORY_PANNING_MEDIA = 'amp-story-panning-media';
const STORY_PLAYER = 'amp-story-player';
const STORY_SHOPPING = 'amp-story-shopping';
const STORY_SHOPPING_ATTACHMENT = 'amp-story-shopping-attachment';
const STORY_SHOPPING_CONFIG = 'amp-story-shopping-config';
const STORY_SHOPPING_TAG = 'amp-story-shopping-tag';
const STORY_SOCIAL_SHARE = 'amp-story-social-share';
const STORY_SUBSCRIPTIONS = 'amp-story-subscriptions';
const STREAM_GALLERY = 'amp-stream-gallery';
const SUBSCRIPTIONS = 'amp-subscriptions';
const SUBSCRIPTIONS_GOOGLE = 'amp-subscriptions-google';
const TIKTOK = 'amp-tiktok';
const TIMEAGO = 'amp-timeago';
const TRUNCATE_TEXT = 'amp-truncate-text';
const TWITTER = 'amp-twitter';
const USER_NOTIFICATION = 'amp-user-notification';
const VIDEO = 'amp-video';
const VIDEO_DOCKING = 'amp-video-docking';
const VIDEO_IFRAME = 'amp-video-iframe';
const VIMEO = 'amp-vimeo';
const VINE = 'amp-vine';
const VIQEO_PLAYER = 'amp-viqeo-player';
const VK = 'amp-vk';
const WEB_PUSH = 'amp-web-push';
const WEB_PUSH_WIDGET = 'amp-web-push-widget';
const WISTIA_PLAYER = 'amp-wistia-player';
const WORDPRESS_EMBED = 'amp-wordpress-embed';
const YOTPO = 'amp-yotpo';
const YOUTUBE = 'amp-youtube';
const _3D_GLTF = 'amp-3d-gltf';
const _3Q_PLAYER = 'amp-3q-player';
/**
* Prefix of an AMP extension.
*
* @var string
*/
const PREFIX = 'amp-';
}

View File

@@ -0,0 +1,306 @@
<?php
/**
* This file was copied from the myclabs/php-enum package, and only adapted for matching this package's namespace, code
* style and minimum PHP requirement.
*
* Note: The base class was renamed from Enum to FakeEnum to avoid conflicts with PHP 8.1's enum language construct.
*
* @link http://github.com/myclabs/php-enum
* @license http://www.opensource.org/licenses/mit-license.php MIT
*/
namespace Google\Web_Stories_Dependencies\AmpProject;
use BadMethodCallException;
use JsonSerializable;
use ReflectionClass;
use UnexpectedValueException;
/**
* Base FakeEnum class.
*
* Create an enum by implementing this class and adding class constants.
*
* Original code found in myclabs/php-enum.
*
* @author Matthieu Napoli <matthieu@mnapoli.fr>
* @author Daniel Costa <danielcosta@gmail.com>
* @author Mirosław Filip <mirfilip@gmail.com>
*
* @psalm-template T
* @psalm-immutable
* @psalm-consistent-constructor
*
* @package ampproject/amp-toolbox
*/
abstract class FakeEnum implements JsonSerializable
{
/**
* Enum value.
*
* @var mixed
* @psalm-var T
*/
protected $value;
/**
* Enum key, the constant name.
*
* @var string
*/
private $key;
/**
* Store existing constants in a static cache per object.
*
* @var array
* @psalm-var array<class-string, array<string, mixed>>
*/
protected static $cache = [];
/**
* Cache of instances of the FakeEnum class.
*
* @var array
* @psalm-var array<class-string, array<string, static>>
*/
protected static $instances = [];
/**
* Creates a new value of some type.
*
* @psalm-pure
* @param mixed $value Value to create the new enum instance for.
*
* @psalm-param T $value
* @throws UnexpectedValueException If incompatible type is given.
*/
public function __construct($value)
{
if ($value instanceof static) {
/** @psalm-var T */
$value = $value->getValue();
}
/** @psalm-suppress ImplicitToStringCast assertValidValueReturningKey returns always a string but psalm has currently an issue here */
$this->key = static::assertValidValueReturningKey($value);
/** @psalm-var T $value */
$this->value = $value;
}
/**
* This method exists only for the compatibility reason when deserializing a previously serialized version
* that didn't have the key property.
*/
public function __wakeup()
{
/** @psalm-suppress DocblockTypeContradiction key can be null when deserializing an enum without the key */
if ($this->key === null) {
/**
* @psalm-suppress InaccessibleProperty key is not readonly as marked by psalm
* @psalm-suppress PossiblyFalsePropertyAssignmentValue deserializing a case that was removed
*/
$this->key = static::search($this->value);
}
}
/**
* Create a new enum instance from a value.
*
* @param mixed $value Value to create the new enum instance for.
* @return static
* @psalm-return static<T>
* @throws UnexpectedValueException If the value is not part of the enum.
*/
public static function from($value)
{
$key = static::assertValidValueReturningKey($value);
return self::__callStatic($key, []);
}
/**
* @psalm-pure
* @return mixed
* @psalm-return T
*/
public function getValue()
{
return $this->value;
}
/**
* Returns the enum key (i.e. the constant name).
*
* @psalm-pure
* @return string
*/
public function getKey()
{
return $this->key;
}
/**
* @psalm-pure
* @psalm-suppress InvalidCast
* @return string
*/
public function __toString()
{
return (string) $this->value;
}
/**
* Determines if Enum should be considered equal with the variable passed as a parameter.
* Returns false if an argument is an object of different class or not an object.
*
* This method is final, for more information read https://github.com/myclabs/php-enum/issues/4
*
* @psalm-pure
* @psalm-param mixed $variable
* @param mixed $variable Variable to compare the enum to.
* @return bool
*/
public final function equals($variable = null)
{
return $variable instanceof self && $this->getValue() === $variable->getValue() && static::class === \get_class($variable);
}
/**
* Returns the names (keys) of all constants in the FakeEnum class.
*
* @psalm-pure
* @psalm-return list<string>
* @return array
*/
public static function keys()
{
return \array_keys(static::toArray());
}
/**
* Returns instances of the FakeEnum class of all Enum constants.
*
* @psalm-pure
* @psalm-return array<string, static>
* @return static[] Constant name in key, FakeEnum instance in value.
*/
public static function values()
{
$values = [];
/** @psalm-var T $value */
foreach (static::toArray() as $key => $value) {
$values[$key] = new static($value);
}
return $values;
}
/**
* Returns all possible values as an array.
*
* @psalm-pure
* @psalm-suppress ImpureStaticProperty
*
* @psalm-return array<string, mixed>
* @return array Constant name in key, constant value in value
*/
public static function toArray()
{
$class = static::class;
if (!isset(static::$cache[$class])) {
/** @psalm-suppress ImpureMethodCall this reflection API usage has no side-effects here */
$reflection = new ReflectionClass($class);
/** @psalm-suppress ImpureMethodCall this reflection API usage has no side-effects here */
static::$cache[$class] = $reflection->getConstants();
}
return static::$cache[$class];
}
/**
* Check if is valid enum value.
*
* @param mixed $value Value to check for validity.
* @psalm-param mixed $value
* @psalm-pure
* @psalm-assert-if-true T $value
* @return bool
*/
public static function isValid($value)
{
return \in_array($value, static::toArray(), \true);
}
/**
* Asserts valid enum value.
*
* @psalm-pure
* @psalm-assert T $value
* @param mixed $value Value to assert for validity.
* @throws UnexpectedValueException If the value is not part of the enum.
*/
public static function assertValidValue($value)
{
self::assertValidValueReturningKey($value);
}
/**
* Asserts valid enum value.
*
* @psalm-pure
* @psalm-assert T $value
* @param mixed $value Value to assert for validity.
* @return string
* @throws UnexpectedValueException If the value is not part of the enum.
*/
protected static function assertValidValueReturningKey($value)
{
if (\false === ($key = static::search($value))) {
throw new UnexpectedValueException("Value '{$value}' is not part of the enum " . static::class);
}
return $key;
}
/**
* Check if is valid enum key.
*
* @param string $key Key to check for validity.
* @psalm-param string $key
* @psalm-pure
* @return bool
*/
public static function isValidKey($key)
{
$array = static::toArray();
return isset($array[$key]) || \array_key_exists($key, $array);
}
/**
* Return key for value.
*
* @param mixed $value Value to search for.
*
* @psalm-param mixed $value
* @psalm-pure
* @return string|false
*/
public static function search($value)
{
return \array_search($value, static::toArray(), \true);
}
/**
* Returns a value when called statically like so: MyEnum::SOME_VALUE() given SOME_VALUE is a class constant.
*
* @param string $name Name of the method that was called.
* @param array $arguments Arguments provided to the method.
*
* @return static
* @throws BadMethodCallException If the the method was not a known constant.
*
* @psalm-pure
*/
public static function __callStatic($name, $arguments)
{
$class = static::class;
if (!isset(self::$instances[$class][$name])) {
$array = static::toArray();
if (!isset($array[$name]) && !\array_key_exists($name, $array)) {
$message = "No static method or enum constant '{$name}' in class " . static::class;
throw new BadMethodCallException($message);
}
return self::$instances[$class][$name] = new static($array[$name]);
}
return clone self::$instances[$class][$name];
}
/**
* Specify data which should be serialized to JSON. This method returns data that can be serialized by json_encode()
* natively.
*
* @return mixed
* @link http://php.net/manual/en/jsonserializable.jsonserialize.php
* @psalm-pure
*/
#[\ReturnTypeWillChange]
public function jsonSerialize()
{
return $this->getValue();
}
}

View File

@@ -0,0 +1,30 @@
<?php
namespace Google\Web_Stories_Dependencies\AmpProject;
/**
* Interface with constants for the different AMP HTML formats.
*
* @package ampproject/amp-toolbox
*/
interface Format
{
/**
* AMP for websites format.
*
* @var string
*/
const AMP = 'AMP';
/**
* AMP for ads format.
*
* @var string
*/
const AMP4ADS = 'AMP4ADS';
/**
* AMP for email format.
*
* @var string
*/
const AMP4EMAIL = 'AMP4EMAIL';
}

View File

@@ -0,0 +1,26 @@
<?php
namespace Google\Web_Stories_Dependencies\AmpProject\Html;
/**
* Interface with constants for the different "at" rules.
*
* @package ampproject/amp-toolbox
*/
interface AtRule
{
const _MOZ_DOCUMENT = '-moz-document';
const CHARSET = 'charset';
const COUNTER_STYLE = 'counter-style';
const DOCUMENT = 'document';
const FONT_FACE = 'font-face';
const FONT_FEATURE_VALUES = 'font-feature-values';
const IMPORT = 'import';
const KEYFRAMES = 'keyframes';
const MEDIA = 'media';
const NAMESPACE_ = 'namespace';
const PAGE = 'page';
const PROPERTY = 'property';
const SUPPORTS = 'supports';
const VIEWPORT = 'viewport';
}

View File

@@ -0,0 +1,183 @@
<?php
namespace Google\Web_Stories_Dependencies\AmpProject\Html;
/**
* Unit of a length.
*
* This interface defines the available units that can be recognized in HTML and/or CSS dimensions.
*
* @see https://developer.mozilla.org/en-US/docs/Learn/CSS/Building_blocks/Values_and_units
*
* @package ampproject/amp-toolbox
*/
final class LengthUnit
{
/**
* Centimeters.
*
* 1cm = 96px/2.54.
*
* @var string
*/
const CM = 'cm';
/**
* Millimeters.
*
* 1mm = 1/10th of 1cm.
*
* @var string
*/
const MM = 'mm';
/**
* Quarter-millimeters.
*
* 1Q = 1/40th of 1cm.
*
* @var string
*/
const Q = 'q';
/**
* Inches.
*
* 1in = 2.54cm = 96px.
*
* @var string
*/
const IN = 'in';
/**
* Picas.
*
* 1pc = 1/6th of 1in.
*
* @var string
*/
const PC = 'pc';
/**
* Points.
*
* 1pt = 1/72th of 1in.
*
* @var string
*/
const PT = 'pt';
/**
* Pixels.
*
* 1px = 1/96th of 1in.
*
* @var string
*/
const PX = 'px';
/**
* Font size of the parent, in the case of typographical properties like font-size, and font size of the element
* itself, in the case of other properties like width.
*
* @var string
*/
const EM = 'em';
/**
* The x-height of the element's font.
*
* @var string
*/
const EX = 'ex';
/**
* The advance measure (width) of the glyph "0" of the element's font.
*
* @var string
*/
const CH = 'ch';
/**
* Font size of the root element.
*
* @var string
*/
const REM = 'rem';
/**
* Line height of the element.
*
* @var string
*/
const LH = 'lh';
/**
* 1% of the viewport's width.
*
* @var string
*/
const VW = 'vw';
/**
* 1% of the viewport's height.
*
* @var string
*/
const VH = 'vh';
/**
* 1% of the viewport's smaller dimension.
*
* @var string
*/
const VMIN = 'vmin';
/**
* 1% of the viewport's larger dimension.
*
* @var string
*/
const VMAX = 'vmax';
/**
* Set of known absolute units.
*
* @var string[]
*/
const ABSOLUTE_UNITS = [self::CM, self::MM, self::Q, self::IN, self::PC, self::PT, self::PX];
/**
* Set of known relative units.
*
* @var string[]
*/
const RELATIVE_UNITS = [self::EM, self::EX, self::CH, self::REM, self::LH, self::VW, self::VH, self::VMIN, self::VMAX];
/**
* Pixels per inch resolution to use for conversions.
*
* @var int
*/
const PPI = 96;
/**
* Centimeters per inch.
*
* @var float
*/
const CM_PER_IN = 2.54;
/**
* Convert a unit-based length into a number of pixels.
*
* @param int|float $value Value to convert.
* @param string $unit Unit of the value.
* @return int|float|false Converted value, or false if it could not be converted.
*/
public static function convertIntoPixels($value, $unit)
{
if (0 === $value) {
return 0;
}
switch ($unit) {
case self::CM:
return $value * self::PPI / self::CM_PER_IN;
case self::MM:
return $value * self::PPI / self::CM_PER_IN / 10;
case self::Q:
return $value * self::PPI / self::CM_PER_IN / 40;
case self::IN:
return $value * self::PPI;
case self::PC:
return $value * self::PPI / 6;
case self::PT:
return $value * self::PPI / 72;
case self::PX:
// No conversion needed for pixel values.
return $value;
default:
return \false;
}
}
}

View File

@@ -0,0 +1,12 @@
<?php
namespace Google\Web_Stories_Dependencies\AmpProject\Html;
/**
* Interface with constants for the different types of tags.
*
* @package ampproject/amp-toolbox
*/
interface LowerCaseTag extends Tag
{
}

View File

@@ -0,0 +1,143 @@
<?php
namespace Google\Web_Stories_Dependencies\AmpProject\Html\Parser;
use Google\Web_Stories_Dependencies\AmpProject\Str;
/**
* Helper for determining the line/column information for SAX events that are being received by a HtmlSaxHandler.
*
* @package ampproject/amp-toolbox
*/
final class DocLocator
{
/**
* Line mapped to a given position.
*
* @var array
*/
private $lineByPosition = [];
/**
* Column mapped to a given position.
*
* @var array
*/
private $columnByPosition = [];
/**
* Size of the document in bytes.
*
* @var int
*/
private $documentByteSize;
/**
* The current position in the htmlText.
*
* @var int
*/
private $position = 0;
/**
* The previous position in the htmlText.
*
* We need this to know where a tag or attribute etc. started.
*
* @var int
*/
private $previousPosition = 0;
/**
* Line within the document.
*
* @var int
*/
private $line = 1;
/**
* Column within the document.
*
* @var int
*/
private $column = 0;
/**
* DocLocator constructor.
*
* @param string $htmlText String of HTML.
*/
public function __construct($htmlText)
{
/*
* Precomputes a mapping from positions within htmlText to line / column numbers.
*
* TODO: This uses a fair amount of space and we can probably do better, but it's also quite simple so here we
* are for now.
*/
$currentLine = 1;
$currentColumn = 0;
$length = Str::length($htmlText);
for ($index = 0; $index < $length; ++$index) {
$this->lineByPosition[$index] = $currentLine;
$this->columnByPosition[$index] = $currentColumn;
$character = Str::substring($htmlText, $index, 1);
if ($character === "\n") {
++$currentLine;
$currentColumn = 0;
} else {
++$currentColumn;
}
}
$this->documentByteSize = Str::length($htmlText);
}
/**
* Advances the internal position by the characters in $tokenText.
*
* This method is to be called only from within the parser.
*
* @param string $tokenText The token text which we examine to advance the line / column location within the doc.
*/
public function advancePosition($tokenText)
{
$this->previousPosition = $this->position;
$this->position += Str::length($tokenText);
}
/**
* Snapshots the previous internal position so that getLine / getCol will return it.
*
* These snapshots happen as the parser enter / exits a tag.
*
* This method is to be called only from within the parser.
*/
public function snapshotPosition()
{
if ($this->previousPosition < \count($this->lineByPosition)) {
$this->line = $this->lineByPosition[$this->previousPosition];
$this->column = $this->columnByPosition[$this->previousPosition];
}
}
/**
* Get the current line in the HTML source from which the most recent SAX event was generated. This value is only
* sensible once an event has been generated, that is, in practice from within the context of the HtmlSaxHandler
* methods - e.g., startTag(), pcdata(), etc.
*
* @return int The current line.
*/
public function getLine()
{
return $this->line;
}
/**
* Get the current column in the HTML source from which the most recent SAX event was generated. This value is only
* sensible once an event has been generated, that is, in practice from within the context of the HtmlSaxHandler
* methods - e.g., startTag(), pcdata(), etc.
*
* @return int The current column.
*/
public function getColumn()
{
return $this->column;
}
/**
* Get the size of the document in bytes.
*
* @return int The size of the document in bytes.
*/
public function getDocumentByteSize()
{
return $this->documentByteSize;
}
}

View File

@@ -0,0 +1,19 @@
<?php
namespace Google\Web_Stories_Dependencies\AmpProject\Html\Parser;
/**
* The html eflags, used internally by the parser.
*
* @package ampproject/amp-toolbox
*/
interface EFlags
{
const OPTIONAL_ENDTAG = 1;
const EMPTY_ = 2;
const CDATA = 4;
const RCDATA = 8;
const UNSAFE = 16;
const FOLDABLE = 32;
const UNKNOWN_OR_CUSTOM = 64;
}

View File

@@ -0,0 +1,393 @@
<?php
namespace Google\Web_Stories_Dependencies\AmpProject\Html\Parser;
use Google\Web_Stories_Dependencies\AmpProject\Encoding;
use Google\Web_Stories_Dependencies\AmpProject\Exception\FailedToParseHtml;
use Google\Web_Stories_Dependencies\AmpProject\Html\UpperCaseTag as Tag;
use Google\Web_Stories_Dependencies\AmpProject\Str;
/**
* An Html parser.
*
* The parse() method takes a string and calls methods on HtmlSaxHandler while it is visiting its tokens.
*
* @package ampproject/amp-toolbox
*/
final class HtmlParser
{
/**
* Regular expression that matches the next token to be processed.
*
* @var string
*/
const INSIDE_TAG_TOKEN = '%^[ \\t\\n\\f\\r\\v]*(?:' . '(?:' . '([^\\t\\r\\n /=>][^\\t\\r\\n =>]*|' . '[^\\t\\r\\n =>]+[^ >]|' . '\\/+(?!>))' . '(' . '\\s*=\\s*' . '(' . '\\"[^\\"]*\\"' . '|\'[^\']*\'' . '|(?=[a-z][a-z-]*\\s+=)' . '|[^>\\s]*' . ')' . ')' . '?' . ')' . '|(/?>)' . '|[^a-z\\s>]+)' . '%i';
/**
* Regular expression that matches the next token to be processed when we are outside a tag.
*
* @var string
*/
const OUTSIDE_TAG_TOKEN = '%^(?:' . '&(\\#[0-9]+|\\#[x][0-9a-f]+|\\w+);' . '|<[!]--[\\s\\S]*?(?:--[!]?>|$)' . '|<(/)?([a-z!\\?][^\\0 \\n\\r\\t\\f\\v>/]*)' . '|([^<&>]+)' . '|([<&>]))' . '%i';
/**
* Regular expression that matches null characters.
*
* @var string
*/
const NULL_REGEX = "/\x00/g";
/**
* Regular expression that matches entities.
*
* @var string
*/
const ENTITY_REGEX = '/&(#\\d+|#x[0-9A-Fa-f]+|\\w+);/g';
/**
* Regular expression that matches loose &s.
*
* @var string
*/
const LOOSE_AMP_REGEX = '/&([^a-z#]|#(?:[^0-9x]|x(?:[^0-9a-f]|$)|$)|$)/gi';
/**
* Regular expression that matches <.
*
* @var string
*/
const LT_REGEX = '/</g';
/**
* Regular expression that matches >.
*
* @var string
*/
const GT_REGEX = '/>/g';
/**
* Regular expression that matches decimal numbers.
*
* @var string
*/
const DECIMAL_ESCAPE_REGEX = '/^#(\\d+)$/';
/**
* Regular expression that matches hexadecimal numbers.
*
* @var string
*/
const HEX_ESCAPE_REGEX = '/^#x([0-9A-Fa-f]+)$/';
/**
* HTML entities that are encoded/decoded.
*
* @type array<string>
*/
const ENTITIES = ['colon' => ':', 'lt' => '<', 'gt' => '>', 'amp' => '&', 'nbsp' => '\\u00a0', 'quot' => '"', 'apos' => '\''];
/**
* A map of element to a bitmap of flags it has, used internally on the parser.
*
* @var array<int>
*/
const ELEMENTS = [
Tag::A => 0,
Tag::ABBR => 0,
Tag::ACRONYM => 0,
Tag::ADDRESS => 0,
Tag::APPLET => EFlags::UNSAFE,
Tag::AREA => EFlags::EMPTY_,
Tag::B => 0,
Tag::BASE => EFlags::EMPTY_ | EFlags::UNSAFE,
Tag::BASEFONT => EFlags::EMPTY_ | EFlags::UNSAFE,
Tag::BDO => 0,
Tag::BIG => 0,
Tag::BLOCKQUOTE => 0,
Tag::BODY => EFlags::OPTIONAL_ENDTAG | EFlags::UNSAFE | EFlags::FOLDABLE,
Tag::BR => EFlags::EMPTY_,
Tag::BUTTON => 0,
Tag::CANVAS => 0,
Tag::CAPTION => 0,
Tag::CENTER => 0,
Tag::CITE => 0,
Tag::CODE => 0,
Tag::COL => EFlags::EMPTY_,
Tag::COLGROUP => EFlags::OPTIONAL_ENDTAG,
Tag::DD => EFlags::OPTIONAL_ENDTAG,
Tag::DEL => 0,
Tag::DFN => 0,
Tag::DIR => 0,
Tag::DIV => 0,
Tag::DL => 0,
Tag::DT => EFlags::OPTIONAL_ENDTAG,
Tag::EM => 0,
Tag::FIELDSET => 0,
Tag::FONT => 0,
Tag::FORM => 0,
Tag::FRAME => EFlags::EMPTY_ | EFlags::UNSAFE,
Tag::FRAMESET => EFlags::UNSAFE,
Tag::H1 => 0,
Tag::H2 => 0,
Tag::H3 => 0,
Tag::H4 => 0,
Tag::H5 => 0,
Tag::H6 => 0,
Tag::HEAD => EFlags::OPTIONAL_ENDTAG | EFlags::UNSAFE | EFlags::FOLDABLE,
Tag::HR => EFlags::EMPTY_,
Tag::HTML => EFlags::OPTIONAL_ENDTAG | EFlags::UNSAFE | EFlags::FOLDABLE,
Tag::I => 0,
Tag::IFRAME => EFlags::UNSAFE | EFlags::CDATA,
Tag::IMG => EFlags::EMPTY_,
Tag::INPUT => EFlags::EMPTY_,
Tag::INS => 0,
Tag::ISINDEX => EFlags::EMPTY_ | EFlags::UNSAFE,
Tag::KBD => 0,
Tag::LABEL => 0,
Tag::LEGEND => 0,
Tag::LI => EFlags::OPTIONAL_ENDTAG,
Tag::LINK => EFlags::EMPTY_ | EFlags::UNSAFE,
Tag::MAP => 0,
Tag::MENU => 0,
Tag::META => EFlags::EMPTY_ | EFlags::UNSAFE,
Tag::NOFRAMES => EFlags::UNSAFE | EFlags::CDATA,
// TODO: This used to read:
// Tag::NOSCRIPT => EFlags::UNSAFE | EFlags::CDATA,
// It appears that the effect of that is that anything inside is then considered cdata, so
// <noscript><style>foo</noscript></noscript> never sees a style start tag / end tag event. But we must
// recognize such style tags and they're also allowed by HTML, e.g. see:
// https://developer.mozilla.org/en-US/docs/Web/HTML/Element/noscript
// On a broader note this also means we may be missing other start/end tag events inside elements marked as
// CDATA which our parser should better reject. Yikes.
Tag::NOSCRIPT => EFlags::UNSAFE,
Tag::OBJECT => EFlags::UNSAFE,
Tag::OL => 0,
Tag::OPTGROUP => 0,
Tag::OPTION => EFlags::OPTIONAL_ENDTAG,
Tag::P => EFlags::OPTIONAL_ENDTAG,
Tag::PARAM => EFlags::EMPTY_ | EFlags::UNSAFE,
Tag::PRE => 0,
Tag::Q => 0,
Tag::S => 0,
Tag::SAMP => 0,
Tag::SCRIPT => EFlags::UNSAFE | EFlags::CDATA,
Tag::SELECT => 0,
Tag::SMALL => 0,
Tag::SPAN => 0,
Tag::STRIKE => 0,
Tag::STRONG => 0,
Tag::STYLE => EFlags::UNSAFE | EFlags::CDATA,
Tag::SUB => 0,
Tag::SUP => 0,
Tag::TABLE => 0,
Tag::TBODY => EFlags::OPTIONAL_ENDTAG,
Tag::TD => EFlags::OPTIONAL_ENDTAG,
Tag::TEXTAREA => EFlags::RCDATA,
Tag::TFOOT => EFlags::OPTIONAL_ENDTAG,
Tag::TH => EFlags::OPTIONAL_ENDTAG,
Tag::THEAD => EFlags::OPTIONAL_ENDTAG,
Tag::TITLE => EFlags::RCDATA | EFlags::UNSAFE,
Tag::TR => EFlags::OPTIONAL_ENDTAG,
Tag::TT => 0,
Tag::U => 0,
Tag::UL => 0,
Tag::VAR_ => 0,
];
/**
* Given a SAX-like HtmlSaxHandler, this parses a $htmlText and lets the $handler know the structure while visiting
* the nodes. If the provided handler is an implementation of HtmlSaxHandlerWithLocation, then its setDocLocator()
* method will get called prior to startDoc(), and the getLine() / getColumn() methods will reflect the current
* line / column while a SAX callback (e.g., startTag()) is active.
*
* @param HtmlSaxHandler $handler The HtmlSaxHandler that will receive the events.
* @param string $htmlText The html text.
*/
public function parse(HtmlSaxHandler $handler, $htmlText)
{
$htmlUpper = null;
$inTag = \false;
// True iff we're currently processing a tag.
$attributes = [];
// Accumulates attribute names and values.
$tagName = null;
// The name of the tag currently being processed.
$eflags = null;
// The element flags for the current tag.
$openTag = \false;
// True if the current tag is an open tag.
$tagStack = new TagNameStack($handler);
Str::setEncoding(Encoding::AMP);
// Only provide location information if the handler implements the setDocLocator method.
$locator = null;
if ($handler instanceof HtmlSaxHandlerWithLocation) {
$locator = new DocLocator($htmlText);
$handler->setDocLocator($locator);
}
// Lets the handler know that we are starting to parse the document.
$handler->startDoc();
// Consumes tokens from the htmlText and stops once all tokens are processed.
while ($htmlText) {
$regex = $inTag ? self::INSIDE_TAG_TOKEN : self::OUTSIDE_TAG_TOKEN;
// Gets the next token.
$matches = null;
Str::regexMatch($regex, $htmlText, $matches);
// Avoid infinite loop in case nothing could be matched.
// This can be caused by documents provided in the wrong encoding, which the regex engine fails to handle.
if (empty($matches[0])) {
throw FailedToParseHtml::forHtml($htmlText);
}
if ($locator) {
$locator->advancePosition($matches[0]);
}
// And removes it from the string.
$htmlText = Str::substring($htmlText, Str::length($matches[0]));
if ($inTag) {
if (!empty($matches[1])) {
// Attribute.
// SetAttribute with uppercase names doesn't work on IE6.
$attributeName = Str::toLowerCase($matches[1]);
// Use empty string as value for valueless attribs, so <input type=checkbox checked> gets attributes
// ['type', 'checkbox', 'checked', ''].
$decodedValue = '';
if (!empty($matches[2])) {
$encodedValue = $matches[3];
switch (Str::substring($encodedValue, 0, 1)) {
// Strip quotes.
case '"':
case "'":
$encodedValue = Str::substring($encodedValue, 1, Str::length($encodedValue) - 2);
break;
}
$decodedValue = $this->unescapeEntities($this->stripNULs($encodedValue));
}
$attributes[] = $attributeName;
$attributes[] = $decodedValue;
} elseif (!empty($matches[4])) {
if ($eflags !== null) {
// False if not in allowlist.
if ($openTag) {
$tagStack->startTag(new ParsedTag($tagName, $attributes));
} else {
$tagStack->endTag(new ParsedTag($tagName));
}
}
if ($openTag && $eflags & (EFlags::CDATA | EFlags::RCDATA)) {
if ($htmlUpper === null) {
$htmlUpper = Str::toUpperCase($htmlText);
} else {
$htmlUpper = Str::substring($htmlUpper, Str::length($htmlUpper) - Str::length($htmlText));
}
$dataEnd = Str::position($htmlUpper, "</{$tagName}");
if ($dataEnd < 0) {
$dataEnd = Str::length($htmlText);
}
if ($eflags & EFlags::CDATA) {
$handler->cdata(Str::substring($htmlText, 0, $dataEnd));
} else {
$handler->rcdata($this->normalizeRCData(Str::substring($htmlText, 0, $dataEnd)));
}
if ($locator) {
$locator->advancePosition(Str::substring($htmlText, 0, $dataEnd));
}
$htmlText = Str::substring($htmlText, $dataEnd);
}
$tagName = null;
$eflags = null;
$openTag = \false;
$attributes = [];
if ($locator) {
$locator->snapshotPosition();
}
$inTag = \false;
}
} else {
if (!empty($matches[1])) {
// Entity.
$tagStack->pcdata($matches[0]);
} elseif (!empty($matches[3])) {
// Tag.
$openTag = !$matches[2];
if ($locator) {
$locator->snapshotPosition();
}
$inTag = \true;
$tagName = Str::toUpperCase($matches[3]);
$eflags = \array_key_exists($tagName, self::ELEMENTS) ? self::ELEMENTS[$tagName] : EFlags::UNKNOWN_OR_CUSTOM;
} elseif (!empty($matches[4])) {
// Text.
if ($locator) {
$locator->snapshotPosition();
}
$tagStack->pcdata($matches[4]);
} elseif (!empty($matches[5])) {
// Cruft.
switch ($matches[5]) {
case '<':
$tagStack->pcdata('&lt;');
break;
case '>':
$tagStack->pcdata('&gt;');
break;
default:
$tagStack->pcdata('&amp;');
break;
}
}
}
}
if (!$inTag && $locator) {
$locator->snapshotPosition();
}
// Lets the handler know that we are done parsing the document.
$tagStack->exitRemainingTags();
$handler->effectiveBodyTag($tagStack->effectiveBodyAttributes());
$handler->endDoc();
}
/**
* Decode an HTML entity.
*
* This method is public as it needs to be passed into Str::regexReplaceCallback().
*
* @param string $entity The full entity (including the & and the ;).
* @return string A single unicode code-point as a string.
*/
public function lookupEntity($entity)
{
$name = Str::toLowerCase(Str::substring($entity, Str::length($entity) - 1));
if (\array_key_exists($name, self::ENTITIES)) {
return self::ENTITIES[$name];
}
$matches = [];
if (Str::regexMatch(self::DECIMAL_ESCAPE_REGEX, $name, $matches)) {
return \chr((int) $matches[1]);
}
if (Str::regexMatch(self::HEX_ESCAPE_REGEX, $name, $matches)) {
return \chr(\hexdec($matches[1]));
}
// If unable to decode, return the name.
return $name;
}
/**
* Remove null characters on the string.
*
* @param string $text The string to have the null characters removed.
* @return string A string without null characters.
* @private
*/
private function stripNULs($text)
{
return Str::regexReplace(self::NULL_REGEX, '', $text);
}
/**
* The plain text of a chunk of HTML CDATA which possibly containing.
*
* @param string $text A chunk of HTML CDATA. It must not start or end inside an HTML entity.
* @return string The unescaped entities.
*/
private function unescapeEntities($text)
{
return Str::regexReplaceCallback(self::ENTITY_REGEX, [$this, 'lookupEntity'], $text);
}
/**
* Escape entities in RCDATA that can be escaped without changing the meaning.
*
* @param string $rcdata The RCDATA string we want to normalize.
* @return string A normalized version of RCDATA.
*/
private function normalizeRCData($rcdata)
{
$rcdata = Str::regexReplace(self::LOOSE_AMP_REGEX, '&amp;$1', $rcdata);
$rcdata = Str::regexReplace(self::LT_REGEX, '&lt;', $rcdata);
$rcdata = Str::regexReplace(self::GT_REGEX, '&gt;', $rcdata);
return $rcdata;
}
}

View File

@@ -0,0 +1,75 @@
<?php
namespace Google\Web_Stories_Dependencies\AmpProject\Html\Parser;
/**
* An interface to the HtmlParser visitor that gets called while the HTML is being parsed.
*
* @package ampproject/amp-toolbox
*/
interface HtmlSaxHandler
{
/**
* Handler called when the parser found a new tag.
*
* @param ParsedTag $tag New tag that was found.
* @return void
*/
public function startTag(ParsedTag $tag);
/**
* Handler called when the parser found a closing tag.
*
* @param ParsedTag $tag Closing tag that was found.
* @return void
*/
public function endTag(ParsedTag $tag);
/**
* Handler called when PCDATA is found.
*
* @param string $text The PCDATA that was found.
* @return void
*/
public function pcdata($text);
/**
* Handler called when RCDATA is found.
*
* @param string $text The RCDATA that was found.
* @return void
*/
public function rcdata($text);
/**
* Handler called when CDATA is found.
*
* @param string $text The CDATA that was found.
* @return void
*/
public function cdata($text);
/**
* Handler called when the parser is starting to parse the document.
*
* @return void
*/
public function startDoc();
/**
* Handler called when the parsing is done.
*
* @return void
*/
public function endDoc();
/**
* Callback for informing that the parser is manufacturing a <body> tag not actually found on the page. This will be
* followed by a startTag() with the actual body tag in question.
*
* @return void
*/
public function markManufacturedBody();
/**
* HTML5 defines how parsers treat documents with multiple body tags: they merge the attributes from the later ones
* into the first one. Therefore, just before the parser sends the endDoc event, it will also send this event which
* will provide the attributes from the effective body tag to the client (the handler).
*
* @param array<ParsedAttribute> $attributes Array of parsed attributes.
* @return void
*/
public function effectiveBodyTag($attributes);
}

View File

@@ -0,0 +1,20 @@
<?php
namespace Google\Web_Stories_Dependencies\AmpProject\Html\Parser;
/**
* An interface to the HtmlParser visitor that gets called while the HTML is being parsed.
*
* @package ampproject/amp-toolbox
*/
interface HtmlSaxHandlerWithLocation extends HtmlSaxHandler
{
/**
* Called prior to parsing a document, that is, before startTag().
*
* @param DocLocator $locator A locator instance which provides access to the line/column information while SAX
* events are being received by the handler.
* @return void
*/
public function setDocLocator(DocLocator $locator);
}

View File

@@ -0,0 +1,53 @@
<?php
namespace Google\Web_Stories_Dependencies\AmpProject\Html\Parser;
/**
* Name/Value pair representing an HTML Tag attribute.
*
* @package ampproject/amp-toolbox
*/
final class ParsedAttribute
{
/**
* Name of the attribute.
*
* @var string
*/
private $name;
/**
* Value of the attribute.
*
* @var string
*/
private $value;
/**
* ParsedAttribute constructor.
*
* @param string $name Name of the attribute.
* @param string $value Value of the attribute.
*/
public function __construct($name, $value)
{
$this->name = $name;
$this->value = $value;
}
/**
* Get the name of the attribute.
*
* @return string Name of the attribute.
*/
public function name()
{
return $this->name;
}
/**
* Get the value of the attribute.
*
* @return string Value of the attribute.
*/
public function value()
{
return $this->value;
}
}

View File

@@ -0,0 +1,214 @@
<?php
namespace Google\Web_Stories_Dependencies\AmpProject\Html\Parser;
use Google\Web_Stories_Dependencies\AmpProject\ScriptReleaseVersion;
use Google\Web_Stories_Dependencies\AmpProject\Str;
/**
* The Html parser makes method calls with ParsedTags as arguments.
*
* @package ampproject/amp-toolbox
*/
final class ParsedTag
{
/**
* Name of the parsed tag.
*
* @var string
*/
private $tagName;
/**
* Associative array of attributes.
*
* @var array<ParsedAttribute>
*/
private $attributes = [];
/**
* Lazily allocated map from attribute name to value.
*
* @var array<string>|null
*/
private $attributesByKey;
/**
* State of a script tag.
*
* @var ScriptTag
*/
private $scriptTag;
/**
* ParsedTag constructor.
*
* @param string $tagName Name of the parsed tag.
* @param array $alternatingAttributes Optional. Array of alternating (name, value) pairs.
*/
public function __construct($tagName, $alternatingAttributes = [])
{
/*
* Tag and Attribute names are case-insensitive. For error messages, we would like to use lower-case names as
* they read a little nicer. However, in validator environments where the parsing is done by the actual browser,
* the DOM API returns tag names in upper case. We stick with this convention for tag names, for performance,
* but convert to lower when producing error messages. Error messages aren't produced in latency sensitive
* contexts.
*/
if (!\is_array($alternatingAttributes)) {
$alternatingAttributes = [];
}
$this->tagName = Str::toUpperCase($tagName);
// Convert attribute names to lower case, not values, which are case-sensitive.
$count = \count($alternatingAttributes);
for ($index = 0; $index < $count; $index += 2) {
$name = Str::toLowerCase($alternatingAttributes[$index]);
$value = $alternatingAttributes[$index + 1];
// Our html parser repeats the key as the value if there is no value. We
// replace the value with an empty string instead in this case.
if ($name === $value) {
$value = '';
}
$this->attributes[] = new ParsedAttribute($name, $value);
}
// Sort the attribute array by (lower case) name.
\usort($this->attributes, function (ParsedAttribute $a, ParsedAttribute $b) {
if (\PHP_MAJOR_VERSION < 7 && $a->name() === $b->name()) {
// Hack required for PHP 5.6, as it does not maintain stable order for equal items.
// See https://bugs.php.net/bug.php?id=69158.
// To get around this, we compare the index within $this->attributes instead to maintain existing order.
return \strcmp(\array_search($a, $this->attributes, \true), \array_search($b, $this->attributes, \true));
}
return \strcmp($a->name(), $b->name());
});
$this->scriptTag = new ScriptTag($this->tagName, $this->attributes);
}
/**
* Get the lower-case tag name.
*
* @return string Lower-case tag name.
*/
public function lowerName()
{
return Str::toLowerCase($this->tagName);
}
/**
* Get the upper-case tag name.
*
* @return string Upper-case tag name.
*/
public function upperName()
{
return $this->tagName;
}
/**
* Returns an array of attributes.
*
* Each attribute has two fields: name and value. Name is always lower-case, value is the case from the original
* document. Values are unescaped.
*
* @return array<ParsedAttribute>
*/
public function attributes()
{
return $this->attributes;
}
/**
* Returns an object mapping attribute name to attribute value.
*
* This is populated lazily, as it's not used for most tags.
*
* @return array<string>
* */
public function attributesByKey()
{
if ($this->attributesByKey === null) {
$this->attributesByKey = [];
foreach ($this->attributes as $attribute) {
$this->attributesByKey[$attribute->name()] = $attribute->value();
}
}
return $this->attributesByKey;
}
/**
* Returns a duplicate attribute name if the tag contains two attributes named the same, but with different
* attribute values.
*
* Same attribute name AND value is OK. Returns null if there are no such duplicate attributes.
*
* @return string|null
*/
public function hasDuplicateAttributes()
{
$lastAttributeName = '';
$lastAttributeValue = '';
foreach ($this->attributes as $attribute) {
if ($lastAttributeName === $attribute->name() && $lastAttributeValue !== $attribute->value()) {
return $attribute->name();
}
$lastAttributeName = $attribute->name();
$lastAttributeValue = $attribute->value();
}
return null;
}
/**
* Removes duplicate attributes from the attribute list.
*
* This is consistent with HTML5 parsing error handling rules, only the first attribute with each attribute name is
* considered, the remainder are ignored.
*/
public function dedupeAttributes()
{
$newAttributes = [];
$lastAttributeName = '';
foreach ($this->attributes as $attribute) {
if ($lastAttributeName !== $attribute->name()) {
$newAttributes[] = $attribute;
}
$lastAttributeName = $attribute->name();
}
$this->attributes = $newAttributes;
}
/**
* Returns the value of a given attribute name. If it does not exist then returns null.
*
* @param string $name Name of the attribute.
* @return string|null Value of the attribute, or null if it does not exist.
*/
public function getAttributeValueOrNull($name)
{
$attributesByKey = $this->attributesByKey();
return \array_key_exists($name, $attributesByKey) ? $attributesByKey[$name] : null;
}
/**
* Returns the script release version, otherwise ScriptReleaseVersion::UNKNOWN.
*
* @return ScriptReleaseVersion
*/
public function getScriptReleaseVersion()
{
return $this->scriptTag->releaseVersion();
}
/**
* Tests if this tag is a script with a src of an AMP domain.
*
* @return bool Whether this tag is a script with a src of an AMP domain.
*/
public function isAmpDomain()
{
return $this->scriptTag->isAmpDomain();
}
/**
* Tests if this is the AMP runtime script tag.
*
* @return bool Whether this is the AMP runtime script tag.
*/
public function isAmpRuntimeScript()
{
return $this->scriptTag->isRuntime();
}
/**
* Tests if this is an extension script tag.
*
* @return bool Whether this is an extension script tag.
*/
public function isExtensionScript()
{
return $this->scriptTag->isExtension();
}
}

View File

@@ -0,0 +1,203 @@
<?php
namespace Google\Web_Stories_Dependencies\AmpProject\Html\Parser;
use Google\Web_Stories_Dependencies\AmpProject\Amp;
use Google\Web_Stories_Dependencies\AmpProject\Html\Attribute;
use Google\Web_Stories_Dependencies\AmpProject\Html\Tag;
use Google\Web_Stories_Dependencies\AmpProject\ScriptReleaseVersion;
use Google\Web_Stories_Dependencies\AmpProject\Str;
/**
* Represents the state of a script tag.
*
* @package ampproject/amp-toolbox
*/
final class ScriptTag
{
/**
* Name of the tag.
*
* @var string
*/
private $tagName;
/**
* Array of parsed attributes.
*
* @var array<ParsedAttribute>
*/
private $attributes;
/**
* Lazily evaluated collection of properties about the script tag.
*
* @var array|null
*/
private $parsedProperties;
/**
* Standard and Nomodule JavaScript.
*
* Examples:
* - v0.js
* - v0/amp-ad-0.1.js
*
* @var string
*/
const STANDARD_SCRIPT_PATH_REGEX = '/(v0|v0/amp-[a-z0-9-]*-[a-z0-9.]*)\\.js$/i';
/**
* LTS and Nomodule LTS JavaScript.
*
* Examples:
* - lts/v0.js
* - lts/v0/amp-ad-0.1.js
*
* @var string
*/
const LTS_SCRIPT_PATH_REGEX = '/lts/(v0|v0/amp-[a-z0-9-]*-[a-z0-9.]*)\\.js$/i';
/**
* Module JavaScript.
*
* Examples:
* - v0.mjs
* - amp-ad-0.1.mjs
*
* @var string
*/
const MODULE_SCRIPT_PATH_REGEX = '/(v0|v0/amp-[a-z0-9-]*-[a-z0-9.]*)\\.mjs$/i';
/**
* Module LTS JavaScript.
*
* Examples:
* - lts/v0.mjs
* - lts/v0/amp-ad-0.1.mjs
*
* @var string
*/
const MODULE_LTS_SCRIPT_PATH_REGEX = '/lts/(v0|v0/amp-[a-z0-9-]*-[a-z0-9.]*)\\.mjs$/i';
/**
* Runtime JavaScript.
*
* Examples:
* - v0.js
* - v0.mjs
* - v0.mjs?f=sxg
* - lts/v0.js
* - lts/v0.js?f=sxg
* -lts/v0.mjs
*
* @var string
*/
const RUNTIME_SCRIPT_PATH_REGEX = '/(lts/)?v0\\.m?js(\\?f=sxg)?/i';
/**
* ScriptTag constructor.
*
* @param string $tagName Name of the tag.
* @param array<ParsedAttribute> $attributes Array of parsed attributes.
*/
public function __construct($tagName, $attributes)
{
$this->tagName = $tagName;
$this->attributes = $attributes;
}
/**
* Returns the script release version, otherwise ScriptReleaseVersion::UNKNOWN.
*
* @return ScriptReleaseVersion
*/
public function releaseVersion()
{
if ($this->tagName !== Tag::SCRIPT) {
return ScriptReleaseVersion::UNKNOWN();
}
$properties = $this->parseAttributes();
return $properties['releaseVersion'];
}
/**
* Tests if this tag is a script with a src of an AMP domain.
*
* @return bool Whether this tag is a script with a src of an AMP domain.
*/
public function isAmpDomain()
{
if ($this->tagName !== Tag::SCRIPT) {
return \false;
}
$properties = $this->parseAttributes();
return $properties['isAmpDomain'];
}
/**
* Tests if this is the AMP runtime script tag.
*
* @return bool Whether this is the AMP runtime script tag.
*/
public function isRuntime()
{
if ($this->tagName !== Tag::SCRIPT) {
return \false;
}
$properties = $this->parseAttributes();
return $properties['isRuntime'];
}
/**
* Tests if this is an extension script tag.
*
* @return bool Whether this is an extension script tag.
*/
public function isExtension()
{
if ($this->tagName !== Tag::SCRIPT) {
return \false;
}
$properties = $this->parseAttributes();
return $properties['isExtension'];
}
/**
* Parse attributes to determine script properties.
*
* @return array Associative array of parsed properties.
*/
private function parseAttributes()
{
if ($this->parsedProperties !== null) {
return $this->parsedProperties;
}
$properties = ['isAsync' => \false, 'isModule' => \false, 'isNomodule' => \false, 'isExtension' => \false, 'path' => '', 'src' => ''];
foreach ($this->attributes as $attribute) {
if ($attribute->name() === Attribute::ASYNC) {
$properties['isAsync'] = \true;
} elseif ($attribute->name() === Attribute::CUSTOM_ELEMENT || $attribute->name() === Attribute::CUSTOM_TEMPLATE || $attribute->name() === Attribute::HOST_SERVICE) {
$properties['isExtension'] = \true;
} elseif ($attribute->name() === Attribute::NOMODULE) {
$properties['isNomodule'] = \true;
} elseif ($attribute->name() === Attribute::SRC) {
$properties['src'] = $attribute->value();
} elseif ($attribute->name() === Attribute::TYPE && $attribute->value() === Attribute::TYPE_MODULE) {
$properties['isModule'] = \true;
}
}
// Determine if this has a valid AMP domain and separate the path from the attribute 'src'.
if (Str::position($properties['src'], Amp::CACHE_ROOT_URL) === 0) {
$properties['isAmpDomain'] = \true;
$properties['path'] = Str::substring($properties['src'], Str::length(Amp::CACHE_ROOT_URL));
// Only look at script tags that have attribute 'async'.
if ($properties['isAsync']) {
// Determine if this is the AMP Runtime.
if (!$properties['isExtension'] && Str::regexMatch(self::RUNTIME_SCRIPT_PATH_REGEX, $properties['path'])) {
$properties['isRuntime'] = \true;
}
// Determine the release version (LTS, module, standard, etc).
if ($properties['isModule'] && Str::regexMatch(self::MODULE_LTS_SCRIPT_PATH_REGEX, $properties['path']) || $properties['isNomodule'] && Str::regexMatch(self::LTS_SCRIPT_PATH_REGEX, $properties['path'])) {
$properties['releaseVersion'] = ScriptReleaseVersion::MODULE_NOMODULE_LTS();
} elseif ($properties['isModule'] && Str::regexMatch(self::MODULE_SCRIPT_PATH_REGEX, $properties['path']) || $properties['isNomodule'] && Str::regexMatch(self::STANDARD_SCRIPT_PATH_REGEX, $properties['path'])) {
$properties['releaseVersion'] = ScriptReleaseVersion::MODULE_NOMODULE();
} elseif (Str::regexMatch(self::LTS_SCRIPT_PATH_REGEX, $properties['path'])) {
$properties['releaseVersion'] = ScriptReleaseVersion::LTS();
} elseif (Str::regexMatch(self::STANDARD_SCRIPT_PATH_REGEX, $properties['path'])) {
$properties['releaseVersion'] = ScriptReleaseVersion::STANDARD();
} else {
$properties['releaseVersion'] = ScriptReleaseVersion::UNKNOWN();
}
}
}
$this->parsedProperties = $properties;
return $this->parsedProperties;
}
}

View File

@@ -0,0 +1,321 @@
<?php
namespace Google\Web_Stories_Dependencies\AmpProject\Html\Parser;
use Google\Web_Stories_Dependencies\AmpProject\Html\UpperCaseTag as Tag;
use Google\Web_Stories_Dependencies\AmpProject\Str;
/**
* Abstraction to keep track of which tags have been opened / closed as we traverse the tags in the document.
*
* Closing tags is tricky:
* - Some tags have no end tag per spec. For example, there is no </img> tag per spec. Since we are making
* startTag()/endTag() calls, we manufacture endTag() calls for these immediately after the startTag().
* - We assume all end tags are optional and we pop tags off our stack as we encounter parent closing tags. This part
* differs slightly from the behavior per spec: instead of closing an <option> tag when a following <option> tag
* is seen, we close it when the parent closing tag (in practice <select>) is encountered.
*
* @package ampproject/amp-toolbox
*/
final class TagNameStack
{
/**
* Regular expression that matches strings composed of all space characters, as defined in
* https://infra.spec.whatwg.org/#ascii-whitespace, and in the various HTML parsing rules at
* https://html.spec.whatwg.org/multipage/parsing.html#parsing-main-inhtml.
*
* Note: Do not USE \s to match whitespace as this includes many other characters that HTML parsing does not
* consider whitespace.
*
* @var string
*/
const SPACE_REGEX = '/^[ \\f\\n\\r\\t]*$/';
/**
* Regular expression that matches the characters considered whitespace by the C++ HTML parser.
*
* @var string
*/
const CPP_SPACE_REGEX = '/^[ \\f\\n\\r\\t\\v' . '\\x{00a0}\\x{1680}\\x{2000}-\\x{200a}\\x{2028}\\x{2029}\\x{202f}\\x{205f}\\x{3000}]*$/u';
/**
* The handler to manage the stack for.
*
* @var HtmlSaxHandler
*/
private $handler;
/**
* The current tag name and its parents.
*
* @var array<string>
*/
private $stack = [];
/**
* The current region within the document.
*
* @var TagRegion
*/
private $region;
/**
* Keeps track of the attributes from all body tags encountered within the document.
*
* @var array<ParsedAttribute>
*/
private $effectiveBodyAttributes = [];
/**
* TagNameStack constructor.
*
* @param HtmlSaxHandler $handler Handler to handle the HTML SAX parser events.
*/
public function __construct(HtmlSaxHandler $handler)
{
$this->handler = $handler;
$this->region = TagRegion::PRE_DOCTYPE();
}
/**
* Returns the attributes from all body tags within the document.
*
* @return array<ParsedAttribute>
*/
public function effectiveBodyAttributes()
{
return $this->effectiveBodyAttributes;
}
/**
* Enter a tag, opening a scope for child tags. Entering a tag can close the previous tag or enter other tags (such
* as opening a <body> tag when encountering a tag not allowed outside the body.
*
* @param ParsedTag $tag Tag that is being started.
*/
public function startTag(ParsedTag $tag)
{
// We only report the first body for each document - either a manufactured one, or the first one encountered.
// However, we collect all attributes in $this->effectiveBodyAttributes.
if ($tag->upperName() === Tag::BODY) {
$this->effectiveBodyAttributes = \array_merge($this->effectiveBodyAttributes, $tag->attributes());
}
// This section deals with manufacturing <head>, </head>, and <body> tags if the document has left them out or
// placed them in the wrong location.
switch ($this->region->getValue()) {
case TagRegion::PRE_DOCTYPE:
if ($tag->upperName() === Tag::_DOCTYPE) {
$this->region = TagRegion::PRE_HTML();
} elseif ($tag->upperName() === Tag::HTML) {
$this->region = TagRegion::PRE_HEAD();
} elseif ($tag->upperName() === Tag::HEAD) {
$this->region = TagRegion::IN_HEAD();
} elseif ($tag->upperName() === Tag::BODY) {
$this->region = TagRegion::IN_BODY();
} elseif (!\in_array($tag->upperName(), Tag::STRUCTURE_TAGS, \true)) {
if (\in_array($tag->upperName(), Tag::ELEMENTS_ALLOWED_IN_HEAD, \true)) {
$this->startTag(new ParsedTag(Tag::HEAD));
} else {
$this->handler->markManufacturedBody();
$this->startTag(new ParsedTag(Tag::BODY));
}
}
break;
case TagRegion::PRE_HTML:
// Stray DOCTYPE/HTML tags are ignored, not emitted twice.
if ($tag->upperName() === Tag::_DOCTYPE) {
return;
}
if ($tag->upperName() === Tag::HTML) {
$this->region = TagRegion::PRE_HEAD();
} elseif ($tag->upperName() === Tag::HEAD) {
$this->region = TagRegion::IN_HEAD();
} elseif ($tag->upperName() === Tag::BODY) {
$this->region = TagRegion::IN_BODY();
} elseif (!\in_array($tag->upperName(), Tag::STRUCTURE_TAGS, \true)) {
if (\in_array($tag->upperName(), Tag::ELEMENTS_ALLOWED_IN_HEAD, \true)) {
$this->startTag(new ParsedTag(Tag::HEAD));
} else {
$this->handler->markManufacturedBody();
$this->startTag(new ParsedTag(Tag::BODY));
}
}
break;
case TagRegion::PRE_HEAD:
// Stray DOCTYPE/HTML tags are ignored, not emitted twice.
if ($tag->upperName() === Tag::_DOCTYPE || $tag->upperName() === Tag::HTML) {
return;
}
if ($tag->upperName() === Tag::HEAD) {
$this->region = TagRegion::IN_HEAD();
} elseif ($tag->upperName() === Tag::BODY) {
$this->region = TagRegion::IN_BODY();
} elseif (!\in_array($tag->upperName(), Tag::STRUCTURE_TAGS, \true)) {
if (\in_array($tag->upperName(), Tag::ELEMENTS_ALLOWED_IN_HEAD, \true)) {
$this->startTag(new ParsedTag(Tag::HEAD));
} else {
$this->handler->markManufacturedBody();
$this->startTag(new ParsedTag(Tag::BODY));
}
}
break;
case TagRegion::IN_HEAD:
// Stray DOCTYPE/HTML/HEAD tags are ignored, not emitted twice.
if ($tag->upperName() === Tag::_DOCTYPE || $tag->upperName() === Tag::HTML || $tag->upperName() === Tag::HEAD) {
return;
}
if (!\in_array($tag->upperName(), Tag::ELEMENTS_ALLOWED_IN_HEAD, \true)) {
$this->endTag(new ParsedTag(Tag::HEAD));
if ($tag->upperName() !== Tag::BODY) {
$this->handler->markManufacturedBody();
$this->startTag(new ParsedTag(Tag::BODY));
} else {
$this->region = TagRegion::IN_BODY();
}
}
break;
case TagRegion::PRE_BODY:
// Stray DOCTYPE/HTML/HEAD tags are ignored, not emitted twice.
if ($tag->upperName() === Tag::_DOCTYPE || $tag->upperName() === Tag::HTML || $tag->upperName() === Tag::HEAD) {
return;
}
if ($tag->upperName() !== Tag::BODY) {
$this->handler->markManufacturedBody();
$this->startTag(new ParsedTag(Tag::BODY));
} else {
$this->region = TagRegion::IN_BODY();
}
break;
case TagRegion::IN_BODY:
// Stray DOCTYPE/HTML/HEAD tags are ignored, not emitted twice.
if ($tag->upperName() === Tag::_DOCTYPE || $tag->upperName() === Tag::HTML || $tag->upperName() === Tag::HEAD) {
return;
}
if ($tag->upperName() === Tag::BODY) {
// We only report the first body for each document - either a manufactured one, or the first one
// encountered.
return;
}
if ($tag->upperName() === Tag::SVG) {
$this->region = TagRegion::IN_SVG();
break;
}
// Check implicit tag closing due to opening tags.
if (\count($this->stack) > 0) {
$parentTagName = $this->stack[\count($this->stack) - 1];
// <p> tags can be implicitly closed by certain other start tags.
// See https://www.w3.org/TR/html-markup/p.html.
if ($parentTagName === Tag::P && \in_array($tag->upperName(), Tag::P_CLOSING_TAGS, \true)) {
$this->endTag(new ParsedTag(Tag::P));
// <dd> and <dt> tags can be implicitly closed by other <dd> and <dt> tags.
// See https://www.w3.org/TR/html-markup/dd.html.
} elseif (($parentTagName === Tag::DD || $parentTagName === Tag::DT) && ($tag->upperName() === Tag::DD || $tag->upperName() === Tag::DT)) {
$this->endTag(new ParsedTag($parentTagName));
// <li> tags can be implicitly closed by other <li> tags.
// See https://www.w3.org/TR/html-markup/li.html.
} elseif ($parentTagName === Tag::LI && $tag->upperName() === Tag::LI) {
$this->endTag(new ParsedTag(Tag::LI));
}
}
break;
case TagRegion::IN_SVG:
$this->handler->startTag($tag);
$this->stack[] = $tag->upperName();
return;
default:
break;
}
$this->handler->startTag($tag);
if (\in_array($tag->upperName(), Tag::SELF_CLOSING_TAGS, \true)) {
// Ignore attributes in end tags.
$this->handler->endTag(new ParsedTag($tag->upperName()));
} else {
$this->stack[] = $tag->upperName();
}
}
/**
* Callback for pcdata.
*
* Some text nodes can trigger the start of the body region.
*
* @param string $text Text of the text node.
*/
public function pcdata($text)
{
if (Str::regexMatch(self::SPACE_REGEX, $text)) {
// Only ASCII whitespace; this can be ignored for validator's purposes.
} elseif (Str::regexMatch(self::CPP_SPACE_REGEX, $text)) {
// Non-ASCII whitespace; if this occurs outside <body>, output a manufactured-body error. Do not create
// implicit tags, in order to match the behavior of the buggy C++ parser. It just so happens this is also
// good UX, since the subsequent validation errors caused by the implicit tags are unhelpful.
switch ($this->region->getValue()) {
// Fallthroughs intentional.
case TagRegion::PRE_DOCTYPE:
case TagRegion::PRE_HTML:
case TagRegion::PRE_HEAD:
case TagRegion::IN_HEAD:
case TagRegion::PRE_BODY:
$this->handler->markManufacturedBody();
}
} else {
// Non-whitespace text; if this occurs outside <body>, output a manufactured-body error and create the
// necessary implicit tags.
switch ($this->region->getValue()) {
case TagRegion::PRE_DOCTYPE:
// Doctype is not manufactured, fallthrough intentional.
case TagRegion::PRE_HTML:
$this->startTag(new ParsedTag(Tag::HTML));
// Fallthrough intentional.
case TagRegion::PRE_HEAD:
$this->startTag(new ParsedTag(Tag::HEAD));
// Fallthrough intentional.
case TagRegion::IN_HEAD:
$this->endTag(new ParsedTag(Tag::HEAD));
// Fallthrough intentional.
case TagRegion::PRE_BODY:
$this->handler->markManufacturedBody();
$this->startTag(new ParsedTag(Tag::BODY));
}
}
$this->handler->pcdata($text);
}
/**
* Upon exiting a tag, validation for the current matcher is triggered, e.g. for checking that the tag had some
* specified number of children.
*
* @param ParsedTag $tag Tag that is being exited.
*/
public function endTag($tag)
{
if ($this->region->equals(TagRegion::IN_HEAD()) && $tag->upperName() === Tag::HEAD) {
$this->region = TagRegion::PRE_BODY();
}
/*
* We ignore close body tags (</body) and instead insert them when their outer scope is closed (/html). This is
* closer to how a browser parser works. The idea here is if other tags are found after the <body>, (ex: <div>)
* which are only allowed in the <body>, we will effectively move them into the body section.
*/
if ($tag->upperName() === Tag::BODY) {
return;
}
/*
* We look for tag.upperName() from the end. If we can find it, we pop everything from thereon off the stack. If
* we can't find it, we don't bother with closing the tag, since it doesn't have a matching open tag, though in
* practice the HtmlParser class will have already manufactured a start tag.
*/
for ($index = \count($this->stack) - 1; $index >= 0; $index--) {
if ($this->stack[$index] === $tag->upperName()) {
while (\count($this->stack) > $index) {
if ($this->stack[\count($this->stack) - 1] === Tag::SVG) {
$this->region = TagRegion::IN_BODY();
}
$this->handler->endTag(new ParsedTag(\array_pop($this->stack)));
}
return;
}
}
}
/**
* This method is called when we're done with the document.
*
* Normally, the parser should actually close the tags, but just in case it doesn't this easy-enough method will
* take care of it.
*/
public function exitRemainingTags()
{
while (\count($this->stack) > 0) {
$this->handler->endTag(new ParsedTag(\array_pop($this->stack)));
}
}
}

View File

@@ -0,0 +1,31 @@
<?php
namespace Google\Web_Stories_Dependencies\AmpProject\Html\Parser;
use Google\Web_Stories_Dependencies\AmpProject\FakeEnum;
/**
* Enum for denoting to which structural region a tag belongs.
*
*
* @method static TagRegion PRE_DOCTYPE()
* @method static TagRegion PRE_HTML()
* @method static TagRegion PRE_HEAD()
* @method static TagRegion IN_HEAD()
* @method static TagRegion PRE_BODY()
* @method static TagRegion IN_BODY()
* @method static TagRegion IN_SVG()
*
* @package ampproject/amp-toolbox
*/
final class TagRegion extends FakeEnum
{
const PRE_DOCTYPE = 0;
const PRE_HTML = 1;
const PRE_HEAD = 2;
const IN_HEAD = 3;
const PRE_BODY = 4;
// After closing <head> tag, but before open <body> tag.
const IN_BODY = 5;
const IN_SVG = 6;
// We don't track the region after the closing body tag.
}

View File

@@ -0,0 +1,90 @@
<?php
namespace Google\Web_Stories_Dependencies\AmpProject\Html;
/**
* Interface with constants for the different request destinations that are supported.
*
* For the purposes of the AMP implementation, we are only interested in the
* request destinations that are valid values for the 'as' attribute in preloads.
*
* Full list of request destinations:
* @see https://fetch.spec.whatwg.org/#concept-request-destination
*
* @package ampproject/amp-toolbox
*/
interface RequestDestination
{
/**
* Audio file, as typically used in <audio>.
*
* @var string
*/
const AUDIO = 'audio';
/**
* An HTML document intended to be embedded by a <frame> or <iframe>.
*
* @var string
*/
const DOCUMENT = 'document';
/**
* A resource to be embedded inside an <embed> element.
*
* @var string
*/
const EMBED = 'embed';
/**
* Resource to be accessed by a fetch or XHR request, such as an ArrayBuffer or JSON file.
*
* @var string
*/
const FETCH = 'fetch';
/**
* Font file.
*
* @var string
*/
const FONT = 'font';
/**
* Image file.
*
* @var string
*/
const IMAGE = 'image';
/**
* A resource to be embedded inside an <object> element.
*
* @var string
*/
const OBJECT = 'object';
/**
* JavaScript file.
*
* @var string
*/
const SCRIPT = 'script';
/**
* CSS stylesheet.
*
* @var string
*/
const STYLE = 'style';
/**
* WebVTT file.
*
* @var string
*/
const TRACK = 'track';
/**
* A JavaScript web worker or shared worker.
*
* @var string
*/
const WORKER = 'worker';
/**
* Video file, as typically used in <video>.
*
* @var string
*/
const VIDEO = 'video';
}

View File

@@ -0,0 +1,454 @@
<?php
namespace Google\Web_Stories_Dependencies\AmpProject\Html;
/**
* Interface with constants for the different types of accessibility roles.
*
* @see https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/ARIA_Techniques
*
* @package ampproject/amp-toolbox
*/
interface Role
{
/**
* A message with an alert or error information.
*
* @var string
*/
const ALERT = 'alert';
/**
* A separate window with an alert or error information.
*
* @var string
*/
const ALERTDIALOG = 'alertdialog';
/**
* A software unit executing a set of tasks for its users.
*
* @var string
*/
const APPLICATION = 'application';
/**
* A section of a page that could easily stand on its own on a page, in a document, or on a website.
*
* @var string
*/
const ARTICLE = 'article';
/**
* A region that contains mostly site-oriented content, rather than page-specific content.
*
* @var string
*/
const BANNER = 'banner';
/**
* Allows for user-triggered actions.
*
* @var string
*/
const BUTTON = 'button';
/**
* An element as being a cell in a tabular container that does not contain column or row header information.
*
* @var string
*/
const CELL = 'cell';
/**
* A control that has three possible values, (true, false, mixed).
*
* @var string
*/
const CHECKBOX = 'checkbox';
/**
* A table cell containing header information for a column.
*
* @var string
*/
const COLUMNHEADER = 'columnheader';
/**
* Combobox is a presentation of a select, where users can type to locate a selected item.
*
* @var string
*/
const COMBOBOX = 'combobox';
/**
* A supporting section of the document, designed to be complementary to the main content at a similar level in the
* DOM hierarchy, but remains meaningful when separated from the main content.
*
* @var string
*/
const COMPLEMENTARY = 'complementary';
/**
* A large perceivable region that contains information about the parent document.
*
* @var string
*/
const CONTENTINFO = 'contentinfo';
/**
* A definition of a term or concept.
*
* @var string
*/
const DEFINITION = 'definition';
/**
* Descriptive content for a page element which references this element via describedby.
*
* @var string
*/
const DESCRIPTION = 'description';
/**
* A dialog is a small application window that sits above the application and is designed to interrupt the current
* processing of an application in order to prompt the user to enter information or require a response.
*
* @var string
*/
const DIALOG = 'dialog';
/**
* A list of references to members of a single group.
*
* @var string
*/
const DIRECTORY = 'directory';
/**
* Content that contains related information, such as a book.
*
* @var string
*/
const DOCUMENT = 'document';
/**
* A scrollable list of articles where scrolling may cause articles to be added to or removed from either end of the
* list.
*
* @var string
*/
const FEED = 'feed';
/**
* A figure inside page content where appropriate semantics do not already exist.
*
* @var string
*/
const FIGURE = 'figure';
/**
* A landmark region that contains a collection of items and objects that, as a whole, combine to create a form.
*
* @var string
*/
const FORM = 'form';
/**
* A grid contains cells of tabular data arranged in rows and columns (e.g., a table).
*
* @var string
*/
const GRID = 'grid';
/**
* A gridcell is a table cell in a grid. Gridcells may be active, editable, and selectable. Cells may have
* relationships such as controls to address the application of functional relationships.
*
* @var string
*/
const GRIDCELL = 'gridcell';
/**
* A group is a section of user interface objects which would not be included in a page summary or table of contents
* by an assistive technology. See region for sections of user interface objects that should be included in a page
* summary or table of contents.
*
* @var string
*/
const GROUP = 'group';
/**
* A heading for a section of the page.
*
* @var string
*/
const HEADING = 'heading';
/**
* An img is a container for a collection elements that form an image.
*
* @var string
*/
const IMG = 'img';
/**
* Interactive reference to a resource (note, that in XHTML 2.0 any element can have an href attribute and thus be a
* link)
*
* @var string
*/
const LINK = 'link';
/**
* Group of non-interactive list items. Lists contain children whose role is listitem.
*
* Uses an underscore as "list" is a conflicting PHP keyword.
*
* @var string
*/
const LIST_ = 'list';
/**
* A list box is a widget that allows the user to select one or more items from a list. Items within the list are
* static and may contain images. List boxes contain children whose role is option.
*
* @var string
*/
const LISTBOX = 'listbox';
/**
* A single item in a list.
*
* @var string
*/
const LISTITEM = 'listitem';
/**
* A region where new information is added and old information may disappear such as chat logs, messaging, game log
* or an error log. In contrast to other regions, in this role there is a relationship between the arrival of new
* items in the log and the reading order. The log contains a meaningful sequence and new information is added only
* to the end of the log, not at arbitrary points.
*
* @var string
*/
const LOG = 'log';
/**
* The main content of a document.
*
* @var string
*/
const MAIN = 'main';
/**
* A marquee is used to scroll text across the page.
*
* @var string
*/
const MARQUEE = 'marquee';
/**
* Content that represents a mathematical expression.
*
* @var string
*/
const MATH = 'math';
/**
* Offers a list of choices to the user.
*
* @var string
*/
const MENU = 'menu';
/**
* A menubar is a container of menu items. Each menu item may activate a new sub-menu. Navigation behavior should be
* similar to the typical menu bar graphical user interface.
*
* @var string
*/
const MENUBAR = 'menubar';
/**
* A link in a menu. This is an option in a group of choices contained in a menu.
*
* @var string
*/
const MENUITEM = 'menuitem';
/**
* Defines a menuitem which is checkable (tri-state).
*
* @var string
*/
const MENUITEMCHECKBOX = 'menuitemcheckbox';
/**
* Indicates a menu item which is part of a group of menuitemradio roles.
*
* @var string
*/
const MENUITEMRADIO = 'menuitemradio';
/**
* A collection of navigational elements (usually links) for navigating the document or related documents.
*
* @var string
*/
const NAVIGATION = 'navigation';
/**
* An element whose implicit native role semantics will not be mapped to the accessibility API.
*
* @var string
*/
const NONE = 'none';
/**
* A section whose content is parenthetic or ancillary to the main content of the resource.
*
* @var string
*/
const NOTE = 'note';
/**
* A selectable item in a list represented by a select.
*
* @var string
*/
const OPTION = 'option';
/**
* An element whose role is presentational does not need to be mapped to the accessibility API.
*
* @var string
*/
const PRESENTATION = 'presentation';
/**
* Used by applications for tasks that take a long time to execute, to show the execution progress.
*
* @var string
*/
const PROGRESSBAR = 'progressbar';
/**
* A radio is an option in single-select list. Only one radio control in a radiogroup can be selected at the same
* time.
*
* @var string
*/
const RADIO = 'radio';
/**
* A group of radio controls.
*
* @var string
*/
const RADIOGROUP = 'radiogroup';
/**
* Region is a large perceivable section on the web page.
*
* @var string
*/
const REGION = 'region';
/**
* A row of table cells.
*
* @var string
*/
const ROW = 'row';
/**
* A structure containing one or more row elements in a tabular container.
*
* @var string
*/
const ROWGROUP = 'rowgroup';
/**
* A table cell containing header information for a row.
*
* @var string
*/
const ROWHEADER = 'rowheader';
/**
* Scroll bar to navigate the horizontal or vertical dimensions of the page.
*
* @var string
*/
const SCROLLBAR = 'scrollbar';
/**
* A section of the page used to search the page, site, or collection of sites.
*
* @var string
*/
const SEARCH = 'search';
/**
* An entry field to provide a query to search for.
*
* @var string
*/
const SEARCHBOX = 'searchbox';
/**
* A line or bar that separates and distinguishes sections of content.
*
* @var string
*/
const SEPARATOR = 'separator';
/**
* A user input where the user selects an input in a given range. This form of range expects an analog keyboard
* interface.
*
* @var string
*/
const SLIDER = 'slider';
/**
* A form of Range that expects a user selecting from discrete choices.
*
* @var string
*/
const SPINBUTTON = 'spinbutton';
/**
* This is a container for process advisory information to give feedback to the user.
*
* @var string
*/
const STATUS = 'status';
/**
* Functionally identical to a checkbox but represents the states "on"/"off" instead of "checked"/"unchecked".
*
* Uses an underscore as "list" is a conflicting PHP keyword.
*
* @var string
*/
const SWITCH_ = 'switch';
/**
* A header for a tabpanel.
*
* @var string
*/
const TAB = 'tab';
/**
* A non-interactive table structure containing data arranged in rows and columns.
*
* @var string
*/
const TABLE = 'table';
/**
* A list of tabs, which are references to tabpanels.
*
* @var string
*/
const TABLIST = 'tablist';
/**
* Tabpanel is a container for the resources associated with a tab.
*
* @var string
*/
const TABPANEL = 'tabpanel';
/**
* A word or phrase with a corresponding definition.
*
* @var string
*/
const TERM = 'term';
/**
* Inputs that allow free-form text as their value.
*
* @var string
*/
const TEXTBOX = 'textbox';
/**
* A numerical counter which indicates an amount of elapsed time from a start point, or the time remaining until an
* end point.
*
* @var string
*/
const TIMER = 'timer';
/**
* A toolbar is a collection of commonly used functions represented in compact visual form.
*
* @var string
*/
const TOOLBAR = 'toolbar';
/**
* A popup that displays a description for an element when a user passes over or rests on that element. Supplement
* to the normal tooltip processing of the user agent.
*
* @var string
*/
const TOOLTIP = 'tooltip';
/**
* A form of a list having groups inside groups, where sub trees can be collapsed and expanded.
*
* @var string
*/
const TREE = 'tree';
/**
* A grid whose rows can be expanded and collapsed in the same manner as for a tree.
*
* @var string
*/
const TREEGRID = 'treegrid';
/**
* An option item of a tree. This is an element within a tree that may be expanded or collapsed.
*
* @var string
*/
const TREEITEM = 'treeitem';
}

View File

@@ -0,0 +1,233 @@
<?php
namespace Google\Web_Stories_Dependencies\AmpProject\Html;
/**
* Interface with constants for the different types of tags.
*
* @package ampproject/amp-toolbox
*/
interface Tag
{
const A = 'a';
const ABBR = 'abbr';
const ACRONYM = 'acronym';
const ADDRESS = 'address';
const APPLET = 'applet';
const AREA = 'area';
const ARTICLE = 'article';
const ASIDE = 'aside';
const AUDIO = 'audio';
const B = 'b';
const BASE = 'base';
const BASEFONT = 'basefont';
const BDI = 'bdi';
const BDO = 'bdo';
const BGSOUND = 'bgsound';
const BIG = 'big';
const BLOCKQUOTE = 'blockquote';
const BODY = 'body';
const BR = 'br';
const BUTTON = 'button';
const CANVAS = 'canvas';
const CAPTION = 'caption';
const CENTER = 'center';
const CIRCLE = 'circle';
const CITE = 'cite';
const CLIPPATH = 'clippath';
const CODE = 'code';
const COL = 'col';
const COLGROUP = 'colgroup';
const DATA = 'data';
const DATALIST = 'datalist';
const DD = 'dd';
const DEFS = 'defs';
const DEL = 'del';
const DESC = 'desc';
const DETAILS = 'details';
const DFN = 'dfn';
const DIR = 'dir';
const DIV = 'div';
const DL = 'dl';
const DT = 'dt';
const ELLIPSE = 'ellipse';
const EM = 'em';
const EMBED = 'embed';
const FEBLEND = 'feblend';
const FECOLORMATRIX = 'fecolormatrix';
const FECOMPONENTTRANSFER = 'fecomponenttransfer';
const FECOMPOSITE = 'fecomposite';
const FECONVOLVEMATRIX = 'feconvolvematrix';
const FEDIFFUSELIGHTING = 'fediffuselighting';
const FEDISPLACEMENTMAP = 'fedisplacementmap';
const FEDISTANTLIGHT = 'fedistantlight';
const FEDROPSHADOW = 'fedropshadow';
const FEFLOOD = 'feflood';
const FEFUNCA = 'fefunca';
const FEFUNCB = 'fefuncb';
const FEFUNCG = 'fefuncg';
const FEFUNCR = 'fefuncr';
const FEGAUSSIANBLUR = 'fegaussianblur';
const FEMERGE = 'femerge';
const FEMERGENODE = 'femergenode';
const FEMORPHOLOGY = 'femorphology';
const FEOFFSET = 'feoffset';
const FEPOINTLIGHT = 'fepointlight';
const FESPECULARLIGHTING = 'fespecularlighting';
const FESPOTLIGHT = 'fespotlight';
const FETILE = 'fetile';
const FETURBULENCE = 'feturbulence';
const FIELDSET = 'fieldset';
const FIGCAPTION = 'figcaption';
const FIGURE = 'figure';
const FILTER = 'filter';
const FONT = 'font';
const FOOTER = 'footer';
const FORM = 'form';
const FRAME = 'frame';
const FRAMESET = 'frameset';
const G = 'g';
const GLYPH = 'glyph';
const GLYPHREF = 'glyphref';
const H1 = 'h1';
const H2 = 'h2';
const H3 = 'h3';
const H4 = 'h4';
const H5 = 'h5';
const H6 = 'h6';
const HEAD = 'head';
const HEADER = 'header';
const HGROUP = 'hgroup';
const HKERN = 'hkern';
const HR = 'hr';
const HTML = 'html';
const I = 'i';
const IFRAME = 'iframe';
const IMAGE = 'image';
const IMG = 'img';
const INPUT = 'input';
const INS = 'ins';
const ISINDEX = 'isindex';
const KBD = 'kbd';
const KEYGEN = 'keygen';
const LABEL = 'label';
const LEGEND = 'legend';
const LI = 'li';
const LINE = 'line';
const LINEARGRADIENT = 'lineargradient';
const LINK = 'link';
const LISTING = 'listing';
const MAIN = 'main';
const MAP = 'map';
const MARK = 'mark';
const MARKER = 'marker';
const MASK = 'mask';
const MENU = 'menu';
const META = 'meta';
const METADATA = 'metadata';
const METER = 'meter';
const MULTICOL = 'multicol';
const NAV = 'nav';
const NEXTID = 'nextid';
const NOBR = 'nobr';
const NOFRAMES = 'noframes';
const NOSCRIPT = 'noscript';
const O_P = 'o:p';
// @todo Will this be usable at present given PHP DOM?
const OBJECT = 'object';
const OL = 'ol';
const OPTGROUP = 'optgroup';
const OPTION = 'option';
const OUTPUT = 'output';
const P = 'p';
const PARAM = 'param';
const PATH = 'path';
const PATTERN = 'pattern';
const PICTURE = 'picture';
const POLYGON = 'polygon';
const POLYLINE = 'polyline';
const PRE = 'pre';
const PROGRESS = 'progress';
const Q = 'Q';
const RADIALGRADIENT = 'radialgradient';
const RB = 'rb';
const RECT = 'rect';
const RP = 'rp';
const RT = 'rt';
const RTC = 'rtc';
const RUBY = 'ruby';
const S = 's';
const SAMP = 'samp';
const SCRIPT = 'script';
const SECTION = 'section';
const SELECT = 'select';
const SLOT = 'slot';
const SMALL = 'small';
const SOLIDCOLOR = 'solidcolor';
const SOURCE = 'source';
const SPACER = 'spacer';
const SPAN = 'span';
const STOP = 'stop';
const STRIKE = 'strike';
const STRONG = 'strong';
const STYLE = 'style';
const SUB = 'sub';
const SUMMARY = 'summary';
const SUP = 'sup';
const SVG = 'svg';
const SWITCH_ = 'switch';
const SYMBOL = 'symbol';
const TABLE = 'table';
const TBODY = 'tbody';
const TD = 'td';
const TEMPLATE = 'template';
const TEXT = 'text';
const TEXTAREA = 'textarea';
const TEXTPATH = 'textpath';
const TFOOT = 'tfoot';
const TH = 'th';
const THEAD = 'thead';
const TIME = 'time';
const TITLE = 'title';
const TR = 'tr';
const TRACK = 'track';
const TREF = 'tref';
const TSPAN = 'tspan';
const TT = 'tt';
const U = 'u';
const UL = 'ul';
const USE_ = 'use';
const VAR_ = 'var';
const VIDEO = 'video';
const VIEW = 'view';
const VKERN = 'vkern';
const WBR = 'wbr';
const _DOCTYPE = '!doctype';
/**
* HTML elements that are self-closing.
*
* @link https://www.w3.org/TR/html5/syntax.html#serializing-html-fragments
*
* @var string[]
*/
const SELF_CLOSING_TAGS = [self::AREA, self::BASE, self::BASEFONT, self::BGSOUND, self::BR, self::COL, self::EMBED, self::FRAME, self::HR, self::IMG, self::INPUT, self::KEYGEN, self::LINK, self::META, self::PARAM, self::SOURCE, self::TRACK, self::WBR];
/**
* List of elements allowed in head.
*
* @link https://github.com/ampproject/amphtml/blob/445d6e3be8a5063e2738c6f90fdcd57f2b6208be/validator/engine/htmlparser.js#L83-L100
* @link https://www.w3.org/TR/html5/document-metadata.html
*
* @var string[]
*/
const ELEMENTS_ALLOWED_IN_HEAD = [self::TITLE, self::BASE, self::LINK, self::META, self::STYLE, self::NOSCRIPT, self::SCRIPT];
/**
* Set of HTML tags which should never trigger an implied open of a <head> or <body> element.
*/
const STRUCTURE_TAGS = [self::_DOCTYPE, self::HTML, self::HEAD, self::BODY];
/**
* The set of HTML tags whose presence will implicitly close a <p> element.
* For example '<p>foo<h1>bar</h1>' should parse the same as '<p>foo</p><h1>bar</h1>'.
* @link https://www.w3.org/TR/html-markup/p.html
*/
const P_CLOSING_TAGS = [self::ADDRESS, self::ARTICLE, self::ASIDE, self::BLOCKQUOTE, self::DIR, self::DL, self::FIELDSET, self::FOOTER, self::FORM, self::H1, self::H2, self::H3, self::H4, self::H5, self::H6, self::HEADER, self::HR, self::MENU, self::NAV, self::OL, self::P, self::PRE, self::SECTION, self::TABLE, self::UL];
}

Some files were not shown because too many files have changed in this diff Show More