vanilla 18.0

This commit is contained in:
Ernad Husremovic 2025-10-08 10:48:09 +02:00
parent 5454004ff9
commit d7f6d2725e
979 changed files with 428093 additions and 0 deletions

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,495 @@
/** @odoo-module */
import { isInstanceOf } from "../hoot_dom_utils";
/**
* @typedef {{
* animationFrame?: boolean;
* blockTimers?: boolean;
* }} AdvanceTimeOptions
*
* @typedef {{
* message?: string | () => string;
* timeout?: number;
* }} WaitOptions
*/
//-----------------------------------------------------------------------------
// Global
//-----------------------------------------------------------------------------
const {
cancelAnimationFrame,
clearInterval,
clearTimeout,
Error,
Math: { ceil: $ceil, floor: $floor, max: $max, min: $min },
Number,
performance,
Promise,
requestAnimationFrame,
setInterval,
setTimeout,
} = globalThis;
/** @type {Performance["now"]} */
const $performanceNow = performance.now.bind(performance);
//-----------------------------------------------------------------------------
// Internal
//-----------------------------------------------------------------------------
/**
* @param {number} id
*/
function animationToId(id) {
return ID_PREFIX.animation + String(id);
}
function getNextTimerValues() {
/** @type {[number, () => any, string] | null} */
let timerValues = null;
for (const [internalId, [callback, init, delay]] of timers.entries()) {
const timeout = init + delay;
if (!timerValues || timeout < timerValues[0]) {
timerValues = [timeout, callback, internalId];
}
}
return timerValues;
}
/**
* @param {string} id
*/
function idToAnimation(id) {
return Number(id.slice(ID_PREFIX.animation.length));
}
/**
* @param {string} id
*/
function idToInterval(id) {
return Number(id.slice(ID_PREFIX.interval.length));
}
/**
* @param {string} id
*/
function idToTimeout(id) {
return Number(id.slice(ID_PREFIX.timeout.length));
}
/**
* @param {number} id
*/
function intervalToId(id) {
return ID_PREFIX.interval + String(id);
}
/**
* Converts a given value to a **natural number** (or 0 if failing to do so).
*
* @param {unknown} value
*/
function parseNat(value) {
return $max($floor(Number(value)), 0) || 0;
}
function now() {
return (frozen ? 0 : $performanceNow()) + timeOffset;
}
/**
* @param {number} id
*/
function timeoutToId(id) {
return ID_PREFIX.timeout + String(id);
}
class HootTimingError extends Error {
name = "HootTimingError";
}
const ID_PREFIX = {
animation: "a_",
interval: "i_",
timeout: "t_",
};
/** @type {Map<string, [() => any, number, number]>} */
const timers = new Map();
let allowTimers = false;
let frozen = false;
let frameDelay = 1000 / 60;
let nextDummyId = 1;
let timeOffset = 0;
//-----------------------------------------------------------------------------
// Exports
//-----------------------------------------------------------------------------
/**
* @param {number} [frameCount]
* @param {AdvanceTimeOptions} [options]
*/
export function advanceFrame(frameCount, options) {
return advanceTime(frameDelay * parseNat(frameCount), options);
}
/**
* Advances the current time by the given amount of milliseconds. This will
* affect all timeouts, intervals, animations and date objects.
*
* It returns a promise resolved after all related callbacks have been executed.
*
* @param {number} ms
* @param {AdvanceTimeOptions} [options]
* @returns {Promise<number>} time consumed by timers (in ms).
*/
export async function advanceTime(ms, options) {
ms = parseNat(ms);
if (options?.blockTimers) {
allowTimers = false;
}
const targetTime = now() + ms;
let remaining = ms;
/** @type {ReturnType<typeof getNextTimerValues>} */
let timerValues;
while ((timerValues = getNextTimerValues()) && timerValues[0] <= targetTime) {
const [timeout, handler, id] = timerValues;
const diff = timeout - now();
if (diff > 0) {
timeOffset += $min(remaining, diff);
remaining = $max(remaining - diff, 0);
}
if (timers.has(id)) {
handler(timeout);
}
}
if (remaining > 0) {
timeOffset += remaining;
}
if (options?.animationFrame ?? true) {
await animationFrame();
}
allowTimers = true;
return ms;
}
/**
* Returns a promise resolved after the next animation frame, typically allowing
* Owl components to render.
*
* @returns {Promise<void>}
*/
export function animationFrame() {
return new Promise((resolve) => requestAnimationFrame(() => setTimeout(resolve)));
}
/**
* Cancels all current timeouts, intervals and animations.
*/
export function cancelAllTimers() {
for (const id of timers.keys()) {
if (id.startsWith(ID_PREFIX.animation)) {
globalThis.cancelAnimationFrame(idToAnimation(id));
} else if (id.startsWith(ID_PREFIX.interval)) {
globalThis.clearInterval(idToInterval(id));
} else if (id.startsWith(ID_PREFIX.timeout)) {
globalThis.clearTimeout(idToTimeout(id));
}
}
}
export function cleanupTime() {
allowTimers = false;
frozen = false;
cancelAllTimers();
// Wait for remaining async code to run
return delay();
}
/**
* Returns a promise resolved after a given amount of milliseconds (default to 0).
*
* @param {number} [duration]
* @returns {Promise<void>}
* @example
* await delay(1000); // waits for 1 second
*/
export function delay(duration) {
return new Promise((resolve) => setTimeout(resolve, duration));
}
export function freezeTime() {
frozen = true;
}
export function unfreezeTime() {
frozen = false;
}
export function getTimeOffset() {
return timeOffset;
}
export function isTimeFrozen() {
return frozen;
}
/**
* Returns a promise resolved after the next microtask tick.
*
* @returns {Promise<void>}
*/
export function microTick() {
return new Promise(queueMicrotask);
}
/** @type {typeof cancelAnimationFrame} */
export function mockedCancelAnimationFrame(handle) {
if (!frozen) {
cancelAnimationFrame(handle);
}
timers.delete(animationToId(handle));
}
/** @type {typeof clearInterval} */
export function mockedClearInterval(intervalId) {
if (!frozen) {
clearInterval(intervalId);
}
timers.delete(intervalToId(intervalId));
}
/** @type {typeof clearTimeout} */
export function mockedClearTimeout(timeoutId) {
if (!frozen) {
clearTimeout(timeoutId);
}
timers.delete(timeoutToId(timeoutId));
}
/** @type {typeof requestAnimationFrame} */
export function mockedRequestAnimationFrame(callback) {
if (!allowTimers) {
return 0;
}
function handler() {
mockedCancelAnimationFrame(handle);
return callback(now());
}
const animationValues = [handler, now(), frameDelay];
const handle = frozen ? nextDummyId++ : requestAnimationFrame(handler);
const internalId = animationToId(handle);
timers.set(internalId, animationValues);
return handle;
}
/** @type {typeof setInterval} */
export function mockedSetInterval(callback, ms, ...args) {
if (!allowTimers) {
return 0;
}
ms = parseNat(ms);
function handler() {
if (allowTimers) {
intervalValues[1] = $max(now(), intervalValues[1] + ms);
} else {
mockedClearInterval(intervalId);
}
return callback(...args);
}
const intervalValues = [handler, now(), ms];
const intervalId = frozen ? nextDummyId++ : setInterval(handler, ms);
const internalId = intervalToId(intervalId);
timers.set(internalId, intervalValues);
return intervalId;
}
/** @type {typeof setTimeout} */
export function mockedSetTimeout(callback, ms, ...args) {
if (!allowTimers) {
return 0;
}
ms = parseNat(ms);
function handler() {
mockedClearTimeout(timeoutId);
return callback(...args);
}
const timeoutValues = [handler, now(), ms];
const timeoutId = frozen ? nextDummyId++ : setTimeout(handler, ms);
const internalId = timeoutToId(timeoutId);
timers.set(internalId, timeoutValues);
return timeoutId;
}
export function resetTimeOffset() {
timeOffset = 0;
}
/**
* Calculates the amount of time needed to run all current timeouts, intervals and
* animations, and then advances the current time by that amount.
*
* @see {@link advanceTime}
* @param {AdvanceTimeOptions} [options]
* @returns {Promise<number>} time consumed by timers (in ms).
*/
export function runAllTimers(options) {
if (!timers.size) {
return 0;
}
const endts = $max(...[...timers.values()].map(([, init, delay]) => init + delay));
return advanceTime($ceil(endts - now()), options);
}
/**
* Sets the current frame rate (in fps) used by animation frames (default to 60fps).
*
* @param {number} frameRate
*/
export function setFrameRate(frameRate) {
frameRate = parseNat(frameRate);
if (frameRate < 1 || frameRate > 1000) {
throw new HootTimingError("frame rate must be an number between 1 and 1000");
}
frameDelay = 1000 / frameRate;
}
export function setupTime() {
allowTimers = true;
}
/**
* Returns a promise resolved after the next task tick.
*
* @returns {Promise<void>}
*/
export function tick() {
return delay();
}
/**
* Returns a promise fulfilled when the given `predicate` returns a truthy value,
* with the value of the promise being the return value of the `predicate`.
*
* The `predicate` is run once initially, and then on each animation frame until
* it succeeds or fail.
*
* The promise automatically rejects after a given `timeout` (defaults to 5 seconds).
*
* @template T
* @param {(last: boolean) => T} predicate
* @param {WaitOptions} [options]
* @returns {Promise<T>}
* @example
* await waitUntil(() => []); // -> []
* @example
* const button = await waitUntil(() => queryOne("button:visible"));
* button.click();
*/
export async function waitUntil(predicate, options) {
await Promise.resolve();
// Early check before running the loop
const result = predicate(false);
if (result) {
return result;
}
const timeout = $floor(options?.timeout ?? 200);
const maxFrameCount = $ceil(timeout / frameDelay);
let frameCount = 0;
let handle;
return new Promise((resolve, reject) => {
function runCheck() {
const isLast = ++frameCount >= maxFrameCount;
const result = predicate(isLast);
if (result) {
resolve(result);
} else if (!isLast) {
handle = requestAnimationFrame(runCheck);
} else {
let message =
options?.message || `'waitUntil' timed out after %timeout% milliseconds`;
if (typeof message === "function") {
message = message();
}
if (isInstanceOf(message, Error)) {
reject(message);
} else {
reject(new HootTimingError(message.replace("%timeout%", String(timeout))));
}
}
}
handle = requestAnimationFrame(runCheck);
}).finally(() => {
cancelAnimationFrame(handle);
});
}
/**
* Manually resolvable and rejectable promise. It introduces 2 new methods:
* - {@link reject} rejects the deferred with the given reason;
* - {@link resolve} resolves the deferred with the given value.
*
* @template [T=unknown]
*/
export class Deferred extends Promise {
/** @type {typeof Promise.resolve<T>} */
_resolve;
/** @type {typeof Promise.reject<T>} */
_reject;
/**
* @param {(resolve: (value?: T) => any, reject: (reason?: any) => any) => any} [executor]
*/
constructor(executor) {
let _resolve, _reject;
super(function deferredResolver(resolve, reject) {
_resolve = resolve;
_reject = reject;
executor?.(_resolve, _reject);
});
this._resolve = _resolve;
this._reject = _reject;
}
/**
* @param {any} [reason]
*/
async reject(reason) {
return this._reject(reason);
}
/**
* @param {T} [value]
*/
async resolve(value) {
return this._resolve(value);
}
}

