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