@yankeeinlondon/happy-wrapper
Version:
A functional API surface wrapping happy-dom
1,534 lines (1,510 loc) • 53.2 kB
JavaScript
// 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