View file

@ -0,0 +1,110 @@
/** @odoo-module alias=@odoo/hoot-dom default=false */
import * as dom from "./helpers/dom";
import * as events from "./helpers/events";
import * as time from "./helpers/time";
import { interactor } from "./hoot_dom_utils";
/**
* @typedef {import("./helpers/dom").Dimensions} Dimensions
* @typedef {import("./helpers/dom").FormatXmlOptions} FormatXmlOptions
* @typedef {import("./helpers/dom").Position} Position
* @typedef {import("./helpers/dom").QueryOptions} QueryOptions
* @typedef {import("./helpers/dom").QueryRectOptions} QueryRectOptions
* @typedef {import("./helpers/dom").QueryTextOptions} QueryTextOptions
* @typedef {import("./helpers/dom").Target} Target
*
* @typedef {import("./helpers/events").DragHelpers} DragHelpers
* @typedef {import("./helpers/events").DragOptions} DragOptions
* @typedef {import("./helpers/events").EventType} EventType
* @typedef {import("./helpers/events").FillOptions} FillOptions
* @typedef {import("./helpers/events").InputValue} InputValue
* @typedef {import("./helpers/events").KeyStrokes} KeyStrokes
* @typedef {import("./helpers/events").PointerOptions} PointerOptions
*/
export {
formatXml,
getActiveElement,
getFocusableElements,
getNextFocusableElement,
getParentFrame,
getPreviousFocusableElement,
isDisplayed,
isEditable,
isFocusable,
isInDOM,
isInViewPort,
isScrollable,
isVisible,
matches,
queryAll,
queryAllAttributes,
queryAllProperties,
queryAllRects,
queryAllTexts,
queryAllValues,
queryAny,
queryAttribute,
queryFirst,
queryOne,
queryRect,
queryText,
queryValue,
} from "./helpers/dom";
export { on } from "./helpers/events";
export {
animationFrame,
cancelAllTimers,
Deferred,
delay,
freezeTime,
unfreezeTime,
microTick,
setFrameRate,
tick,
waitUntil,
} from "./helpers/time";
//-----------------------------------------------------------------------------
// Interactors
//-----------------------------------------------------------------------------
// DOM
export const observe = interactor("query", dom.observe);
export const waitFor = interactor("query", dom.waitFor);
export const waitForNone = interactor("query", dom.waitForNone);
// Events
export const check = interactor("interaction", events.check);
export const clear = interactor("interaction", events.clear);
export const click = interactor("interaction", events.click);
export const dblclick = interactor("interaction", events.dblclick);
export const drag = interactor("interaction", events.drag);
export const edit = interactor("interaction", events.edit);
export const fill = interactor("interaction", events.fill);
export const hover = interactor("interaction", events.hover);
export const keyDown = interactor("interaction", events.keyDown);
export const keyUp = interactor("interaction", events.keyUp);
export const leave = interactor("interaction", events.leave);
export const manuallyDispatchProgrammaticEvent = interactor("interaction", events.dispatch);
export const middleClick = interactor("interaction", events.middleClick);
export const pointerDown = interactor("interaction", events.pointerDown);
export const pointerUp = interactor("interaction", events.pointerUp);
export const press = interactor("interaction", events.press);
export const resize = interactor("interaction", events.resize);
export const rightClick = interactor("interaction", events.rightClick);
export const scroll = interactor("interaction", events.scroll);
export const select = interactor("interaction", events.select);
export const setInputFiles = interactor("interaction", events.setInputFiles);
export const setInputRange = interactor("interaction", events.setInputRange);
export const uncheck = interactor("interaction", events.uncheck);
export const unload = interactor("interaction", events.unload);
// Time
export const advanceFrame = interactor("time", time.advanceFrame);
export const advanceTime = interactor("time", time.advanceTime);
export const runAllTimers = interactor("time", time.runAllTimers);
// Debug
export { exposeHelpers } from "./hoot_dom_utils";

