← Files DefuddleARCHIVED FILE
skills/defuddle-web-content/references/source/src/utils/dom.ts
4.65 KB · Oct 2, 2026 · 00:35 UTC
/**
* Move all child nodes from source to target.
* Clears target first, then moves each child node from source.
*/
export function transferContent(source: Node, target: Node): void {
if ('replaceChildren' in target) {
(target as Element).replaceChildren();
} else {
while (target.firstChild) {
target.removeChild(target.firstChild);
}
}
while (source.firstChild) {
target.appendChild(source.firstChild);
}
}
/**
* Read an element's inner HTML.
*/
export function serializeHTML(el: { innerHTML: string }): string {
return el.innerHTML;
}
/**
* Decode HTML entities in a string (e.g. `&` → `&`).
* Uses a <textarea> element which is safe for entity decoding.
*/
export function decodeHTMLEntities(doc: Document, text: string): string {
const textarea = doc.createElement('textarea');
textarea.innerHTML = text;
return textarea.value;
}
/**
* Escape HTML special characters in a string.
*/
export function escapeHtml(text: string): string {
return text
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"');
}
/**
* Safely get an element's class name as a string.
* Handles SVG elements where className is an SVGAnimatedString.
*/
export function getClassName(el: Element): string {
return typeof el.className === 'string' ? el.className : el.getAttribute('class') || '';
}
/**
* Check if a class string contains responsive Tailwind show utilities
* (e.g. "sm:block", "lg:flex") indicating the element is visible at some breakpoints.
*/
const RESPONSIVE_SHOW_RE = /^(sm|md|lg|xl|2xl|min-\[|max-\[):(?:block|flex|grid|inline|table|contents)/;
export function hasResponsiveShowClass(className: string): boolean {
return className.split(/\s+/).some(t => RESPONSIVE_SHOW_RE.test(t));
}
/**
* Check if a URL uses a dangerous protocol (javascript:, blob:, non-image data:).
* Strips whitespace and control characters before checking.
*
* data: and blob: smuggle a whole document into an attribute, bypassing the
* script and event-handler stripping done elsewhere. Inline images are the one
* benign use of data:, so those are allowed and every other media type is
* rejected. Relative URLs (no scheme) are always allowed — the bbcode and
* comment builders pass them.
*/
export function isDangerousUrl(url: string, allowInlineImage: boolean = true): boolean {
const normalized = url.replace(/[\s\u0000-\u001F]+/g, '').toLowerCase();
if (normalized.startsWith('javascript:') || normalized.startsWith('blob:')) return true;
if (normalized.startsWith('data:')) {
return !(allowInlineImage && normalized.startsWith('data:image/'));
}
return false;
}
/**
* Check if an element belongs directly to an ancestor table,
* not to an intervening nested TABLE.
*/
export function isDirectTableChild(el: Node, ancestor: Node): boolean {
let parent = el.parentNode;
while (parent && parent !== ancestor) {
if (parent.nodeName === 'TABLE') return false;
parent = parent.parentNode;
}
return parent === ancestor;
}
/** Document order without relying on linkedom's cross-parent compareDocumentPosition. */
export function isNodeBefore(node: Node, other: Node): boolean {
if (node === other) return false;
const ancestors = (current: Node): Node[] => {
const path: Node[] = [];
for (let n: Node | null = current; n; n = n.parentNode) path.push(n);
return path;
};
const left = ancestors(node);
const right = ancestors(other);
// Detached subtrees have no shared document order.
if (left[left.length - 1] !== right[right.length - 1]) return false;
while (left.length && right.length && left[left.length - 1] === right[right.length - 1]) {
left.pop();
right.pop();
}
if (!left.length) return true; // node is an ancestor of other
if (!right.length) return false;
for (let sibling: Node | null = left[left.length - 1].nextSibling; sibling; sibling = sibling.nextSibling) {
if (sibling === right[right.length - 1]) return true;
}
return false;
}
/**
* Parse an HTML string into a DocumentFragment.
* Uses a <template> element when available (safer: no script execution,
* no resource loading). Falls back to a <div> for environments that
* don't support template.content (e.g. some server-side DOM libraries).
*/
export function parseHTML(doc: Document, html: string): DocumentFragment {
if (!html) return doc.createDocumentFragment();
const template = doc.createElement('template');
template.innerHTML = html;
if (template.content) {
return template.content;
}
// Fallback for environments without template.content support
const div = doc.createElement('div');
div.innerHTML = html;
const fragment = doc.createDocumentFragment();
while (div.firstChild) {
fragment.appendChild(div.firstChild);
}
return fragment;
}
SHA-256: fe1132705444b94fac218eb7ec1f8dfcd7bdd4b245349419578fce2c078d8744