Skip to content

Commit

Permalink
Facilitate using AMP components outside of AMP documents (dirty AMP)
Browse files Browse the repository at this point in the history
* Add AMP scripts the WordPress way: register all AMP component scripts as defined in spec via WP_Scripts via new function amp_register_default_scripts(), allowing simple enqueueing via wp_enqueue_script( 'amp-list' ), for example.
* Also register the AMP Shadow API as a default script.
* Use wp_print_scripts() with filtering script_loader_tag to output scripts with necessary attributes.
* Update whitelist sanitizer to allow, validate, and sanitize AMP scripts.
* Eliminate needless amp_component_scripts filter.
  • Loading branch information
westonruter committed Mar 12, 2018
1 parent e2c5b19 commit 51664cf
Show file tree
Hide file tree
Showing 23 changed files with 285 additions and 158 deletions.
9 changes: 9 additions & 0 deletions amp.php
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,15 @@ function amp_deactivate() {
flush_rewrite_rules();
}

/*
* Register AMP scripts regardless of whether AMP is enabled or it is the AMP endpoint
* for the sake of being able to use AMP components on non-AMP documents ("dirty AMP").
*/
add_action( 'wp_default_scripts', 'amp_register_default_scripts' );

// Ensure async and custom-element/custom-template attributes are present on script tags.
add_filter( 'script_loader_tag', 'amp_filter_script_loader_tag', PHP_INT_MAX, 2 );

/**
* Set up AMP.
*
Expand Down
120 changes: 120 additions & 0 deletions includes/amp-helper-functions.php
Original file line number Diff line number Diff line change
Expand Up @@ -167,13 +167,133 @@ function amp_get_asset_url( $file ) {
*
* @since 0.7
* @link https://www.ampproject.org/docs/reference/spec#boilerplate
*
* @return string Boilerplate code.
*/
function amp_get_boilerplate_code() {
return '<style amp-boilerplate>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}}</style>'
. '<noscript><style amp-boilerplate>body{-webkit-animation:none;-moz-animation:none;-ms-animation:none;animation:none}</style></noscript>';
}

/**
* Register default scripts for AMP components.
*
* @param WP_Scripts $wp_scripts Scripts.
*/
function amp_register_default_scripts( $wp_scripts ) {

// AMP Runtime.
$handle = 'amp-runtime';
$wp_scripts->add(
$handle,
'https://cdn.ampproject.org/v0.js',
array(),
null
);
$wp_scripts->add_data( $handle, 'amp_script_attributes', array(
'async' => true,
) );

// Shadow AMP API.
$handle = 'amp-shadow';
$wp_scripts->add(
$handle,
'https://cdn.ampproject.org/shadow-v0.js',
array(),
null
);
$wp_scripts->add_data( $handle, 'amp_script_attributes', array(
'async' => true,
) );

// Get all AMP components as defined in the spec.
$extensions = array();
foreach ( AMP_Allowed_Tags_Generated::get_allowed_tags() as $allowed_tag ) {
foreach ( $allowed_tag as $rule_spec ) {
if ( ! isset( $rule_spec[ AMP_Rule_Spec::TAG_SPEC ] ) ) {
continue;
}
$tag_spec = $rule_spec[ AMP_Rule_Spec::TAG_SPEC ];
if ( ! empty( $tag_spec['also_requires_tag_warning'] ) ) {
$extensions[] = strtok( $tag_spec['also_requires_tag_warning'][0], ' ' );
}
if ( ! empty( $tag_spec['requires_extension'] ) ) {
$extensions = array_merge( $extensions, $tag_spec['requires_extension'] );
}
}
}

/**
* List of components that are custom elements.
*
* Per the spec, "Most extensions are custom-elements." In fact, there is only one custom template.
*
* @link https://github.com/ampproject/amphtml/blob/cd685d4e62153557519553ffa2183aedf8c93d62/validator/validator.proto#L326-L328
* @link https://github.com/ampproject/amphtml/blob/cd685d4e62153557519553ffa2183aedf8c93d62/extensions/amp-mustache/validator-amp-mustache.protoascii#L27
*/
$custom_templates = array( 'amp-mustache' );

foreach ( $extensions as $extension ) {
$src = sprintf(
'https://cdn.ampproject.org/v0/%s-%s.js',
$extension,
'latest'
);

$wp_scripts->add(
$extension,
$src,
array( 'amp-runtime' ),
null
);
$attributes = array(
'async' => true,
);
if ( in_array( $extension, $custom_templates, true ) ) {
$attributes['custom-template'] = $extension;
} else {
$attributes['custom-element'] = $extension;
}
$wp_scripts->add_data( $extension, 'amp_script_attributes', $attributes );
}
}

