vanilla 19.0

This commit is contained in:
Ernad Husremovic 2025-10-08 10:49:46 +02:00
parent 991d2234ca
commit d1963a3c3a
3066 changed files with 1651266 additions and 922560 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;