/** * This module contains an extension to the CSSStyleSheet class for simple generation of CSS style elements. * * @module css */ interface ToString { toString(): string; } /** * This unexported type represents a CSS value, as either a string, number or any object with the {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/toString | toString} method. */ type Value = string | number | ToString; /** * This unexported interface defines the structure of the CSS data provided to the {@link CSS/add | add} method. * * The key can refer to property name or an extended selector. * * When the key is a property name, the value will be a {@link Value} type. * * When the key is an extended selector, it will be logically appended to the current selector and processed as in a call to the {@link CSS/add | add} method with the value Def. The logical appending splits both the parent and extended selectors at the ',' separator and concatenates all pairs together, separated by ',' separators. */ interface Def { [key: string]: Value | Def; } type IDs = U['length'] extends N ? U : IDs; /** * The default export is a class that extends the {@link https://developer.mozilla.org/en-US/docs/Web/API/CSSStyleSheet | CSSStyleSheet} interface. */ export default class CSS extends CSSStyleSheet { #idPrefix: string; #id: number; /** * Used to create a new instance of the class. * * @param {string} [prefix=""] An optional prefix, which will be applied to all returns from the {@link CSS/id | id} method. It will default to the underscore character if it is not provided or if the prefix given is not allowed for classes or IDs. * @param {number} [idStart=0] The `idStart` param defines the starting ID for returns from the {@link CSS/id | id} method. */ constructor(prefix: string = "", idStart: number = 0) { super(); this.#idPrefix = idRE.test(prefix) ? prefix : "_"; this.#id = idStart; } /** * This method can either be called with a CSS selector string and a {@link Def} object containing all of the style information, or and object with selector keys and {@link Def} values. The CSS instance is returned for simple method chaining. * * @param {Record} defs * * @return {this} */ add(defs: Record): this; /** * @param {string} selector * @param {Def} def * * @return {this} */ add(selector: string, def: Def): this; add(selectorOrDefs: string | Record, defs: Def = {}) { for (const [selector, def] of typeof selectorOrDefs === "string" ? [[selectorOrDefs, defs]] as const : Object.entries(selectorOrDefs)) { for (const rule of compileRule(selector, def)) { this.insertRule(rule, this.cssRules.length); } } return this; } /** * This method adds an {@link https://developer.mozilla.org/en-US/docs/Web/CSS/At-rule | At-rule} section to the CSS object, filled with the styling generated by the defs object if one is provided (as per the {@link CSS/add | add} method). Does not currently support nested at-rule queries. * * @param {string} at The `at` rule identifier * @param {Record} [defs] Definitions to be added to the 'at' rule. * * @return {this} */ at(at: string, defs?: Record): this { if (defs) { let data = ""; for (const def in defs) { for (const rule of compileRule(def, defs[def])) { data += rule; } } if (data) { this.insertRule(at + "{" + data + "}", this.cssRules.length); } } else { this.insertRule(at, this.cssRules.length); } return this; } /** * This method will return sequential unique ids to be used as either class names or element IDs. The prefix of the string will be as provided to the {@link CSS/constructor | constructor} and the suffix will be an increasing number starting at the value provided to the {@link CSS/constructor}. * * @return {string} An ID. */ id(): string { return this.#idPrefix + this.#id++; } /** * This method will return a number (n) of unique ids, as per the {@link CSS/id | id} method. * * @param {number} length Number of IDs to generate. * * @return {string[]} Generated IDs. */ ids(length: N): string[] { return Array.from({length}, () => this.id()) as IDs; } /** * This method complies all of the rules into a single string. * * @return {string} Compiled CSS text. */ toString(): string { let r = ""; for (const rule of this.cssRules) { r += rule.cssText; } return r; } /** * This method compiles all of the current rules and returns them in a {@link https://developer.mozilla.org/en-US/docs/Web/API/HTMLStyleElement | HTMLStyleElement}. * * @return {HTMLStyleElement} */ render(): HTMLStyleElement { const style = document.createElement("style"); style.setAttribute("type", "text/css"); style.textContent = this + ""; return style; } } const split = (selector: string) => { const stack: string[] = [], parts: string[] = []; let pos = 0; for (let i = 0; i < selector.length; i++) { const c = selector.charAt(i); if (c === '"' || c === "'") { for (i++; i < selector.length; i++) { const d = selector.charAt(i); if (d === "\\") { i++; } else if (d === c) { break; } } } else if (c === "," && !stack.length) { parts.push(selector.slice(pos, i)); pos = i + 1; } else if (c === stack.at(-1)) { stack.pop(); } else if (c === "[") { stack.push("]"); } else if (c === "(") { stack.push(")"); } } parts.push(selector.slice(pos, selector.length)); return parts; }, join = (selector: string, add: string) => { const addParts = split(add); let out = ""; for (const part of split(selector)) { for (const addPart of addParts) { out += (out.length ? "," : "") + part + addPart; } } return out; }, isDef = (v: Value | Def): v is Def => Object.getPrototypeOf(v) === Object.prototype, idRE = /^\-?[_a-z\240-\377][_a-z0-9\-\240-\377]*$/i, compileRule = (selector: string, def: Def) => { const rules: string[] = []; let data = ""; for (const key in def) { const v = def[key]; if (isDef(v)) { rules.push(...compileRule(join(selector, key), v)); } else { data += `${key}:${v};`; } } if (data) { rules.unshift(selector + "{" + data + "}"); } return rules; }, defaultCSS = new CSS(); export const /** * A binding to the {@link CSS/add | add} method on a default instantiation of the CSS class. */ add = defaultCSS.add.bind(defaultCSS), /** * A binding to the {@link CSS/at | at} method on a default instantiation of the CSS class. */ at = defaultCSS.at.bind(defaultCSS), /** * A binding to the {@link CSS/id | id} method on a default instantiation of the CSS class. */ id = defaultCSS.id.bind(defaultCSS), /** * A binding to the {@link CSS/ids | ids} method on a default instantiation of the CSS class. */ ids = defaultCSS.ids.bind(defaultCSS) as (length: N) => IDs, /** * A binding to the {@link CSS/render | render} method on a default instantiation of the CSS class. */ render = defaultCSS.render.bind(defaultCSS), /** * This function deeply adds the `add` values to the `base`, and returns the merged `base` {@link Def} Object. * * @param {Def} base Base Def object to be added to. * @param {Def} add Def object to add to base. * * @return {Def} Combined Def object. */ mixin = (base: Def, add: Def) => { for (const key in add) { const v = add[key]; if (isDef(v)) { const w = base[key] ??= {}; if (isDef(w)) { mixin(w, v); } } else { base[key] = v; } } return base; };