View file

@ -0,0 +1,426 @@
/** @odoo-module */
/**
* @typedef {ArgumentPrimitive | `${ArgumentPrimitive}[]` | null} ArgumentType
*
* @typedef {"any"
* | "bigint"
* | "boolean"
* | "error"
* | "function"
* | "integer"
* | "node"
* | "number"
* | "object"
* | "regex"
* | "string"
* | "symbol"
* | "undefined"} ArgumentPrimitive
*
* @typedef {[string, any[], any]} InteractionDetails
*
* @typedef {"interaction" | "query" | "server" | "time"} InteractionType
*/
/**
* @template T
* @typedef {T | Iterable<T>} MaybeIterable
*/
/**
* @template T
* @typedef {T | PromiseLike<T>} MaybePromise
*/
//-----------------------------------------------------------------------------
// Global
//-----------------------------------------------------------------------------
const {
Array: { isArray: $isArray },
matchMedia,
navigator: { userAgent: $userAgent },
Object: { assign: $assign, getPrototypeOf: $getPrototypeOf },
RegExp,
SyntaxError,
} = globalThis;
const $toString = Object.prototype.toString;
//-----------------------------------------------------------------------------
// Internal
//-----------------------------------------------------------------------------
/**
* @template {(...args: any[]) => any} T
* @param {InteractionType} type
* @param {T} fn
* @param {string} name
* @returns {T}
*/
function makeInteractorFn(type, fn, name) {
return {
[name](...args) {
const result = fn(...args);
if (isInstanceOf(result, Promise)) {
for (let i = 0; i < args.length; i++) {
if (isInstanceOf(args[i], Promise)) {
// Get promise result for async arguments if possible
args[i].then((result) => (args[i] = result));
}
}
return result.then((promiseResult) =>
dispatchInteraction(type, name, args, promiseResult)
);
} else {
return dispatchInteraction(type, name, args, result);
}
},
}[name];
}
function polyfillIsError(value) {
return $toString.call(value) === "[object Error]";
}
const GRAYS = {
100: "#f1f5f9",
200: "#e2e8f0",
300: "#cbd5e1",
400: "#94a3b8",
500: "#64748b",
600: "#475569",
700: "#334155",
800: "#1e293b",
900: "#0f172a",
};
const COLORS = {
default: {
// Generic colors
black: "#000000",
white: "#ffffff",
// Grays
"gray-100": GRAYS[100],
"gray-200": GRAYS[200],
"gray-300": GRAYS[300],
"gray-400": GRAYS[400],
"gray-500": GRAYS[500],
"gray-600": GRAYS[600],
"gray-700": GRAYS[700],
"gray-800": GRAYS[800],
"gray-900": GRAYS[900],
},
light: {
// Generic colors
primary: "#714b67",
secondary: "#74b4b9",
amber: "#f59e0b",
"amber-900": "#fef3c7",
blue: "#3b82f6",
"blue-900": "#dbeafe",
cyan: "#0891b2",
"cyan-900": "#e0f2fe",
emerald: "#047857",
"emerald-900": "#ecfdf5",
gray: GRAYS[400],
lime: "#84cc16",
"lime-900": "#f7fee7",
orange: "#ea580c",
"orange-900": "#ffedd5",
purple: "#581c87",
"purple-900": "#f3e8ff",
rose: "#9f1239",
"rose-900": "#fecdd3",
// App colors
bg: GRAYS[100],
text: GRAYS[900],
"status-bg": GRAYS[300],
"link-text-hover": "var(--primary)",
"btn-bg": "#714b67",
"btn-bg-hover": "#624159",
"btn-text": "#ffffff",
"bg-result": "rgba(255, 255, 255, 0.6)",
"border-result": GRAYS[300],
"border-search": "#d8dadd",
"shadow-opacity": 0.1,
// HootReporting colors
"bg-report": "#ffffff",
"text-report": "#202124",
"border-report": "#f0f0f0",
"bg-report-error": "#fff0f0",
"text-report-error": "#ff0000",
"border-report-error": "#ffd6d6",
"text-report-number": "#1a1aa6",
"text-report-string": "#c80000",
"text-report-key": "#881280",
"text-report-html-tag": "#881280",
"text-report-html-id": "#1a1aa8",
"text-report-html-class": "#994500",
},
dark: {
// Generic colors
primary: "#14b8a6",
amber: "#fbbf24",
"amber-900": "#422006",
blue: "#60a5fa",
"blue-900": "#172554",
cyan: "#22d3ee",
"cyan-900": "#083344",
emerald: "#34d399",
"emerald-900": "#064e3b",
gray: GRAYS[500],
lime: "#bef264",
"lime-900": "#365314",
orange: "#fb923c",
"orange-900": "#431407",
purple: "#a855f7",
"purple-900": "#3b0764",
rose: "#fb7185",
"rose-900": "#4c0519",
// App colors
bg: GRAYS[900],
text: GRAYS[100],
"status-bg": GRAYS[700],
"btn-bg": "#00dac5",
"btn-bg-hover": "#00c1ae",
"btn-text": "#000000",
"bg-result": "rgba(0, 0, 0, 0.5)",
"border-result": GRAYS[600],
"border-search": "#3c3f4c",
"shadow-opacity": 0.4,
// HootReporting colors
"bg-report": "#202124",
"text-report": "#e8eaed",
"border-report": "#3a3a3a",
"bg-report-error": "#290000",
"text-report-error": "#ff8080",
"border-report-error": "#5c0000",
"text-report-number": "#9980ff",
"text-report-string": "#f28b54",
"text-report-key": "#5db0d7",
"text-report-html-tag": "#5db0d7",
"text-report-html-id": "#f29364",
"text-report-html-class": "#9bbbdc",
},
};
const DEBUG_NAMESPACE = "hoot";
const isError = typeof Error.isError === "function" ? Error.isError : polyfillIsError;
const interactionBus = new EventTarget();
const preferredColorScheme = matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
//-----------------------------------------------------------------------------
// Exports
//-----------------------------------------------------------------------------
/**
* @param {Iterable<InteractionType>} types
* @param {(event: CustomEvent<InteractionDetails>) => any} callback
*/
export function addInteractionListener(types, callback) {
for (const type of types) {
interactionBus.addEventListener(type, callback);
}
return function removeInteractionListener() {
for (const type of types) {
interactionBus.removeEventListener(type, callback);
}
};
}
/**
* @param {InteractionType} type
* @param {string} name
* @param {any[]} args
* @param {any} returnValue
*/
export function dispatchInteraction(type, name, args, returnValue) {
interactionBus.dispatchEvent(
new CustomEvent(type, {
detail: [name, args, returnValue],
})
);
return returnValue;
}
/**
* @param {...any} helpers
*/
export function exposeHelpers(...helpers) {
let nameSpaceIndex = 1;
let nameSpace = DEBUG_NAMESPACE;
while (nameSpace in globalThis) {
nameSpace = `${DEBUG_NAMESPACE}${nameSpaceIndex++}`;
}
globalThis[nameSpace] = new HootDebugHelpers(...helpers);
return nameSpace;
}
/**
* @param {keyof typeof COLORS} [scheme]
*/
export function getAllColors(scheme) {
return scheme ? COLORS[scheme] : COLORS;
}
/**
* @param {keyof typeof COLORS["light"]} varName
*/
export function getColorHex(varName) {
return COLORS[preferredColorScheme][varName];
}
export function getPreferredColorScheme() {
return preferredColorScheme;
}
/**
* @param {Node} node
*/
export function getTag(node) {
return node?.nodeName?.toLowerCase() || "";
}
/**
* @template {(...args: any[]) => any} T
* @param {InteractionType} type
* @param {T} fn
* @returns {T & {
* as: (name: string) => T;
* readonly silent: T;
* }}
*/
export function interactor(type, fn) {
return $assign(makeInteractorFn(type, fn, fn.name), {
as(alias) {
return makeInteractorFn(type, fn, alias);
},
get silent() {
return fn;
},
});
}
/**
* @returns {boolean}
*/
export function isFirefox() {
return /firefox/i.test($userAgent);
}
/**
* Cross-realm equivalent to 'instanceof'.
* Can be called with multiple constructors, and will return true if the given object
* is an instance of any of them.
*
* @param {unknown} instance
* @param {...{ name: string }} classes
*/
export function isInstanceOf(instance, ...classes) {
if (!classes.length) {
return instance instanceof classes[0];
}
if (!instance || Object(instance) !== instance) {
// Object is falsy or a primitive (null, undefined and primitives cannot be the instance of anything)
return false;
}
for (const cls of classes) {
if (instance instanceof cls) {
return true;
}
const targetName = cls.name;
if (!targetName) {
return false;
}
if (targetName === "Array") {
return $isArray(instance);
}
if (targetName === "Error") {
return isError(instance);
}
if ($toString.call(instance) === `[object ${targetName}]`) {
return true;
}
let { constructor } = instance;
while (constructor) {
if (constructor.name === targetName) {
return true;
}
constructor = $getPrototypeOf(constructor);
}
}
return false;
}
/**
* Returns whether the given object is iterable (*excluding strings*).
*
* @template T
* @template {T | Iterable<T>} V
* @param {V} object
* @returns {V extends Iterable<T> ? true : false}
*/
export function isIterable(object) {
return !!(object && typeof object === "object" && object[Symbol.iterator]);
}
/**
* @param {string} value
* @param {{ safe?: boolean }} [options]
* @returns {string | RegExp}
*/
export function parseRegExp(value, options) {
const regexParams = value.match(R_REGEX);
if (regexParams) {
const unified = regexParams[1].replace(R_WHITE_SPACE, "\\s+");
const flag = regexParams[2];
try {
return new RegExp(unified, flag);
} catch (error) {
if (isInstanceOf(error, SyntaxError) && options?.safe) {
return value;
} else {
throw error;
}
}
}
return value;
}
/**
* @param {Node} node
* @param {{ raw?: boolean }} [options]
*/
export function toSelector(node, options) {
const tagName = getTag(node);
const id = node.id ? `#${node.id}` : "";
const classNames = node.classList
? [...node.classList].map((className) => `.${className}`)
: [];
if (options?.raw) {
return { tagName, id, classNames };
} else {
return [tagName, id, ...classNames].join("");
}
}
export class HootDebugHelpers {
/**
* @param {...any} helpers
*/
constructor(...helpers) {
$assign(this, ...helpers);
}
}
export const REGEX_MARKER = "/";
// Common regular expressions
export const R_REGEX = new RegExp(`^${REGEX_MARKER}(.*)${REGEX_MARKER}([dgimsuvy]+)?$`);
export const R_WHITE_SPACE = /\s+/g;