// Copyright: Ankitects Pty Ltd and contributors // License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html import { getSelection } from "./cross-browser"; export function nodeIsElement(node: Node): node is Element { return node.nodeType === Node.ELEMENT_NODE; } /** * In the web this is probably equivalent to `nodeIsElement`, but this is * convenient to convince Typescript. */ export function nodeIsCommonElement(node: Node): node is HTMLElement | SVGElement { return node instanceof HTMLElement || node instanceof SVGElement; } export function nodeIsText(node: Node): node is Text { return node.nodeType === Node.TEXT_NODE; } export function nodeIsComment(node: Node): node is Comment { return node.nodeType === Node.COMMENT_NODE; } // https://developer.mozilla.org/en-US/docs/Web/HTML/Block-level_elements export const BLOCK_ELEMENTS = [ "ADDRESS", "ARTICLE", "ASIDE", "BLOCKQUOTE", "DETAILS", "DIALOG", "DD", "DIV", "DL", "DT", "FIELDSET", "FIGCAPTION", "FIGURE", "FOOTER", "FORM", "H1", "H2", "H3", "H4", "H5", "H6", "HEADER", "HGROUP", "HR", "LI", "MAIN", "NAV", "OL", "P", "PRE", "SECTION", "TABLE", "UL", ]; export function hasBlockAttribute(element: Element): boolean { return element.hasAttribute("block") && element.getAttribute("block") !== "false"; } export function elementIsBlock(element: Element): boolean { return BLOCK_ELEMENTS.includes(element.tagName) || hasBlockAttribute(element); } export const NO_SPLIT_TAGS = ["RUBY"]; export function elementShouldNotBeSplit(element: Element): boolean { return elementIsBlock(element) || NO_SPLIT_TAGS.includes(element.tagName); } // https://developer.mozilla.org/en-US/docs/Glossary/Empty_element export const EMPTY_ELEMENTS = [ "AREA", "BASE", "BR", "COL", "EMBED", "HR", "IMG", "INPUT", "LINK", "META", "PARAM", "SOURCE", "TRACK", "WBR", ]; export function elementIsEmpty(element: Element): boolean { return EMPTY_ELEMENTS.includes(element.tagName); } export function nodeContainsInlineContent(node: Node): boolean { for (const child of node.childNodes) { if ( (nodeIsElement(child) && elementIsBlock(child)) || !nodeContainsInlineContent(child) ) { return false; } } // empty node is trivially inline return true; } /** * Consumes the input fragment. */ export function fragmentToString(fragment: DocumentFragment): string { const fragmentDiv = document.createElement("div"); fragmentDiv.appendChild(fragment); const html = fragmentDiv.innerHTML; return html; } const getAnchorParent = (predicate: (element: Element) => element is T) => (root: Node): T | null => { const anchor = getSelection(root)?.anchorNode; if (!anchor) { return null; } let anchorParent: T | null = null; let element = nodeIsElement(anchor) ? anchor : anchor.parentElement; while (element) { anchorParent = anchorParent || (predicate(element) ? element : null); element = element.parentElement; } return anchorParent; }; const isListItem = (element: Element): element is HTMLLIElement => window.getComputedStyle(element).display === "list-item"; const isParagraph = (element: Element): element is HTMLParamElement => element.tagName === "P"; const isBlockElement = ( element: Element, ): element is HTMLLIElement & HTMLParamElement => isListItem(element) || isParagraph(element); export const getListItem = getAnchorParent(isListItem); export const getParagraph = getAnchorParent(isParagraph); export const getBlockElement = getAnchorParent(isBlockElement);