UNPKG

flairup

Version:

🎩 Lightweight CSS-in-JS solution for npm packages

397 lines (386 loc) • 11.1 kB
// src/utils/asArray.ts function asArray(v) { return [].concat(v); } // src/utils/is.ts function isPsuedoSelector(selector) { return selector.startsWith(":"); } function isStyleCondition(selector) { return isString(selector) && (selector === "*" || selector.length > 1 && ":>~.+*".includes(selector.slice(0, 1)) || isImmediatePostcondition(selector)); } function isValidProperty(property, value) { return (isString(value) || typeof value === "number") && !isCssVariables(property) && !isPsuedoSelector(property) && !isMediaQuery(property); } function isMediaQuery(selector) { return selector.startsWith("@media"); } function isDirectClass(selector) { return selector === "."; } function isCssVariables(selector) { return selector === "--"; } function isString(value) { return value + "" === value; } function isImmediatePostcondition(value) { return isString(value) && (value.startsWith("&") || isPsuedoSelector(value)); } // src/utils/joinTruthy.ts function joinTruthy(arr, delimiter = "") { return arr.filter(Boolean).join(delimiter); } // src/utils/stableHash.ts function stableHash(prefix, seed) { let hash = 0; if (seed.length === 0) return hash.toString(); for (let i = 0; i < seed.length; i++) { const char = seed.charCodeAt(i); hash = (hash << 5) - hash + char; hash = hash & hash; } return `${prefix ?? "cl"}_${hash.toString(36)}`; } // src/utils/stringManipulators.ts function handlePropertyValue(property, value) { if (property === "content") { return `"${value}"`; } return value; } function camelCaseToDash(str) { return str.replace(/([a-z])([A-Z])/g, "$1-$2").toLowerCase(); } function joinedProperty(property, value) { return `${property}:${value}`; } function toClass(str) { return str ? `.${str}` : ""; } function appendString(base, line) { return appendStringInline(base, line, "\n"); } function appendStringInline(base, line, separator = " ") { return base ? `${base}${separator}${line}` : line; } // src/Rule.ts var Rule = class _Rule { constructor(sheet, property, value, selector) { this.sheet = sheet; this.property = property; this.value = value; this.selector = selector; this.property = property; this.value = value; this.joined = joinedProperty(property, value); const joinedConditions = this.selector.preconditions.concat( this.selector.postconditions ); this.hash = this.selector.hasConditions ? this.selector.scopeClassName : stableHash(this.sheet.name, this.joined); this.key = joinTruthy([this.joined, joinedConditions, this.hash]); } toString() { let selectors = mergeSelectors(this.selector.preconditions, { right: this.hash }); selectors = mergeSelectors(this.selector.postconditions, { left: selectors }); return `${selectors} {${_Rule.genRule(this.property, this.value)}}`; } static genRule(property, value) { const transformedProperty = camelCaseToDash(property); return joinedProperty( transformedProperty, handlePropertyValue(property, value) ) + ";"; } }; function mergeSelectors(selectors, { left = "", right = "" } = {}) { const output = selectors.reduce((selectors2, current) => { if (isPsuedoSelector(current)) { return selectors2 + current; } if (isImmediatePostcondition(current)) { return selectors2 + current.slice(1); } return joinTruthy([selectors2, current], " "); }, left); return joinTruthy([output, toClass(right)], " "); } var Selector = class _Selector { constructor(sheet, scopeName = null, { preconditions, postconditions } = {}) { this.sheet = sheet; this.preconditions = []; this.scopeClassName = null; this.scopeName = null; this.postconditions = []; this.preconditions = preconditions ? asArray(preconditions) : []; this.postconditions = postconditions ? asArray(postconditions) : []; this.setScope(scopeName); } setScope(scopeName) { if (!scopeName) { return this; } if (!this.scopeClassName) { this.scopeName = scopeName; this.scopeClassName = stableHash( this.sheet.name, // adding the count guarantees uniqueness across style.create calls scopeName + this.sheet.count ); } return this; } get hasConditions() { return this.preconditions.length > 0 || this.postconditions.length > 0; } addScope(scopeName) { return new _Selector(this.sheet, scopeName, { preconditions: this.preconditions, postconditions: this.postconditions }); } addPrecondition(precondition) { return new _Selector(this.sheet, this.scopeClassName, { postconditions: this.postconditions, preconditions: this.preconditions.concat(precondition) }); } addPostcondition(postcondition) { return new _Selector(this.sheet, this.scopeClassName, { preconditions: this.preconditions, postconditions: this.postconditions.concat(postcondition) }); } createRule(property, value) { return new Rule(this.sheet, property, value, this); } }; // src/utils/forIn.ts function forIn(obj, fn) { for (const key in obj) { fn(key.trim(), obj[key]); } } // src/keyframes.ts function addKeyframes(sheet, input) { const keyframes = {}; forIn(input, (name, stages) => { const hashed = [sheet.name, sheet.seq(), name].join("_"); keyframes[name] = hashed; sheet.append(`@keyframes ${hashed} {`); forIn(stages, (stage, stylesObject) => { sheet.append(`${stage} {`); forIn(stylesObject, (key, value) => { if (isValidProperty(key, value)) { sheet.appendInline(Rule.genRule(key, value)); } }); sheet.appendInline("}"); }); sheet.append("}"); }); return keyframes; } // src/Sheet.ts var Sheet = class { constructor(name, rootNode) { this.name = name; this.rootNode = rootNode; // Hash->css this.storedStyles = {}; // styles->hash this.storedClasses = {}; this.style = ""; this.count = 0; this.id = `flairup-${name}`; this.styleTag = this.createStyleTag(); } getStyle() { return this.style; } append(css) { this.style = appendString(this.style, css); } appendInline(css) { this.style = appendStringInline(this.style, css); } seq() { return this.count++; } apply() { this.seq(); if (!this.styleTag) { return; } this.styleTag.innerHTML = this.style; } isApplied() { return !!this.styleTag; } createStyleTag() { if (typeof document === "undefined" || this.isApplied() || // Explicitly disallow mounting to the DOM this.rootNode === null) { return this.styleTag; } const styleTag = document.createElement("style"); styleTag.type = "text/css"; styleTag.id = this.id; (this.rootNode ?? document.head).appendChild(styleTag); return styleTag; } addRule(rule) { const storedClass = this.storedClasses[rule.key]; if (isString(storedClass)) { return storedClass; } this.storedClasses[rule.key] = rule.hash; this.storedStyles[rule.hash] = [rule.property, rule.value]; this.append(rule.toString()); return rule.hash; } }; // src/cx.ts function cx(...args) { const classes = args.reduce((classes2, arg) => { if (arg instanceof Set) { classes2.push(...arg); } else if (typeof arg === "string") { classes2.push(arg); } else if (Array.isArray(arg)) { classes2.push(cx(...arg)); } else if (typeof arg === "object") { Object.entries(arg).forEach(([key, value]) => { if (value) { classes2.push(key); } }); } return classes2; }, []); return joinTruthy(classes, " ").trim(); } // src/treeParseUtils.ts function iterateTopLevel(sheet, styles, selector) { const output = { preconditions: [] }; forIn(styles, (key, value) => { if (isStyleCondition(key)) { const res = iterateTopLevel( sheet, value, selector.addPrecondition(key) ); res.preconditions.forEach((item) => output.preconditions.push(item)); return output; } output.preconditions.push([key, styles[key], selector.addScope(key)]); return output; }); return output; } function iterateStyles(sheet, styles, selector) { const output = /* @__PURE__ */ new Set(); forIn(styles, (property, value) => { let res = []; if (isStyleCondition(property)) { res = iterateStyles( sheet, value, selector.addPostcondition(property) ); } else if (isDirectClass(property)) { res = asArray(value); } else if (isMediaQuery(property)) { res = handleMediaQuery(sheet, value, property, selector); } else if (isCssVariables(property)) { res = cssVariablesBlock(sheet, value, selector); } else if (isValidProperty(property, value)) { const rule = selector.createRule(property, value); sheet.addRule(rule); output.add(rule.hash); } return addEachClass(res, output); }); return output; } function addEachClass(list, to) { list.forEach((className) => to.add(className)); return to; } function cssVariablesBlock(sheet, styles, selector) { const classes = /* @__PURE__ */ new Set(); const chunkRows = []; forIn(styles, (property, value) => { if (isValidProperty(property, value)) { chunkRows.push(Rule.genRule(property, value)); return; } const res = iterateStyles(sheet, value ?? {}, selector); addEachClass(res, classes); }); if (!selector.scopeClassName) { return classes; } if (chunkRows.length) { const output = chunkRows.join(" "); sheet.append( `${mergeSelectors(selector.preconditions, { right: selector.scopeClassName })} {${output}}` ); } classes.add(selector.scopeClassName); return classes; } function handleMediaQuery(sheet, styles, mediaQuery, selector) { sheet.append(mediaQuery + " {"); const output = iterateStyles(sheet, styles, selector); sheet.append("}"); return output; } // src/index.ts function createSheet(name, rootNode) { const sheet = new Sheet(name, rootNode); return { create: genCreate(sheet), keyframes: genKeyframes(sheet), getStyle: sheet.getStyle.bind(sheet), isApplied: sheet.isApplied.bind(sheet) }; } function genCreate(sheet) { return function create(styles) { const scopedStyles = {}; const topLevel = iterateTopLevel(sheet, styles, new Selector(sheet)); topLevel.preconditions.forEach(([scopeName, styles2, selector]) => { iterateStyles(sheet, styles2, selector).forEach((className) => { addScopedStyle(scopeName, className); }); }); sheet.apply(); return scopedStyles; function addScopedStyle(name, className) { scopedStyles[name] = scopedStyles[name] ?? /* @__PURE__ */ new Set(); scopedStyles[name].add(className); } }; } function genKeyframes(sheet) { return function keyframes(keyframesInput) { return addKeyframes(sheet, keyframesInput); }; } export { createSheet, cx }; //# sourceMappingURL=index.js.map