@limetech/lime-elements
Version:
115 lines (114 loc) • 3.41 kB
JavaScript
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() === '';
};