UNPKG

@yankeeinlondon/happy-wrapper

Version:
1,534 lines (1,510 loc) 53.2 kB
// src/attributes.ts import { pipe as pipe4 } from "fp-ts/lib/function.js"; // src/create.ts import { identity } from "fp-ts/lib/function.js"; import { Comment, Text, Window } from "happy-dom-without-node"; import { dasherize } from "native-dash"; // node_modules/.pnpm/callsites@4.1.0/node_modules/callsites/index.js function callsites() { const _prepareStackTrace = Error.prepareStackTrace; try { let result = []; Error.prepareStackTrace = (_, callSites) => { const callSitesWithoutCurrent = callSites.slice(1); result = callSitesWithoutCurrent; return callSitesWithoutCurrent; }; new Error().stack; return result; } finally { Error.prepareStackTrace = _prepareStackTrace; } } // src/diagnostics.ts import { flow as flow2, pipe as pipe3 } from "fp-ts/lib/function.js"; // src/nodes.ts import { pipe as pipe2 } from "fp-ts/lib/function.js"; // src/type-guards.ts import { isObject } from "inferred-types"; function isHappyWrapperError(err) { return typeof err === "object" && err.kind === "HappyWrapper"; } var isInspectionTuple = (thing) => { return Array.isArray(thing) && thing.length === 2 && typeof thing[0] === "string" && !Array.isArray(thing[1]); }; function isDocument(dom) { return typeof dom === "object" && dom !== null && !isElement(dom) && "body" in dom; } function isFragment(dom) { return typeof dom === "object" && dom !== null && !isElement(dom) && !isTextNode(dom) && !("body" in dom); } var nodeStartsWithElement = (node) => { return !!("firstElementChild" in node && "firstChild" in node && "firstElementChild" in node && node.firstChild === node.firstElementChild); }; var nodeEndsWithElement = (node) => { return "lastElementChild" in node && node.lastChild === node.lastElementChild; }; var nodeBoundedByElements = (node) => { return nodeStartsWithElement(node) && nodeEndsWithElement(node); }; var hasSingularElement = (node) => { return nodeBoundedByElements(node) && node.childNodes.length === 1; }; function isElement(el) { return typeof el === "object" && el !== null && "outerHTML" in el && el.nodeType === 1; } var isHtmlElement = (val) => { return isObject(val) && "nodeType" in val && val.nodeType === 1; }; var isElementLike = (container) => { if (isDocument(container)) { return container.body.childNodes.length === 1 && container.body.firstChild === container.body.firstElementChild; } return isFragment(container) && container.childNodes.length === 1 && container.firstChild === container.firstElementChild; }; function isTextNodeLike(node) { return (isDocument(node) || isFragment(node)) && node?.childNodes?.length === 1 && isTextNode(node.firstChild); } var isUpdateSignature = (args) => { return Array.isArray(args) && args.length === 3 && // && (typeof args[0] === 'string' || typeof args[0] === 'object') typeof args[1] === "number" && typeof args[2] === "number"; }; function isTextNode(node) { if (typeof node === "string") { const test = createFragment(node); return isTextNodeLike(test); } else { return typeof node === "object" && node !== null && !("firstElementChild" in node); } } var isContainer = (thing) => { return isDocument(thing) || isFragment(thing) || isElement(thing) || isTextNode(thing); }; var nodeChildrenAllElements = (node) => { return node.childNodes.every((n) => isElement(n)); }; var isNodeList = (val) => { return isObject(val) && "length" in val && typeof val.length === "number" && "item" in val && "forEach" in val; }; // src/utils.ts import { flow, pipe } from "fp-ts/lib/function.js"; var nodeTypeLookup = (type) => { switch (type) { case 1: { return "element"; } case 3: { return "text"; } case 8: { return "comment"; } case 11: { return "fragment"; } } }; var getNodeType = (node) => { if (typeof node === "string") { return "html"; } const byType = nodeTypeLookup(node.nodeType); if (byType) { return byType; } return isTextNode(node) ? "text" : isElement(node) ? "element" : isDocument(node) ? "document" : isFragment(node) ? "fragment" : "node"; }; var solveForNodeType = (_ = void 0) => { const solver = () => ({ solver: (s) => (node, parent) => { if (node === null) { throw new Error("Value passed into solver was NULL!"); } if (node === void 0) { throw new Error("Value passed into solver was UNDEFINED!"); } const type = getNodeType(node); if (type in s) { const fn = s[type]; return fn(node, parent); } else { if (type === "node" && "element" in s && isElement(node)) { const fn = s.element; return fn(node, parent); } else if (type === "node" && "text" in s && isTextNode(node)) { const fn = s.text; return fn(node); } throw new HappyMishap(`Problem finding "${type}" in solver.`, { name: `solveForNodeType(${type})` }); } } }); return { outputType: () => solver(), mirror: () => solver() }; }; function toHtml(node) { if (node === null) { return ""; } const n = Array.isArray(node) ? node : [node]; try { const results = n.map((i) => { const convert = solveForNodeType().outputType().solver({ html: (h) => h, text: (t) => t.textContent, comment: (h) => `<!-- ${h.textContent} -->`, element: (e) => e.outerHTML, node: (ne) => { if (isElement(ne)) { convert(ne); } if (isTextNode(ne)) { convert(ne); } throw new Error( `Unknown node type detected while converting to HTML: [ name: ${ne.nodeName}, type: ${ne.nodeType}, value: ${ne.nodeValue} ]` ); }, document: (d) => `<html>${d.head.hasChildNodes() ? d.head.outerHTML : ""}${d.body.outerHTML}</html>`, fragment: (f) => { return isElementLike(f) ? f.firstElementChild.outerHTML : f.childNodes.map((c) => convert(c, f)).join(""); } }); return convert(i); }); return results.join(""); } catch (error_) { const error = Array.isArray(node) ? new HappyMishap( `Problem converting an array of ${n.length} nodes [${n.map((i) => getNodeType(i)).join(", ")}] to HTML`, { name: "toHTML([...])", inspect: ["first node", node[0]], error: error_ } ) : new HappyMishap(`Problem converting "${getNodeType(node)}" to HTML!`, { name: "toHTML(getNodeType(node))", inspect: node, error: error_ }); throw error; } } function clone(container) { const clone2 = solveForNodeType().mirror().solver({ html: (h) => `${h}`, fragment: flow(toHtml, createFragment), document: (d) => { return createDocument(d.body.innerHTML, d.head.innerHTML); }, element: (e) => pipe(e, toHtml, createElement), node: (n) => { throw new HappyMishap("Can't clone an unknown node!", { inspect: n }); }, text: flow(toHtml, createTextNode), comment: flow(toHtml, createCommentNode) }); return clone2(container); } function safeString(str) { const node = createFragment(str); return node.textContent; } // src/nodes.ts var getChildren = (el) => { if (!el.hasChildNodes()) { return []; } const output = []; let child = el.firstChild; for (let idx = 0; idx < el.childNodes.length; idx++) { if (isElement(child) || isTextNode(child)) { output.push(child); } else if (isFragment(child) || isDocument(child)) { for (const fragChild of getChildren(child)) { output.push(fragChild); } } else { throw new HappyMishap( `unknown node type [${getNodeType( child )}] found while trying to convert children to an array`, { name: "getChildrenAsArray", inspect: child } ); } child = child.nextSibling; } return output; }; var getChildElements = (el) => { return getChildren(el).filter((c) => isElement(c)); }; var extract = (memory) => (node) => { if (memory) { memory.push(clone(node)); } return false; }; var placeholder = (memory, placeholder2) => (node) => { if (memory) { memory.push(clone(node)); } const el = placeholder2 ? placeholder2 : createElement("<placeholder></placeholder>"); addClass(...getClassList(node))(el); node.replaceWith(el); return node; }; var replaceElement = (newElement) => (oldElement) => { const parent = oldElement.parentElement; if (isElement(parent) || isTextNode(parent)) { parent.replaceChild(createElement(newElement), oldElement); } const newEl = typeof newElement === "string" ? createElement(newElement) : newElement; if (parent) { const children = getChildElements(parent); const childIdx = children.findIndex( (c) => toHtml(c) === toHtml(oldElement) ); const updated = (children || []).map( (c, i) => i === childIdx ? newEl : c ); parent.replaceChildren(...updated); } return newEl; }; var append = (...nodes) => { const n = nodes.flat(); return (parent) => { const result = solveForNodeType("text", "node", "comment").mirror().solver({ html: (h) => pipe2(h, createElement, append(...nodes), toHtml), element: (e) => { for (const i of n) { e.append(i); } return e; }, fragment: (f) => { for (const i of n) { f.append(i); } return f; }, document: (d) => { for (const i of n) { d.body.append(i); } return d; } })(isUpdateSignature(parent) ? parent[0] : parent); return result; }; }; var into = (parent) => (...content) => { const wrapped = !!(typeof parent === "string"); let normalizedParent = wrapped ? createFragment(parent) : isElement(parent) ? parent : parent ? parent : createFragment(); const flat = isUpdateSignature(content) ? [content[0]] : content.flatMap((c) => c); if (isTextNodeLike(normalizedParent)) { throw new HappyMishap( `The wrapper node -- when calling into() -- is wrapping a text node; this is not allowed. Parent HTML: "${toHtml( normalizedParent )}"`, { name: "into()", inspect: [["parent node", parent]] } ); } const contentHtml = flat.map((c) => toHtml(c)).join(""); const transient = createFragment(contentHtml); const parentHasChildElements = normalizedParent.childElementCount > 0; if (parentHasChildElements) { for (const c of getChildren(transient)) { normalizedParent.firstChild.appendChild(clone(c)); } } else { for (const c of getChildren(transient)) { normalizedParent.append(c); } } if (isUpdateSignature(content) && isElement(content[0])) { normalizedParent = isElementLike(normalizedParent) ? normalizedParent.firstElementChild : createElement(normalizedParent); content[0].replaceWith(normalizedParent); } return wrapped && !isUpdateSignature(content) ? toHtml(normalizedParent) : normalizedParent; }; var changeTagName = (tagName) => (...args) => { const node = args[0]; const replacer = (el, tagName2) => { const open = new RegExp(`^<${el.tagName.toLowerCase()}`); const close = new RegExp(`</${el.tagName.toLowerCase()}>$`); const newTag = toHtml(el).replace(open, `<${tagName2}`).replace(close, `</${tagName2}>`); if (el.parentNode && el.parentNode !== null) { el.parentNode.replaceChild(createNode(newTag), el); } return newTag; }; const areTheSame = (before2, after2) => before2.toLocaleLowerCase() === after2.toLocaleLowerCase(); return solveForNodeType().mirror().solver({ html: (h) => { const before2 = createFragment(h).firstElementChild.tagName; return areTheSame(before2, tagName) ? h : toHtml(replacer(createFragment(h).firstElementChild, tagName)); }, text: (t) => { throw new HappyMishap( "Attempt to change a tag name for a IText node. This is not allowed.", { inspect: t, name: "changeTagName(IText)" } ); }, comment: (t) => { throw new HappyMishap( "Attempt to change a tag name for a IComment node. This is not allowed.", { inspect: t, name: "changeTagName(IComment)" } ); }, node: (n) => { throw new HappyMishap( "Attempt to change a generic INode node's tag name. This is not allowed.", { inspect: n, name: "changeTagName(INode)" } ); }, element: (el) => areTheSame(el.tagName, tagName) ? el : replaceElement(replacer(el, tagName))(el), fragment: (f) => { if (f.firstElementChild) { if (f.firstElementChild.parentElement) { f.firstElementChild.replaceWith( changeTagName(tagName)(f.firstElementChild) ); } else { throw new HappyMishap( "Fragment's first child node must have a parent node to change the tag name!", { name: "changeTagName(Fragment)", inspect: f } ); } } else { throw new HappyMishap( "Fragment passed into changeTagName() has no elements as children!", { name: "changeTagName(Fragment)", inspect: f } ); } return f; }, document: (d) => { d.body.firstElementChild.replaceWith( changeTagName(tagName)(d.body.firstElementChild) ); const body = toHtml(d.body); const head = d.head.innerHTML; return createDocument(body, head); } })(node); }; var prepend = (prepend2) => (el) => { const p = typeof prepend2 === "string" ? createFragment(prepend2).firstChild : prepend2; el.prepend(p); return el; }; var before = (beforeNode) => (...afterNode) => { const outputIsHtml = typeof afterNode[0] === "string"; const beforeNormalized = typeof beforeNode === "string" ? createFragment(beforeNode).firstElementChild || createFragment(beforeNode).firstChild : createNode(beforeNode); const afterNormalized = typeof afterNode[0] === "string" ? createFragment(afterNode[0]) : isUpdateSignature(afterNode[0]) ? afterNode[0][0] : afterNode[0]; const invalidType = (n) => { throw new HappyMishap( `The before() utility was passed an invalid container type for the "after" node: ${getNodeType( n )}`, { name: `before(${getNodeType(beforeNormalized)})(${getNodeType(n)})`, inspect: n } ); }; const noParent = (n) => new HappyMishap( `the before() utility for depends on having a parent element in the "afterNode" as the parent's value must be mutated. If you do genuinely want this behavior then use a Fragment (or just HTML strings)`, { name: `before(${getNodeType(beforeNode)})(${getNodeType(n)})` } ); const node = solveForNodeType().mirror().solver({ html: (h) => pipe2(h, createFragment, before(beforeNode), toHtml), text: (t) => { if (!t.parentElement) { throw noParent(t); } t.before(beforeNormalized); return t; }, comment: (t) => { if (!t.parentElement) { throw noParent(t); } t.before(beforeNormalized); return t; }, node: (n) => invalidType(n), document: (d) => { d.body.prepend(beforeNormalized); return d; }, fragment: (f) => { f.prepend(beforeNormalized); return f; }, element: (el) => { if (el.parentElement) { el.before(beforeNormalized); return el; } else { throw noParent(el); } } })(afterNormalized); return outputIsHtml && !isUpdateSignature(afterNode) ? toHtml(node) : node; }; var after = (afterNode) => (beforeNode) => { const afterNormalized = typeof afterNode === "string" ? createFragment(afterNode).firstElementChild : afterNode; const invalidType = (n) => { throw new HappyMishap( `The after function was passed an invalid container type: ${getNodeType( n )}`, { name: `after(${getNodeType(beforeNode)})(invalid)` } ); }; return solveForNodeType().mirror().solver({ html: (h) => pipe2(h, createFragment, after(afterNode), toHtml), text: (t) => invalidType(t), comment: (t) => invalidType(t), node: (n) => invalidType(n), document: (d) => { d.body.append(afterNormalized); return d; }, fragment: (f) => { f.append(afterNormalized); return f; }, element: (el) => { if (el.parentElement) { el.after(afterNormalized); return el; } else { throw new HappyMishap( `the after() utility for depends on having a parent element in the "afterNode" as the parent's value must be mutated. If you do genuinely want this behavior then use a Fragment (or just HTML strings)`, { name: `after(${getNodeType(afterNode)})(IElement)` } ); } } })(beforeNode); }; var wrap = (...children) => (parent) => { return into(parent)(...children); }; // src/diagnostics.ts function descClass(n) { const list = getClassList(n); return list.length > 0 ? `{ ${list.join(" ")} }` : ""; } function descFrag(n) { const children = getChildren(n).map((i) => describeNode(i)); return isElementLike(n) ? `[el: ${n.firstElementChild.tagName.toLowerCase()}]${descClass}` : isTextNodeLike(n) ? `[text: ${n.textContent.slice(0, 4).replace(/\n+/g, "")}...]` : `[children: ${children.length > 0 ? `${children.join(", ")}` : "none"}]`; } var describeNode = (node) => { if (!node) { return node === null ? "[null]" : "undefined"; } else if (isUpdateSignature(node)) { return `UpdateSignature(${describeNode(node[0])})`; } else if (Array.isArray(node)) { return node.map((i) => describeNode(i)).join("\n"); } return solveForNodeType().outputType().solver({ html: (h) => pipe3(h, createFragment, describeNode), node: (n) => `node${descClass(n)}`, text: (t) => `text[${t.textContent.slice(0, 5).replace("\n", "")}...]`, comment: (t) => `comment[${t.textContent.slice(0, 5).replace("\n", "")}...]`, element: (e) => `element[${e.tagName.toLowerCase()}]${descClass(e)}`, fragment: (f) => `fragment${descFrag(f)}`, document: (d) => `doc[head: ${!!d.head}, body: ${!!d.body}]: ${describeNode( createFragment(d.body) )}` })(node); }; var inspect = (item, toJSON = false) => { const solver = Array.isArray(item) ? () => item.map((i) => describeNode(i)) : solveForNodeType().outputType().solver({ html: (h) => pipe3(h, createFragment, (f) => inspect(f)), fragment: (x) => ({ kind: "Fragment", children: `${x.children.length} / ${x.childNodes.length}`, ...x.childNodes.length > 1 ? { leadsWith: isElement(x.firstChild) ? "element" : isTextNode(x.firstChild) ? "text" : "other", endsWith: isElement(x.lastChild) ? "element" : isTextNode(x.lastChild) ? "text" : "other" } : { childNode: inspect(x.firstChild) }, content: x.textContent.length > 128 ? `${x.textContent.slice(0, 128)} ...` : x.textContent, childDetails: x.childNodes.map((i) => { try { return { html: toHtml(i), nodeType: getNodeType(i), hasParentElement: !!i.parentElement, hasParentNode: i.parentNode ? `${getNodeType(i.parentNode)} [type:${i.parentNode.nodeType}]` : false, childNodes: i.childNodes.length }; } catch { return "N/A"; } }), html: toHtml(x) }), document: (x) => ({ kind: "Document", headerChildren: x.head.childNodes?.length, bodyChildren: x.body.childNodes?.length, body: toHtml(x.body), children: `${x.body.children?.length} / ${x.body.childNodes?.length}`, childTextContent: x.body.childNodes.map((i) => i.textContent), childDetails: x.childNodes.map((i) => { try { return { html: toHtml(i), nodeType: getNodeType(i), hasParentElement: !!i.parentElement, hasParentNode: i.parentNode ? `${getNodeType(i.parentNode)} [type:${i.parentNode.nodeType}]` : false, childNodes: i.childNodes.length }; } catch { return "N/A"; } }) }), text: (x) => ({ kind: "IText node", textContent: x.textContent.length > 128 ? `${x.textContent.slice(0, 128)} ...` : x.textContent, children: x.childNodes?.length, childContent: x?.childNodes?.map((i) => i.textContent) || [] }), comment: (c) => ({ kind: "IComment node", textContent: c.textContent.length > 128 ? `${c.textContent.slice(0, 128)} ...` : c.textContent, children: c.childNodes?.length, childContent: c?.childNodes?.map((i) => i.textContent) || [] }), element: (x) => ({ kind: "IElement node", tagName: x.tagName, classes: getClassList(x), /** * in functions like wrap and pretty print, a "parent element" is provided * as a synthetic parent but if this flag indicates whether the flag has * a connected parent in a DOM tree. */ hasNaturalParent: !!x.parentElement, ...x.parentElement ? { parent: describeNode(x.parentElement) } : {}, textContent: x.textContent, children: `${x.children.length} / ${x.childNodes.length}`, childContent: x.childNodes?.map((i) => i.textContent) || [], childDetails: x?.childNodes?.map((i) => { try { return { html: toHtml(i), nodeType: getNodeType(i), hasParentElement: !!i.parentElement, hasParentNode: i.parentNode ? `${getNodeType(i.parentNode)} [type:${i.parentNode.nodeType}]` : false, childNodes: i.childNodes.length }; } catch { return "N/A"; } }) || [], html: truncate(512)(toHtml(x)) }), node: (n) => ({ kind: "INode (generic)", looksLike: isElement(n) ? "element" : isTextNode(n) ? "text" : "unknown", children: `${n.childNodes?.length}`, childContent: n?.childNodes?.map((i) => truncate(128)(i.textContent)) || [], html: truncate(512)(n.toString()) }) }); const result = isContainer(item) || typeof item === "string" ? solver(item) : { result: "not found", type: typeof item, ...typeof item === "object" && item !== null ? { keys: Object.keys(item) } : { value: JSON.stringify(item) } }; return toJSON ? JSON.stringify(result, null, 2) : result; }; var removeSpecialChars = (input) => input.replace(/\\t/g, "").replace(/\\n/g, "").trim(); var truncate = (maxLength) => (input) => input.slice(0, maxLength); var tree = (node) => { const summarize = (tree2) => { const summary = (n) => { let ts; switch (n.type) { case "text": { ts = { node: `t(${pipe3( n.node.textContent, removeSpecialChars, truncate(10) )})`, children: n.children.map((c) => summary(c)) }; break; } case "comment": { ts = { node: `c(${pipe3( n.node.textContent, removeSpecialChars, truncate(10) )})`, children: n.children.map((c) => summary(c)) }; break; } case "element": { const el = n.node; ts = { node: `el(${el.tagName.toLowerCase()})`, children: n.children.map((c) => summary(c)) }; break; } case "node": { const node2 = n.node; ts = { node: `n(${pipe3(node2.nodeName, removeSpecialChars, truncate(10)) || pipe3(node2.textContent, removeSpecialChars, truncate(10))}`, children: n.children.map((c) => summary(c)) }; break; } case "fragment": { const f = n.node; ts = { node: `frag(${f.firstElementChild ? f.firstElementChild.tagName.toLowerCase() : removeSpecialChars(f.textContent).trim().slice(0, 10)})`, children: n.children.map((c) => summary(c)) }; break; } case "document": { const d = n.node; ts = { node: `doc(${isElementLike(d) ? d.body.firstElementChild.tagName.toLowerCase() : d.textContent.slice(0, 10)})`, children: n.children.map((c) => summary(c)) }; break; } default: { ts = { node: `u(${n.toString()})`, children: n.children.map((c) => summary(c)) }; break; } } return ts; }; const recurse = (level = 0) => (node2) => { const indent = `${"".padStart(level * 6, " ")}${level > 0 ? `${level}) ` : "ROOT: "}`; return `${indent}${node2.node} ${node2.children.length > 0 ? "\u2935" : "\u21A4"} ${node2.children.map((i) => recurse(level + 1)(i))}`; }; return { ...tree2, summary: () => summary(tree2), toString: () => { const describe = summary(tree2); return ` Tree Summary: ${describe.node} ${"".padStart( 40, "-" )} ${recurse(0)(describe)}`; } }; }; const convert = (level) => solveForNodeType().outputType().solver({ html: flow2(createFragment, tree), text: (t) => summarize({ type: "text", node: t, level, children: t.childNodes.map((c) => convert(level + 1)(c)) }), comment: (c) => summarize({ type: "comment", node: c, level, children: [] }), element: (e) => summarize({ type: "element", node: e, level, children: [] }), node: (n) => summarize({ type: "node", node: n, level, children: [] }), fragment: (f) => summarize({ type: "fragment", node: f, level, children: f.childNodes.map((c) => convert(level + 1)(c)) }), document: (d) => summarize({ type: "document", node: d, level, children: d.childNodes.map((c) => convert(level + 1)(c)) }) }); return convert(0)(node); }; var siblings = (...content) => { return into()(...content); }; // src/errors.ts var HappyMishap = class extends Error { name = "HappyWrapper"; kind = "HappyWrapper"; trace = []; line; fn; file; structuredStack; toJSON() { return { name: this.name, message: this.message }; } toString() { return { name: this.name, message: this.message }; } constructor(message, options = {}) { super(); this.message = ` ${message}`; if (options.name) { this.name = `HappyWrapper::${options.name || "unknown"}`; } try { const sites = callsites(); this.structuredStack = (sites || []).slice(1).map((i) => { return { fn: i.getFunctionName() || i.getMethodName() || i.getFunction()?.name || void 0, line: i.getLineNumber() || void 0, file: i.getFileName() ? i.getFileName() : null }; }) || []; } catch { this.structuredStack = []; } this.fn = this.structuredStack[0].fn || ""; this.file = this.structuredStack[0].file || ""; this.line = this.structuredStack[0].line || null; if (isHappyWrapperError(options.error)) { this.name = `[file: ${this.file}, line: ${this.line}] HappyWrapper::${options.name || options.error.name}`; } if (options.error) { const name = options.error instanceof Error ? options.error.name.replace("HappyWrapper::", "") : "unknown"; const underlying = ` The underlying error message [${name}] was: ${options.error instanceof Error ? options.error.message : String(options.error)}`; this.message = `${this.message}${underlying}`; this.trace = [...this.trace, name]; } else { if (options.inspect) { const inspections = isInspectionTuple(options.inspect) ? [options.inspect] : Array.isArray(options.inspect) ? options.inspect : [options.inspect]; for (const [idx, i] of inspections.entries()) { const intro = isInspectionTuple(i) ? `${i[0]} ` : `${[idx]}: `; const container = isInspectionTuple(i) ? i[1] : i; this.message = `${this.message} ${intro}${JSON.stringify( inspect(container), null, 2 )}`; } } if (this.trace.length > 1) { this.message = `${this.message} Trace:${this.trace.map( (i, idx) => `${idx}. ${i}` )}`; } } this.message = `${this.message} `; for (const l of this.structuredStack) { this.message = l.file?.includes(".pnpm") ? this.message : `${this.message} - ${l.fn ? `${l.fn}() ` : ""}${l.file}:${l.line}`; } this.structuredStack = []; } }; // src/create.ts function createDocument(body, head) { const window = new Window(); const document = window.document; document.body.innerHTML = body; if (head) { document.head.innerHTML = head; } return document; } function createFragment(content) { const window = new Window(); const document = window.document; const fragment = document.createDocumentFragment(); if (content) { fragment.append(clone(content)); } return fragment; } var createNode = (node) => { const frag = createFragment(node); if (isElementLike(frag)) { return frag.firstElementChild; } else if (isTextNodeLike(frag)) { return frag.firstChild; } else { throw new HappyMishap( "call to createNode() couldn't be converted to IElement or IText node", { name: "createNode()", inspect: node } ); } }; function createTextNode(text) { if (!text) { return new Text(""); } const frag = createFragment(text); if (isTextNodeLike(frag)) { return frag.firstChild; } else { throw new HappyMishap( `The HTML passed in cannot be converted to a single text node: "${text}".`, { name: "createFragment(text)", inspect: frag } ); } } function createCommentNode(comment) { return new Comment(comment); } var createElement = (el, parent) => solveForNodeType().outputType().solver({ node: (n) => { if (isElement(n)) { return createElement(n); } else { throw new HappyMishap( "can't create an IElement from an INode node because it doesn't have a tagName property", { inspect: n } ); } }, html: (h) => { const frag = createFragment(h); if (isElementLike(frag)) { if (parent) { parent.append(frag.firstElementChild); return parent?.lastElementChild.cloneNode(); } return frag.firstElementChild; } else { throw new HappyMishap( "The HTML passed into createElement() is not convertible to a IElement node!", { name: "createElement(html)", inspect: frag } ); } }, element: identity, text: (t) => { throw new HappyMishap( "An IElement can not be created from a IText node because element's require a wrapping tag name!", { name: "createElement(text)", inspect: t } ); }, comment: (t) => { throw new HappyMishap( "An IElement can not be created from a IComment node because element's require a wrapping tag name!", { name: "createElement(comment)", inspect: t } ); }, fragment: (f) => { if (isElement(f.firstElementChild)) { return f.firstElementChild; } else { throw new HappyMishap( `Unable to create a IElement node from: ${toHtml(f)}`, { name: "createElement()" } ); } }, document: (d) => { if (isElementLike(d)) { if (parent) { throw new HappyMishap( "A Document and a parent IElement were passed into createElement. This is not a valid combination!" ); } return d.firstElementChild; } else { throw new HappyMishap( "Can not create an Element from passed in Document", { name: "createElement(document)", inspect: d } ); } } })(el); var renderClasses = (klasses) => { return klasses.map( ([selector, defn]) => ` ${selector} { ${Object.keys(defn).map((p) => ` ${dasherize(p)}: ${defn[p]};`).join("\n")} }` ).join("\n"); }; var createInlineStyle = (type = "text/css") => { const cssVariables = {}; const cssClasses = []; let isVueBlock = false; let isScoped = true; let vueLang = "css"; const api = { addCssVariable(prop, value, scope = ":root") { if (!(scope in cssVariables)) { cssVariables[scope] = []; } cssVariables[scope].push({ prop: prop.replace(/^--/, ""), value }); return api; }, addClassDefinition(selector, cb) { const classApi = { addChild: (child, defn) => { const childSelector = `${selector} ${child}`; cssClasses.push([childSelector, defn]); return classApi; }, addProps: (defn) => { cssClasses.push([selector, defn]); return classApi; } }; cb(classApi); return api; }, addCssVariables(dictionary, scope = ":root") { for (const p of Object.keys(dictionary)) { api.addCssVariable(p, dictionary[p], scope); } return api; }, convertToVueStyleBlock(lang, scoped = true) { vueLang = lang; isVueBlock = true; isScoped = scoped; return api; }, finish() { const setVariable = (scope, defn) => `${scope} { ${Object.keys(defn).map( (prop) => ` --${defn[prop].prop}: ${defn[prop].value}${String(defn.prop).endsWith(";") ? "" : ";"}` ).join("\n")} }`; let text = ""; for (const v of Object.keys(cssVariables)) { text = `${text}${setVariable(v, cssVariables[v])} `; } text = `${text}${renderClasses(cssClasses)}`; return isVueBlock ? createElement( `<style lang="${vueLang}"${isScoped ? " scoped" : ""}> ${text} </style>` ) : createElement(`<style type="${type}"> ${text} </style>`); } }; return api; }; // src/attributes.ts var setAttribute = (attr) => (value) => (node) => { const invalidNode = (n) => { throw new HappyMishap( `You can not use the setAttribute() utility on a node of type: "${getNodeType( n )}"`, { name: `setAttribute(${attr})(${value})(INVALID)` } ); }; const result = solveForNodeType().mirror().solver({ html: (h) => pipe4(h, createFragment, (f) => setAttribute(attr)(value)(f), toHtml), text: (t) => invalidNode(t), comment: (t) => invalidNode(t), node: (n) => invalidNode(n), fragment: (f) => { f.firstElementChild.setAttribute(attr, value); return f; }, document: (d) => { d.body.firstElementChild.setAttribute(attr, value); return d; }, element: (e) => { e.setAttribute(attr, value); return e; } })(node); return result; }; var getAttribute = (attr) => { return solveForNodeType("text", "node", "comment").outputType().solver({ html: (h) => pipe4(h, createFragment, getAttribute(attr)), fragment: (f) => f.firstElementChild.getAttribute(attr), document: (doc) => doc.body.firstElementChild.getAttribute(attr), element: (el) => el.getAttribute(attr) }); }; var getClass = getAttribute("class"); var setClass = setAttribute("class"); var getClassList = (container) => { if (!container) { return []; } return solveForNodeType().outputType().solver({ html: (h) => pipe4(h, createFragment, getClassList), document: (d) => getClass(d.body.firstElementChild)?.split(/\s+/) || [], fragment: (f) => getClass(f.firstElementChild)?.split(/\s+/) || [], element: (e) => getClass(e)?.split(/\s+/) || [], text: (n) => { throw new HappyMishap("Passed in a text node to getClassList!", { name: "getClassList", inspect: n }); }, comment: (n) => { throw new HappyMishap("Passed in a comment node to getClassList!", { name: "getClassList", inspect: n }); }, node: (n) => { throw new HappyMishap( "Passed in an unknown node type to getClassList!", { name: "getClassList", inspect: n } ); } })(container).filter(Boolean); }; var removeClass = (remove) => (doc) => { const current = getClass(doc)?.split(/\s+/g) || []; const toRemove = Array.isArray(remove) ? remove : [remove]; const resultantClassString = [ ...new Set(current.filter((c) => !toRemove.includes(c))) ].filter(Boolean).join(" "); return setClass(resultantClassString)(doc); }; var addClass = (...add) => (doc) => { const toAdd = Array.isArray(add) ? add.flat() : [add]; const currentClasses = getClass(doc)?.split(/\s+/g) || []; const resultantClasses = [ .../* @__PURE__ */ new Set([...currentClasses, ...toAdd]) ]; return setClass(resultantClasses.join(" ").trim())(doc); }; var addVueEvent = (event, value) => { return (el) => { const isHtml = typeof el === "string"; const bound = getAttribute("v-bind")(isHtml ? createElement(el) : el); const bind = bound ? bound.replace("}", `, ${event}: '${value}' }`) : `{ ${event}: "${value}" }`; const e2 = setAttribute("v-bind")(bind)(el); return isHtml ? toHtml(e2) : el; }; }; function hasFilterCallback(filters) { return typeof filters[0] === "function"; } var filterClasses = (...args) => (doc) => { const el = isDocument(doc) || isFragment(doc) ? doc.firstElementChild : isElement(doc) ? doc : null; if (!el) { throw new HappyMishap( "An invalid container was passed into filterClasses()!", { name: "filterClasses", inspect: doc } ); } const filters = hasFilterCallback(args) ? args.slice(1) : args; const cb = hasFilterCallback(args) ? args[0] : void 0; const classes = getClassList(el); const removed = []; for (const klass of classes) { const matched = !filters.every( (f) => typeof f === "string" ? f.trim() !== klass.trim() : !f.test(klass) ); if (matched) { removed.push(klass); } } setClass(classes.filter((k) => !removed.includes(k)).join(" "))(doc); if (cb) { cb(removed); } return doc; }; var hasParentElement = (node) => { const n = typeof node === "string" ? createNode(node) : node; return solveForNodeType().outputType().solver({ html: () => false, text: (t) => !!t.parentElement, comment: (t) => !!t.parentElement, element: (e) => !!e.parentElement, fragment: (f) => !!f.parentElement, document: () => true, node: (n2) => !!n2.parentElement })(n); }; var getParent = (node) => { return hasParentElement(node) ? node.parentElement : null; }; // src/select.ts var select = (node) => { const originIsHtml = typeof node === "string"; let rootNode = originIsHtml ? createFragment(node) : isElement(node) ? node : isDocument(node) || isFragment(node) ? node : void 0; if (!rootNode) { throw new HappyMishap( `Attempt to select() an invalid node type: ${getNodeType(node)}`, { name: "select(INode)", inspect: node } ); } const api = { type: () => { return originIsHtml ? "html" : getNodeType(rootNode); }, findAll: (sel) => { return sel ? rootNode.querySelectorAll(sel) : getChildElements(rootNode); }, findFirst: (sel, errorMsg) => { const result = rootNode.querySelector(sel); if (!result && errorMsg) { throw new HappyMishap( `${errorMsg}. The HTML from the selected DOM node is: ${toHtml( rootNode )}`, { name: "select.findFirst()", inspect: rootNode } ); } return result; }, append: (content) => { if (!content) { return api; } const nodes = Array.isArray(content) ? content.filter(Boolean) : [content]; rootNode.append(...nodes); return api; }, /** * Queries for the DOM node which matches the first DOM * node within the DOM tree which was selected and provides * a callback you can add to mutate this node. * * If no selector is provided, the root selection is used as the element * to update. * * Note: by default if the query selection doesn't resolve any nodes then * this is a no-op but you can optionally express that you'd like it to * throw an error by setting "errorIfFound" to `true` or as a string if * you want to state the error message. */ update: (selection, errorIfNotFound = false) => (mutate) => { const el = selection ? rootNode?.querySelector(selection) : isElement(rootNode) ? rootNode : rootNode.firstElementChild ? rootNode.firstElementChild : null; if (el) { let elReplacement; try { elReplacement = mutate( el, 0, 1 ); } catch (error) { throw new HappyMishap( `update(): the passed in callback to select(container).update('${selection}')(): mutate(${describeNode( el )}, 0, 1) ${error instanceof Error ? error.message : String(error)}.`, { name: `select(${typeof rootNode}).updateAll(${selection})(mutation fn)`, inspect: el } ); } if (elReplacement === false) { el.remove(); } else if (!isElement(elReplacement)) { throw new HappyMishap( `The return value for a call to select(${getNodeType( rootNode )}).update(${selection}) return an invalid value! Value return values are an IElement or false.`, { name: "select.update", inspect: el } ); } } else { if (errorIfNotFound) { throw new HappyMishap( errorIfNotFound === true ? `The selection "${selection}" was not found so the update() operation wasn't able to be run` : errorIfNotFound, { name: `select(${selection}).update(sel)`, inspect: ["parent node", rootNode] } ); } if (!selection) { throw new HappyMishap( `Call to select(container).update() was intended to target the root node of the selection but nothing was selected! This shouldn't really happen ... the rootNode's type is ${typeof rootNode}${typeof rootNode === "object" ? `, ${getNodeType(rootNode)} [element-like: ${isElementLike( rootNode )}, element: ${isElement(rootNode)}, children: ${rootNode.childNodes.length}]` : ""}` ); } } return api; }, /** * mutate _all_ nodes with given selector; if no selector provided then * all child nodes will be selected. * * Note: when passing in a selector you will get based on the DOM query but * if nothing is passed in then you'll get the array of `IElement` nodes which * are direct descendants of the root selector. */ updateAll: (selection) => (mutate) => { const elements = selection ? rootNode.querySelectorAll(selection) : getChildElements(rootNode); for (const [idx, el] of elements.entries()) { if (isElement(el)) { let elReplacement; try { elReplacement = mutate( el, idx, elements.length ); } catch (error) { throw new HappyMishap( `updateAll(): the passed in callback to select(container).updateAll('${selection}')(): mutate(${describeNode( el )}, ${idx} idx, ${elements.length} elements) ${error instanceof Error ? error.message : String(error)}.`, { name: `select(${typeof rootNode}).updateAll(${selection})(mutation fn)`, inspect: el } ); } if (elReplacement === false) { el.remove(); } else if (!isElement(elReplacement)) { throw new HappyMishap( `The return value from the "select(container).updateAll('${selection}')(${describeNode( el )}, ${idx} idx, ${elements.length} elements)" call was invalid! Valid return values are FALSE or an IElement but instead got: ${typeof elReplacement}.`, { name: "select().updateAll -> invalid return value" } ); } } else { throw new Error( `Ran into an unknown node type while running updateAll(): ${JSON.stringify( inspect(el), null, 2 )}` ); } } return api; }, /** * Maps over all IElement's which match the selection criteria (or all child * elements if no selection provided) and provides a callback hook which allows * a mutation to any data structure the caller wants. * * This method is non-destructive to the parent selection captured with the * call to `select(dom)` and returns the map results to the caller instead of * continuing the selection API surface. */ mapAll: (selection) => (mutate) => { const collection = []; const elements = selection ? rootNode.querySelectorAll(selection) : getChildElements(rootNode); for (const el of elements) { collection.push(mutate(clone(el))); } return collection; }, /** * Filters out `IElement` nodes out of the selected DOM tree which match * a particular DOM query. Also allows passing in an optional callback to * receive elements which were filtered out */ filterAll: (selection, cb) => { for (const el of rootNode?.querySelectorAll(selection) || []) { if (cb) { cb(el); } el.remove(); } return api; }, wrap: (wrapper, errMsg) => { try { const safeWrap = typeof wrapper === "string" ? createElement(wrapper) : wrapper; rootNode = wrap(rootNode)(safeWrap); return api; } catch (error) { if (isHappyWrapperError(error) || error instanceof Error) { error.message = errMsg ? `Error calling select.wrap(): ${errMsg} ${error.message}` : `Error calling select.wrap(): ${error.message}`; throw error; } throw error; } }, toContainer: () => { return originIsHtml ? toHtml(rootNode) : rootNode; } }; return api; }; // src/helpers.ts import { isString } from "inferred-types"; var hasSelector = (source, sel) => { let container; if (typeof source === "string") { if (source.includes("<html>")) { container = createDocument(source); } else { container = createFragment(source); } } else if (isDocument(source)) { container = source.body; } else if (isElement(source)) { container = source; } else { container = source; } const result = container.querySelector(sel); return result ? true : false; }; var traverseUpward = (node, sel) => { let el = isElement(node) ? node : isString(node) ? createElement(node) : void 0; if (!el) { throw new Error(`Unexpected node passed into traverseUpward: ${typeof node}`); } while (el.parentElement && !el.parentElement.matches(sel)) { el = el.parentElement; } if (el?.parentElement?.matches(sel)) { return el.parentElement; } else { const err = new Error(`Failed to find parent node of selector "${sel}" using traverseUpward() utility!`); err.name = "DomError"; err.element = el; err.selector = sel; throw err; } }; var peers = (input, sel) => { if (typeof input === "string") { return peers(createElement(input), sel); } if (isNodeList(input)) { input.forEach((el) => { if (isElement(el) && el.matches(sel)) { return el; } }); const err = new Error(`Failed to find a peer node matching "${sel}" using peers() utility over a NodeList!`); err.name = "DomError"; err.selector = sel; err.element = input; throw err; } if (isHtmlElement(input)) { let el = input; while (isElement(el?.nextElementSibling) && !el?.nextElementSibling?.matches(sel)) { el = el.nextElementSibling; } if (el.nextElementSibling?.matches(sel)) { return el.nextElementSibling; } else { const err = new Error(`Failed to find a peer node matching "${sel}" using peers() utility!`); err.name = "DomError"; err.element = el; err.selector = sel; throw err; } } throw new Error(`Unknown input type [${typeof input}] provided to peers()!`); }; // src/query.ts import { Never, isFunction, isString as isString2 } from "inferred-types"; var containerName = (node) => { return isElement(node) ? "IElement" : isDocument(node) ? "HappyDoc" : isString2(node) ? node.includes("<html>") ? "HappyDoc" : "IFragment" : "IElement"; }; var query = (node, sel, handling = "empty") => { let container; if (typeof node === "string") { if (node.includes("<html>")) { container = createDocument(node); } else { container = createFragment(node); } } else if (isDocument(node)) { container = node.body; } else if (isElement(node)) { container = node; } else { container = node; } const result = container.querySelector(sel); if (handling === "throw" && !result) { const err = new Error(`Failed to find an HTML element for the selector "${sel}"`); err.name = "DomError"; err.container = containerName(node); err.selector = sel; throw err; } return result !== void 0 ? result : handling === "empty" ? {} : handling === "undefined" ? void 0 : isFunction(handling) ? handling() : Never; }; var queryAll = (dom, sel) => { let container; if (typeof dom === "string") { if (dom.includes("<html>")) { container = createDocument(dom); } else { container = createFragment(dom); } } else if (isDocument(dom)) { cont