) => {
for (const listItem of root.querySelectorAll("li")) {
const meaningfulChildren = listItem.childNodes.filter(isMeaningfulNode);
if (meaningfulChildren.length !== 1) continue;
const child = meaningfulChildren[0];
if (!child || !isElement(child) || getTagName(child) !== "p") continue;
listItem.innerHTML = child.innerHTML;
}
};
const isInlineNode = (node: Node): boolean => {
if (node.nodeType === NodeType.TEXT_NODE || node.nodeType === NodeType.COMMENT_NODE) return true;
if (node.nodeType !== NodeType.ELEMENT_NODE) return false;
return inlineTags.has(getTagName(node)) && !hasBlockDescendant(node);
};
// Allow optional leading whitespace + LRM/RLM marks before the bullet character.
const PSEUDO_BULLET_LEAD = /^[\sāā]*[-ā¢*]\s+/;
const stripEmptyInlineWrappers = (html: string): string =>
html.replace(/<(strong|b|em|i|u|span)\b[^>]*>\s*<\/\1>/gi, "");
// Treat a bare
or one wrapped in an inline tag (e.g. `
` from
// the editor) as the segment separator.
const splitByBreaks = (html: string): string[] =>
html.split(/(?:<(?:strong|b|em|i|u|span)\b[^>]*>\s*
]*\/?>\s*<\/(?:strong|b|em|i|u|span)>)|
]*\/?>/gi);
const tryConvertPseudoBulletParagraph = (paragraphInnerHtml: string): string | null => {
const cleaned = stripEmptyInlineWrappers(paragraphInnerHtml);
if (!/
0) segments.push(trimmed);
}
if (segments.length < 2) return null;
if (!segments.every((segment) => PSEUDO_BULLET_LEAD.test(segment))) return null;
const items = segments.map((segment) => segment.replace(PSEUDO_BULLET_LEAD, ""));
return `${items.map((item) => `- ${item}
`).join("")}
`;
};
export const convertPseudoBulletParagraphs = (html: string): string =>
html.replace(/]*)>([\s\S]*?)<\/p>/gi, (full, _attrs, inner) => {
const converted = tryConvertPseudoBulletParagraph(inner);
return converted ?? full;
});
export const normalizeRichTextHtml = (html: string): string => {
const root = parse(html.trim(), { comment: false });
const normalized: string[] = [];
let inlineNodes: string[] = [];
normalizeMarkElements(root);
unwrapSingleParagraphListItems(root);
const flushInlineNodes = () => {
const inlineHtml = inlineNodes.join("").trim();
if (inlineHtml) normalized.push(`
${inlineHtml}
`);
inlineNodes = [];
};
for (const node of root.childNodes) {
const nodeHtml = node.toString();
if (isInlineNode(node)) {
inlineNodes.push(nodeHtml);
continue;
}
flushInlineNodes();
normalized.push(nodeHtml);
}
flushInlineNodes();
return normalized.join("");
};