UNPKG

@wordpress/blocks

Version:
432 lines (430 loc) 14 kB
var __create = Object.create; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __getProtoOf = Object.getPrototypeOf; var __hasOwnProp = Object.prototype.hasOwnProperty; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps( // If the importer is in node compatibility mode or this is not an ESM // file that has been converted to a CommonJS file using a Babel- // compatible transform (i.e. "__esModule" has not been set), then set // "default" to the CommonJS "module.exports" for node compatibility. isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target, mod )); var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); // packages/blocks/src/api/validation/index.js var validation_exports = {}; __export(validation_exports, { DecodeEntityParser: () => DecodeEntityParser, getMeaningfulAttributePairs: () => getMeaningfulAttributePairs, getNextNonWhitespaceToken: () => getNextNonWhitespaceToken, getNormalizedLength: () => getNormalizedLength, getNormalizedStyleValue: () => getNormalizedStyleValue, getStyleProperties: () => getStyleProperties, getTextPiecesSplitOnWhitespace: () => getTextPiecesSplitOnWhitespace, getTextWithCollapsedWhitespace: () => getTextWithCollapsedWhitespace, isClosedByToken: () => isClosedByToken, isEqualAttributesOfName: () => isEqualAttributesOfName, isEqualTagAttributePairs: () => isEqualTagAttributePairs, isEqualTokensOfType: () => isEqualTokensOfType, isEquivalentHTML: () => isEquivalentHTML, isEquivalentTextTokens: () => isEquivalentTextTokens, isValidBlockContent: () => isValidBlockContent, isValidCharacterReference: () => isValidCharacterReference, validateBlock: () => validateBlock }); module.exports = __toCommonJS(validation_exports); var import_simple_html_tokenizer = require("simple-html-tokenizer"); var import_es6 = __toESM(require("fast-deep-equal/es6")); var import_deprecated = __toESM(require("@wordpress/deprecated")); var import_html_entities = require("@wordpress/html-entities"); var import_logger = require("./logger"); var import_serializer = require("../serializer"); var import_registration = require("../registration"); var import_utils = require("../utils"); var identity = (x) => x; var REGEXP_WHITESPACE = /[\t\n\r\v\f ]+/g; var REGEXP_ONLY_WHITESPACE = /^[\t\n\r\v\f ]*$/; var REGEXP_STYLE_URL_TYPE = /^url\s*\(['"\s]*(.*?)['"\s]*\)$/; var BOOLEAN_ATTRIBUTES = [ "allowfullscreen", "allowpaymentrequest", "allowusermedia", "async", "autofocus", "autoplay", "checked", "controls", "default", "defer", "disabled", "download", "formnovalidate", "hidden", "ismap", "itemscope", "loop", "multiple", "muted", "nomodule", "novalidate", "open", "playsinline", "readonly", "required", "reversed", "selected", "typemustmatch" ]; var ENUMERATED_ATTRIBUTES = [ "autocapitalize", "autocomplete", "charset", "contenteditable", "crossorigin", "decoding", "dir", "draggable", "enctype", "formenctype", "formmethod", "http-equiv", "inputmode", "kind", "method", "preload", "scope", "shape", "spellcheck", "translate", "type", "wrap" ]; var MEANINGFUL_ATTRIBUTES = [ ...BOOLEAN_ATTRIBUTES, ...ENUMERATED_ATTRIBUTES ]; var TEXT_NORMALIZATIONS = [identity, getTextWithCollapsedWhitespace]; var REGEXP_NAMED_CHARACTER_REFERENCE = /^[\da-z]+$/i; var REGEXP_DECIMAL_CHARACTER_REFERENCE = /^#\d+$/; var REGEXP_HEXADECIMAL_CHARACTER_REFERENCE = /^#x[\da-f]+$/i; function isValidCharacterReference(text) { return REGEXP_NAMED_CHARACTER_REFERENCE.test(text) || REGEXP_DECIMAL_CHARACTER_REFERENCE.test(text) || REGEXP_HEXADECIMAL_CHARACTER_REFERENCE.test(text); } var DecodeEntityParser = class { /** * Returns a substitute string for an entity string sequence between `&` * and `;`, or undefined if no substitution should occur. * * @param {string} entity Entity fragment discovered in HTML. * * @return {string | undefined} Entity substitute value. */ parse(entity) { if (isValidCharacterReference(entity)) { return (0, import_html_entities.decodeEntities)("&" + entity + ";"); } } }; function getTextPiecesSplitOnWhitespace(text) { return text.trim().split(REGEXP_WHITESPACE); } function getTextWithCollapsedWhitespace(text) { return getTextPiecesSplitOnWhitespace(text).join(" "); } function getMeaningfulAttributePairs(token) { return token.attributes.filter((pair) => { const [key, value] = pair; return value || key.indexOf("data-") === 0 || MEANINGFUL_ATTRIBUTES.includes(key); }); } function isEquivalentTextTokens(actual, expected, logger = (0, import_logger.createLogger)()) { let actualChars = actual.chars; let expectedChars = expected.chars; for (let i = 0; i < TEXT_NORMALIZATIONS.length; i++) { const normalize = TEXT_NORMALIZATIONS[i]; actualChars = normalize(actualChars); expectedChars = normalize(expectedChars); if (actualChars === expectedChars) { return true; } } logger.warning( "Expected text `%s`, saw `%s`.", expected.chars, actual.chars ); return false; } function getNormalizedLength(value) { if (0 === parseFloat(value)) { return "0"; } if (value.indexOf(".") === 0) { return "0" + value; } return value; } function getNormalizedStyleValue(value) { const textPieces = getTextPiecesSplitOnWhitespace(value); const normalizedPieces = textPieces.map(getNormalizedLength); const result = normalizedPieces.join(" "); return result.replace(REGEXP_STYLE_URL_TYPE, "url($1)"); } function getStyleProperties(text) { const pairs = text.replace(/;?\s*$/, "").split(";").map((style) => { const [key, ...valueParts] = style.split(":"); const value = valueParts.join(":"); return [key.trim(), getNormalizedStyleValue(value.trim())]; }); return Object.fromEntries(pairs); } var isEqualAttributesOfName = { class: (actual, expected) => { const [actualPieces, expectedPieces] = [actual, expected].map( getTextPiecesSplitOnWhitespace ); const actualDiff = actualPieces.filter( (c) => !expectedPieces.includes(c) ); const expectedDiff = expectedPieces.filter( (c) => !actualPieces.includes(c) ); return actualDiff.length === 0 && expectedDiff.length === 0; }, style: (actual, expected) => { return (0, import_es6.default)( ...[actual, expected].map(getStyleProperties) ); }, // For each boolean attribute, mere presence of attribute in both is enough // to assume equivalence. ...Object.fromEntries( BOOLEAN_ATTRIBUTES.map((attribute) => [attribute, () => true]) ) }; function isEqualTagAttributePairs(actual, expected, logger = (0, import_logger.createLogger)()) { if (actual.length !== expected.length) { logger.warning( "Expected attributes %o, instead saw %o.", expected, actual ); return false; } const expectedAttributes = {}; for (let i = 0; i < expected.length; i++) { expectedAttributes[expected[i][0].toLowerCase()] = expected[i][1]; } for (let i = 0; i < actual.length; i++) { const [name, actualValue] = actual[i]; const nameLower = name.toLowerCase(); if (!expectedAttributes.hasOwnProperty(nameLower)) { logger.warning("Encountered unexpected attribute `%s`.", name); return false; } const expectedValue = expectedAttributes[nameLower]; const isEqualAttributes = isEqualAttributesOfName[nameLower]; if (isEqualAttributes) { if (!isEqualAttributes(actualValue, expectedValue)) { logger.warning( "Expected attribute `%s` of value `%s`, saw `%s`.", name, expectedValue, actualValue ); return false; } } else if (actualValue !== expectedValue) { logger.warning( "Expected attribute `%s` of value `%s`, saw `%s`.", name, expectedValue, actualValue ); return false; } } return true; } var isEqualTokensOfType = { StartTag: (actual, expected, logger = (0, import_logger.createLogger)()) => { if (actual.tagName !== expected.tagName && // Optimization: Use short-circuit evaluation to defer case- // insensitive check on the assumption that the majority case will // have exactly equal tag names. actual.tagName.toLowerCase() !== expected.tagName.toLowerCase()) { logger.warning( "Expected tag name `%s`, instead saw `%s`.", expected.tagName, actual.tagName ); return false; } return isEqualTagAttributePairs( ...[actual, expected].map(getMeaningfulAttributePairs), logger ); }, Chars: isEquivalentTextTokens, Comment: isEquivalentTextTokens }; function getNextNonWhitespaceToken(tokens) { let token; while (token = tokens.shift()) { if (token.type !== "Chars") { return token; } if (!REGEXP_ONLY_WHITESPACE.test(token.chars)) { return token; } } } function getHTMLTokens(html, logger = (0, import_logger.createLogger)()) { try { return new import_simple_html_tokenizer.Tokenizer(new DecodeEntityParser()).tokenize(html); } catch (e) { logger.warning("Malformed HTML detected: %s", html); } return null; } function isClosedByToken(currentToken, nextToken) { if (!currentToken.selfClosing) { return false; } if (nextToken && nextToken.tagName === currentToken.tagName && nextToken.type === "EndTag") { return true; } return false; } function isEquivalentHTML(actual, expected, logger = (0, import_logger.createLogger)()) { if (actual === expected) { return true; } const [actualTokens, expectedTokens] = [actual, expected].map( (html) => getHTMLTokens(html, logger) ); if (!actualTokens || !expectedTokens) { return false; } let actualToken, expectedToken; while (actualToken = getNextNonWhitespaceToken(actualTokens)) { expectedToken = getNextNonWhitespaceToken(expectedTokens); if (!expectedToken) { logger.warning( "Expected end of content, instead saw %o.", actualToken ); return false; } if (actualToken.type !== expectedToken.type) { logger.warning( "Expected token of type `%s` (%o), instead saw `%s` (%o).", expectedToken.type, expectedToken, actualToken.type, actualToken ); return false; } const isEqualTokens = isEqualTokensOfType[actualToken.type]; if (isEqualTokens && !isEqualTokens(actualToken, expectedToken, logger)) { return false; } if (isClosedByToken(actualToken, expectedTokens[0])) { getNextNonWhitespaceToken(expectedTokens); } else if (isClosedByToken(expectedToken, actualTokens[0])) { getNextNonWhitespaceToken(actualTokens); } } if (expectedToken = getNextNonWhitespaceToken(expectedTokens)) { logger.warning( "Expected %o, instead saw end of content.", expectedToken ); return false; } return true; } function validateBlock(block, blockTypeOrName = block.name) { const isFallbackBlock = block.name === (0, import_registration.getFreeformContentHandlerName)() || block.name === (0, import_registration.getUnregisteredTypeHandlerName)(); if (isFallbackBlock) { return [true, []]; } const logger = (0, import_logger.createQueuedLogger)(); const blockType = (0, import_utils.normalizeBlockType)(blockTypeOrName); let generatedBlockContent; try { generatedBlockContent = (0, import_serializer.getSaveContent)(blockType, block.attributes); } catch (error) { logger.error( "Block validation failed because an error occurred while generating block content:\n\n%s", error.toString() ); return [false, logger.getItems()]; } const isValid = isEquivalentHTML( block.originalContent, generatedBlockContent, logger ); if (!isValid) { logger.error( "Block validation failed for `%s` (%o).\n\nContent generated by `save` function:\n\n%s\n\nContent retrieved from post body:\n\n%s", blockType.name, blockType, generatedBlockContent, block.originalContent ); } return [isValid, logger.getItems()]; } function isValidBlockContent(blockTypeOrName, attributes, originalBlockContent) { (0, import_deprecated.default)("isValidBlockContent introduces opportunity for data loss", { since: "12.6", plugin: "Gutenberg", alternative: "validateBlock" }); const blockType = (0, import_utils.normalizeBlockType)(blockTypeOrName); const block = { name: blockType.name, attributes, innerBlocks: [], originalContent: originalBlockContent }; const [isValid] = validateBlock(block, blockType); return isValid; } // Annotate the CommonJS export names for ESM import in node: 0 && (module.exports = { DecodeEntityParser, getMeaningfulAttributePairs, getNextNonWhitespaceToken, getNormalizedLength, getNormalizedStyleValue, getStyleProperties, getTextPiecesSplitOnWhitespace, getTextWithCollapsedWhitespace, isClosedByToken, isEqualAttributesOfName, isEqualTagAttributePairs, isEqualTokensOfType, isEquivalentHTML, isEquivalentTextTokens, isValidBlockContent, isValidCharacterReference, validateBlock }); //# sourceMappingURL=index.js.map