@wordpress/blocks
Version:
Block API for WordPress.
432 lines (430 loc) • 14 kB
JavaScript
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