UNPKG

@wordpress/interactivity

Version:

Package that provides a standard and simple way to handle the frontend interactivity of Gutenberg blocks.

190 lines (180 loc) 6.61 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.hydratedIslands = void 0; exports.toVdom = toVdom; var _preact = require("preact"); var _constants = require("./constants"); var _utils = require("./utils"); /** * External dependencies */ /** * Internal dependencies */ const ignoreAttr = `data-${_constants.directivePrefix}-ignore`; const islandAttr = `data-${_constants.directivePrefix}-interactive`; const fullPrefix = `data-${_constants.directivePrefix}-`; const namespaces = []; const currentNamespace = () => { var _namespaces; return (_namespaces = namespaces[namespaces.length - 1]) !== null && _namespaces !== void 0 ? _namespaces : null; }; const isObject = item => Boolean(item && typeof item === 'object' && item.constructor === Object); /** * This regex pattern must be kept in sync with the server-side implementation in * wp-includes/interactivity-api/class-wp-interactivity-api.php. * * The pattern validates directive attribute names to ensure consistency between * client and server processing. Invalid directive names (containing characters like * square brackets or colons) should be ignored by both client and server. * * @see https://github.com/WordPress/wordpress-develop/blob/trunk/src/wp-includes/interactivity-api/class-wp-interactivity-api.php */ const directiveParser = new RegExp(`^data-${_constants.directivePrefix}-` + // ${p} must be a prefix string, like 'wp'. // Match alphanumeric characters including hyphen-separated // segments. It excludes underscore intentionally to prevent confusion. // E.g., "custom-directive". '([a-z0-9]+(?:-[a-z0-9]+)*)' + // (Optional) Match '--' followed by any alphanumeric characters. It // excludes underscore intentionally to prevent confusion, but it can // contain multiple hyphens. E.g., "--custom-prefix--with-more-info". '(?:--([a-z0-9_-]+))?$', 'i' // Case insensitive. ); // Regular expression for reference parsing. It can contain a namespace before // the reference, separated by `::`, like `some-namespace::state.somePath`. // Namespaces can contain any alphanumeric characters, hyphens, underscores or // forward slashes. References don't have any restrictions. const nsPathRegExp = /^([\w_\/-]+)::(.+)$/; const hydratedIslands = exports.hydratedIslands = new WeakSet(); /** * Recursive function that transforms a DOM tree into vDOM. * * @param root The root element or node to start traversing on. * @return The resulting vDOM tree. */ function toVdom(root) { const nodesToRemove = new Set(); const nodesToReplace = new Set(); const treeWalker = document.createTreeWalker(root, 205 // TEXT + CDATA_SECTION + COMMENT + PROCESSING_INSTRUCTION + ELEMENT ); function walk(node) { const { nodeType } = node; // TEXT_NODE (3) if (nodeType === 3) { return node.data; } // CDATA_SECTION_NODE (4) if (nodeType === 4) { nodesToReplace.add(node); return node.nodeValue; } // COMMENT_NODE (8) || PROCESSING_INSTRUCTION_NODE (7) if (nodeType === 8 || nodeType === 7) { nodesToRemove.add(node); return null; } const elementNode = node; const { attributes } = elementNode; const localName = elementNode.localName; const props = {}; const children = []; const directives = []; let ignore = false; let island = false; for (let i = 0; i < attributes.length; i++) { const attributeName = attributes[i].name; const attributeValue = attributes[i].value; if (attributeName[fullPrefix.length] && attributeName.slice(0, fullPrefix.length) === fullPrefix) { if (attributeName === ignoreAttr) { ignore = true; } else { var _regexResult$, _regexResult$2; const regexResult = nsPathRegExp.exec(attributeValue); const namespace = (_regexResult$ = regexResult?.[1]) !== null && _regexResult$ !== void 0 ? _regexResult$ : null; let value = (_regexResult$2 = regexResult?.[2]) !== null && _regexResult$2 !== void 0 ? _regexResult$2 : attributeValue; try { const parsedValue = JSON.parse(value); value = isObject(parsedValue) ? parsedValue : value; } catch {} if (attributeName === islandAttr) { island = true; const islandNamespace = // eslint-disable-next-line no-nested-ternary typeof value === 'string' ? value : typeof value?.namespace === 'string' ? value.namespace : null; namespaces.push(islandNamespace); } else { directives.push([attributeName, namespace, value]); } } } else if (attributeName === 'ref') { continue; } props[attributeName] = attributeValue; } if (ignore && !island) { return [(0, _preact.h)(localName, { ...props, innerHTML: elementNode.innerHTML, __directives: { ignore: true } })]; } if (island) { hydratedIslands.add(elementNode); } if (directives.length) { props.__directives = directives.reduce((obj, [name, ns, value]) => { const directiveMatch = directiveParser.exec(name); if (directiveMatch === null) { (0, _utils.warn)(`Found malformed directive name: ${name}.`); return obj; } const prefix = directiveMatch[1] || ''; const suffix = directiveMatch[2] || null; obj[prefix] = obj[prefix] || []; obj[prefix].push({ namespace: ns !== null && ns !== void 0 ? ns : currentNamespace(), value: value, suffix }); return obj; }, {}); } if (localName === 'template') { props.content = [...elementNode.content.childNodes].map(childNode => toVdom(childNode)); } else { let child = treeWalker.firstChild(); if (child) { while (child) { const vnode = walk(child); if (vnode) { children.push(vnode); } child = treeWalker.nextSibling(); } treeWalker.parentNode(); } } // Restore previous namespace. if (island) { namespaces.pop(); } return (0, _preact.h)(localName, props, children); } const vdom = walk(treeWalker.currentNode); nodesToRemove.forEach(node => node.remove()); nodesToReplace.forEach(node => { var _nodeValue; return node.replaceWith(new window.Text((_nodeValue = node.nodeValue) !== null && _nodeValue !== void 0 ? _nodeValue : '')); }); return vdom; } //# sourceMappingURL=vdom.js.map