/**
* Add AMP script attributes to enqueued scripts.
*
* @link https://core.trac.wordpress.org/ticket/12009
* @since 0.7
*
* @param string $tag The script tag.
* @param string $handle The script handle.
* @return string Script loader tag.
*/
function amp_filter_script_loader_tag( $tag, $handle ) {
$attributes = wp_scripts()->get_data( $handle, 'amp_script_attributes' );
if ( ! is_array( $attributes ) ) {
return $tag;
}

// Add each attribute (if it hasn't already been added).
foreach ( $attributes as $key => $value ) {
if ( ! preg_match( ":\s$key(=|>|\s):", $tag ) ) {
if ( true === $value ) {
$attribute_string = sprintf( ' %s', esc_attr( $key ) );
} else {
$attribute_string = sprintf( ' %s="%s"', esc_attr( $key ), esc_attr( $value ) );
}
$tag = preg_replace(
':(?=></script>):',
$attribute_string,
$tag,
1
);
}
}

return $tag;
}

/**
* Retrieve analytics data added in backend.
*
Expand Down
28 changes: 16 additions & 12 deletions includes/amp-post-template-actions.php
Original file line number Diff line number Diff line change
Expand Up @@ -46,23 +46,27 @@ function amp_post_template_add_canonical( $amp_template ) {
/**
* Print scripts.
*
* @see amp_register_default_scripts()
* @see amp_filter_script_loader_tag()
* @param AMP_Post_Template $amp_template Template.
*/
function amp_post_template_add_scripts( $amp_template ) {

// Just in case the runtime has been overridden by amp_post_template_data filter.
wp_scripts()->registered['amp-runtime']->src = $amp_template->get( 'amp_runtime_script' );

// Make sure any filtered extension script URLs get updated in registered scripts before printing.
$scripts = $amp_template->get( 'amp_component_scripts', array() );
foreach ( $scripts as $element => $script ) {
$custom_type = ( 'amp-mustache' === $element ) ? 'template' : 'element';
printf(
'<script custom-%s="%s" src="%s" async></script>', // phpcs:ignore WordPress.WP.EnqueuedResources.NonEnqueuedScript
esc_attr( $custom_type ),
esc_attr( $element ),
esc_url( $script )
);
foreach ( $scripts as $handle => $value ) {
if ( is_string( $value ) && wp_script_is( $handle, 'registered' ) ) {
wp_scripts()->registered[ $handle ]->src = $value;
}
}
printf(
'<script src="%s" async></script>', // phpcs:ignore WordPress.WP.EnqueuedResources.NonEnqueuedScript
esc_url( $amp_template->get( 'amp_runtime_script' ) )
);

wp_print_scripts( array_merge(
array( 'amp-runtime' ),
array_keys( $scripts )
) );
}

/**
Expand Down
115 changes: 33 additions & 82 deletions includes/class-amp-theme-support.php
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,12 @@ public static function finish_init() {
self::register_paired_hooks();
}

// Enqueue AMP runtime.
wp_enqueue_script( 'amp-runtime' );

// Enqueue default styles expected by sanitizer.
wp_enqueue_style( 'amp-default', amp_get_asset_url( 'css/amp-default.css' ) );

self::add_hooks();
self::$sanitizer_classes = amp_get_content_sanitizers();
self::$embed_handlers = self::register_content_embed_handlers();
Expand Down Expand Up @@ -212,9 +218,7 @@ public static function add_hooks() {
* in this case too we should defer to the theme as well to output the meta charset because it is possible the
* install is not on utf-8 and we may need to do a encoding conversion.
*/
add_action( 'wp_head', array( __CLASS__, 'add_amp_component_scripts' ), 10 );
add_action( 'wp_print_styles', array( __CLASS__, 'print_amp_styles' ), 0 ); // Print boilerplate before theme and plugin stylesheets.
add_action( 'wp_enqueue_scripts', array( __CLASS__, 'enqueue_amp_default_styles' ), 9 );
add_action( 'wp_head', 'amp_add_generator_metadata', 20 );
add_action( 'wp_head', 'amp_print_schemaorg_metadata' );

Expand Down Expand Up @@ -566,16 +570,6 @@ public static function filter_paired_template_include( $template ) {
return $template;
}

/**
* Print AMP script and placeholder for others.
*
* @link https://www.ampproject.org/docs/reference/spec#scrpt
*/
public static function add_amp_component_scripts() {
// Replaced after output buffering with all AMP component scripts.
echo self::SCRIPTS_PLACEHOLDER; // phpcs:ignore WordPress.Security.EscapeOutput, WordPress.XSS.EscapeOutput
}

/**
* Get canonical URL for current request.
*
Expand Down Expand Up @@ -748,69 +742,6 @@ public static function print_amp_styles() {
echo "<style amp-custom></style>\n"; // This will by populated by AMP_Style_Sanitizer.
}

/**
* Adds default styles expected by sanitizer.
*/
public static function enqueue_amp_default_styles() {
wp_enqueue_style( 'amp-default', amp_get_asset_url( 'css/amp-default.css' ) );
}

/**
* Determine required AMP scripts.
*
* @param array $amp_scripts Initial scripts.
* @return string Scripts to inject into the HEAD.
*/
public static function get_amp_scripts( $amp_scripts ) {

foreach ( self::$embed_handlers as $embed_handler ) {
$amp_scripts = array_merge(
$amp_scripts,
$embed_handler->get_scripts()
);
}

/**
* List of components that are custom elements.
*
* Per the spec, "Most extensions are custom-elements." In fact, there is only one custom template.
*
* @link https://github.com/ampproject/amphtml/blob/cd685d4e62153557519553ffa2183aedf8c93d62/validator/validator.proto#L326-L328
* @link https://github.com/ampproject/amphtml/blob/cd685d4e62153557519553ffa2183aedf8c93d62/extensions/amp-mustache/validator-amp-mustache.protoascii#L27
*/
$custom_templates = array( 'amp-mustache' );

/**
* Filters AMP component scripts before they are injected onto the output buffer for the response.
*
* Plugins may add their own component scripts which have been rendered but which the plugin doesn't yet
* recognize.
*
* @since 0.7
*
* @param array $amp_scripts AMP Component scripts, mapping component names to component source URLs.
*/
$amp_scripts = apply_filters( 'amp_component_scripts', $amp_scripts );

$scripts = '<script async src="https://cdn.ampproject.org/v0.js"></script>'; // phpcs:ignore WordPress.WP.EnqueuedResources.NonEnqueuedScript
foreach ( $amp_scripts as $amp_script_component => $amp_script_source ) {

$custom_type = 'custom-element';
if ( in_array( $amp_script_component, $custom_templates, true ) ) {
$custom_type = 'custom-template';
}

$scripts .= sprintf(
'<script async %s="%s" src="%s"></script>', // phpcs:ignore WordPress.WP.EnqueuedResources, WordPress.XSS.EscapeOutput.OutputNotEscaped
$custom_type,
$amp_script_component,
$amp_script_source
);
}

return $scripts;
}

/**
* Ensure markup required by AMP <https://www.ampproject.org/docs/reference/spec#required-markup>.
*
Expand Down Expand Up @@ -1032,13 +963,33 @@ public static function prepare_response( $response, $args = array() ) {
$response = "<!DOCTYPE html>\n";
$response .= AMP_DOM_Utils::get_content_from_dom_node( $dom, $dom->documentElement );

// Inject required scripts.
$response = preg_replace(
'#' . preg_quote( self::SCRIPTS_PLACEHOLDER, '#' ) . '#',
self::get_amp_scripts( $assets['scripts'] ),
$response,
1
);
$amp_scripts = $assets['scripts'];
foreach ( self::$embed_handlers as $embed_handler ) {
$amp_scripts = array_merge(
$amp_scripts,
$embed_handler->get_scripts()
);
}

// Allow for embed handlers to override the default extension version by defining a different URL.
foreach ( $amp_scripts as $handle => $value ) {
if ( is_string( $value ) && wp_script_is( $handle, 'registered' ) ) {
wp_scripts()->registered[ $handle ]->src = $value;
}
}

// Print all scripts, some of which may have already been printed and inject into head.
ob_start();
wp_print_scripts( array_keys( $amp_scripts ) );
$script_tags = ob_get_clean();
if ( ! empty( $script_tags ) ) {
$response = preg_replace(
'#(?=</head>)#',
$script_tags,
$response,
1
);
}

return $response;
}
Expand Down
Loading

0 comments on commit 51664cf

Please sign in to comment.