diff --git a/.dev-lib b/.dev-lib
index 663a5ce0043..629b7a036ce 100644
--- a/.dev-lib
+++ b/.dev-lib
@@ -7,4 +7,5 @@ SKIP_ECHO_PATHS_SCOPE=1
function after_wp_install {
echo "Installing REST API..."
svn export -q https://plugins.svn.wordpress.org/jetpack/trunk/ "$WP_CORE_DIR/src/wp-content/plugins/jetpack"
+ svn export -q https://plugins.svn.wordpress.org/gutenberg/trunk/ "$WP_CORE_DIR/src/wp-content/plugins/gutenberg"
}
diff --git a/.eslintrc b/.eslintrc
deleted file mode 120000
index 68832c00e46..00000000000
--- a/.eslintrc
+++ /dev/null
@@ -1 +0,0 @@
-dev-lib/.eslintrc
\ No newline at end of file
diff --git a/.eslintrc b/.eslintrc
new file mode 100644
index 00000000000..cd4e73e7b35
--- /dev/null
+++ b/.eslintrc
@@ -0,0 +1,187 @@
+{
+ "root": true,
+ "extends": [
+ "wordpress"
+ ],
+ "env": {
+ "browser": true
+ },
+ "globals": {
+ "wp": true,
+ "window": true,
+ "document": true
+ },
+ "settings": {
+ "react": {
+ "pragma": "wp"
+ }
+ },
+ "rules": {
+ "no-magic-numbers": [2, { "ignoreArrayIndexes": true, "ignore": [ -1, 0, 1 ] }],
+ "array-bracket-spacing": [
+ "error",
+ "always"
+ ],
+ "brace-style": [
+ "error",
+ "1tbs"
+ ],
+ "camelcase": [
+ "error",
+ {
+ "properties": "never"
+ }
+ ],
+ "comma-dangle": [
+ "error",
+ "never"
+ ],
+ "comma-spacing": "error",
+ "comma-style": "error",
+ "computed-property-spacing": [
+ "error",
+ "always"
+ ],
+ "dot-notation": "error",
+ "eol-last": "error",
+ "eqeqeq": "error",
+ "func-call-spacing": "error",
+ "indent": [
+ "error",
+ "tab",
+ {
+ "SwitchCase": 1
+ }
+ ],
+ "key-spacing": "error",
+ "keyword-spacing": "error",
+ "lines-around-comment": "off",
+ "no-alert": "error",
+ "no-bitwise": "error",
+ "no-caller": "error",
+ "no-console": "error",
+ "no-debugger": "error",
+ "no-dupe-args": "error",
+ "no-dupe-keys": "error",
+ "no-duplicate-case": "error",
+ "no-else-return": "error",
+ "no-eval": "error",
+ "no-extra-semi": "error",
+ "no-fallthrough": "error",
+ "no-lonely-if": "error",
+ "no-mixed-operators": "error",
+ "no-mixed-spaces-and-tabs": "error",
+ "no-multiple-empty-lines": [
+ "error",
+ {
+ "max": 1
+ }
+ ],
+ "no-multi-spaces": "error",
+ "no-multi-str": "off",
+ "no-negated-in-lhs": "error",
+ "no-nested-ternary": "error",
+ "no-redeclare": "error",
+ "no-restricted-syntax": [
+ "error",
+ {
+ "selector": "CallExpression[callee.name=/^__|_n|_x$/]:not([arguments.0.type=/^Literal|BinaryExpression$/])",
+ "message": "Translate function arguments must be string literals."
+ },
+ {
+ "selector": "CallExpression[callee.name=/^_n|_x$/]:not([arguments.1.type=/^Literal|BinaryExpression$/])",
+ "message": "Translate function arguments must be string literals."
+ },
+ {
+ "selector": "CallExpression[callee.name=_nx]:not([arguments.2.type=/^Literal|BinaryExpression$/])",
+ "message": "Translate function arguments must be string literals."
+ }
+ ],
+ "no-shadow": "error",
+ "no-undef": "error",
+ "no-undef-init": "error",
+ "no-unreachable": "error",
+ "no-unsafe-negation": "error",
+ "no-unused-expressions": "error",
+ "no-unused-vars": "error",
+ "no-useless-return": "error",
+ "no-whitespace-before-property": "error",
+ "object-curly-spacing": [
+ "error",
+ "always"
+ ],
+ "padded-blocks": [
+ "error",
+ "never"
+ ],
+ "quote-props": [
+ "error",
+ "as-needed"
+ ],
+ "react/display-name": "off",
+ "react/no-children-prop": "off",
+ "react/prop-types": "off",
+ "semi": "error",
+ "semi-spacing": "error",
+ "space-before-blocks": [
+ "error",
+ "always"
+ ],
+ "space-before-function-paren": [
+ "error",
+ {
+ "anonymous": "never",
+ "named": "never",
+ "asyncArrow": "always"
+ }
+ ],
+ "space-in-parens": [
+ "error",
+ "always"
+ ],
+ "space-infix-ops": [
+ "error",
+ {
+ "int32Hint": false
+ }
+ ],
+ "space-unary-ops": [
+ "error",
+ {
+ "overrides": {
+ "!": true,
+ "yield": true
+ }
+ }
+ ],
+ "valid-jsdoc": [
+ "error",
+ {
+ "prefer": {
+ "arg": "param",
+ "argument": "param",
+ "extends": "augments",
+ "returns": "return"
+ },
+ "preferType": {
+ "array": "Array",
+ "bool": "boolean",
+ "Boolean": "boolean",
+ "float": "number",
+ "Float": "number",
+ "int": "number",
+ "integer": "number",
+ "Integer": "number",
+ "Number": "number",
+ "object": "Object",
+ "String": "string",
+ "Void": "void"
+ },
+ "requireParamDescription": false,
+ "requireReturn": false
+ }
+ ],
+ "valid-typeof": "error",
+ "yoda": "off"
+ }
+}
diff --git a/.jscsrc b/.jscsrc
deleted file mode 120000
index 99b5cea7128..00000000000
--- a/.jscsrc
+++ /dev/null
@@ -1 +0,0 @@
-dev-lib/.jscsrc
\ No newline at end of file
diff --git a/Gruntfile.js b/Gruntfile.js
index 09b3eec6eb9..d591117d092 100644
--- a/Gruntfile.js
+++ b/Gruntfile.js
@@ -1,5 +1,6 @@
/* eslint-env node */
/* jshint node:true */
+/* eslint-disable camelcase */
module.exports = function( grunt ) {
'use strict';
diff --git a/assets/css/amp-post-meta-box.css b/assets/css/amp-post-meta-box.css
index d6164792cdb..d9c75248206 100644
--- a/assets/css/amp-post-meta-box.css
+++ b/assets/css/amp-post-meta-box.css
@@ -64,3 +64,16 @@
line-height: 280%;
}
}
+
+.amp-block-validation-errors {
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif;
+ font-size: 13px;
+ line-height: 1.5;
+}
+.amp-block-validation-errors .amp-block-validation-errors__summary {
+ margin: 0.5em 0;
+ padding: 2px;
+}
+.amp-block-validation-errors .amp-block-validation-errors__list {
+ padding-left: 2.5em;
+}
diff --git a/assets/js/amp-block-validation.js b/assets/js/amp-block-validation.js
new file mode 100644
index 00000000000..07591f98176
--- /dev/null
+++ b/assets/js/amp-block-validation.js
@@ -0,0 +1,365 @@
+/**
+ * Validates blocks for AMP compatibility.
+ *
+ * This uses the REST API response from saving a page to find validation errors.
+ * If one exists for a block, it display it inline with a Notice component.
+ */
+
+/* exported ampBlockValidation */
+/* global wp, _ */
+var ampBlockValidation = ( function() {
+ 'use strict';
+
+ var module = {
+
+ /**
+ * Data exported from server.
+ *
+ * @param {Object}
+ */
+ data: {
+ i18n: {},
+ ampValidityRestField: ''
+ },
+
+ /**
+ * Name of the store.
+ *
+ * @param {string}
+ */
+ storeName: 'amp/blockValidation',
+
+ /**
+ * Boot module.
+ *
+ * @param {Object} data - Module data.
+ * @return {void}
+ */
+ boot: function boot( data ) {
+ module.data = data;
+
+ wp.i18n.setLocaleData( module.data.i18n, 'amp' );
+
+ wp.hooks.addFilter(
+ 'blocks.BlockEdit',
+ 'amp/add-notice',
+ module.conditionallyAddNotice
+ );
+
+ module.store = module.registerStore();
+
+ wp.data.subscribe( module.handleValidationErrorsStateChange );
+ },
+
+ /**
+ * Register store.
+ *
+ * @return {Object} Store.
+ */
+ registerStore: function registerStore() {
+ return wp.data.registerStore( module.storeName, {
+ reducer: function( _state, action ) {
+ var state = _state || {
+ blockValidationErrorsByUid: {}
+ };
+
+ switch ( action.type ) {
+ case 'UPDATE_BLOCKS_VALIDATION_ERRORS':
+ return _.extend( {}, state, {
+ blockValidationErrorsByUid: action.blockValidationErrorsByUid
+ } );
+ default:
+ return state;
+ }
+ },
+ actions: {
+ updateBlocksValidationErrors: function( blockValidationErrorsByUid ) {
+ return {
+ type: 'UPDATE_BLOCKS_VALIDATION_ERRORS',
+ blockValidationErrorsByUid: blockValidationErrorsByUid
+ };
+ }
+ },
+ selectors: {
+ getBlockValidationErrors: function( state, uid ) {
+ return state.blockValidationErrorsByUid[ uid ] || [];
+ }
+ }
+ } );
+ },
+
+ /**
+ * Handle state change regarding validation errors.
+ *
+ * @return {void}
+ */
+ handleValidationErrorsStateChange: function handleValidationErrorsStateChange() {
+ var currentPost, validationErrors, blockValidationErrors, noticeElement, noticeMessage, blockErrorCount, ampValidity;
+
+ // @todo Gutenberg currently is not persisting isDirty state if changes are made during save request. Block order mismatch.
+ // We can only align block validation errors with blocks in editor when in saved state, since only here will the blocks be aligned with the validation errors.
+ if ( wp.data.select( 'core/editor' ).isEditedPostDirty() ) {
+ return;
+ }
+
+ currentPost = wp.data.select( 'core/editor' ).getCurrentPost();
+ ampValidity = currentPost[ module.data.ampValidityRestField ] || {};
+ validationErrors = ampValidity.errors;
+
+ // Short-circuit if there was no change to the validation errors.
+ if ( ! validationErrors || _.isEqual( module.lastValidationErrors, validationErrors ) ) {
+ return;
+ }
+ module.lastValidationErrors = validationErrors;
+
+ // Remove any existing notice.
+ if ( module.validationWarningNoticeId ) {
+ wp.data.dispatch( 'core/editor' ).removeNotice( module.validationWarningNoticeId );
+ module.validationWarningNoticeId = null;
+ }
+
+ // If there are no validation errors then just make sure the validation notices are cleared from the blocks.
+ if ( ! validationErrors.length ) {
+ wp.data.dispatch( module.storeName ).updateBlocksValidationErrors( {} );
+ return;
+ }
+
+ noticeMessage = wp.i18n.sprintf(
+ wp.i18n._n(
+ 'There is %s issue from AMP validation.',
+ 'There are %s issues from AMP validation.',
+ validationErrors.length,
+ 'amp'
+ ),
+ validationErrors.length
+ );
+
+ try {
+ blockValidationErrors = module.getBlocksValidationErrors();
+ wp.data.dispatch( module.storeName ).updateBlocksValidationErrors( blockValidationErrors.byUid );
+
+ blockErrorCount = validationErrors.length - blockValidationErrors.other.length;
+ if ( blockErrorCount > 0 ) {
+ noticeMessage += ' ' + wp.i18n.sprintf(
+ wp.i18n._n(
+ 'And %s is directly due to content here.',
+ 'And %s are directly due to content here.',
+ blockErrorCount,
+ 'amp'
+ ),
+ blockErrorCount
+ );
+ } else {
+ noticeMessage += ' ' + wp.i18n.sprintf(
+ wp.i18n._n(
+ 'But it is not directly due to content here.',
+ 'But none are directly due to content here.',
+ validationErrors.length,
+ 'amp'
+ ),
+ validationErrors.length
+ );
+ }
+ } catch ( e ) {
+ // Clear out block validation errors in case the block sand errors cannot be aligned.
+ wp.data.dispatch( module.storeName ).updateBlocksValidationErrors( {} );
+
+ noticeMessage += ' ' + wp.i18n._n(
+ 'It may not be due to content here.',
+ 'Some may be due to content here.',
+ validationErrors.length,
+ 'amp'
+ );
+ }
+
+ noticeMessage += ' ' + wp.i18n.__( 'Invalid code is stripped when displaying AMP.', 'amp' );
+ noticeElement = wp.element.createElement( 'p', {}, [
+ noticeMessage + ' ',
+ ampValidity.link && wp.element.createElement(
+ 'a',
+ { key: 'details', href: ampValidity.link, target: '_blank' },
+ wp.i18n.__( 'Details', 'amp' )
+ )
+ ] );
+
+ module.validationWarningNoticeId = wp.data.dispatch( 'core/editor' ).createWarningNotice( noticeElement, { spokenMessage: noticeMessage } ).notice.id;
+ },
+
+ /**
+ * Get flattened block order.
+ *
+ * @param {Object[]} blocks - List of blocks which maty have nested blocks inside them.
+ * @return {string[]} Block IDs in flattened order.
+ */
+ getFlattenedBlockOrder: function getFlattenedBlockOrder( blocks ) {
+ var blockOrder = [];
+ _.each( blocks, function( block ) {
+ blockOrder.push( block.uid );
+ if ( block.innerBlocks.length > 0 ) {
+ Array.prototype.push.apply( blockOrder, module.getFlattenedBlockOrder( block.innerBlocks ) );
+ }
+ } );
+ return blockOrder;
+ },
+
+ /**
+ * Update blocks' validation errors in the store.
+ *
+ * @return {Object} Validation errors grouped by block ID other ones.
+ */
+ getBlocksValidationErrors: function getBlocksValidationErrors() {
+ var blockValidationErrorsByUid, editorSelect, currentPost, blockOrder, validationErrors, otherValidationErrors;
+ editorSelect = wp.data.select( 'core/editor' );
+ currentPost = editorSelect.getCurrentPost();
+ validationErrors = currentPost[ module.data.ampValidityRestField ].errors;
+ blockOrder = module.getFlattenedBlockOrder( editorSelect.getBlocks() );
+
+ otherValidationErrors = [];
+ blockValidationErrorsByUid = {};
+ _.each( blockOrder, function( uid ) {
+ blockValidationErrorsByUid[ uid ] = [];
+ } );
+
+ _.each( validationErrors, function( validationError ) {
+ var i, source, uid, block, matched;
+ if ( ! validationError.sources ) {
+ otherValidationErrors.push( validationError );
+ return;
+ }
+
+ // Find the inner-most nested block source only; ignore any nested blocks.
+ matched = false;
+ for ( i = validationError.sources.length - 1; 0 <= i; i-- ) {
+ source = validationError.sources[ i ];
+
+ // Skip sources that are not for blocks.
+ if ( ! source.block_name || _.isUndefined( source.block_content_index ) || currentPost.id !== source.post_id ) {
+ continue;
+ }
+
+ // Look up the block ID by index, assuming the blocks of content in the editor are the same as blocks rendered on frontend.
+ uid = blockOrder[ source.block_content_index ];
+ if ( _.isUndefined( uid ) ) {
+ throw new Error( 'undefined_block_index' );
+ }
+
+ // Sanity check that block exists for uid.
+ block = editorSelect.getBlock( uid );
+ if ( ! block ) {
+ throw new Error( 'block_lookup_failure' );
+ }
+
+ // Check the block type in case a block is dynamically added/removed via the_content filter to cause alignment error.
+ if ( block.name !== source.block_name ) {
+ throw new Error( 'ordered_block_alignment_mismatch' );
+ }
+
+ blockValidationErrorsByUid[ uid ].push( validationError );
+ matched = true;
+
+ // Stop looking for sources, since we aren't looking for parent blocks.
+ break;
+ }
+
+ if ( ! matched ) {
+ otherValidationErrors.push( validationError );
+ }
+ } );
+
+ return {
+ byUid: blockValidationErrorsByUid,
+ other: otherValidationErrors
+ };
+ },
+
+ /**
+ * Get message for validation error.
+ *
+ * @param {Object} validationError - Validation error.
+ * @param {string} validationError.code - Validation error code.
+ * @param {string} [validationError.node_name] - Node name.
+ * @param {string} [validationError.message] - Validation error message.
+ * @return {wp.element.Component[]|string[]} Validation error message.
+ */
+ getValidationErrorMessage: function getValidationErrorMessage( validationError ) {
+ if ( validationError.message ) {
+ return validationError.message;
+ }
+ if ( 'invalid_element' === validationError.code && validationError.node_name ) {
+ return [
+ wp.i18n.__( 'Invalid element: ' ),
+ wp.element.createElement( 'code', { key: 'name' }, validationError.node_name )
+ ];
+ } else if ( 'invalid_attribute' === validationError.code && validationError.node_name ) {
+ return [
+ wp.i18n.__( 'Invalid attribute: ' ),
+ wp.element.createElement( 'code', { key: 'name' }, validationError.parent_name ? wp.i18n.sprintf( '%s[%s]', validationError.parent_name, validationError.node_name ) : validationError.node_name )
+ ];
+ }
+ return [
+ wp.i18n.__( 'Error code: ', 'amp' ),
+ wp.element.createElement( 'code', { key: 'name' }, validationError.code || wp.i18n.__( 'unknown' ) )
+ ];
+ },
+
+ /**
+ * Wraps the edit() method of a block, and conditionally adds a Notice.
+ *
+ * @param {Function} BlockEdit - The original edit() method of the block.
+ * @return {Function} The edit() method, conditionally wrapped in a notice for AMP validation error(s).
+ */
+ conditionallyAddNotice: function conditionallyAddNotice( BlockEdit ) {
+ function AmpNoticeBlockEdit( props ) {
+ var edit, details;
+ edit = wp.element.createElement(
+ BlockEdit,
+ props
+ );
+
+ if ( 0 === props.ampBlockValidationErrors.length ) {
+ return edit;
+ }
+
+ details = wp.element.createElement( 'details', { className: 'amp-block-validation-errors' }, [
+ wp.element.createElement( 'summary', { key: 'summary', className: 'amp-block-validation-errors__summary' }, wp.i18n.sprintf(
+ wp.i18n._n(
+ 'There is %s issue from AMP validation.',
+ 'There are %s issues from AMP validation.',
+ props.ampBlockValidationErrors.length,
+ 'amp'
+ ),
+ props.ampBlockValidationErrors.length
+ ) ),
+ wp.element.createElement(
+ 'ul',
+ { key: 'list', className: 'amp-block-validation-errors__list' },
+ _.map( props.ampBlockValidationErrors, function( error, key ) {
+ return wp.element.createElement( 'li', { key: key }, module.getValidationErrorMessage( error ) );
+ } )
+ )
+ ] );
+
+ return wp.element.createElement(
+ wp.element.Fragment, {},
+ wp.element.createElement(
+ wp.components.Notice,
+ {
+ status: 'warning',
+ isDismissible: false
+ },
+ details
+ ),
+ edit
+ );
+ }
+
+ return wp.data.withSelect( function( select, ownProps ) {
+ return _.extend( {}, ownProps, {
+ ampBlockValidationErrors: select( module.storeName ).getBlockValidationErrors( ownProps.id )
+ } );
+ } )( AmpNoticeBlockEdit );
+ }
+ };
+
+ return module;
+}() );
diff --git a/assets/js/amp-customize-controls.js b/assets/js/amp-customize-controls.js
index 5075f235bd8..ea1ebd2a0cb 100644
--- a/assets/js/amp-customize-controls.js
+++ b/assets/js/amp-customize-controls.js
@@ -46,7 +46,7 @@ var ampCustomizeControls = ( function( api, $ ) {
/**
* Add state for AMP.
*
- * @returns {void}
+ * @return {void}
*/
component.addState = function addState() {
api.state.add( 'ampEnabled', new api.Value( false ) );
@@ -84,7 +84,7 @@ var ampCustomizeControls = ( function( api, $ ) {
urlParser.href = url;
urlParser.pathname = urlParser.pathname.replace( regexEndpoint, '' );
- if ( urlParser.search.length > 1 ) {
+ if ( 1 < urlParser.search.length ) {
params = wp.customize.utils.parseQueryString( urlParser.search.substr( 1 ) );
delete params[ component.data.queryVar ];
urlParser.search = $.param( params );
@@ -112,7 +112,7 @@ var ampCustomizeControls = ( function( api, $ ) {
/**
* Try to close the tooltip after a given timeout.
*
- * @returns {void}
+ * @return {void}
*/
component.tryToCloseTooltip = function tryToCloseTooltip() {
clearTimeout( component.tooltipTimeoutId );
@@ -120,7 +120,7 @@ var ampCustomizeControls = ( function( api, $ ) {
if ( ! component.tooltipVisible.get() ) {
return;
}
- if ( component.tooltipFocused.get() > 0 ) {
+ if ( 0 < component.tooltipFocused.get() ) {
component.tryToCloseTooltip();
} else {
component.tooltipVisible.set( false );
@@ -157,7 +157,7 @@ var ampCustomizeControls = ( function( api, $ ) {
* Enable AMP and navigate to the given URL.
*
* @param {string} url - URL.
- * @returns {void}
+ * @return {void}
*/
component.enableAndNavigateToUrl = function enableAndNavigateToUrl( url ) {
api.state( 'ampEnabled' ).set( true );
@@ -167,10 +167,11 @@ var ampCustomizeControls = ( function( api, $ ) {
/**
* Update panel notifications.
*
- * @returns {void}
+ * @return {void}
*/
component.updatePanelNotifications = function updatePanelNotifications() {
- var panel = api.panel( component.data.panelId ), containers;
+ var panel = api.panel( component.data.panelId ),
+ containers;
containers = panel.sections().concat( [ panel ] );
if ( api.state( 'ampAvailable' ).get() ) {
_.each( containers, function( container ) {
@@ -223,7 +224,6 @@ var ampCustomizeControls = ( function( api, $ ) {
if ( api.state( 'ampAvailable' ).get() ) {
api.state( 'ampEnabled' ).set( panel.expanded.get() );
} else if ( ! panel.notifications ) {
-
/*
* This is only done if panel notifications aren't supported.
* If they are (as of 4.9) then a notification will be shown
@@ -270,7 +270,7 @@ var ampCustomizeControls = ( function( api, $ ) {
}
return val;
};
- } )( api.previewer.previewUrl.validate );
+ }( api.previewer.previewUrl.validate ) );
// Listen for ampEnabled state changes.
api.state( 'ampEnabled' ).bind( function( enabled ) {
@@ -302,7 +302,7 @@ var ampCustomizeControls = ( function( api, $ ) {
tooltip.attr( 'aria-hidden', visible ? 'false' : 'true' );
if ( visible ) {
$( document ).on( 'click.amp-toggle-outside', function( event ) {
- if ( ! $.contains( ampToggleContainer[0], event.target ) ) {
+ if ( ! $.contains( ampToggleContainer[ 0 ], event.target ) ) {
component.tooltipVisible.set( false );
}
} );
@@ -347,5 +347,4 @@ var ampCustomizeControls = ( function( api, $ ) {
};
return component;
-
-} )( wp.customize, jQuery );
+}( wp.customize, jQuery ) );
diff --git a/assets/js/amp-customize-preview.js b/assets/js/amp-customize-preview.js
index 1e7dfec76ab..e67fd6c8f2b 100644
--- a/assets/js/amp-customize-preview.js
+++ b/assets/js/amp-customize-preview.js
@@ -22,5 +22,4 @@ var ampCustomizePreview = ( function( api ) {
};
return component;
-
-} )( wp.customize );
+}( wp.customize ) );
diff --git a/assets/js/amp-customizer-design-preview.js b/assets/js/amp-customizer-design-preview.js
index 4c58d29877c..26d7c9ecb20 100644
--- a/assets/js/amp-customizer-design-preview.js
+++ b/assets/js/amp-customizer-design-preview.js
@@ -24,10 +24,10 @@
// AMP background color scheme.
wp.customize( 'amp_customizer[color_scheme]', function( value ) {
value.bind( function( to ) {
- var colors = amp_customizer_design.color_schemes[ to ];
+ var colors = amp_customizer_design.color_schemes[ to ]; // eslint-disable-line
if ( ! colors ) {
- console.error( 'Selected color scheme "%s" not registered.', to );
+ console.error( 'Selected color scheme "%s" not registered.', to ); // eslint-disable-line
return;
}
@@ -45,5 +45,4 @@
$( '.amp-wp-header .amp-site-title, .amp-wp-footer h2' ).text( title );
} );
} );
-
-} )( jQuery );
+}( jQuery ) );
diff --git a/assets/js/amp-post-meta-box.js b/assets/js/amp-post-meta-box.js
index efeadeb7485..7f6575dfc95 100644
--- a/assets/js/amp-post-meta-box.js
+++ b/assets/js/amp-post-meta-box.js
@@ -104,8 +104,8 @@ var ampPostMetaBox = ( function( $ ) {
.clone()
.insertAfter( previewBtn )
.prop( {
- 'href': component.data.previewLink,
- 'id': component.ampPreviewBtnSelector.replace( '#', '' )
+ href: component.data.previewLink,
+ id: component.ampPreviewBtnSelector.replace( '#', '' )
} )
.text( component.data.l10n.ampPreviewBtnLabel )
.parent()
@@ -126,9 +126,9 @@ var ampPostMetaBox = ( function( $ ) {
// Flag the AMP preview referer.
$input = $( '' )
.prop( {
- 'type': 'hidden',
- 'name': 'amp-preview',
- 'value': 'do-preview'
+ type: 'hidden',
+ name: 'amp-preview',
+ value: 'do-preview'
} )
.insertAfter( component.ampPreviewBtnSelector );
@@ -176,4 +176,4 @@ var ampPostMetaBox = ( function( $ ) {
};
return component;
-})( window.jQuery );
+}( window.jQuery ) );
diff --git a/includes/class-amp-theme-support.php b/includes/class-amp-theme-support.php
index 67fad611c80..613e0fc5506 100644
--- a/includes/class-amp-theme-support.php
+++ b/includes/class-amp-theme-support.php
@@ -1008,7 +1008,7 @@ public static function prepare_response( $response, $args = array() ) {
return $error . $response;
}
- $is_validation_debug_mode = ! empty( $_REQUEST[ AMP_Validation_Utils::DEBUG_QUERY_VAR ] ); // WPCS: csrf ok.
+ $is_validation_debug_mode = isset( $_REQUEST[ AMP_Validation_Utils::DEBUG_QUERY_VAR ] ); // WPCS: csrf ok.
$args = array_merge(
array(
diff --git a/includes/utils/class-amp-dom-utils.php b/includes/utils/class-amp-dom-utils.php
index 4a08dce4eb1..346f0b7a47d 100644
--- a/includes/utils/class-amp-dom-utils.php
+++ b/includes/utils/class-amp-dom-utils.php
@@ -67,22 +67,6 @@ public static function get_dom( $document ) {
// @todo In the future consider an AMP_DOMDocument subclass that does this automatically. See .
$document = self::convert_amp_bind_attributes( $document );
- /*
- * Prevent amp-mustache syntax from getting URL-encoded in attributes when saveHTML is done.
- * While this is applying to the entire document, it only really matters inside of
- * elements, since URL-encoding of curly braces in href attributes would not normally matter.
- * But when this is done inside of a then it breaks Mustache. Since Mustache
- * is logic-less and curly braces are not unsafe for HTML, we can do a global replacement.
- * The replacement is done on the entire HTML document instead of just inside of the
- * elements since it is faster and wouldn't change the outcome.
- */
- $placeholders = self::get_mustache_tag_placeholders();
- $document = str_replace(
- array_keys( $placeholders ),
- array_values( $placeholders ),
- $document
- );
-
// Force all self-closing tags to have closing tags since DOMDocument isn't fully aware.
$document = preg_replace(
'#<(' . implode( '|', self::$self_closing_tags ) . ')[^>]*>(?!\1>)#',
@@ -390,6 +374,35 @@ public static function get_content_from_dom_node( $dom, $node ) {
$self_closing_tags_regex = "#({$self_closing_tags})>#i";
}
+ /*
+ * Prevent amp-mustache syntax from getting URL-encoded in attributes when saveHTML is done.
+ * While this is applying to the entire document, it only really matters inside of
+ * elements, since URL-encoding of curly braces in href attributes would not normally matter.
+ * But when this is done inside of a then it breaks Mustache. Since Mustache
+ * is logic-less and curly braces are not unsafe for HTML, we can do a global replacement.
+ * The replacement is done on the entire HTML document instead of just inside of the
+ * elements since it is faster and wouldn't change the outcome.
+ */
+ $mustache_tag_placeholders = self::get_mustache_tag_placeholders();
+ $mustache_tags_replaced = false;
+ $xpath = new DOMXPath( $dom );
+ $templates = $dom->getElementsByTagName( 'template' );
+ foreach ( $templates as $template ) {
+
+ // These attributes are the only ones that saveHTML() will URL-encode.
+ foreach ( $xpath->query( './/*/@src|.//*/@href|.//*/@action', $template ) as $attribute ) {
+ $attribute->nodeValue = str_replace(
+ array_keys( $mustache_tag_placeholders ),
+ array_values( $mustache_tag_placeholders ),
+ $attribute->nodeValue,
+ $count
+ );
+ if ( $count ) {
+ $mustache_tags_replaced = true;
+ }
+ }
+ }
+
$html = $dom->saveHTML( $node );
// Whitespace just causes unit tests to fail... so whitespace begone.
@@ -397,6 +410,15 @@ public static function get_content_from_dom_node( $dom, $node ) {
return '';
}
+ // Restore amp-mustache placeholders which were replaced to prevent URL-encoded corruption by saveHTML.
+ if ( $mustache_tags_replaced ) {
+ $html = str_replace(
+ array_values( $mustache_tag_placeholders ),
+ array_keys( $mustache_tag_placeholders ),
+ $html
+ );
+ }
+
// Restore noscript elements which were temporarily removed to prevent libxml<2.8 parsing problems.
if ( version_compare( LIBXML_DOTTED_VERSION, '2.8', '<' ) ) {
$html = str_replace(
@@ -408,14 +430,6 @@ public static function get_content_from_dom_node( $dom, $node ) {
$html = self::restore_amp_bind_attributes( $html );
- // Restore amp-mustache placeholders which were replaced to prevent URL-encoded corruption by saveHTML.
- $placeholders = self::get_mustache_tag_placeholders();
- $html = str_replace(
- array_values( $placeholders ),
- array_keys( $placeholders ),
- $html
- );
-
/*
* Travis w/PHP 7.1 generates
and
vs.
and
, respectively.
* Travis w/PHP 7.x generates vs. . Etc.
diff --git a/includes/utils/class-amp-validation-utils.php b/includes/utils/class-amp-validation-utils.php
index 9982f4f3839..3f3e53f4908 100644
--- a/includes/utils/class-amp-validation-utils.php
+++ b/includes/utils/class-amp-validation-utils.php
@@ -152,6 +152,13 @@ class AMP_Validation_Utils {
*/
const VALIDATION_ERRORS_META_BOX = 'amp_validation_errors';
+ /**
+ * The name of the REST API field with the AMP validation results.
+ *
+ * @var string
+ */
+ const VALIDITY_REST_FIELD_NAME = 'amp_validity';
+
/**
* The errors encountered when validating.
*
@@ -180,7 +187,9 @@ class AMP_Validation_Utils {
/**
* Post IDs for posts that have been updated which need to be re-validated.
*
- * @var int[]
+ * Keys are post IDs and values are whether the post has been re-validated.
+ *
+ * @var bool[]
*/
public static $posts_pending_frontend_validation = array();
@@ -193,6 +202,13 @@ class AMP_Validation_Utils {
*/
protected static $current_hook_source_stack = array();
+ /**
+ * Index for where block appears in a post's content.
+ *
+ * @var int
+ */
+ protected static $block_content_index = 0;
+
/**
* Add the actions.
*
@@ -204,6 +220,8 @@ public static function init() {
add_filter( 'dashboard_glance_items', array( __CLASS__, 'filter_dashboard_glance_items' ) );
add_action( 'rightnow_end', array( __CLASS__, 'print_dashboard_glance_styles' ) );
add_action( 'save_post', array( __CLASS__, 'handle_save_post_prompting_validation' ), 10, 2 );
+ add_action( 'enqueue_block_editor_assets', array( __CLASS__, 'enqueue_block_validation' ) );
+ add_action( 'rest_api_init', array( __CLASS__, 'add_rest_api_fields' ) );
}
add_action( 'edit_form_top', array( __CLASS__, 'print_edit_form_validation_status' ), 10, 2 );
@@ -304,6 +322,16 @@ public static function add_validation_hooks() {
add_filter( 'do_shortcode_tag', array( __CLASS__, 'decorate_shortcode_source' ), -1, 2 );
add_filter( 'amp_content_sanitizers', array( __CLASS__, 'add_validation_callback' ) );
+
+ $do_blocks_priority = has_filter( 'the_content', 'do_blocks' );
+ $is_gutenberg_active = (
+ false !== $do_blocks_priority
+ &&
+ class_exists( 'WP_Block_Type_Registry' )
+ );
+ if ( $is_gutenberg_active ) {
+ add_filter( 'the_content', array( __CLASS__, 'add_block_source_comments' ), $do_blocks_priority - 1 );
+ }
}
/**
@@ -320,9 +348,11 @@ public static function handle_save_post_prompting_validation( $post_id, $post )
! wp_is_post_autosave( $post )
&&
! wp_is_post_revision( $post )
+ &&
+ ! isset( self::$posts_pending_frontend_validation[ $post_id ] )
);
if ( $should_validate_post ) {
- self::$posts_pending_frontend_validation[] = $post_id;
+ self::$posts_pending_frontend_validation[ $post_id ] = true;
// The reason for shutdown is to ensure that all postmeta changes have been saved, including whether AMP is enabled.
if ( ! has_action( 'shutdown', array( __CLASS__, 'validate_queued_posts_on_frontend' ) ) ) {
@@ -335,29 +365,39 @@ public static function handle_save_post_prompting_validation( $post_id, $post )
* Validate the posts pending frontend validation.
*
* @see AMP_Validation_Utils::handle_save_post_prompting_validation()
+ *
+ * @return array Mapping of post ID to the result of validating or storing the validation result.
*/
public static function validate_queued_posts_on_frontend() {
$posts = array_filter(
- array_map( 'get_post', self::$posts_pending_frontend_validation ),
+ array_map( 'get_post', array_keys( array_filter( self::$posts_pending_frontend_validation ) ) ),
function( $post ) {
return $post && post_supports_amp( $post ) && 'trash' !== $post->post_status;
}
);
+ $validation_posts = array();
+
// @todo Only validate the first and then queue the rest in WP Cron?
foreach ( $posts as $post ) {
$url = amp_get_permalink( $post->ID );
if ( ! $url ) {
+ $validation_posts[ $post->ID ] = new WP_Error( 'no_amp_permalink' );
continue;
}
+ // Prevent re-validating.
+ self::$posts_pending_frontend_validation[ $post->ID ] = false;
+
$validation_errors = self::validate_url( $url );
if ( is_wp_error( $validation_errors ) ) {
- continue;
+ $validation_posts[ $post->ID ] = $validation_errors;
+ } else {
+ $validation_posts[ $post->ID ] = self::store_validation_errors( $validation_errors, $url );
}
-
- self::store_validation_errors( $validation_errors, $url );
}
+
+ return $validation_posts;
}
/**
@@ -550,16 +590,9 @@ public static function print_edit_form_validation_status( $post ) {
}
// Incorporate frontend validation status if there is a known URL for the post.
- if ( is_post_type_viewable( $post->post_type ) ) {
- $url = amp_get_permalink( $post->ID );
-
- $validation_status_post = self::get_validation_status_post( $url );
- if ( $validation_status_post ) {
- $data = json_decode( $validation_status_post->post_content, true );
- if ( is_array( $data ) ) {
- $validation_errors = array_merge( $validation_errors, $data );
- }
- }
+ $existing_validation_errors = self::get_existing_validation_errors( $post );
+ if ( isset( $existing_validation_errors ) ) {
+ $validation_errors = $existing_validation_errors;
}
if ( empty( $validation_errors ) ) {
@@ -615,6 +648,29 @@ public static function print_edit_form_validation_status( $post ) {
echo '';
}
+ /**
+ * Gets the validation errors for a given post.
+ *
+ * These are stored in a custom post type.
+ * If none exist, returns null.
+ *
+ * @param WP_Post $post The post for which to get the validation errors.
+ * @return array|null $errors The validation errors, if they exist.
+ */
+ public static function get_existing_validation_errors( $post ) {
+ if ( is_post_type_viewable( $post->post_type ) ) {
+ $url = amp_get_permalink( $post->ID );
+ $validation_status_post = self::get_validation_status_post( $url );
+ if ( $validation_status_post ) {
+ $data = json_decode( $validation_status_post->post_content, true );
+ if ( is_array( $data ) ) {
+ return $data;
+ }
+ }
+ }
+ return null;
+ }
+
/**
* Get source start comment.
*
@@ -695,6 +751,82 @@ public static function remove_source_comments( $dom ) {
}
}
+ /**
+ * Add block source comments.
+ *
+ * @param string $content Content prior to blocks being processed.
+ * @return string Content with source comments added.
+ */
+ public static function add_block_source_comments( $content ) {
+ self::$block_content_index = 0;
+
+ $start_block_pattern = implode( '', array(
+ '##s',
+ ) );
+
+ return preg_replace_callback(
+ $start_block_pattern,
+ array( __CLASS__, 'handle_block_source_comment_replacement' ),
+ $content
+ );
+ }
+
+ /**
+ * Handle block source comment replacement.
+ *
+ * @see \AMP_Validation_Utils::add_block_source_comments()
+ * @param array $matches Matches.
+ * @return string Replaced.
+ */
+ protected static function handle_block_source_comment_replacement( $matches ) {
+ $replaced = $matches[0];
+
+ // Obtain source information for block.
+ $source = array(
+ 'block_name' => $matches['name'],
+ 'post_id' => get_the_ID(),
+ );
+
+ if ( empty( $matches['closing'] ) ) {
+ $source['block_content_index'] = self::$block_content_index;
+ self::$block_content_index++;
+ }
+
+ // Make implicit core namespace explicit.
+ $is_implicit_core_namespace = ( false === strpos( $source['block_name'], '/' ) );
+ $source['block_name'] = $is_implicit_core_namespace ? 'core/' . $source['block_name'] : $source['block_name'];
+
+ if ( ! empty( $matches['attributes'] ) ) {
+ $source['block_attrs'] = json_decode( $matches['attributes'] );
+ }
+ $block_type = WP_Block_Type_Registry::get_instance()->get_registered( $source['block_name'] );
+ if ( $block_type && $block_type->is_dynamic() ) {
+ $callback_source = self::get_source( $block_type->render_callback );
+ if ( $callback_source ) {
+ $source = array_merge(
+ $source,
+ $callback_source
+ );
+ }
+ }
+
+ if ( ! empty( $matches['closing'] ) ) {
+ $replaced .= self::get_source_comment( $source, false );
+ } else {
+ $replaced = self::get_source_comment( $source, true ) . $replaced;
+ if ( ! empty( $matches['self_closing'] ) ) {
+ unset( $source['block_content_index'] );
+ $replaced .= self::get_source_comment( $source, false );
+ }
+ }
+ return $replaced;
+ }
+
/**
* Wrap callbacks for registered widgets to keep track of queued assets and the source for anything printed for validation.
*
@@ -1027,8 +1159,6 @@ protected static function output_removed_set( $set ) {
/**
* Whether to validate the front end response.
*
- * Either the user has the capability and the query var is present.
- *
* @return boolean Whether to validate.
*/
public static function should_validate_response() {
@@ -1141,7 +1271,7 @@ public static function send_validation_errors_header() {
*
* @param array $validation_errors Validation errors.
* @param string $url URL on which the validation errors occurred.
- * @return int|null $post_id The post ID of the custom post type used, or null.
+ * @return int|WP_Error $post_id The post ID of the custom post type used, null if post was deleted due to no validation errors, or WP_Error on failure.
* @global WP $wp
*/
public static function store_validation_errors( $validation_errors, $url ) {
@@ -1186,9 +1316,9 @@ public static function store_validation_errors( $validation_errors, $url ) {
'post_name' => $post_name,
'post_content' => $encoded_errors,
'post_status' => 'publish',
- ) ) );
- if ( ! $post_id ) {
- return null;
+ ) ), true );
+ if ( is_wp_error( $post_id ) ) {
+ return $post_id;
}
if ( ! in_array( $url, get_post_meta( $post_id, self::AMP_URL_META, false ), true ) ) {
add_post_meta( $post_id, self::AMP_URL_META, wp_slash( $url ), false );
@@ -1794,6 +1924,108 @@ public static function get_recheck_link( $post, $redirect_url, $recheck_url = nu
);
}
+ /**
+ * Enqueues the block validation script.
+ *
+ * @return void
+ */
+ public static function enqueue_block_validation() {
+ $slug = 'amp-block-validation';
+
+ wp_enqueue_script(
+ $slug,
+ amp_get_asset_url( "js/{$slug}.js" ),
+ array( 'underscore' ),
+ AMP__VERSION,
+ true
+ );
+
+ $data = wp_json_encode( array(
+ 'i18n' => gutenberg_get_jed_locale_data( 'amp' ), // @todo POT file.
+ 'ampValidityRestField' => self::VALIDITY_REST_FIELD_NAME,
+ ) );
+ wp_add_inline_script( $slug, sprintf( 'ampBlockValidation.boot( %s );', $data ) );
+ }
+
+ /**
+ * Adds fields to the REST API responses, in order to display validation errors.
+ *
+ * @return void
+ */
+ public static function add_rest_api_fields() {
+ if ( amp_is_canonical() ) {
+ $object_types = get_post_types_by_support( 'editor' );
+ } else {
+ $object_types = array_intersect(
+ get_post_types_by_support( 'amp' ),
+ get_post_types( array(
+ 'show_in_rest' => true,
+ ) )
+ );
+ }
+
+ register_rest_field(
+ $object_types,
+ self::VALIDITY_REST_FIELD_NAME,
+ array(
+ 'get_callback' => array( __CLASS__, 'get_amp_validity_rest_field' ),
+ 'schema' => array(
+ 'description' => __( 'AMP validity status', 'amp' ),
+ 'type' => 'object',
+ ),
+ )
+ );
+ }
+
+ /**
+ * Adds a field to the REST API responses to display the validation status.
+ *
+ * First, get existing errors for the post.
+ * If there are none, validate the post and return any errors.
+ *
+ * @param array $post_data Data for the post.
+ * @param string $field_name The name of the field to add.
+ * @param WP_REST_Request $request The name of the field to add.
+ * @return array|null $validation_data Validation data if it's available, or null.
+ */
+ public static function get_amp_validity_rest_field( $post_data, $field_name, $request ) {
+ unset( $field_name );
+ if ( ! current_user_can( 'edit_post', $post_data['id'] ) ) {
+ return null;
+ }
+ $post = get_post( $post_data['id'] );
+
+ $validation_status_post = null;
+ if ( in_array( $request->get_method(), array( 'PUT', 'POST' ), true ) ) {
+ if ( ! isset( self::$posts_pending_frontend_validation[ $post->ID ] ) ) {
+ self::$posts_pending_frontend_validation[ $post->ID ] = true;
+ }
+ $results = self::validate_queued_posts_on_frontend();
+ if ( isset( $results[ $post->ID ] ) && is_int( $results[ $post->ID ] ) ) {
+ $validation_status_post = get_post( $results[ $post->ID ] );
+ }
+ }
+
+ if ( empty( $validation_status_post ) ) {
+ // @todo Consider process_markup() if not post type is not viewable and if post type supports editor.
+ $validation_status_post = self::get_validation_status_post( amp_get_permalink( $post->ID ) );
+ }
+
+ if ( ! $validation_status_post ) {
+ $field = array(
+ 'errors' => array(),
+ 'link' => null,
+ );
+ } else {
+ $field = array(
+ 'errors' => json_decode( $validation_status_post->post_content, true ),
+ 'link' => get_edit_post_link( $validation_status_post->ID, 'raw' ),
+ );
+ }
+
+ return $field;
+ }
+
/**
* Outputs an admin notice if persistent object cache is not present.
*
diff --git a/package-lock.json b/package-lock.json
index 56ff9a78a50..95657844d0b 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -11,9 +11,9 @@
"dev": true
},
"acorn": {
- "version": "5.2.1",
- "resolved": "https://registry.npmjs.org/acorn/-/acorn-5.2.1.tgz",
- "integrity": "sha512-jG0u7c4Ly+3QkkW18V+NRDN+4bWHdln30NL1ZL2AvFZZmQe/BfopYCtghCKKVBUSetZ4QKcyA0pY6/4Gw8Pv8w==",
+ "version": "5.5.3",
+ "resolved": "https://registry.npmjs.org/acorn/-/acorn-5.5.3.tgz",
+ "integrity": "sha512-jd5MkIUlbbmb07nXH0DT3y7rDVtkzDi4XZOUVWAer8ajmF/DTSSbl5oNFyDOl/OXA33Bl79+ypHhl2pN20VeOQ==",
"dev": true
},
"acorn-jsx": {
@@ -40,7 +40,7 @@
"dev": true,
"requires": {
"co": "4.6.0",
- "fast-deep-equal": "1.0.0",
+ "fast-deep-equal": "1.1.0",
"fast-json-stable-stringify": "2.0.0",
"json-schema-traverse": "0.3.1"
}
@@ -52,9 +52,9 @@
"dev": true
},
"ansi-escapes": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-3.0.0.tgz",
- "integrity": "sha512-O/klc27mWNUigtv0F8NJWbLF00OcegQalkqKURWdosW08YZKi4m6CnSUSvIZG1otNJbTWhN01Hhz389DW7mvDQ==",
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-3.1.0.tgz",
+ "integrity": "sha512-UgAb8H9D41AQnu/PbWlCofQVcnV4Gs2bBJi9eZPxfU/hgglFh3SMDMENRIqdr7H6XFnXdoknctFByVsCOotTVw==",
"dev": true
},
"ansi-regex": {
@@ -162,6 +162,12 @@
"concat-map": "0.0.1"
}
},
+ "buffer-from": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.0.0.tgz",
+ "integrity": "sha512-83apNb8KK0Se60UE1+4Ukbe3HbfELJ6UlI4ldtOGs7So4KD26orJM8hIY9lxdzP+UpItH1Yh/Y8GUvNFWFFRxA==",
+ "dev": true
+ },
"builtin-modules": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-1.1.1.tgz",
@@ -200,32 +206,32 @@
}
},
"chalk": {
- "version": "2.3.0",
- "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.3.0.tgz",
- "integrity": "sha512-Az5zJR2CBujap2rqXGaJKaPHyJ0IrUimvYNX+ncCy8PJP4ltOGTrHUIo097ZaL2zMeKYpiCdqDvS6zdrTFok3Q==",
+ "version": "2.3.2",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.3.2.tgz",
+ "integrity": "sha512-ZM4j2/ld/YZDc3Ma8PgN7gyAk+kHMMMyzLNryCPGhWrsfAuDVeuid5bpRFTDgMH9JBK2lA4dyyAkkZYF/WcqDQ==",
"dev": true,
"requires": {
- "ansi-styles": "3.2.0",
+ "ansi-styles": "3.2.1",
"escape-string-regexp": "1.0.5",
- "supports-color": "4.5.0"
+ "supports-color": "5.3.0"
},
"dependencies": {
"ansi-styles": {
- "version": "3.2.0",
- "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.0.tgz",
- "integrity": "sha512-NnSOmMEYtVR2JVMIGTzynRkkaxtiq1xnFBcdQD/DnNCYPoEPsVJhM98BDyaoNOQIi7p4okdi3E27eN7GQbsUug==",
+ "version": "3.2.1",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
+ "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
"dev": true,
"requires": {
"color-convert": "1.9.1"
}
},
"supports-color": {
- "version": "4.5.0",
- "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-4.5.0.tgz",
- "integrity": "sha1-vnoN5ITexcXN34s9WRJQRJEvY1s=",
+ "version": "5.3.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.3.0.tgz",
+ "integrity": "sha512-0aP01LLIskjKs3lq52EC0aGBAJhLq7B2Rd8HC/DR/PtNNpcLilNmHC12O+hu0usQpo7wtHNRqtrhBwtDb0+dNg==",
"dev": true,
"requires": {
- "has-flag": "2.0.0"
+ "has-flag": "3.0.0"
}
}
}
@@ -317,13 +323,14 @@
"dev": true
},
"concat-stream": {
- "version": "1.6.0",
- "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.0.tgz",
- "integrity": "sha1-CqxmL9Ur54lk1VMvaUeE5wEQrPc=",
+ "version": "1.6.2",
+ "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz",
+ "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==",
"dev": true,
"requires": {
+ "buffer-from": "1.0.0",
"inherits": "2.0.3",
- "readable-stream": "2.3.3",
+ "readable-stream": "2.3.6",
"typedarray": "0.0.6"
}
},
@@ -348,7 +355,7 @@
"integrity": "sha1-6L0O/uWPz/b4+UUQoKVUu/ojVEk=",
"dev": true,
"requires": {
- "lru-cache": "4.1.1",
+ "lru-cache": "4.1.2",
"shebang-command": "1.2.0",
"which": "1.3.0"
}
@@ -407,7 +414,7 @@
"requires": {
"globby": "5.0.0",
"is-path-cwd": "1.0.0",
- "is-path-in-cwd": "1.0.0",
+ "is-path-in-cwd": "1.0.1",
"object-assign": "4.1.1",
"pify": "2.3.0",
"pinkie-promise": "2.0.1",
@@ -415,9 +422,9 @@
}
},
"doctrine": {
- "version": "2.0.2",
- "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.0.2.tgz",
- "integrity": "sha512-y0tm5Pq6ywp3qSTZ1vPgVdAnbDEoeoc5wlOHXoY1c4Wug/a7JvqHIl7BTvwodaHmejWkK/9dSb3sCYfyo/om8A==",
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz",
+ "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==",
"dev": true,
"requires": {
"esutils": "2.0.2"
@@ -500,35 +507,35 @@
"dev": true
},
"eslint": {
- "version": "4.13.1",
- "resolved": "https://registry.npmjs.org/eslint/-/eslint-4.13.1.tgz",
- "integrity": "sha512-UCJVV50RtLHYzBp1DZ8CMPtRSg4iVZvjgO9IJHIKyWU/AnJVjtdRikoUPLB29n5pzMB7TnsLQWf0V6VUJfoPfw==",
+ "version": "4.19.1",
+ "resolved": "https://registry.npmjs.org/eslint/-/eslint-4.19.1.tgz",
+ "integrity": "sha512-bT3/1x1EbZB7phzYu7vCr1v3ONuzDtX8WjuM9c0iYxe+cq+pwcKEoQjl7zd3RpC6YOLgnSy3cTN58M2jcoPDIQ==",
"dev": true,
"requires": {
"ajv": "5.5.2",
"babel-code-frame": "6.26.0",
- "chalk": "2.3.0",
- "concat-stream": "1.6.0",
+ "chalk": "2.3.2",
+ "concat-stream": "1.6.2",
"cross-spawn": "5.1.0",
"debug": "3.1.0",
- "doctrine": "2.0.2",
+ "doctrine": "2.1.0",
"eslint-scope": "3.7.1",
- "espree": "3.5.2",
- "esquery": "1.0.0",
- "estraverse": "4.2.0",
+ "eslint-visitor-keys": "1.0.0",
+ "espree": "3.5.4",
+ "esquery": "1.0.1",
"esutils": "2.0.2",
"file-entry-cache": "2.0.0",
"functional-red-black-tree": "1.0.1",
"glob": "7.1.2",
- "globals": "11.1.0",
+ "globals": "11.4.0",
"ignore": "3.3.7",
"imurmurhash": "0.1.4",
"inquirer": "3.3.0",
- "is-resolvable": "1.0.1",
- "js-yaml": "3.10.0",
+ "is-resolvable": "1.1.0",
+ "js-yaml": "3.11.0",
"json-stable-stringify-without-jsonify": "1.0.1",
"levn": "0.3.0",
- "lodash": "4.17.4",
+ "lodash": "4.17.5",
"minimatch": "3.0.4",
"mkdirp": "0.5.1",
"natural-compare": "1.4.0",
@@ -536,6 +543,7 @@
"path-is-inside": "1.0.2",
"pluralize": "7.0.0",
"progress": "2.0.0",
+ "regexpp": "1.1.0",
"require-uncached": "1.0.3",
"semver": "5.4.1",
"strip-ansi": "4.0.0",
@@ -544,23 +552,35 @@
"text-table": "0.2.0"
}
},
+ "eslint-config-wordpress": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/eslint-config-wordpress/-/eslint-config-wordpress-2.0.0.tgz",
+ "integrity": "sha1-UgEgbGlk1kgxUjLt9t+9LpJeTNY=",
+ "dev": true
+ },
"eslint-scope": {
"version": "3.7.1",
"resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-3.7.1.tgz",
"integrity": "sha1-PWPD7f2gLgbgGkUq2IyqzHzctug=",
"dev": true,
"requires": {
- "esrecurse": "4.2.0",
+ "esrecurse": "4.2.1",
"estraverse": "4.2.0"
}
},
+ "eslint-visitor-keys": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz",
+ "integrity": "sha512-qzm/XxIbxm/FHyH341ZrbnMUpe+5Bocte9xkmFMzPMjRaZMcXww+MpBptFvtU+79L362nqiLhekCxCxDPaUMBQ==",
+ "dev": true
+ },
"espree": {
- "version": "3.5.2",
- "resolved": "https://registry.npmjs.org/espree/-/espree-3.5.2.tgz",
- "integrity": "sha512-sadKeYwaR/aJ3stC2CdvgXu1T16TdYN+qwCpcWbMnGJ8s0zNWemzrvb2GbD4OhmJ/fwpJjudThAlLobGbWZbCQ==",
+ "version": "3.5.4",
+ "resolved": "https://registry.npmjs.org/espree/-/espree-3.5.4.tgz",
+ "integrity": "sha512-yAcIQxtmMiB/jL32dzEp2enBeidsB7xWPLNiw3IIkpVds1P+h7qF9YwJq1yUNzp2OKXgAprs4F61ih66UsoD1A==",
"dev": true,
"requires": {
- "acorn": "5.2.1",
+ "acorn": "5.5.3",
"acorn-jsx": "3.0.1"
}
},
@@ -571,22 +591,21 @@
"dev": true
},
"esquery": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.0.0.tgz",
- "integrity": "sha1-z7qLV9f7qT8XKYqKAGoEzaE9gPo=",
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.0.1.tgz",
+ "integrity": "sha512-SmiyZ5zIWH9VM+SRUReLS5Q8a7GxtRdxEBVZpm98rJM7Sb+A9DVCndXfkeFUd3byderg+EbDkfnevfCwynWaNA==",
"dev": true,
"requires": {
"estraverse": "4.2.0"
}
},
"esrecurse": {
- "version": "4.2.0",
- "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.2.0.tgz",
- "integrity": "sha1-+pVo2Y04I/mkHZHpAtyrnqblsWM=",
+ "version": "4.2.1",
+ "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.2.1.tgz",
+ "integrity": "sha512-64RBB++fIOAXPw3P9cy89qfMlvZEXZkqqJkjqqXIvzP5ezRZjW+lPWjw35UX/3EhUPFYbg5ER4JYgDw4007/DQ==",
"dev": true,
"requires": {
- "estraverse": "4.2.0",
- "object-assign": "4.1.1"
+ "estraverse": "4.2.0"
}
},
"estraverse": {
@@ -623,9 +642,9 @@
"dev": true
},
"external-editor": {
- "version": "2.1.0",
- "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-2.1.0.tgz",
- "integrity": "sha512-E44iT5QVOUJBKij4IIV3uvxuNlbKS38Tw1HiupxEIHPv9qtC2PrDYohbXV5U+1jnfIXttny8gUhj+oZvflFlzA==",
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-2.2.0.tgz",
+ "integrity": "sha512-bSn6gvGxKt+b7+6TKEv1ZycHleA7aHhRHyAqJyp5pbUFuYYNIzpZnQDk7AsYckyWdEnTeAnay0aCy2aV6iTk9A==",
"dev": true,
"requires": {
"chardet": "0.4.2",
@@ -634,9 +653,9 @@
}
},
"fast-deep-equal": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-1.0.0.tgz",
- "integrity": "sha1-liVqO8l1WV6zbYLpkp0GDYk0Of8=",
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-1.1.0.tgz",
+ "integrity": "sha1-wFNHeBfIa1HaqFPIHgWbcz0CNhQ=",
"dev": true
},
"fast-json-stable-stringify": {
@@ -761,9 +780,9 @@
}
},
"globals": {
- "version": "11.1.0",
- "resolved": "https://registry.npmjs.org/globals/-/globals-11.1.0.tgz",
- "integrity": "sha512-uEuWt9mqTlPDwSqi+sHjD4nWU/1N+q0fiWI9T1mZpD2UENqX20CFD5T/ziLZvztPaBKl7ZylUi1q6Qfm7E2CiQ==",
+ "version": "11.4.0",
+ "resolved": "https://registry.npmjs.org/globals/-/globals-11.4.0.tgz",
+ "integrity": "sha512-Dyzmifil8n/TmSqYDEXbm+C8yitzJQqQIlJQLNRMwa+BOUJpRC19pyVeN12JAjt61xonvXjtff+hJruTRXn5HA==",
"dev": true
},
"globby": {
@@ -1123,9 +1142,9 @@
}
},
"has-flag": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-2.0.0.tgz",
- "integrity": "sha1-6CB68cx7MNRGzHC3NLXovhj4jVE=",
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
+ "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=",
"dev": true
},
"hooker": {
@@ -1228,13 +1247,13 @@
"integrity": "sha512-h+xtnyk4EwKvFWHrUYsWErEVR+igKtLdchu+o0Z1RL7VU/jVMFbYir2bp6bAj8efFNxWqHX0dIss6fJQ+/+qeQ==",
"dev": true,
"requires": {
- "ansi-escapes": "3.0.0",
- "chalk": "2.3.0",
+ "ansi-escapes": "3.1.0",
+ "chalk": "2.3.2",
"cli-cursor": "2.1.0",
"cli-width": "2.2.0",
- "external-editor": "2.1.0",
+ "external-editor": "2.2.0",
"figures": "2.0.0",
- "lodash": "4.17.4",
+ "lodash": "4.17.5",
"mute-stream": "0.0.7",
"run-async": "2.3.0",
"rx-lite": "4.0.8",
@@ -1281,9 +1300,9 @@
"dev": true
},
"is-path-in-cwd": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/is-path-in-cwd/-/is-path-in-cwd-1.0.0.tgz",
- "integrity": "sha1-ZHdYK4IU1gI0YJRWcAO+ip6sBNw=",
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/is-path-in-cwd/-/is-path-in-cwd-1.0.1.tgz",
+ "integrity": "sha512-FjV1RTW48E7CWM7eE/J2NJvAEEVektecDBVBE5Hh3nM1Jd0kvhHtX68Pr3xsDf857xt3Y4AkwVULK1Vku62aaQ==",
"dev": true,
"requires": {
"is-path-inside": "1.0.1"
@@ -1305,9 +1324,9 @@
"dev": true
},
"is-resolvable": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/is-resolvable/-/is-resolvable-1.0.1.tgz",
- "integrity": "sha512-y5CXYbzvB3jTnWAZH1Nl7ykUWb6T3BcTs56HUruwBf8MhF56n1HWqhDWnVFo8GHrUPDgvUUNVhrc2U8W7iqz5g==",
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/is-resolvable/-/is-resolvable-1.1.0.tgz",
+ "integrity": "sha512-qgDYXFSR5WvEfuS5dMj6oTMEbrrSaM0CrFk2Yiq/gXnBvD9pMa2jGXxyhGLfvhZpuMZe18CJpFxAt3CRs42NMg==",
"dev": true
},
"is-utf8": {
@@ -1335,9 +1354,9 @@
"dev": true
},
"js-yaml": {
- "version": "3.10.0",
- "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.10.0.tgz",
- "integrity": "sha512-O2v52ffjLa9VeM43J4XocZE//WT9N0IiwDa3KSHH7Tu8CtH+1qM8SIZvnsTh6v+4yFy5KUY3BHUVwjpfAWsjIA==",
+ "version": "3.11.0",
+ "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.11.0.tgz",
+ "integrity": "sha512-saJstZWv7oNeOyBh3+Dx1qWzhW0+e6/8eDzo7p5rDFqxntSztloLtuKu+Ejhtq82jsilwOIZYsCz+lIjthg1Hw==",
"dev": true,
"requires": {
"argparse": "1.0.9",
@@ -1410,9 +1429,9 @@
}
},
"lodash": {
- "version": "4.17.4",
- "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.4.tgz",
- "integrity": "sha1-eCA6TRwyiuHYbcpkYONptX9AVa4=",
+ "version": "4.17.5",
+ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.5.tgz",
+ "integrity": "sha512-svL3uiZf1RwhH+cWrfZn3A4+U58wbP0tGVTLQPbjplZxZ8ROD9VLuNgsRniTlLe7OlSqR79RUehXgpBW/s0IQw==",
"dev": true
},
"loud-rejection": {
@@ -1426,9 +1445,9 @@
}
},
"lru-cache": {
- "version": "4.1.1",
- "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.1.tgz",
- "integrity": "sha512-q4spe4KTfsAS1SUHLO0wz8Qiyf1+vMIAgpRYioFYDMNqKfHQbg+AVDH3i4fvpl71/P1L0dBl+fQi+P37UYf0ew==",
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.2.tgz",
+ "integrity": "sha512-wgeVXhrDwAWnIF/yZARsFnMBtdFXOg1b8RIrhilp+0iDYN4mdQcNZElDZ0e4B64BhaxeQ5zN7PMyvu7we1kPeQ==",
"dev": true,
"requires": {
"pseudomap": "1.0.2",
@@ -1479,9 +1498,9 @@
}
},
"mimic-fn": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-1.1.0.tgz",
- "integrity": "sha1-5md4PZLonb00KBi1IwudYqZyrRg=",
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-1.2.0.tgz",
+ "integrity": "sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ==",
"dev": true
},
"minimatch": {
@@ -1589,7 +1608,7 @@
"integrity": "sha1-BnQoIw/WdEOyeUsiu6UotoZ5YtQ=",
"dev": true,
"requires": {
- "mimic-fn": "1.1.0"
+ "mimic-fn": "1.2.0"
}
},
"optionator": {
@@ -1693,9 +1712,9 @@
"dev": true
},
"process-nextick-args": {
- "version": "1.0.7",
- "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-1.0.7.tgz",
- "integrity": "sha1-FQ4gt1ZZCtP5EJPyWk8q2L/zC6M=",
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz",
+ "integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==",
"dev": true
},
"progress": {
@@ -1732,17 +1751,17 @@
}
},
"readable-stream": {
- "version": "2.3.3",
- "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.3.tgz",
- "integrity": "sha512-m+qzzcn7KUxEmd1gMbchF+Y2eIUbieUaxkWtptyHywrX0rE8QEYqPC07Vuy4Wm32/xE16NcdBctb8S0Xe/5IeQ==",
+ "version": "2.3.6",
+ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz",
+ "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==",
"dev": true,
"requires": {
"core-util-is": "1.0.2",
"inherits": "2.0.3",
"isarray": "1.0.0",
- "process-nextick-args": "1.0.7",
+ "process-nextick-args": "2.0.0",
"safe-buffer": "5.1.1",
- "string_decoder": "1.0.3",
+ "string_decoder": "1.1.1",
"util-deprecate": "1.0.2"
}
},
@@ -1756,6 +1775,12 @@
"strip-indent": "1.0.1"
}
},
+ "regexpp": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-1.1.0.tgz",
+ "integrity": "sha512-LOPw8FpgdQF9etWMaAfG/WRthIdXJGYp4mJ2Jgn/2lpkbod9jPn0t9UqN7AxBOKNfzRbYyVfgc7Vk4t/MpnXgw==",
+ "dev": true
+ },
"repeating": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/repeating/-/repeating-2.0.1.tgz",
@@ -1916,9 +1941,9 @@
}
},
"string_decoder": {
- "version": "1.0.3",
- "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.0.3.tgz",
- "integrity": "sha512-4AH6Z5fzNNBcH+6XDMfA/BTt87skxqJlO0lAh3Dker5zThcAxG6mKz+iGu308UKoPPQ8Dcqx/4JhujzltRa+hQ==",
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
+ "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
"dev": true,
"requires": {
"safe-buffer": "5.1.1"
@@ -1979,8 +2004,8 @@
"requires": {
"ajv": "5.5.2",
"ajv-keywords": "2.1.1",
- "chalk": "2.3.0",
- "lodash": "4.17.4",
+ "chalk": "2.3.2",
+ "lodash": "4.17.5",
"slice-ansi": "1.0.0",
"string-width": "2.1.1"
}
diff --git a/package.json b/package.json
index e1963439270..82c3265c0d3 100644
--- a/package.json
+++ b/package.json
@@ -10,7 +10,8 @@
"license": "GPL-2.0+",
"private": true,
"devDependencies": {
- "eslint": "^4.13.1",
+ "eslint": "^4.19.1",
+ "eslint-config-wordpress": "^2.0.0",
"grunt": "^1.0.1",
"grunt-contrib-clean": "^1.1.0",
"grunt-contrib-copy": "^1.0.0",
diff --git a/phpunit.xml b/phpunit.xml
index 4d7d8f842a4..a0b6f006a38 100644
--- a/phpunit.xml
+++ b/phpunit.xml
@@ -7,7 +7,7 @@
convertWarningsToExceptions="true"
>
-
+
diff --git a/tests/test-class-amp-dom-utils.php b/tests/test-class-amp-dom-utils.php
index 7fe8106fd07..e6542424465 100644
--- a/tests/test-class-amp-dom-utils.php
+++ b/tests/test-class-amp-dom-utils.php
@@ -123,6 +123,7 @@ public function test_amp_bind_conversion() {
* Test handling of empty elements.
*
* @covers \AMP_DOM_Utils::get_dom()
+ * @covers \AMP_DOM_Utils::get_content_from_dom_node()
*/
public function test_html5_empty_elements() {
$original = '';
@@ -146,6 +147,70 @@ public function test_html5_empty_elements() {
$this->assertEquals( 'span', $video->childNodes->item( 5 )->nodeName );
}
+ /**
+ * Test parsing DOM with Mustache or Mustache-like templates.
+ *
+ * @covers \AMP_DOM_Utils::get_dom()
+ * @covers \AMP_DOM_Utils::get_content_from_dom_node()
+ */
+ public function test_mustache_replacements() {
+
+ $data = array(
+ 'foo' => array(
+ 'bar' => array(
+ 'baz' => array(),
+ ),
+ ),
+ );
+
+ $html = implode( "\n", array(
+ '',
+ '',
+ '',
+ '
Quote
Famous
',
+ '',
+ '',
+ '
',
+ '',
+ '
',
+ '',
+ '',
+ '',
+ 'Hello {{world}}! {{quote}}
',
+ '',
+ ) );
+
+ $dom = AMP_DOM_Utils::get_dom_from_content( $html );
+ $xpath = new DOMXPath( $dom );
+
+ // Ensure that JSON in scripts are left intact.
+ $script = $xpath->query( '//script' )->item( 0 );
+ $this->assertEquals(
+ $data,
+ json_decode( $script->nodeValue, true )
+ );
+
+ // Ensure that mustache var in a[href] attribute is intact.
+ $template_link = $xpath->query( '//template/a' )->item( 0 );
+ $this->assertSame( '{{href}}', $template_link->getAttribute( 'href' ) );
+ $this->assertEquals( 'Hello {{name}}', $template_link->getAttribute( 'title' ) );
+
+ // Ensure that mustache var in img[src] attribute is intact.
+ $template_img = $xpath->query( '//template/a/img' )->item( 0 );
+ $this->assertEquals( '{{src}}', $template_img->getAttribute( 'src' ) );
+
+ // Ensure that mustache var in blockquote[cite] is not changed.
+ $template_blockquote = $xpath->query( '//template/blockquote' )->item( 0 );
+ $this->assertEquals( '{{cite}}', $template_blockquote->getAttribute( 'cite' ) );
+
+ $serialized_html = AMP_DOM_Utils::get_content_from_dom_node( $dom, $dom->documentElement );
+
+ $this->assertContains( '', $serialized_html );
+ $this->assertContains( '', $serialized_html );
+ $this->assertContains( '', $serialized_html );
+ $this->assertContains( '"block_attrs":{"layout":"column-1"}}', $serialized_html );
+ }
+
/**
* Test encoding.
*
diff --git a/tests/test-class-amp-validation-utils.php b/tests/test-class-amp-validation-utils.php
index f8c4b82f156..0b098b676f3 100644
--- a/tests/test-class-amp-validation-utils.php
+++ b/tests/test-class-amp-validation-utils.php
@@ -94,6 +94,7 @@ public function setUp() {
*/
public function tearDown() {
$GLOBALS['wp_registered_widgets'] = $this->original_wp_registered_widgets; // WPCS: override ok.
+ remove_theme_support( 'amp' );
unset( $GLOBALS['current_screen'] );
parent::tearDown();
}
@@ -118,10 +119,11 @@ public function test_init() {
$this->assertEquals( 10, has_action( 'admin_notices', self::TESTED_CLASS . '::persistent_object_caching_notice' ) );
$this->assertEquals( 10, has_action( 'admin_menu', self::TESTED_CLASS . '::remove_publish_meta_box' ) );
$this->assertEquals( 10, has_action( 'add_meta_boxes', self::TESTED_CLASS . '::add_meta_boxes' ) );
+ $this->assertEquals( 10, has_action( 'rest_api_init', self::TESTED_CLASS . '::add_rest_api_fields' ) );
}
/**
- * Test init.
+ * Test add_validation_hooks.
*
* @covers AMP_Validation_Utils::add_validation_hooks()
*/
@@ -133,6 +135,113 @@ public function test_add_validation_hooks() {
$this->assertEquals( -1, has_action( 'do_shortcode_tag', array( self::TESTED_CLASS, 'decorate_shortcode_source' ) ) );
}
+ /**
+ * Test add_validation_hooks with Gutenberg active.
+ *
+ * @covers AMP_Validation_Utils::add_validation_hooks()
+ */
+ public function test_add_validation_hooks_gutenberg() {
+ if ( ! function_exists( 'do_blocks' ) ) {
+ $this->markTestSkipped( 'Gutenberg not active.' );
+ }
+ if ( ( version_compare( get_bloginfo( 'version' ), '4.9', '<' ) ) ) {
+ $this->markTestSkipped( 'The WP version is less than 4.9, so Gutenberg did not init.' );
+ }
+
+ $priority = has_filter( 'the_content', 'do_blocks' );
+ $this->assertNotFalse( $priority );
+ AMP_Validation_Utils::add_validation_hooks();
+ $this->assertEquals( $priority - 1, has_filter( 'the_content', array( self::TESTED_CLASS, 'add_block_source_comments' ) ) );
+ }
+
+ /**
+ * Get block data.
+ *
+ * @see Test_AMP_Validation_Utils::test_add_block_source_comments()
+ * @return array
+ */
+ public function get_block_data() {
+ return array(
+ 'paragraph' => array(
+ "\nLatest posts:
\n",
+ "\nLatest posts:
\n",
+ array(
+ 'element' => 'p',
+ 'blocks' => array( 'core/paragraph' ),
+ ),
+ ),
+ 'latest_posts' => array(
+ '',
+ '',
+ array(
+ 'element' => 'ul',
+ 'blocks' => array( 'core/latest-posts' ),
+ ),
+ ),
+ 'columns' => array(
+ "\n\n \n
\n A quotation!
Famous
\n \n\n \n
\n \n
\n \n
\n",
+ "\n\n\n\n\n\n
\n A quotation!
Famous
\n \n
\n \n
\n
\n",
+ array(
+ 'element' => 'blockquote',
+ 'blocks' => array(
+ 'core/columns',
+ 'core/quote',
+ ),
+ ),
+ ),
+ );
+ }
+
+ /**
+ * Test add_block_source_comments.
+ *
+ * @param string $content Content.
+ * @param string $expected Expected content.
+ * @param array $query Query.
+ * @dataProvider get_block_data
+ * @covers AMP_Validation_Utils::add_block_source_comments()
+ */
+ public function test_add_block_source_comments( $content, $expected, $query ) {
+ if ( ! function_exists( 'do_blocks' ) ) {
+ $this->markTestSkipped( 'Gutenberg not active.' );
+ }
+ if ( ( version_compare( get_bloginfo( 'version' ), '4.9', '<' ) ) ) {
+ $this->markTestSkipped( 'The WP version is less than 4.9, so Gutenberg did not init.' );
+ }
+
+ global $post;
+ $post = $this->factory()->post->create_and_get(); // WPCS: Override ok.
+ $this->assertInstanceOf( 'WP_Post', get_post() );
+
+ $rendered_block = do_blocks( AMP_Validation_Utils::add_block_source_comments( $content ) );
+
+ $expected = str_replace(
+ array(
+ '{{post_id}}',
+ '{{title}}',
+ '{{url}}',
+ ),
+ array(
+ $post->ID,
+ get_the_title( $post ),
+ get_permalink( $post ),
+ ),
+ $expected
+ );
+ $this->assertEquals(
+ preg_replace( '/(?<=>)\s+(?=<)/', '', str_replace( '%d', $post->ID, $expected ) ),
+ preg_replace( '/(?<=>)\s+(?=<)/', '', $rendered_block )
+ );
+
+ $dom = AMP_DOM_Utils::get_dom_from_content( $rendered_block );
+ $el = $dom->getElementsByTagName( $query['element'] )->item( 0 );
+
+ $this->assertEquals(
+ $query['blocks'],
+ wp_list_pluck( AMP_Validation_Utils::locate_sources( $el ), 'block_name' )
+ );
+ }
+
/**
* Test add_validation_error.
*
@@ -299,6 +408,25 @@ public function test_print_edit_form_validation_status() {
AMP_Validation_Utils::reset_validation_results();
}
+ /**
+ * Test get_existing_validation_errors.
+ *
+ * @covers AMP_Validation_Utils::get_existing_validation_errors()
+ */
+ public function test_get_existing_validation_errors() {
+ add_theme_support( 'amp' );
+ AMP_Validation_Utils::register_post_type();
+ $post = $this->factory()->post->create_and_get();
+ $this->assertEquals( null, AMP_Validation_Utils::get_existing_validation_errors( $post ) );
+
+ // Create an error custom post for the $post_id, so the function will return existing errors.
+ $this->create_custom_post( amp_get_permalink( $post->ID ) );
+ $this->assertEquals(
+ $this->get_mock_errors(),
+ AMP_Validation_Utils::get_existing_validation_errors( $post )
+ );
+ }
+
/**
* Test source comments.
*
@@ -1298,12 +1426,133 @@ public function test_get_recheck_link() {
$this->assertContains( 'Recheck the URL for AMP validity', $link );
}
+ /**
+ * Test enqueue_block_validation.
+ *
+ * @covers AMP_Validation_Utils::enqueue_block_validation()
+ */
+ public function test_enqueue_block_validation() {
+ if ( ! function_exists( 'gutenberg_get_jed_locale_data' ) ) {
+ $this->markTestSkipped( 'Gutenberg not available.' );
+ }
+
+ global $post;
+ $post = $this->factory()->post->create_and_get(); // WPCS: global override ok.
+ $slug = 'amp-block-validation';
+ $this->set_capability();
+ AMP_Validation_Utils::enqueue_block_validation();
+
+ $script = wp_scripts()->registered[ $slug ];
+ $inline_script = $script->extra['after'][1];
+ $this->assertContains( 'js/amp-block-validation.js', $script->src );
+ $this->assertEquals( array( 'underscore' ), $script->deps );
+ $this->assertEquals( AMP__VERSION, $script->ver );
+ $this->assertTrue( in_array( $slug, wp_scripts()->queue, true ) );
+ $this->assertContains( 'ampBlockValidation.boot', $inline_script );
+ $this->assertContains( AMP_Validation_Utils::VALIDITY_REST_FIELD_NAME, $inline_script );
+ $this->assertContains( '"domain":"amp"', $inline_script );
+ }
+
+ /**
+ * Test add_rest_api_fields.
+ *
+ * @covers AMP_Validation_Utils::add_rest_api_fields()
+ */
+ public function test_add_rest_api_fields() {
+ // Test in a non Native-AMP (canonical) context.
+ AMP_Validation_Utils::add_rest_api_fields();
+ $post_types_non_canonical = array_intersect(
+ get_post_types_by_support( 'amp' ),
+ get_post_types( array(
+ 'show_in_rest' => true,
+ ) )
+ );
+ $this->assert_rest_api_field_present( $post_types_non_canonical );
+
+ // Test in a Native AMP (canonical) context.
+ add_theme_support( 'amp' );
+ AMP_Validation_Utils::add_rest_api_fields();
+ $post_types_canonical = get_post_types_by_support( 'editor' );
+ $this->assert_rest_api_field_present( $post_types_canonical );
+ }
+
+ /**
+ * Asserts that the post types have the additional REST field.
+ *
+ * @covers AMP_Validation_Utils::add_rest_api_fields()
+ * @param array $post_types The post types that should have the REST field.
+ * @return void
+ */
+ public function assert_rest_api_field_present( $post_types ) {
+ foreach ( $post_types as $post_type ) {
+ $field = $GLOBALS['wp_rest_additional_fields'][ $post_type ][ AMP_Validation_Utils::VALIDITY_REST_FIELD_NAME ];
+ $this->assertEquals(
+ $field['schema'],
+ array(
+ 'description' => 'AMP validity status',
+ 'type' => 'object',
+ )
+ );
+ $this->assertEquals( $field['get_callback'], array( self::TESTED_CLASS, 'get_amp_validity_rest_field' ) );
+ }
+ }
+
+ /**
+ * Test get_amp_validity_rest_field.
+ *
+ * @covers AMP_Validation_Utils::get_amp_validity_rest_field()
+ */
+ public function test_rest_field_amp_validation() {
+ AMP_Validation_Utils::register_post_type();
+ $id = $this->factory()->post->create();
+ $this->assertNull( AMP_Validation_Utils::get_amp_validity_rest_field(
+ compact( 'id' ),
+ '',
+ new WP_REST_Request( 'GET' )
+ ) );
+
+ // Create an error custom post for the ID, so this will return the errors in the field.
+ $this->create_custom_post( amp_get_permalink( $id ) );
+
+ // Make sure capability check is honored.
+ $this->assertNull( AMP_Validation_Utils::get_amp_validity_rest_field(
+ compact( 'id' ),
+ '',
+ new WP_REST_Request( 'GET' )
+ ) );
+
+ wp_set_current_user( $this->factory()->user->create( array( 'role' => 'administrator' ) ) );
+
+ // GET request.
+ $field = AMP_Validation_Utils::get_amp_validity_rest_field(
+ compact( 'id' ),
+ '',
+ new WP_REST_Request( 'GET' )
+ );
+ $this->assertArrayHasKey( 'errors', $field );
+ $this->assertArrayHasKey( 'link', $field );
+ $this->assertEquals( $field['errors'], $this->get_mock_errors() );
+
+ // @todo Test successful loopback request to test.
+ // PUT request.
+ $field = AMP_Validation_Utils::get_amp_validity_rest_field(
+ compact( 'id' ),
+ '',
+ new WP_REST_Request( 'PUT' )
+ );
+ $this->assertArrayHasKey( 'errors', $field );
+ $this->assertArrayHasKey( 'link', $field );
+ $this->assertEquals( $field['errors'], $this->get_mock_errors() );
+ }
+
/**
* Creates and inserts a custom post.
*
+ * @param string|null $url The URL where there are errors, or null.
* @return int|WP_Error $error_post The ID of new custom post, or an error.
*/
- public function create_custom_post() {
+ public function create_custom_post( $url = null ) {
+ $url = isset( $url ) ? $url : home_url( '/' );
$content = wp_json_encode( $this->get_mock_errors() );
$encoded_errors = md5( $content );
$post_args = array(
@@ -1313,7 +1562,6 @@ public function create_custom_post() {
'post_status' => 'publish',
);
$error_post = wp_insert_post( wp_slash( $post_args ) );
- $url = home_url( '/' );
update_post_meta( $error_post, AMP_Validation_Utils::AMP_URL_META, $url );
return $error_post;
}