UNPKG

@limetech/lime-elements

Version:
115 lines (114 loc) 3.41 kB
export const createRemoveEmptyParagraphsPlugin = (enabled = false) => { return () => { if (!enabled) { return (tree) => tree; } return (tree) => { pruneEmptyParagraphs(tree, null); return tree; }; }; }; const NBSP_REGEX = /\u00A0/g; const ZERO_WIDTH_HEX_CODES = ['200B', '200C', '200D', 'FEFF']; const MEANINGFUL_VOID_ELEMENTS = new Set([ 'audio', 'canvas', 'embed', 'iframe', 'img', 'input', 'object', 'svg', 'video', ]); const TREAT_AS_EMPTY_ELEMENTS = new Set(['br']); const stripZeroWidthCharacters = (text) => { let cleaned = text; for (const hexCode of ZERO_WIDTH_HEX_CODES) { const character = String.fromCodePoint(Number.parseInt(hexCode, 16)); cleaned = cleaned.split(character).join(''); } return cleaned; }; const pruneEmptyParagraphs = (node, parent) => { if (!node || typeof node !== 'object') { return; } if (node.type === 'element' && node.tagName === 'p' && parent && isParagraphEffectivelyEmpty(node) && Array.isArray(parent.children)) { const index = parent.children.indexOf(node); if (index !== -1) { parent.children.splice(index, 1); return; } } if (!Array.isArray(node.children) || node.children.length === 0) { return; } for (let i = node.children.length - 1; i >= 0; i--) { pruneEmptyParagraphs(node.children[i], node); } }; const isParagraphEffectivelyEmpty = (element) => { if (!Array.isArray(element.children) || element.children.length === 0) { return true; } return element.children.every((child) => isNodeEffectivelyEmpty(child)); }; const isNodeEffectivelyEmpty = (node) => { if (!node) { return true; } if (node.type === 'text') { return isWhitespace(typeof node.value === 'string' ? node.value : ''); } if (node.type === 'comment') { return true; } if (node.type === 'element') { const element = node; const tagName = element.tagName; if (typeof tagName !== 'string') { return true; } if (isMeaningfulElement(tagName)) { return false; } if (TREAT_AS_EMPTY_ELEMENTS.has(tagName)) { return true; } if (!Array.isArray(element.children) || element.children.length === 0) { return true; } return element.children.every((child) => isNodeEffectivelyEmpty(child)); } return true; }; /** * Returns true if the tag name belongs to a custom element (web component). * Per the HTML spec, custom element names must contain a hyphen. * @param tagName */ const isCustomElement = (tagName) => { return tagName.includes('-'); }; /** * Returns true for elements that are meaningful even without children. * Includes standard void elements (img, video, etc.) and custom elements * (web components), which render their own shadow DOM content. * @param tagName */ const isMeaningfulElement = (tagName) => { return MEANINGFUL_VOID_ELEMENTS.has(tagName) || isCustomElement(tagName); }; const isWhitespace = (value) => { if (!value) { return true; } const normalized = stripZeroWidthCharacters(value.replaceAll(NBSP_REGEX, ' ')); return normalized.trim() === ''; };