← Files DefuddleARCHIVED FILE
skills/defuddle-web-content/references/source/src/utils.ts
3.49 KB · Oct 3, 2026 · 06:36 UTC
const NODE_TYPE = {
ELEMENT_NODE: 1,
ATTRIBUTE_NODE: 2,
TEXT_NODE: 3,
CDATA_SECTION_NODE: 4,
ENTITY_REFERENCE_NODE: 5,
ENTITY_NODE: 6,
PROCESSING_INSTRUCTION_NODE: 7,
COMMENT_NODE: 8,
DOCUMENT_NODE: 9,
DOCUMENT_TYPE_NODE: 10,
DOCUMENT_FRAGMENT_NODE: 11,
NOTATION_NODE: 12
};
export function isElement(node: Node): node is Element {
return node.nodeType === NODE_TYPE.ELEMENT_NODE;
}
export function isTextNode(node: Node): node is Text {
return node.nodeType === NODE_TYPE.TEXT_NODE;
}
export function isCommentNode(node: Node): node is Comment {
return node.nodeType === NODE_TYPE.COMMENT_NODE;
}
// Uses closest('svg') as fallback since linkedom may not set namespaceURI correctly
export function isSVGElement(el: Element): boolean {
return el.closest?.('svg') !== null || el.namespaceURI === 'http://www.w3.org/2000/svg';
}
export function getComputedStyle(element: Element): CSSStyleDeclaration | null {
const win = getWindow(element.ownerDocument);
if (!win || typeof win.getComputedStyle !== 'function') return null;
return win.getComputedStyle(element);
}
export function getWindow(doc: Document): Window | null {
// First try defaultView
if (doc.defaultView) {
return doc.defaultView;
}
// Then try ownerWindow
if ((doc as any).ownerWindow) {
return (doc as any).ownerWindow;
}
// Finally try to get window from document
if ((doc as any).window) {
return (doc as any).window;
}
return null;
}
export function textPreview(el: Element): string {
return (el.textContent || '').trim().substring(0, 200);
}
export function logDebug(debug: boolean, message: string, ...args: any[]): void {
if (debug) {
console.log('Defuddle:', message, ...args);
}
}
// CJK character ranges for use in regex character classes (BMP only)
export const CJK_CHAR_RANGES = '\\u3040-\\u309f\\u30a0-\\u30ff\\u3400-\\u4dbf\\u4e00-\\u9fff\\uf900-\\ufaff\\uac00-\\ud7af';
/**
* Canonicalize text for title/heading comparison: normalize smart quotes,
* dashes, ellipses, and whitespace; lowercase. Two strings that humans would
* read as "the same" should compare equal after this pass.
*/
export function normalizeText(text: string): string {
return text
.replace(/\u00A0/g, ' ')
.replace(/[\u2018\u2019\u201A\u201B]/g, "'")
.replace(/[\u2012\u2013\u2014\u2015]/g, '-')
.replace(/[\u201C\u201D\u201E\u201F]/g, '"')
.replace(/\u2026/g, '...')
.replace(/\s+/g, ' ')
.trim()
.toLowerCase();
}
/**
* Count words in text, handling CJK characters (Chinese, Japanese, Korean).
* CJK characters are counted individually since they don't use spaces between words.
* Non-CJK text is counted by splitting on whitespace.
*/
export function countWords(text: string): number {
if (!text) return 0;
let cjkCount = 0;
let wordCount = 0;
let inWord = false;
for (let i = 0; i < text.length; i++) {
const code = text.charCodeAt(i);
// Check for CJK character ranges (BMP only — Extension B+ are
// surrogate pairs and would need codePointAt, rare in practice)
if (
(code >= 0x3040 && code <= 0x309f) || // Hiragana
(code >= 0x30a0 && code <= 0x30ff) || // Katakana
(code >= 0x3400 && code <= 0x4dbf) || // CJK Extension A
(code >= 0x4e00 && code <= 0x9fff) || // CJK Unified Ideographs
(code >= 0xf900 && code <= 0xfaff) || // CJK Compatibility Ideographs
(code >= 0xac00 && code <= 0xd7af) // Korean Hangul
) {
cjkCount++;
inWord = false;
} else if (code <= 32) {
inWord = false;
} else if (!inWord) {
wordCount++;
inWord = true;
}
}
return cjkCount + wordCount;
}
SHA-256: d433ba8ad0264b61f4ff6c9451d5b108b6128014c78133abf11d6f5aa19f9102