Skip to content

Commit

Permalink
Merge pull request #210 from ampproject/add/32-auto-extensions
Browse files Browse the repository at this point in the history
  • Loading branch information
schlessera authored Oct 4, 2021
2 parents a0e9613 + cd86886 commit b9403b0
Show file tree
Hide file tree
Showing 13 changed files with 1,455 additions and 5 deletions.
15 changes: 15 additions & 0 deletions src/Dom/Document.php
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
* @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.
Expand Down Expand Up @@ -193,6 +194,7 @@ public function __construct($version = '', $encoding = null)
Filter\AmpEmojiAttribute::class,
Filter\AmpBindAttributes::class,
Filter\SelfClosingTags::class,
Filter\SelfClosingSVGElements::class,
Filter\NoscriptElements::class,
Filter\DeduplicateTag::class,
Filter\ConvertHeadProfileToLink::class,
Expand Down Expand Up @@ -866,6 +868,19 @@ public function __get($name)

$this->body = $body;
return $this->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.
Expand Down
61 changes: 61 additions & 0 deletions src/Dom/Document/Filter/SelfClosingSVGElements.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
<?php

namespace AmpProject\Dom\Document\Filter;

use AmpProject\Dom\Document\AfterSaveFilter;
use AmpProject\Dom\Document\BeforeLoadFilter;
use AmpProject\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>)#';
}

return preg_replace($regexPattern, '<$1$2></$1>', $html);
}

