Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add AutoExtensions transformer #210

Merged
merged 48 commits into from
Oct 4, 2021
Merged
Show file tree
Hide file tree
Changes from 44 commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
e56b859
Use custom array dumping with callback support
schlessera Jan 15, 2021
efb0632
Add initial AutoExtensions transformer scaffold
schlessera Dec 15, 2020
6cdd0d3
Inject spec instance into AutoExtensions transformer
schlessera Jan 17, 2021
1b1270e
Add initial dummy test
schlessera Jan 17, 2021
e3a0919
Add node walking to fetch required extensions
schlessera Jan 18, 2021
3caea57
Use tag properties
schlessera Jan 26, 2021
34ff5be
Hook spec tests up to the unit tests
schlessera Jan 26, 2021
0899857
Use latest version via spec
schlessera Feb 18, 2021
5a32bab
Add more edge cases
schlessera Feb 19, 2021
6d78f35
Ensure mustache extension gets the right type attribute
schlessera Feb 19, 2021
8694d99
Improve JSON parsing
schlessera Feb 20, 2021
2dd189b
Add missing trait
schlessera Feb 20, 2021
6ac5c60
Add configuration handling
schlessera Feb 21, 2021
aa83998
No need for manual removal of duplicates because of array key hashes …
schlessera Feb 21, 2021
80c8dca
Hook up spec tests and complete configuration
schlessera May 22, 2021
439f38f
Clean up rebase cruft
schlessera May 25, 2021
aaa0880
Readd ordering logic
schlessera May 25, 2021
9f5af9d
Lazily instantiate remote request implementation
schlessera May 26, 2021
a086077
Use AUTO_EXTENSION_IMPORT config value
schlessera May 26, 2021
3545077
Append scripts after boilerplate if it is present
schlessera Jun 17, 2021
c3995d1
Update AutoExtensions to adapt new validator spec
ediamin Aug 20, 2021
d1fcaed
Fix version calculation for the extensions
ediamin Aug 20, 2021
50f3a1e
Add attribute to extension mapping
ediamin Aug 20, 2021
3d5f001
Add extensions for form
ediamin Aug 20, 2021
0ff4057
Add extension for custom templates
ediamin Aug 20, 2021
b7bd8ae
Add auto extension import support for amp-bind-bindX attributes
ediamin Aug 22, 2021
d4bec29
Add auto extension import support for amp-bind-bind-data- attributes
ediamin Aug 22, 2021
c42fd67
Add auto extension import support for node attributes
ediamin Aug 22, 2021
7c14c55
Add auto extension import support for tag attributes
ediamin Aug 23, 2021
2739abc
Ignore AutoExtensions - ignore-mask-in-svgs spec test
ediamin Aug 23, 2021
9aaaed7
Add AutoExtensionsConfiguration unit tests
ediamin Aug 23, 2021
991fc87
Add ignore option in AutoExtensionsConfiguration
ediamin Aug 23, 2021
db5473a
Ignore extension from being auto import if found in ignore list
ediamin Aug 23, 2021
01738a2
Fix fail tests for php 5.6
ediamin Aug 23, 2021
c03d811
Add issue reference to the ignored spec test
ediamin Aug 23, 2021
fcfbae6
Add support to auto import amp-access-scroll
ediamin Aug 26, 2021
b91a1c8
Re-enable SVG test and normalize away case-inconsistencies
schlessera Sep 6, 2021
ccc523f
Add maskUnits to SVG terms to fix
schlessera Sep 6, 2021
5c49190
Update comment
ediamin Sep 7, 2021
924fb7e
Apply suggestions from code review
ediamin Sep 7, 2021
9164104
Fix AutoExtensionsConfiguration::IGNORED_EXTENSIONS related issues
ediamin Sep 7, 2021
aae0c51
Add support for self closing SVG related elements
ediamin Sep 24, 2021
7231700
Add removeUnneededExtensions AutoExtensionsConfiguration option
ediamin Sep 24, 2021
1d2aefe
Add logic for removeUnneededExtensions
ediamin Sep 27, 2021
f59d413
Apply suggestions from code review
ediamin Oct 1, 2021
149a2a0
Add missing docblock for CannotParseJsonData
ediamin Oct 1, 2021
3dc530a
Add missing docblock for AutoExtensions
ediamin Oct 1, 2021
cd86886
Update logic and tests for protected extensions
ediamin Oct 1, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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,
];
schlessera marked this conversation as resolved.
Show resolved Hide resolved

/**
* 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 bool.
ediamin marked this conversation as resolved.
Show resolved Hide resolved
*/
const AUTO_EXTENSION_IMPORT = 'autoExtensionImport';

/**
* Configuration key that enables experimental conversion of bind attributes.
*
* @var bool
ediamin marked this conversation as resolved.
Show resolved Hide resolved
*/
const EXPERIMENT_BIND_ATTRIBUTE = 'experimentBindAttribute';

/**
* Configuration key that allows individual configuration of extension versions.
*
* @var array
ediamin marked this conversation as resolved.
Show resolved Hide resolved
*/
const EXTENSION_VERSIONS = 'extensionVersions';

/**
* An array of extension names that will not auto import.
*
* @var string[]
ediamin marked this conversation as resolved.
Show resolved Hide resolved
*/
const IGNORED_EXTENSIONS = 'ignoredExtensions';

/**
* An array of extension names that will not auto import.
*
* @var bool
ediamin marked this conversation as resolved.
Show resolved Hide resolved
*/
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)) {
ediamin marked this conversation as resolved.
Show resolved Hide resolved
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
27 changes: 27 additions & 0 deletions src/Optimizer/Error/CannotParseJsonData.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<?php

namespace AmpProject\Optimizer\Error;

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

final class CannotParseJsonData implements Error
ediamin marked this conversation as resolved.
Show resolved Hide resolved
{
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