UNPKG

@trifrost/core

Version:

Blazingly fast, runtime-agnostic server framework for modern edge and node environments

497 lines (496 loc) 17 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.setActiveStyleEngine = setActiveStyleEngine; exports.getActiveStyleEngine = getActiveStyleEngine; exports.createCss = createCss; const caching_1 = require("@valkyriestudios/utils/caching"); const object_1 = require("@valkyriestudios/utils/object"); const string_1 = require("@valkyriestudios/utils/string"); const Engine_1 = require("./Engine"); const util_1 = require("./util"); const Generic_1 = require("../../../utils/Generic"); const RGX_SEPARATOR = /[:.#[]| /; const FIXED_FEATURE_QUERIES = { reducedMotion: '@media (prefers-reduced-motion: reduce)', dark: '@media (prefers-color-scheme: dark)', light: '@media (prefers-color-scheme: light)', hover: '@media (hover: hover)', touch: '@media (hover: none)', }; const DEFAULT_BREAKPOINTS = { mobile: '@media (max-width: 600px)', tablet: '@media (max-width: 1199px)', tabletUp: '@media (min-width: 601px)', tabletOnly: '@media (min-width: 601px) and (max-width: 1199px)', desktop: '@media (min-width: 1200px)', }; const ANIM_TUPLES = [ ['duration', 'animationDuration'], ['easingFunction', 'animationTimingFunction'], ['delay', 'animationDelay'], ['iterationCount', 'animationIterationCount'], ['direction', 'animationDirection'], ['fillMode', 'animationFillMode'], ['playState', 'animationPlayState'], ]; /** * MARK: Active Engine */ let active_engine = null; function setActiveStyleEngine(engine) { active_engine = engine; return engine; } function getActiveStyleEngine() { return active_engine; } /** * MARK: Css Factory */ function normalizeSelector(val) { switch (val[0]) { case '>': case '+': case '~': case '*': return ' ' + val; default: { /** * Eg: 'div:hover' {...} -> we should auto-space prefix to ' div:hover': {...} * Eg: 'ul li' -> should be auto-space prefixed to ' ul li' */ const separator_idx = val.search(RGX_SEPARATOR); const base = separator_idx === -1 ? val : val.slice(0, separator_idx); return util_1.HTML_TAGS.has(base) ? ' ' + val : val; } } } function normalizeVariable(val, prefix) { return val[0] === '-' && val[1] === '-' ? val : prefix + val; } function flatten(obj, parent_query = '', parent_selector = '') { const result = []; const base = {}; let base_has = false; for (const key in obj) { const val = obj[key]; if (Object.prototype.toString.call(val) === '[object Object]') { if (key[0] === '@') { result.push(...flatten(val, key, parent_selector)); } else { result.push(...flatten(val, parent_query, parent_selector + normalizeSelector(key))); } } else if (val !== undefined && val !== null) { base[normalizeSelector(key)] = val; base_has = true; } } if (base_has) { result.push({ query: parent_query || undefined, selector: parent_selector || undefined, declarations: base, }); } return result; } /** * Factory which generates a CSS helper instance * * @returns {CssGeneric} */ function cssFactory(breakpoints) { /* Global cache for cross-engine reuse */ const GLOBAL_LRU = new caching_1.LRU({ max_size: 500 }); const GLOBAL_KEYFRAMES_LRU = new caching_1.LRU({ max_size: 200 }); /** * CSS Helper which works with the active style engine and registers as well as returns a unique class name * for example: * * const className = css({ * color: 'white', * backgroundColor: 'black', * ':hover': { * color: 'black', * backgroundColor: 'white' * } * }); * * @param {Record<string, unknown>} style - Raw style object * @param {CSSOptions} opts - Options for css, eg: {inject:false} will simply return the unique classname rather than adding to engine */ const mod = (style, opts) => { if (Object.prototype.toString.call(style) !== '[object Object]') return ''; const engine = active_engine || setActiveStyleEngine(new Engine_1.StyleEngine()); const raw = JSON.stringify(style); const cached = engine.cache.get(raw); if (cached) return cached; /* Inject or not */ const inject = opts?.inject !== false; /* Check global LRU and replay off of it if exists */ const replay = GLOBAL_LRU.get(raw); if (replay) { if (!inject) return replay.cname; engine.cache.set(raw, replay.cname); for (let i = 0; i < replay.rules.length; i++) { const r = replay.rules[i]; engine.register(r.rule, replay.cname, { query: r.query, selector: r.selector }); } return replay.cname; } /* Flatten */ const flattened = flatten(style); if (!flattened.length) { engine.cache.set(raw, ''); return ''; } /* Get class name and register on engine */ const cname = 'tf' + (0, Generic_1.djb2Hash)(raw); engine.cache.set(raw, cname); if (!inject) return cname; /* Loop through flattened behavior and register each op */ const lru_entries = []; for (let i = 0; i < flattened.length; i++) { const { declarations, selector = undefined, query = undefined } = flattened[i]; const rule = (0, util_1.styleToString)(declarations); if (rule) { const normalized_selector = selector ? '.' + cname + normalizeSelector(selector) : undefined; engine.register(rule, cname, { query, selector: normalized_selector }); lru_entries.push({ rule, query, selector: normalized_selector }); } } /* Push to global lru */ GLOBAL_LRU.set(raw, { cname, rules: lru_entries }); return cname; }; /* Pseudo Classes */ mod.hover = ':hover'; mod.active = ':active'; mod.focus = ':focus'; mod.focusVibisle = ':focus-visible'; mod.focusWithin = ':focus-within'; mod.disabled = ':disabled'; mod.checked = ':checked'; mod.visited = ':visited'; mod.firstChild = ':first-child'; mod.lastChild = ':last-child'; mod.firstOfType = ':first-of-type'; mod.lastOfType = ':last-of-type'; mod.empty = ':empty'; /* Pseudo Elements */ mod.before = '::before'; mod.after = '::after'; mod.placeholder = '::placeholder'; mod.selection = '::selection'; /* Dynamic Selectors */ mod.nthChild = (i) => ':nth-child(' + i + ')'; mod.nthLastChild = (i) => ':nth-last-child(' + i + ')'; mod.nthOfType = (i) => ':nth-of-type(' + i + ')'; mod.nthLastOfType = (i) => ':nth-last-of-type(' + i + ')'; mod.not = (selector) => ':not(' + selector + ')'; mod.is = (...selectors) => ':is(' + selectors.join(', ') + ')'; mod.where = (selector) => ':where(' + selector + ')'; mod.has = (selector) => ':has(' + selector + ')'; mod.dir = (dir) => ':dir(' + dir + ')'; mod.attr = (name, value) => { if (value === undefined) return '[' + name + ']'; return '[' + name + '="' + String(value) + '"]'; }; mod.attrStartsWith = (name, value) => '[' + name + '^="' + String(value) + '"]'; mod.attrEndsWith = (name, value) => '[' + name + '$="' + String(value) + '"]'; mod.attrContains = (name, value) => '[' + name + '*="' + String(value) + '"]'; /* Media Queries */ mod.media = { ...FIXED_FEATURE_QUERIES, ...breakpoints }; /* Root injector */ mod.root = (style = {}) => { if (!(0, object_1.isNeObject)(style) || !active_engine) return; const flattened = flatten(style); if (!flattened.length) return; for (let i = 0; i < flattened.length; i++) { const { declarations, query, selector } = flattened[i]; const rule = (0, util_1.styleToString)(declarations); if (rule) { const selector_path = selector ? selector[0] === '[' && selector[selector.length - 1] === ']' ? ':root' + selector : selector.trimStart() : ':root'; active_engine.register(rule, '', { selector: selector_path, query }); } } }; /* KeyFrames */ mod.keyframes = (frames, opts) => { const raw = JSON.stringify(frames); const engine = active_engine || setActiveStyleEngine(new Engine_1.StyleEngine()); /* Check engine */ const cached = engine.cache.get(raw); if (cached) return cached; /* Inject or not */ const inject = opts?.inject !== false; /* Check global LRU */ const replay = GLOBAL_KEYFRAMES_LRU.get(raw); if (replay) { if (!inject) return replay.cname; engine.register(replay.rule, '', { selector: null }); return replay.cname; } const cname = 'tf' + (0, Generic_1.djb2Hash)(raw); engine.cache.set(raw, cname); let rule = '@keyframes ' + cname + ' {'; for (const point in frames) { const style = (0, util_1.styleToString)(frames[point]); if (style) rule += point + '{' + style + '}'; } rule += '}'; if (inject) engine.register(rule, '', { selector: null }); GLOBAL_KEYFRAMES_LRU.set(raw, { cname, rule }); return cname; }; return mod; } const CSS_RESET = { '*, *::before, *::after': { boxSizing: 'border-box', }, [[ 'html', 'body', 'div', 'span', 'object', 'iframe', 'figure', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'p', 'blockquote', 'pre', 'a', 'code', 'em', 'img', 'small', 'strike', 'strong', 'sub', 'sup', 'tt', 'b', 'u', 'i', 'ol', 'ul', 'li', 'fieldset', 'form', 'label', 'table', 'caption', 'tbody', 'tfoot', 'thead', 'tr', 'th', 'td', 'main', 'canvas', 'embed', 'footer', 'header', 'nav', 'section', 'video', ].join(', ')]: { margin: 0, padding: 0, border: 0, fontSize: '100%', font: 'inherit', verticalAlign: 'baseline', textRendering: 'optimizeLegibility', webkitFontSmoothing: 'antialiased', webkitTapHighlightColor: 'transparent', textSizeAdjust: 'none', }, 'footer, header, nav, section, main': { display: 'block', }, 'ol, ul': { listStyle: 'none', }, 'q, blockquote': { quotes: 'none', '::before': { content: 'none' }, '::after': { content: 'none' }, }, table: { borderCollapse: 'collapse', borderSpacing: 0, }, }; function createCss(config = {}) { const mod = cssFactory(Object.prototype.toString.call(config.breakpoints) === '[object Object]' ? config.breakpoints : DEFAULT_BREAKPOINTS); /* Is mounted on */ let mountPath = null; /* Specific symbol for this css instance */ mod.$uid = (0, Generic_1.hexId)(8); const sym = Symbol('trifrost.jsx.style.css{' + mod.$uid + '}'); /* Define definitions */ mod.defs = {}; /* Variable collectors */ const root_vars = {}; const theme_light = {}; const theme_dark = {}; let has_theme = false; /* Attach var tokens */ mod.var = {}; if (Object.prototype.toString.call(config.var) === '[object Object]') { for (const key in config.var) { const v_key = normalizeVariable(key, '--v-'); mod.var[key] = ('var(' + v_key + ')'); root_vars[v_key] = config.var[key]; } } mod.$v = mod.var; /* Attach theme tokens */ mod.theme = {}; if (Object.prototype.toString.call(config.theme) === '[object Object]') { for (const key in config.theme) { const entry = config.theme[key]; if ((0, string_1.isNeString)(entry)) { const t_key = normalizeVariable(key, '--t-'); root_vars[t_key] = entry; mod.theme[key] = ('var(' + t_key + ')'); } else if ((0, string_1.isNeString)(entry?.light) && (0, string_1.isNeString)(entry?.dark)) { const t_key = normalizeVariable(key, '--t-'); theme_light[t_key] = entry.light; theme_dark[t_key] = entry.dark; mod.theme[key] = ('var(' + t_key + ')'); has_theme = true; } else { throw new Error(`Theme token '${key}' is invalid, must either be a string or define both 'light' and 'dark' values`); } } } mod.$t = mod.theme; /* Attach definitions */ if (typeof config.definitions === 'function') { const def = config.definitions(mod); for (const key in def) mod.defs[key] = def[key]; } /* Define animation registry */ const animations = config.animations ?? {}; if (Object.prototype.toString.call(config.animations) === '[object Object]') { for (const key in config.animations) animations[key] = config.animations[key]; } /* Determine default root injection */ const ROOT_INJECTION = { ...(config.reset === true && CSS_RESET), ...root_vars, ...(has_theme && { [mod.media.light]: { ...theme_light, [':root[data-theme="dark"]']: theme_dark, }, [mod.media.dark]: { ...theme_dark, [':root[data-theme="light"]']: theme_light, }, }), }; /* Attach root generator */ const ogRoot = mod.root; mod.root = (styles = {}) => { if (!active_engine) setActiveStyleEngine(new Engine_1.StyleEngine()); /* Set mounted path */ if (mountPath) active_engine.setMountPath(mountPath); /* If our root variables are already injected, simply run og root */ if (mountPath || Reflect.get(active_engine, sym)) { ogRoot(styles); } else { Reflect.set(active_engine, sym, true); ogRoot({ ...ROOT_INJECTION, ...styles }); } }; /* Use a definition or set of definitions and combine into single style object */ mod.mix = (...args) => { const acc = []; for (let i = 0; i < args.length; i++) { const val = args[i]; if (val) { if (typeof val === 'string' && val in mod.defs) { acc.push(mod.defs[val]()); } else if (Object.prototype.toString.call(val) === '[object Object]') { acc.push(val); } } } switch (acc.length) { case 0: return {}; case 1: return acc[0]; default: return (0, object_1.merge)(acc.shift(), acc, { union: true }); } }; /* Use a definition or set of definitions and register them with a classname*/ mod.use = (...args) => mod(mod.mix(...args)); /* Make use of previously defined animations */ mod.animation = (key, opts = {}) => { const cfg = animations[key]; if (!cfg) throw new Error('[TriFrost css.animation] Unknown animation "' + String(key) + '"'); /* Build animation */ const acc = { animationName: mod.keyframes(cfg.keyframes) }; for (let i = 0; i < ANIM_TUPLES.length; i++) { const [prop, name] = ANIM_TUPLES[i]; if (opts[prop] !== undefined) acc[name] = opts[prop]; else if (cfg[prop] !== undefined) acc[name] = cfg[prop]; } return acc; }; /* Generates a unique classname */ mod.cid = () => 'tf-' + (0, Generic_1.hexId)(8); /* Sets mount path */ mod.setMountPath = (path) => { mountPath = typeof path === 'string' ? path : null; }; /* Disable injection on the current active engine */ mod.disableInjection = (val = true) => { if (!active_engine) setActiveStyleEngine(new Engine_1.StyleEngine()); active_engine?.setDisabled(val); }; return mod; }