Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
62 commits
Select commit Hold shift + click to select a range
4c446c9
Add typing to FallbackRemoteRewquest
schlessera Mar 31, 2021
129d804
Add appendRuntimeVersion() to RuntimeVersion object for convenient ac…
schlessera Mar 31, 2021
7e89798
Allow for the config.json file to be synced for test spec suite
schlessera Mar 31, 2021
27eeb15
Add RewriteAmpUrls transformer and configuration
schlessera Mar 31, 2021
73470d8
Use static runtime version append method in RuntimeCss transformer
schlessera Mar 31, 2021
70f131d
Add exceptions and errors
schlessera Mar 31, 2021
ec42883
Add transformer to default list of transformers
schlessera Apr 1, 2021
470b289
Adapt SpecTest setup
schlessera Apr 1, 2021
157ad94
Sync test suite to add suite-wide config files
schlessera Apr 1, 2021
ce5a657
Adapt type-hints to use extended Element
schlessera Apr 1, 2021
f0d6901
Add missing types to configuration property declarations
schlessera Apr 1, 2021
37c984e
Use modulepreload instead of preload
schlessera Apr 1, 2021
5e31b23
Use final config option variant
schlessera Apr 1, 2021
fcf0da4
Copy attributes over
schlessera Apr 1, 2021
6b28aa3
Fix remaining test failures
schlessera Apr 1, 2021
81b5a3a
Add RewriteAmpUrls transformer to README file
schlessera Apr 1, 2021
728fa5c
Add test for Document::createElementWithAttributes()
schlessera Apr 1, 2021
34031d3
Add tests for exceptions
schlessera Apr 2, 2021
2ceda32
Add missing element append to test
schlessera Apr 2, 2021
0329bdb
Add test for mutually exclusive flags
schlessera Apr 2, 2021
d22e4c9
Use RewriteAmpUrls as very last transformer
schlessera Apr 5, 2021
3a6d459
Change reference node to use
schlessera Apr 5, 2021
fac02dc
Use constants for attribute names
schlessera Apr 6, 2021
6672ac9
Remove left-over since tag
schlessera Apr 6, 2021
7b30254
Use setAttribute() for adding an attribute
schlessera Apr 6, 2021
e378c9b
Deduplicate scripts by src as opposed to custom-element/custom-templa…
westonruter Apr 6, 2021
b29a21f
Update tests
westonruter Apr 6, 2021
3305726
Add ReorderHeadTransformer/supports_esm test files
westonruter Apr 6, 2021
f46ecf0
Remove stray duplicated semicolon
westonruter Apr 6, 2021
a3a99fd
Add modulepreload link to ampResourceHints
westonruter Apr 6, 2021
4bad718
Account for .mjs scripts in isRuntimeScript
westonruter Apr 6, 2021
cda4bd8
Account for multiple runtime scripts in reordering
westonruter Apr 6, 2021
6f3e006
Implement script deduplication scheme which keeps module/nomodule scr…
westonruter Apr 6, 2021
6a0532a
Restore extension sorting
westonruter Apr 6, 2021
5ac47b9
Update test with modulepreload link
westonruter Apr 6, 2021
c158850
Ensure nomodule scripts come after module ones
westonruter Apr 6, 2021
7b23a67
Fix running phpcs when installed as composer package
westonruter Apr 6, 2021
7c08d4e
Fix phpcs issues
westonruter Apr 6, 2021
5824c99
Add exclude-pattern for non-vendor dependency
westonruter Apr 6, 2021
bd76b1a
Account for non-Element nodes in the head when ordering
westonruter Apr 6, 2021
63244cc
Add performance TODO
schlessera Apr 7, 2021
b62d924
Revert change to PHPCS config
schlessera Apr 7, 2021
9f4b14f
Fix whitespace issues
schlessera Apr 7, 2021
f8a8c47
Merge pull request #130 from ampproject/fix/reorder-head-deduplication
schlessera Apr 7, 2021
439419d
Use ReorderHead transformer as the very last transformer again
schlessera Apr 7, 2021
c8a83e5
Move up non-AMP resource hints per https://github.com/ampproject/amp-…
westonruter Apr 7, 2021
3bd853f
Add internal state for Url abstraction
schlessera Apr 7, 2021
8c1b41b
Update ordering in ReorderHeadTest
westonruter Apr 7, 2021
54b7080
Add tests for URL parsing and re-assembly
schlessera Apr 7, 2021
4d01f4f
Merge branch 'add/20-rewrite-amp-urls-transformer' of https://github.…
schlessera Apr 7, 2021
1652c72
Add read-only property access to Url
schlessera Apr 7, 2021
5022fdf
Fix query property test
schlessera Apr 7, 2021
94540b1
Fix logic for adding runtime-host meta tag
schlessera Apr 7, 2021
e0dd638
Use substr_compare() in usesAmpCacheUrl()
westonruter Apr 7, 2021
ff5995e
Add filter to required extensions
westonruter Apr 7, 2021
f27de56
Move viewport after meta charset
westonruter Apr 7, 2021
1a0f1dd
Update spec test files
schlessera Apr 8, 2021
02ccd66
Adapt tests
schlessera Apr 8, 2021
b96aadf
Add phpdoc blocks for errors
schlessera Apr 8, 2021
9b66f33
Annotate exception that is thrown on URL parse failure.
schlessera Apr 8, 2021
3581e1c
Revert "Use substr_compare() in usesAmpCacheUrl()"
westonruter Apr 8, 2021
b2a8478
Merge branch 'main' of https://github.com/ampproject/amp-toolbox-php …
westonruter Apr 9, 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
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,7 @@ Note that this only lets you check whether an error "category" popped up. It can
| [`AmpRuntimeCss`](https://github.com/ampproject/amp-toolbox-php/blob/main/src/Optimizer/Transformer/AmpRuntimeCss.php) | Transformer adding `https://cdn.ampproject.org/v0.css` if server-side-rendering is applied (known by the presence of the `<style amp-runtime>` tag). AMP runtime css (`v0.css`) will always be inlined as it'll get automatically updated to the latest version once the AMP runtime has loaded. |
| [`PreloadHeroImage`](https://github.com/ampproject/amp-toolbox-php/blob/main/src/Optimizer/Transformer/PreloadHeroImage.php) | Transformer that optimizes image rendering times for hero images by adding preload and serverside-rendered `<img>` tags when possible. Viable hero images are `<amp-img>` tags, `<amp-video>` tags with a `poster` attribute as well as `<amp-iframe>` and `<amnp-video-iframe>` tags with a `placeholder` attribute. The first viable image that is encountered is used by default, but this behavior can be overridden by adding the `data-hero` attribute to a maximum of two images. The preloads only work work images that don't use `srcset`, as that is not supported as a preload in most browsers. The serverside-rendered image will not be created for `<amp-video>` tags. |
| [`ReorderHead`](https://github.com/ampproject/amp-toolbox-php/blob/main/src/Optimizer/Transformer/ReorderHead.php) | Transformer applying the head reordering transformations to the HTML input. `ReorderHead` reorders the children of `<head>`. Specifically, it orders the `<head>` like so:<br>(0) `<meta charset>` tag<br>(1) `<style amp-runtime>` (inserted by `AmpRuntimeCss`)<br>(2) remaining `<meta>` tags (those other than `<meta charset>`)<br>(3) AMP runtime `.js` `<script>` tag<br>(4) AMP viewer runtime `.js` `<script>`<br>(5) `<script>` tags that are render delaying<br>(6) `<script>` tags for remaining extensions<br>(7) `<link>` tag for favicons<br>(8) `<link>` tag for resource hints<br>(9) `<link rel=stylesheet>` tags before `<style amp-custom>`<br>(10) `<style amp-custom>`<br>(11) any other tags allowed in `<head>`<br>(12) AMP boilerplate (first `<style>` boilerplate, then `<noscript>`) |
| [`RewriteAmpUrls`](https://github.com/ampproject/amp-toolbox-php/blob/main/src/Optimizer/Transformer/RewriteAmpUrls.php) | Transformer that rewrites AMP runtime URLs to decide what version of the runtime to use. This allows you to do such things as switching to the LTS version or disabling ES modules.|
| [`ServerSideRendering`](https://github.com/ampproject/amp-toolbox-php/blob/main/src/Optimizer/Transformer/ServerSideRendering.php) | Transformer applying the server-side rendering transformations to the HTML input. This does immediately on the server what would normally be done on the client _after_ the runtime was downloaded and executed to process the DOM. As such, it allows for the removal of the boilerplate CSS that _hides_ the page while it has not yet been processed on the client, drastically improving time it takes for the First Contentful Paint (FCP).|
| [`TransformedIdentifier`](https://github.com/ampproject/amp-toolbox-php/blob/main/src/Optimizer/Transformer/TransformedIdentifier.php) | Transformer applying the transformed identifier transformations to the HTML input. This is what marks an AMP document as "already optimized", so that the AMP runtime does not need to process it anymore. |

Expand Down
2 changes: 1 addition & 1 deletion bin/sync-amp-toolbox-test-suite.php
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ function ensureDirExists($directory)
for ($index = 0; $index < $zip->numFiles; $index++) {
$archivedPath = $zip->statIndex($index)['name'];

if (substr($archivedPath, -5) !== '.html') {
if (substr($archivedPath, -5) !== '.html' && substr($archivedPath, -11) !== 'config.json') {
continue;
}

Expand Down
1 change: 1 addition & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
"require": {
"php": "^5.6 || ^7.0 || ^8.0",
"ext-dom": "*",
"ext-filter": "*",
"ext-iconv": "*",
"ext-json": "*",
"ext-libxml": "*"
Expand Down
3 changes: 3 additions & 0 deletions src/Amp.php
Original file line number Diff line number Diff line change
Expand Up @@ -150,8 +150,11 @@ public static function isRuntimeScript(DOMNode $node)
}

if (
// @TODO Compare performance against single regex.
substr($src, -6) !== '/v0.js'
&& substr($src, -7) !== '/v0.mjs'
&& substr($src, -14) !== '/amp4ads-v0.js'
&& substr($src, -15) !== '/amp4ads-v0.mjs'
) {
return false;
}
Expand Down
28 changes: 17 additions & 11 deletions src/Attribute.php
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ interface Attribute
const CHARSET = 'charset';
const CLASS_ = 'class'; // Underscore needed because 'class' is a PHP keyword.
const CONTENT = 'content';
const CROSSORIGIN = 'crossorigin';
const CUSTOM_ELEMENT = 'custom-element';
const CUSTOM_TEMPLATE = 'custom-template';
const DECODING = 'decoding';
Expand All @@ -59,6 +60,7 @@ interface Attribute
const MEDIA = 'media';
const NAME = 'name';
const NOLOADING = 'noloading';
const NOMODULE = 'nomodule';
const OBJECT_FIT = 'object-fit';
const OBJECT_POSITION = 'object-position';
const ON = 'on';
Expand Down Expand Up @@ -89,21 +91,25 @@ interface Attribute
const TYPE_HTML = 'text/html';
const TYPE_JSON = 'application/json';
const TYPE_LD_JSON = 'application/ld+json';
const TYPE_MODULE = 'module';
const TYPE_TEXT_PLAIN = 'text/plain';

const REL_AMPHTML = 'amphtml';
const REL_CANONICAL = 'canonical';
const REL_DNS_PREFETCH = 'dns-prefetch';
const REL_ICON = 'icon';
const REL_NOAMPHTML = 'noamphtml';
const REL_NOFOLLOW = 'nofollow';
const REL_PRECONNECT = 'preconnect';
const REL_PREFETCH = 'prefetch';
const REL_PRELOAD = 'preload';
const REL_PRERENDER = 'prerender';
const REL_STYLESHEET = 'stylesheet';
const REL_AMPHTML = 'amphtml';
const REL_CANONICAL = 'canonical';
const REL_DNS_PREFETCH = 'dns-prefetch';
const REL_ICON = 'icon';
const REL_MODULEPRELOAD = 'modulepreload';
const REL_NOAMPHTML = 'noamphtml';
const REL_NOFOLLOW = 'nofollow';
const REL_PRECONNECT = 'preconnect';
const REL_PREFETCH = 'prefetch';
const REL_PRELOAD = 'preload';
const REL_PRERENDER = 'prerender';
const REL_STYLESHEET = 'stylesheet';

const DATA_AMP_STORY_PLAYER_POSTER_IMG = 'data-amp-story-player-poster-img';
const DATA_HERO = 'data-hero';
const DATA_HERO_CANDIDATE = 'data-hero-candidate';

const CROSSORIGIN_ANONYMOUS = 'anonymous';
}
33 changes: 30 additions & 3 deletions src/Dom/Document.php
Original file line number Diff line number Diff line change
Expand Up @@ -346,6 +346,7 @@ public function __construct($version = '', $encoding = null)
$this->originalEncoding = (string)$encoding ?: Encoding::UNKNOWN;
parent::__construct($version ?: '1.0', Encoding::AMP);
$this->registerNodeClass(DOMElement::class, Element::class);
$this->options = Option::DEFAULTS;
}

/**
Expand Down Expand Up @@ -500,7 +501,7 @@ public function loadHTMLFragment($source, $options = [])
$options = [Option::LIBXML_FLAGS => $options];
}

$this->options = array_merge(Option::DEFAULTS, $options);
$this->options = array_merge($this->options, $options);

$this->reset();

Expand Down Expand Up @@ -2022,7 +2023,7 @@ public function __get($name)
}

// Mimic regular PHP behavior for missing notices.
trigger_error(self::PROPERTY_GETTER_ERROR_MESSAGE . $name, E_USER_NOTICE); // phpcs:ignore WordPress.PHP.DevelopmentFunctions,WordPress.Security.EscapeOutput,Generic.Files.LineLength.TooLong
trigger_error(self::PROPERTY_GETTER_ERROR_MESSAGE . $name, E_USER_NOTICE);
return null;
}

Expand Down Expand Up @@ -2059,6 +2060,32 @@ public function createElement($name, $value = null)
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 = null)
{
$element = parent::createElement($name, $value);

if (!$element instanceof Element) {
return false;
}

$element->addAttributes($attributes);

return $element;
}

/**
* Check whether the CSS maximum byte count is enforced.
*
Expand All @@ -2074,7 +2101,7 @@ public function isCssMaxByteCountEnforced()
*
* @param int $maxByteCount Maximum number of bytes to limit the CSS to. A negative number disables the limit.
*/
public function enforceCssMaxByteCount($maxByteCount = AMP::MAX_CSS_BYTE_COUNT)
public function enforceCssMaxByteCount($maxByteCount = Amp::MAX_CSS_BYTE_COUNT)
{
$this->cssMaxByteCountEnforced = $maxByteCount;
}
Expand Down
184 changes: 180 additions & 4 deletions src/Dom/Element.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
use AmpProject\Optimizer\CssRule;
use DOMAttr;
use DOMElement;
use DOMException;

/**
* Class AmpProject\Dom\Element.
Expand All @@ -19,6 +20,20 @@
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.
*
Expand Down Expand Up @@ -77,6 +92,170 @@ public function setAttribute($name, $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 addAttributes($attributes)
{
foreach ($attributes as $name => $value) {
try {
$this->setAttribute($name, $value);
} catch (DOMException $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.
*
Expand All @@ -95,10 +274,7 @@ public function __get($name)
}

// Mimic regular PHP behavior for missing notices.
trigger_error(
self::PROPERTY_GETTER_ERROR_MESSAGE . $name,
E_USER_NOTICE
); // phpcs:ignore WordPress.PHP.DevelopmentFunctions,WordPress.Security.EscapeOutput,Generic.Files.LineLength.TooLong
trigger_error(self::PROPERTY_GETTER_ERROR_MESSAGE . $name, E_USER_NOTICE);

return null;
}
Expand Down
2 changes: 1 addition & 1 deletion src/Exception/FailedToGetCachedResponse.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ final class FailedToGetCachedResponse extends RuntimeException implements Failed
{

/**
* Instantiate a FailedToGetCachedResponseData exception for a URL if the cached response data could not be
* 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.
Expand Down
Loading