diff --git a/amp.php b/amp.php index 885010d95cc..1a9350de59c 100644 --- a/amp.php +++ b/amp.php @@ -143,6 +143,7 @@ function amp_init() { AMP_Post_Type_Support::add_post_type_support(); add_filter( 'request', 'amp_force_query_var_value' ); add_action( 'admin_init', 'AMP_Options_Manager::register_settings' ); + add_action( 'wp_loaded', 'amp_editor_core_blocks' ); add_action( 'wp_loaded', 'amp_post_meta_box' ); add_action( 'wp_loaded', 'amp_add_options_menu' ); add_action( 'parse_query', 'amp_correct_query_when_is_front_page' ); diff --git a/assets/js/amp-editor-blocks.js b/assets/js/amp-editor-blocks.js new file mode 100644 index 00000000000..2aa59766ff2 --- /dev/null +++ b/assets/js/amp-editor-blocks.js @@ -0,0 +1,448 @@ +/* exported ampEditorBlocks */ +/* eslint no-magic-numbers: [ "error", { "ignore": [ 1, -1, 0 ] } ] */ + +var ampEditorBlocks = ( function() { + var component, __; + + __ = wp.i18n.__; + + component = { + + /** + * Holds data. + */ + data: { + ampLayoutOptions: [ + { + value: 'nodisplay', + label: __( 'No Display' ), + notAvailable: [ + 'core-embed/vimeo', + 'core-embed/dailymotion', + 'core-embed/hulu', + 'core-embed/reddit', + 'core-embed/soundcloud' + ] + }, + { + // Not supported by amp-audio and amp-pixel. + value: 'fixed', + label: __( 'Fixed' ), + notAvailable: [ + 'core-embed/soundcloud' + ] + }, + { + // To ensure your AMP element displays, you must specify a width and height for the containing element. + value: 'responsive', + label: __( 'Responsive' ), + notAvailable: [ + 'core/audio', + 'core-embed/soundcloud' + ] + }, + { + value: 'fixed-height', + label: __( 'Fixed height' ), + notAvailable: [] + }, + { + value: 'fill', + label: __( 'Fill' ), + notAvailable: [ + 'core/audio', + 'core-embed/soundcloud' + ] + }, + { + value: 'flex-item', + label: __( 'Flex Item' ), + notAvailable: [ + 'core/audio', + 'core-embed/soundcloud' + ] + }, + { + // Not supported by video. + value: 'intrinsic', + label: __( 'Intrinsic' ), + notAvailable: [ + 'core/audio', + 'core-embed/youtube', + 'core-embed/facebook', + 'core-embed/instagram', + 'core-embed/vimeo', + 'core-embed/dailymotion', + 'core-embed/hulu', + 'core-embed/reddit', + 'core-embed/soundcloud' + ] + } + ], + defaultWidth: 608, // Max-width in the editor. + defaultHeight: 400, + mediaBlocks: [ + 'core/image', + 'core/video', + 'core/audio' + ] + } + }; + + /** + * Set data, add filters. + */ + component.boot = function boot() { + wp.hooks.addFilter( 'blocks.registerBlockType', 'ampEditorBlocks/addAttributes', component.addAMPAttributes ); + wp.hooks.addFilter( 'blocks.getSaveElement', 'ampEditorBlocks/filterSave', component.filterBlocksSave ); + wp.hooks.addFilter( 'blocks.BlockEdit', 'ampEditorBlocks/filterEdit', component.filterBlocksEdit ); + wp.hooks.addFilter( 'blocks.getSaveContent.extraProps', 'ampEditorBlocks/addExtraAttributes', component.addAMPExtraProps ); + }; + + /** + * Check if layout is available for the block. + * + * @param {string} blockName Block name. + * @param {Object} option Layout option object. + * @return {boolean} If is available. + */ + component.isLayoutAvailable = function isLayoutAvailable( blockName, option ) { + return -1 === option.notAvailable.indexOf( blockName ); + }; + + /** + * Get layout options depending on the block. + * + * @param {string} blockName Block name. + * @return {[*]} Options. + */ + component.getLayoutOptions = function getLayoutOptions( blockName ) { + var layoutOptions = [ + { + value: '', + label: __( 'Default' ) + } + ]; + + _.each( component.data.ampLayoutOptions, function( option ) { + if ( component.isLayoutAvailable( blockName, option ) ) { + layoutOptions.push( { + value: option.value, + label: option.label + } ); + } + } ); + + return layoutOptions; + }; + + /** + * Add extra data-amp-layout attribute to save to DB. + * + * @param {Object} props Properties. + * @param {string} blockType Block type. + * @param {Object} attributes Attributes. + * @return {Object} Props. + */ + component.addAMPExtraProps = function addAMPExtraProps( props, blockType, attributes ) { + var ampAttributes = {}; + if ( ! attributes.ampLayout && ! attributes.ampNoLoading ) { + return props; + } + + if ( attributes.ampLayout ) { + ampAttributes[ 'data-amp-layout' ] = attributes.ampLayout; + } + if ( attributes.ampNoLoading ) { + ampAttributes[ 'data-amp-noloading' ] = attributes.ampNoLoading; + } + + return _.extend( ampAttributes, props ); + }; + + /** + * Add AMP attributes (in this test case just ampLayout) to every core block. + * + * @param {Object} settings Settings. + * @param {string} name Block name. + * @return {Object} Settings. + */ + component.addAMPAttributes = function addAMPAttributes( settings, name ) { + // Gallery settings for shortcode. + if ( 'core/shortcode' === name ) { + if ( ! settings.attributes ) { + settings.attributes = {}; + } + settings.attributes.ampCarousel = { + type: 'boolean' + }; + } + + // Layout settings for embeds and media blocks. + if ( 0 === name.indexOf( 'core-embed' ) || -1 !== component.data.mediaBlocks.indexOf( name ) ) { + if ( ! settings.attributes ) { + settings.attributes = {}; + } + settings.attributes.ampLayout = { + type: 'string' + }; + settings.attributes.ampNoLoading = { + type: 'boolean' + }; + } + return settings; + }; + + /** + * Filters blocks edit function of all blocks. + * + * @param {Function} BlockEdit Edit function. + * @return {Function} Edit function. + */ + component.filterBlocksEdit = function filterBlocksEdit( BlockEdit ) { + var el = wp.element.createElement; + + return function( props ) { + var attributes = props.attributes, + name = props.name, + ampLayout, + inspectorControls; + + ampLayout = attributes.ampLayout; + + if ( 'core/shortcode' === name ) { + inspectorControls = component.setUpShortcodeInspectorControls( props ); + if ( '' === inspectorControls ) { + // Return original. + return [ + el( BlockEdit, _.extend( { + key: 'original' + }, props ) ) + ]; + } + } else if ( -1 !== component.data.mediaBlocks.indexOf( name ) || 0 === name.indexOf( 'core-embed/' ) ) { + inspectorControls = component.setUpInspectorControls( props ); + } + + // Return just inspector controls in case of 'nodisplay'. + if ( ampLayout && 'nodisplay' === ampLayout ) { + return [ + inspectorControls + ]; + } + + return [ + inspectorControls, + el( BlockEdit, _.extend( { + key: 'original' + }, props ) ) + ]; + }; + }; + + /** + * Set width and height in case of image block. + * + * @param {Object} props Props. + * @param {string} layout Layout. + */ + component.setImageBlockLayoutAttributes = function setImageBlockLayoutAttributes( props, layout ) { + var attributes = props.attributes; + switch ( layout ) { + case 'fixed-height': + if ( ! attributes.height ) { + props.setAttributes( { height: component.data.defaultHeight } ); + } + break; + + case 'fixed': + if ( ! attributes.height ) { + props.setAttributes( { height: component.data.defaultHeight } ); + } + if ( ! attributes.width ) { + props.setAttributes( { width: component.data.defaultWidth } ); + } + break; + } + }; + + /** + * Default setup for inspector controls. + * + * @param {Object} props Props. + * @return {Object|Element|*|{$$typeof, type, key, ref, props, _owner}} Inspector Controls. + */ + component.setUpInspectorControls = function setUpInspectorControls( props ) { + var ampLayout = props.attributes.ampLayout, + ampNoLoading = props.attributes.ampNoLoading, + isSelected = props.isSelected, + name = props.name, + el = wp.element.createElement, + InspectorControls = wp.editor.InspectorControls, + SelectControl = wp.components.SelectControl, + ToggleControl = wp.components.ToggleControl, + PanelBody = wp.components.PanelBody, + label = __( 'AMP Layout' ); + + if ( 'core/image' === name ) { + label = __( 'AMP Layout (modifies width/height)' ); + } + + return isSelected && ( + el( InspectorControls, { key: 'inspector' }, + el( PanelBody, { title: __( 'AMP Settings' ) }, + el( SelectControl, { + label: label, + value: ampLayout, + options: component.getLayoutOptions( name ), + onChange: function( value ) { + props.setAttributes( { ampLayout: value } ); + if ( 'core/image' === props.name ) { + component.setImageBlockLayoutAttributes( props, value ); + } + } + } ), + el( ToggleControl, { + label: __( 'AMP loading indicator disabled' ), + checked: ampNoLoading, + onChange: function() { + props.setAttributes( { ampNoLoading: ! ampNoLoading } ); + } + } ) + ) + ) + ); + }; + + /** + * Set up inspector controls for shortcode block. + * Adds ampCarousel attribute in case of gallery shortcode. + * + * @param {Object} props Props. + * @return {*} Inspector controls. + */ + component.setUpShortcodeInspectorControls = function setUpShortcodeInspectorControls( props ) { + var ampCarousel = props.attributes.ampCarousel, + isSelected = props.isSelected, + el = wp.element.createElement, + InspectorControls = wp.blocks.InspectorControls, + ToggleControl = wp.components.ToggleControl, + PanelBody = wp.components.PanelBody, + toggleControl; + + if ( component.isGalleryShortcode( props.attributes ) ) { + toggleControl = el( ToggleControl, { + label: __( 'Display as AMP carousel' ), + checked: ampCarousel, + onChange: function() { + props.setAttributes( { ampCarousel: ! ampCarousel } ); + } + } ); + return isSelected && ( + el( InspectorControls, { key: 'inspector' }, + el( PanelBody, { title: __( 'AMP Settings' ) }, + toggleControl + ) + ) + ); + } + + return ''; + }; + + /** + * Filters blocks' save function. + * + * @param {Object} element Element. + * @param {string} blockType Block type. + * @param {Object} attributes Attributes. + * @return {*} Output element. + */ + component.filterBlocksSave = function filterBlocksSave( element, blockType, attributes ) { + var text; + if ( 'core/shortcode' === blockType.name && component.isGalleryShortcode( attributes ) ) { + if ( attributes.ampCarousel ) { + // If the text contains amp-carousel, lets remove it. + if ( component.hasGalleryShortcodeCarouselAttribute( attributes.text || '' ) ) { + text = component.removeAmpCarouselFromShortcodeAtts( attributes.text ); + + return wp.element.createElement( + wp.element.RawHTML, + {}, + text + ); + } + + // Else lets return original. + return element; + } + + // If the text already contains amp-carousel, return original. + if ( component.hasGalleryShortcodeCarouselAttribute( attributes.text || '' ) ) { + return element; + } + + // Add amp-carousel=false attribut to the shortcode. + text = attributes.text.replace( '[gallery', '[gallery amp-carousel=false' ); + + return wp.element.createElement( + wp.element.RawHTML, + {}, + text + ); + } + return element; + }; + + /** + * Check if AMP NoLoading is set. + * + * @param {Object} attributes Attributes. + * @return {boolean} If is set. + */ + component.hasAmpNoLoadingSet = function hasAmpNoLoadingSet( attributes ) { + return attributes.ampNoLoading && false !== attributes.ampNoLoading; + }; + + /** + * Check if AMP Layout is set. + * + * @param {Object} attributes Attributes. + * @return {boolean} If AMP Layout is set. + */ + component.hasAmpLayoutSet = function hasAmpLayoutSet( attributes ) { + return attributes.ampLayout && attributes.ampLayout.length; + }; + + /** + * Removes amp-carousel=false from attributes. + * + * @param {string} shortcode Shortcode text. + * @return {string} Modified shortcode. + */ + component.removeAmpCarouselFromShortcodeAtts = function removeAmpCarouselFromShortcodeAtts( shortcode ) { + return shortcode.replace( ' amp-carousel=false', '' ); + }; + + /** + * Check if shortcode includes amp-carousel attribute. + * + * @param {string} text Shortcode. + * @return {boolean} If has amp-carousel. + */ + component.hasGalleryShortcodeCarouselAttribute = function galleryShortcodeHasCarouselAttribute( text ) { + return -1 !== text.indexOf( 'amp-carousel=false' ); + }; + + /** + * Check if shortcode is gallery shortcode. + * + * @param {Object} attributes Attributes. + * @return {boolean} If is gallery shortcode. + */ + component.isGalleryShortcode = function isGalleryShortcode( attributes ) { + return attributes.text && -1 !== attributes.text.indexOf( 'gallery' ); + }; + + return component; +}() ); diff --git a/includes/admin/class-amp-editor-blocks.php b/includes/admin/class-amp-editor-blocks.php new file mode 100644 index 00000000000..37977cbd5af --- /dev/null +++ b/includes/admin/class-amp-editor-blocks.php @@ -0,0 +1,53 @@ +init(); } + +/** + * Bootstrap AMP Editor core blocks. + */ +function amp_editor_core_blocks() { + $editor_blocks = new AMP_Editor_Blocks(); + $editor_blocks->init(); +} diff --git a/includes/amp-helper-functions.php b/includes/amp-helper-functions.php index 2cfa173c630..404d1ae7882 100644 --- a/includes/amp-helper-functions.php +++ b/includes/amp-helper-functions.php @@ -484,6 +484,7 @@ function amp_get_content_sanitizers( $post = null ) { 'AMP_Comments_Sanitizer' => array(), 'AMP_Video_Sanitizer' => array(), 'AMP_Audio_Sanitizer' => array(), + 'AMP_Block_Sanitizer' => array(), 'AMP_Playbuzz_Sanitizer' => array(), 'AMP_Iframe_Sanitizer' => array( 'add_placeholder' => true, diff --git a/includes/class-amp-autoloader.php b/includes/class-amp-autoloader.php index c86307f0a77..651952cdfee 100644 --- a/includes/class-amp-autoloader.php +++ b/includes/class-amp-autoloader.php @@ -29,6 +29,7 @@ class AMP_Autoloader { * @var string[] */ private static $_classmap = array( + 'AMP_Editor_Blocks' => 'includes/admin/class-amp-editor-blocks', 'AMP_Theme_Support' => 'includes/class-amp-theme-support', 'AMP_Response_Headers' => 'includes/class-amp-response-headers', 'AMP_Comment_Walker' => 'includes/class-amp-comment-walker', @@ -70,6 +71,7 @@ class AMP_Autoloader { 'AMP_Audio_Sanitizer' => 'includes/sanitizers/class-amp-audio-sanitizer', 'AMP_Base_Sanitizer' => 'includes/sanitizers/class-amp-base-sanitizer', 'AMP_Blacklist_Sanitizer' => 'includes/sanitizers/class-amp-blacklist-sanitizer', + 'AMP_Block_Sanitizer' => 'includes/sanitizers/class-amp-block-sanitizer', 'AMP_Iframe_Sanitizer' => 'includes/sanitizers/class-amp-iframe-sanitizer', 'AMP_Img_Sanitizer' => 'includes/sanitizers/class-amp-img-sanitizer', 'AMP_Comments_Sanitizer' => 'includes/sanitizers/class-amp-comments-sanitizer', diff --git a/includes/embeds/class-amp-gallery-embed.php b/includes/embeds/class-amp-gallery-embed.php index 46d44fd6951..87a80f30bbf 100644 --- a/includes/embeds/class-amp-gallery-embed.php +++ b/includes/embeds/class-amp-gallery-embed.php @@ -16,7 +16,7 @@ class AMP_Gallery_Embed_Handler extends AMP_Base_Embed_Handler { * Register embed. */ public function register_embed() { - add_filter( 'post_gallery', array( $this, 'override_gallery' ), 10, 2 ); + add_filter( 'post_gallery', array( $this, 'maybe_override_gallery' ), 10, 2 ); } /** @@ -119,7 +119,7 @@ public function shortcode( $attr ) { } /** - * Override the output of gallery_shortcode(). + * Override the output of gallery_shortcode() if amp-carousel is not false. * * The 'Gallery' widget also uses this function. * This ensures that it outputs an . @@ -128,7 +128,10 @@ public function shortcode( $attr ) { * @param array $attributes Shortcode attributes. * @return string $html Markup for the gallery. */ - public function override_gallery( $html, $attributes ) { + public function maybe_override_gallery( $html, $attributes ) { + if ( isset( $attributes['amp-carousel'] ) && false === filter_var( $attributes['amp-carousel'], FILTER_VALIDATE_BOOLEAN ) ) { + return $html; + } return $this->shortcode( $attributes ); } diff --git a/includes/options/class-amp-analytics-options-submenu.php b/includes/options/class-amp-analytics-options-submenu.php index 82da7dba0b0..1b515209f91 100644 --- a/includes/options/class-amp-analytics-options-submenu.php +++ b/includes/options/class-amp-analytics-options-submenu.php @@ -16,8 +16,8 @@ class AMP_Analytics_Options_Submenu { public function __construct( $parent_menu_slug ) { $this->parent_menu_slug = $parent_menu_slug; - $this->menu_slug = 'amp-analytics-options'; - $this->menu_page = new AMP_Analytics_Options_Submenu_Page(); + $this->menu_slug = 'amp-analytics-options'; + $this->menu_page = new AMP_Analytics_Options_Submenu_Page(); } public function init() { diff --git a/includes/sanitizers/class-amp-audio-sanitizer.php b/includes/sanitizers/class-amp-audio-sanitizer.php index 2854603e6bf..4c593a2e124 100644 --- a/includes/sanitizers/class-amp-audio-sanitizer.php +++ b/includes/sanitizers/class-amp-audio-sanitizer.php @@ -34,7 +34,9 @@ public function sanitize() { for ( $i = $num_nodes - 1; $i >= 0; $i-- ) { $node = $nodes->item( $i ); + $amp_data = $this->get_data_amp_attributes( $node ); $old_attributes = AMP_DOM_Utils::get_node_attributes_as_assoc_array( $node ); + $old_attributes = $this->filter_data_amp_attributes( $old_attributes, $amp_data ); $new_attributes = $this->filter_attributes( $old_attributes ); @@ -83,6 +85,14 @@ public function sanitize() { if ( 0 === $new_node->childNodes->length && empty( $new_attributes['src'] ) ) { $this->remove_invalid_child( $node ); } else { + + $layout = isset( $new_attributes['layout'] ) ? $new_attributes['layout'] : false; + + // The width has to be unset / auto in case of fixed-height. + if ( 'fixed-height' === $layout ) { + $new_node->setAttribute( 'width', 'auto' ); + } + $node->parentNode->replaceChild( $new_node, $node ); } @@ -133,6 +143,14 @@ private function filter_attributes( $attributes ) { } break; + case 'data-amp-layout': + $out['layout'] = $value; + break; + + case 'data-amp-noloading': + $out['noloading'] = $value; + break; + default: break; } diff --git a/includes/sanitizers/class-amp-base-sanitizer.php b/includes/sanitizers/class-amp-base-sanitizer.php index 9cbabda3cd6..ad13f565107 100644 --- a/includes/sanitizers/class-amp-base-sanitizer.php +++ b/includes/sanitizers/class-amp-base-sanitizer.php @@ -212,9 +212,12 @@ public function sanitize_dimension( $value, $dimension ) { * @type string $class * @type string $layout * } - * @return string[] + * @return array Attributes. */ public function set_layout( $attributes ) { + if ( isset( $attributes['layout'] ) && ( 'fill' === $attributes['layout'] || 'flex-item' !== $attributes['layout'] ) ) { + return $attributes; + } if ( empty( $attributes['height'] ) ) { unset( $attributes['width'] ); $attributes['height'] = self::FALLBACK_HEIGHT; @@ -342,4 +345,82 @@ public function remove_invalid_attribute( $element, $attribute, $args = array() } } } + + /** + * Get data-amp-* values from the parent node 'figure' added by editor block. + * + * @param DOMNode $node Base node. + * @return array AMP data array. + */ + public function get_data_amp_attributes( $node ) { + $attributes = array(); + + // Editor blocks add 'figure' as the parent node for images. If this node has data-amp-layout then we should add this as the layout attribute. + $parent_node = $node->parentNode; + if ( 'figure' === $parent_node->tagName ) { + $parent_attributes = AMP_DOM_Utils::get_node_attributes_as_assoc_array( $parent_node ); + if ( isset( $parent_attributes['data-amp-layout'] ) ) { + $attributes['layout'] = $parent_attributes['data-amp-layout']; + } + if ( isset( $parent_attributes['data-amp-noloading'] ) && true === filter_var( $parent_attributes['data-amp-noloading'], FILTER_VALIDATE_BOOLEAN ) ) { + $attributes['noloading'] = $parent_attributes['data-amp-noloading']; + } + } + + return $attributes; + } + + /** + * Set AMP attributes. + * + * @param array $attributes Array of attributes. + * @param array $amp_data Array of AMP attributes. + * @return array Updated attributes. + */ + public function filter_data_amp_attributes( $attributes, $amp_data ) { + if ( isset( $amp_data['layout'] ) ) { + $attributes['data-amp-layout'] = $amp_data['layout']; + } + if ( isset( $amp_data['noloading'] ) ) { + $attributes['data-amp-noloading'] = ''; + } + return $attributes; + } + + /** + * Set attributes to node's parent element according to layout. + * + * @param DOMNode $node Node. + * @param array $new_attributes Attributes array. + * @param string $layout Layout. + * @return array New attributes. + */ + public function filter_attachment_layout_attributes( $node, $new_attributes, $layout ) { + + // The width has to be unset / auto in case of fixed-height. + if ( 'fixed-height' === $layout ) { + if ( ! isset( $new_attributes['height'] ) ) { + $new_attributes['height'] = self::FALLBACK_HEIGHT; + } + $new_attributes['width'] = 'auto'; + $node->parentNode->setAttribute( 'style', 'height: ' . $new_attributes['height'] . 'px; width: auto;' ); + + // The parent element should have width/height set and position set in case of 'fill'. + } elseif ( 'fill' === $layout ) { + if ( ! isset( $new_attributes['height'] ) ) { + $new_attributes['height'] = self::FALLBACK_HEIGHT; + } + $node->parentNode->setAttribute( 'style', 'position:relative; width: 100%; height: ' . $new_attributes['height'] . 'px;' ); + unset( $new_attributes['width'] ); + unset( $new_attributes['height'] ); + } elseif ( 'responsive' === $layout ) { + $node->parentNode->setAttribute( 'style', 'position:relative; width: 100%; height: auto' ); + } elseif ( 'fixed' === $layout ) { + if ( ! isset( $new_attributes['height'] ) ) { + $new_attributes['height'] = self::FALLBACK_HEIGHT; + } + } + + return $new_attributes; + } } diff --git a/includes/sanitizers/class-amp-block-sanitizer.php b/includes/sanitizers/class-amp-block-sanitizer.php new file mode 100644 index 00000000000..a70f876fdb7 --- /dev/null +++ b/includes/sanitizers/class-amp-block-sanitizer.php @@ -0,0 +1,129 @@ + element where necessary. + * + * @since 0.2 + */ + public function sanitize() { + $nodes = $this->dom->getElementsByTagName( self::$tag ); + $num_nodes = $nodes->length; + if ( 0 === $num_nodes ) { + return; + } + + for ( $i = $num_nodes - 1; $i >= 0; $i-- ) { + $node = $nodes->item( $i ); + + // We're looking for
elements that have one child node only. + if ( 1 !== count( $node->childNodes ) ) { + continue; + } + + $attributes = AMP_DOM_Utils::get_node_attributes_as_assoc_array( $node ); + + // We are only looking for
elements which have wp-block-embed as class. + if ( ! isset( $attributes['class'] ) || false === strpos( $attributes['class'], 'wp-block-embed' ) ) { + continue; + } + + // We are looking for
elements with layout attribute only. + if ( ! isset( $attributes['data-amp-layout'] ) && ! isset( $attributes['data-amp-noloading'] ) ) { + continue; + } + + $amp_el_found = false; + + foreach ( $node->childNodes as $child_node ) { + + // We are looking for child elements which start with 'amp-'. + if ( 0 !== strpos( $child_node->tagName, 'amp-' ) ) { + continue; + } + $amp_el_found = true; + + $this->set_attributes( $child_node, $node, $attributes ); + } + + if ( false === $amp_el_found ) { + continue; + } + $this->did_convert_elements = true; + } + } + + /** + * Sets necessary attributes to both parent and AMP element node. + * + * @param DOMNode $node AMP element node. + * @param DOMNode $parent_node
node. + * @param array $attributes Current attributes of the AMP element. + */ + protected function set_attributes( $node, $parent_node, $attributes ) { + + if ( isset( $attributes['data-amp-layout'] ) ) { + $node->setAttribute( 'layout', $attributes['data-amp-layout'] ); + } + if ( isset( $attributes['data-amp-noloading'] ) && true === filter_var( $attributes['data-amp-noloading'], FILTER_VALIDATE_BOOLEAN ) ) { + $node->setAttribute( 'noloading', '' ); + } + + $layout = $node->getAttribute( 'layout' ); + + // The width has to be unset / auto in case of fixed-height. + if ( 'fixed-height' === $layout ) { + if ( ! isset( $attributes['height'] ) ) { + $node->setAttribute( 'height', self::FALLBACK_HEIGHT ); + } + $node->setAttribute( 'width', 'auto' ); + + $height = $node->getAttribute( 'height' ); + if ( is_numeric( $height ) ) { + $height .= 'px'; + } + $parent_node->setAttribute( 'style', "height: $height; width: auto;" ); + + // The parent element should have width/height set and position set in case of 'fill'. + } elseif ( 'fill' === $layout ) { + if ( ! isset( $attributes['height'] ) ) { + $attributes['height'] = self::FALLBACK_HEIGHT; + } + $parent_node->setAttribute( 'style', 'position:relative; width: 100%; height: ' . $attributes['height'] . 'px;' ); + $node->removeAttribute( 'width' ); + $node->removeAttribute( 'height' ); + } elseif ( 'responsive' === $layout ) { + $parent_node->setAttribute( 'style', 'position:relative; width: 100%; height: auto' ); + } elseif ( 'fixed' === $layout ) { + if ( ! isset( $attributes['height'] ) ) { + $node->setAttribute( 'height', self::FALLBACK_HEIGHT ); + } + } + + // Set the fallback layout in case needed. + $attributes = AMP_DOM_Utils::get_node_attributes_as_assoc_array( $node ); + $attributes = $this->set_layout( $attributes ); + if ( $layout !== $attributes['layout'] ) { + $node->setAttribute( 'layout', $attributes['layout'] ); + } + } +} diff --git a/includes/sanitizers/class-amp-img-sanitizer.php b/includes/sanitizers/class-amp-img-sanitizer.php index e081887fe83..5d1bab0f911 100644 --- a/includes/sanitizers/class-amp-img-sanitizer.php +++ b/includes/sanitizers/class-amp-img-sanitizer.php @@ -133,6 +133,10 @@ private function filter_attributes( $attributes ) { $out['layout'] = $value; break; + case 'data-amp-noloading': + $out['noloading'] = $value; + break; + default: break; } @@ -222,12 +226,20 @@ private function adjust_and_replace_nodes_in_array_map( $node_lists ) { * @param DOMNode $node The DOMNode to adjust and replace. */ private function adjust_and_replace_node( $node ) { + + $amp_data = $this->get_data_amp_attributes( $node ); $old_attributes = AMP_DOM_Utils::get_node_attributes_as_assoc_array( $node ); + $old_attributes = $this->filter_data_amp_attributes( $old_attributes, $amp_data ); + $new_attributes = $this->filter_attributes( $old_attributes ); + $layout = isset( $amp_data['layout'] ) ? $amp_data['layout'] : false; + $new_attributes = $this->filter_attachment_layout_attributes( $node, $new_attributes, $layout ); + $this->add_or_append_attribute( $new_attributes, 'class', 'amp-wp-enforced-sizes' ); if ( empty( $new_attributes['layout'] ) && ! empty( $new_attributes['height'] ) && ! empty( $new_attributes['width'] ) ) { $new_attributes['layout'] = 'intrinsic'; } + if ( $this->is_gif_url( $new_attributes['src'] ) ) { $this->did_convert_elements = true; diff --git a/includes/sanitizers/class-amp-video-sanitizer.php b/includes/sanitizers/class-amp-video-sanitizer.php index 0512da17754..fdea8947bbd 100644 --- a/includes/sanitizers/class-amp-video-sanitizer.php +++ b/includes/sanitizers/class-amp-video-sanitizer.php @@ -46,9 +46,14 @@ public function sanitize() { for ( $i = $num_nodes - 1; $i >= 0; $i-- ) { $node = $nodes->item( $i ); + $amp_data = $this->get_data_amp_attributes( $node ); $old_attributes = AMP_DOM_Utils::get_node_attributes_as_assoc_array( $node ); + $old_attributes = $this->filter_data_amp_attributes( $old_attributes, $amp_data ); $new_attributes = $this->filter_attributes( $old_attributes ); + $layout = isset( $amp_data['layout'] ) ? $amp_data['layout'] : false; + $new_attributes = $this->filter_video_dimensions( $new_attributes ); + $new_attributes = $this->filter_attachment_layout_attributes( $node, $new_attributes, $layout ); $new_attributes = $this->set_layout( $new_attributes ); if ( empty( $new_attributes['layout'] ) && ! empty( $new_attributes['width'] ) && ! empty( $new_attributes['height'] ) ) { $new_attributes['layout'] = 'responsive'; @@ -104,6 +109,40 @@ public function sanitize() { } } + /** + * Filter video dimensions, try to get width and height from original file if missing. + * + * @param array $new_attributes Attributes. + * @return array Modified attributes. + */ + protected function filter_video_dimensions( $new_attributes ) { + if ( empty( $new_attributes['width'] ) || empty( $new_attributes['height'] ) ) { + + // Get the width and height from the file. + $ext = pathinfo( $new_attributes['src'], PATHINFO_EXTENSION ); + $name = wp_basename( $new_attributes['src'], ".$ext" ); + $args = array( + 'name' => $name, + 'post_type' => 'attachment', + 'post_status' => 'inherit', + 'numberposts' => 1, + ); + + $attachment = get_posts( $args ); + + if ( ! empty( $attachment ) ) { + $meta_data = wp_get_attachment_metadata( $attachment[0]->ID ); + if ( empty( $new_attributes['width'] ) && ! empty( $meta_data['width'] ) ) { + $new_attributes['width'] = $meta_data['width']; + } + if ( empty( $new_attributes['height'] ) && ! empty( $meta_data['height'] ) ) { + $new_attributes['height'] = $meta_data['height']; + } + } + } + return $new_attributes; + } + /** * "Filter" HTML attributes for elements. * @@ -153,6 +192,14 @@ private function filter_attributes( $attributes ) { } break; + case 'data-amp-layout': + $out['layout'] = $value; + break; + + case 'data-amp-noloading': + $out['noloading'] = $value; + break; + default: break; } diff --git a/tests/amp-tag-and-attribute-sanitizer-private-methods-tests.php b/tests/amp-tag-and-attribute-sanitizer-private-methods-tests.php index d485a1bf969..dd6a4f456df 100644 --- a/tests/amp-tag-and-attribute-sanitizer-private-methods-tests.php +++ b/tests/amp-tag-and-attribute-sanitizer-private-methods-tests.php @@ -1,5 +1,7 @@ ', ), + 'audio_with_layout_from_editor_fixed_height' => array( + '
', + '
', + ), + 'multiple_same_audio' => array( '