/**
* 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';
}

return preg_replace($regexPattern, '<$1$2 />', $html);
}
}
183 changes: 183 additions & 0 deletions src/Optimizer/Configuration/AutoExtensionsConfiguration.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
<?php

namespace AmpProject\Optimizer\Configuration;

use AmpProject\Amp;
use AmpProject\Exception\InvalidExtension;
use AmpProject\Extension;
use AmpProject\Format;
use AmpProject\Optimizer\Exception\InvalidConfigurationValue;
use ReflectionClass;

/**
* Configuration for the AutoExtensions transformer.
*
* @property string $format Specifies the AMP format. Defaults to `AMP`.
* @property bool $autoExtensionImport Set to `false` to disable the auto extension import. Defaults to `true`.
* @property bool $experimentBindAttribute Enables experimental conversion of bind attributes. Defaults to `false`.
*
* @package ampproject/amp-toolbox
*/
final class AutoExtensionsConfiguration extends BaseTransformerConfiguration
{

/**
* Configuration key that specifies the AMP format.
*
* @var string
*/
const FORMAT = 'format';

/**
* Configuration key that can disable the automatic importing of extension.
*
* @var string
*/
const AUTO_EXTENSION_IMPORT = 'autoExtensionImport';

/**
* Configuration key that enables experimental conversion of bind attributes.
*
* @var string
*/
const EXPERIMENT_BIND_ATTRIBUTE = 'experimentBindAttribute';

/**
* Configuration key that allows individual configuration of extension versions.
*
* @var string
*/
const EXTENSION_VERSIONS = 'extensionVersions';

/**
* An array of extension names that will not auto import.
*
* @var string
*/
const IGNORED_EXTENSIONS = 'ignoredExtensions';

/**
* An array of extension names that will not auto import.
*
* @var string
*/
const REMOVE_UNNEEDED_EXTENSIONS = 'removeUnneededExtensions';

/**
* Get the associative array of allowed keys and their respective default values.
*
* The array index is the key and the array value is the key's default value.
*
* @return array Associative array of allowed keys and their respective default values.
*/
protected function getAllowedKeys()
{
return [
self::FORMAT => Format::AMP,
self::AUTO_EXTENSION_IMPORT => true,
self::EXPERIMENT_BIND_ATTRIBUTE => false,
self::EXTENSION_VERSIONS => [],
self::IGNORED_EXTENSIONS => [],
self::REMOVE_UNNEEDED_EXTENSIONS => false,
];
}

/**
* Validate an individual configuration entry.
*
* @param string $key Key of the configuration entry to validate.
* @param mixed $value Value of the configuration entry to validate.
* @return mixed Validated value.
*/
protected function validate($key, $value)
{
switch ($key) {
case self::FORMAT:
if (! is_string($value)) {
throw InvalidConfigurationValue::forInvalidSubValueType(
self::class,
self::FORMAT,
'string',
gettype($value)
);
}

if (! in_array($value, Amp::FORMATS, true)) {
throw InvalidConfigurationValue::forUnknownSubValue(
self::class,
self::FORMAT,
Amp::FORMATS,
$value
);
}
break;

case self::AUTO_EXTENSION_IMPORT:
if (! is_bool($value)) {
throw InvalidConfigurationValue::forInvalidSubValueType(
self::class,
self::AUTO_EXTENSION_IMPORT,
'boolean',
gettype($value)
);
}
break;

case self::EXPERIMENT_BIND_ATTRIBUTE:
if (! is_bool($value)) {
throw InvalidConfigurationValue::forInvalidSubValueType(
self::class,
self::EXPERIMENT_BIND_ATTRIBUTE,
'boolean',
gettype($value)
);
}
break;

case self::EXTENSION_VERSIONS:
if (! is_array($value)) {
throw InvalidConfigurationValue::forInvalidSubValueType(
self::class,
self::EXTENSION_VERSIONS,
'array',
gettype($value)
);
}
break;

case self::IGNORED_EXTENSIONS:
if (! is_array($value)) {
throw InvalidConfigurationValue::forInvalidSubValueType(
self::class,
self::IGNORED_EXTENSIONS,
'array',
gettype($value)
);
}

// Assert that the extension names in the ignore list are valid extensions.
$reflection = new ReflectionClass(Extension::class);
$constants = $reflection->getConstants();

foreach ($value as $extension) {
if (! in_array($extension, $constants, true)) {
throw InvalidExtension::forExtension($extension);
}
}
break;

case self::REMOVE_UNNEEDED_EXTENSIONS:
if (! is_bool($value)) {
throw InvalidConfigurationValue::forInvalidSubValueType(
self::class,
self::REMOVE_UNNEEDED_EXTENSIONS,
'boolean',
gettype($value)
);
}
break;
}

return $value;
}
}
1 change: 1 addition & 0 deletions src/Optimizer/DefaultConfiguration.php
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ class DefaultConfiguration implements Configuration
*/
protected $transformerConfigurationClasses = [
Transformer\AmpRuntimeCss::class => Configuration\AmpRuntimeCssConfiguration::class,
Transformer\AutoExtensions::class => Configuration\AutoExtensionsConfiguration::class,
Transformer\OptimizeAmpBind::class => Configuration\OptimizeAmpBindConfiguration::class,
Transformer\OptimizeHeroImages::class => Configuration\OptimizeHeroImagesConfiguration::class,
Transformer\PreloadHeroImage::class => Configuration\PreloadHeroImageConfiguration::class,
Expand Down
32 changes: 32 additions & 0 deletions src/Optimizer/Error/CannotParseJsonData.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<?php

namespace AmpProject\Optimizer\Error;

use AmpProject\Dom\Element;
use AmpProject\Dom\ElementDump;
use AmpProject\Optimizer\Error;
use Exception;

/**
* Optimizer error object for invalid JSON data.
*
* @package ampproject/amp-toolbox
*/
final class CannotParseJsonData implements Error
{
use ErrorProperties;

const SCRIPT_EXCEPTION_MESSAGE = 'Cannot parse JSON data for script element %2$s: %1$s.';

/**
* Instantiate a CannotParseJsonData object for an exception that was thrown.
*
* @param Exception $exception Exception that was thrown.
* @param Element $script DOM element of the <style amp-runtime> tag that was targeted.
* @return self
*/
public static function fromExceptionForScriptElement(Exception $exception, Element $script)
{
return new self(sprintf(self::SCRIPT_EXCEPTION_MESSAGE, $exception, new ElementDump($script)));
}
}
18 changes: 18 additions & 0 deletions src/Optimizer/Exception/InvalidConfigurationValue.php
Original file line number Diff line number Diff line change
Expand Up @@ -43,4 +43,22 @@ public static function forInvalidSubValueType($key, $index, $expected, $actual)

return new self($message);
}

/**
* Instantiate an InvalidConfigurationValue exception for an unknown value.
*
* @param string $key Key that was invalid.
* @param string|int $index Index of the sub-value that was invalid.
* @param array<string> $accepted Array of acceptable values.
* @param string $actual Value that was actually provided.
* @return self
*/
public static function forUnknownSubValue($key, $index, $accepted, $actual)
{
$acceptedString = implode(', ', $accepted);
$message = "The configuration value '{$index}' for the key '{$key}' expected the value to be one of "
. "[{$acceptedString}], got '{$actual}' instead.";

return new self($message);
}
}
Loading

0 comments on commit b9403b0

Please sign in to comment.