@trifrost/core
Version:
Blazingly fast, runtime-agnostic server framework for modern edge and node environments
497 lines (496 loc) • 17 kB
JavaScript
;
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;
}