@aesthetic/style
Version:
A low-level, high-performance, atomic-based CSS-in-JS style engine.
582 lines (476 loc) • 18.2 kB
JavaScript
// Bundled with Packemon: https://packemon.dev
// Platform: browser, Support: stable, Format: esm
import { hyphenate, arrayReduce, arrayLoop, objectReduce, generateHash, objectLoop, isObject, joinQueries } from '@aesthetic/utils';
function createCacheKey(property, value, {
media = '',
selector = '',
supports = ''
}) {
return supports + media + selector + property + String(value);
}
function createCacheManager(defaultItems = {}) {
const cache = defaultItems;
return {
read(key, minimumRank) {
const items = cache[key];
if (!items) {
return null;
}
if (minimumRank === undefined) {
return items[0];
}
return items.find(item => item.rank >= minimumRank) ?? null;
},
write(key, item) {
const result = cache[key] || [];
result.push(item);
cache[key] = result;
}
};
} // We duplicate these as they're not available in SSR
const STYLE_RULE = 1;
const IMPORT_RULE = 3;
const MEDIA_RULE = 4;
const FONT_FACE_RULE = 5;
const KEYFRAME_RULE = 6;
const KEYFRAMES_RULE = 7;
const SUPPORTS_RULE = 12;
const VARIANT_PATTERN = /([a-z][a-z0-9]*:[a-z0-9_-]+)/iu;
const VARIANT_COMBO_PATTERN = new RegExp(`^${VARIANT_PATTERN.source}( \\+ ${VARIANT_PATTERN.source})*$`, 'iu');
/* eslint-disable @typescript-eslint/prefer-string-starts-ends-with */
function insertRule(sheet, rule, index) {
try {
return sheet.insertRule(rule, index ?? sheet.cssRules.length);
} catch {
// Vendor prefixed properties, pseudos, etc, that are inserted
// into different vendors will trigger a failure. For example,
// `-moz` or `-ms` being inserted into WebKit.
// There's no easy way around this, so let's just ignore the
// error so that subsequent styles are inserted.
// istanbul ignore next
return -1;
}
}
function insertAtRule$1(sheet, rule) {
const {
length
} = sheet.cssRules;
let index = 0; // At-rules must be inserted before normal style rules.
for (let i = 0; i <= length; i += 1) {
index = i;
if (sheet.cssRules[i]?.type === STYLE_RULE) {
break;
}
}
return insertRule(sheet, rule, index);
}
function insertImportRule(sheet, rule) {
const {
length
} = sheet.cssRules;
let index = 0; // Import rules must be inserted at the top of the style sheet,
// but we also want to persist the existing order.
for (let i = 0; i <= length; i += 1) {
index = i;
if (sheet.cssRules[i]?.type !== IMPORT_RULE) {
break;
}
}
return insertRule(sheet, rule, index);
}
function isAtRule(value) {
return value[0] === '@';
}
function isImportRule(value) {
// eslint-disable-next-line no-magic-numbers
return value.slice(0, 7) === '@import';
}
function isNestedSelector(value) {
const char = value[0];
return char === ':' || char === '[' || char === '>' || char === '~' || char === '+' || char === '*' || char === '|';
}
const unitlessProperties = new Set();
['animationIterationCount', 'borderImage', 'borderImageOutset', 'borderImageSlice', 'borderImageWidth', 'columnCount', 'columns', 'flex', 'flexGrow', 'flexPositive', 'flexShrink', 'flexNegative', 'flexOrder', 'fontWeight', 'gridArea', 'gridRow', 'gridRowEnd', 'gridRowSpan', 'gridRowStart', 'gridColumn', 'gridColumnEnd', 'gridColumnSpan', 'gridColumnStart', 'lineClamp', 'lineHeight', 'maskBorder', 'maskBorderOutset', 'maskBorderSlice', 'maskBorderWidth', 'opacity', 'order', 'orphans', 'tabSize', 'widows', 'zIndex', 'zoom', // SVG
'fillOpacity', 'floodOpacity', 'stopOpacity', 'strokeDasharray', 'strokeDashoffset', 'strokeMiterlimit', 'strokeOpacity', 'strokeWidth'].forEach(property => {
unitlessProperties.add(property);
unitlessProperties.add(hyphenate(property));
});
function isUnitlessProperty(property) {
return unitlessProperties.has(property);
}
function isValidValue(property, value) {
if (value === undefined) {
return false;
}
if (value === null || value === true || value === false || value === '') {
if (process.env.NODE_ENV !== "production") {
// eslint-disable-next-line no-console
console.warn(`Invalid value "${value}" for "${property}".`);
}
return false;
}
return true;
}
function isVariable(value) {
return value.slice(0, 2) === '--';
}
function formatVariable(name) {
let varName = hyphenate(name);
if (!isVariable(varName)) {
varName = `--${varName}`;
}
return varName;
}
function formatProperty(property) {
return hyphenate(property);
}
function formatValue(property, value, options, engine) {
if (typeof value === 'string' || isUnitlessProperty(property) || value === 0) {
return String(value);
}
let suffix = options.unit;
if (!suffix) {
const {
unitSuffixer
} = engine;
suffix = typeof unitSuffixer === 'function' ? unitSuffixer(property) : unitSuffixer;
}
return String(value) + (suffix ?? 'px');
}
function formatDeclaration(key, value) {
if (Array.isArray(value)) {
return arrayReduce(value, val => formatDeclaration(key, val));
}
return `${key}:${value};`;
}
const FORMATS = {
'.eot': 'embedded-opentype',
'.otf': 'opentype',
'.svg': 'svg',
'.svgz': 'svg',
'.ttf': 'truetype',
'.woff': 'woff',
'.woff2': 'woff2'
};
function formatFontFace(properties) {
const fontFace = { ...properties
};
const src = [];
if (Array.isArray(fontFace.local)) {
arrayLoop(fontFace.local, alias => {
src.push(`local('${alias}')`);
});
delete fontFace.local;
}
if (Array.isArray(fontFace.srcPaths)) {
arrayLoop(fontFace.srcPaths, srcPath => {
let ext = srcPath.slice(srcPath.lastIndexOf('.'));
if (ext.includes('?')) {
[ext] = ext.split('?');
}
if (FORMATS[ext]) {
src.push(`url('${srcPath}') format('${FORMATS[ext]}')`);
} else if (process.env.NODE_ENV !== "production") {
throw new Error(`Unsupported font format "${ext}".`);
}
});
delete fontFace.srcPaths;
} else {
return fontFace;
}
fontFace.src = src.join(', ');
return fontFace;
}
function formatImport(value) {
if (typeof value === 'string') {
return value;
}
let path = `"${value.path}"`;
if (value.url) {
path = `url(${path})`;
}
if (value.media) {
path += ` ${value.media}`;
}
return path;
}
function formatRule(className, block, {
media,
selector = '',
supports
}) {
let rule = `.${className}${selector} { ${block} }`; // Server-side rendering recursively creates CSS rules to collapse
// conditionals to their smallest representation, so we need to avoid
// wrapping with the outer conditional for this to work correctly.
if (typeof process !== 'undefined' && !process.env.AESTHETIC_SSR) {
if (media) {
rule = `@media ${media} { ${rule} }`;
}
if (supports) {
rule = `@supports ${supports} { ${rule} }`;
}
}
return rule;
}
function formatVariableBlock(variables) {
return objectReduce(variables, (value, key) => formatDeclaration(formatVariable(key), value));
}
function createDeclaration(property, value, options, engine) {
const {
directionConverter,
vendorPrefixer
} = engine;
let key = formatProperty(property);
let val = formatValue(key, value, options, engine); // Convert between LTR and RTL
if (options.direction && directionConverter) {
({
property: key,
value: val
} = directionConverter.convert(engine.direction, options.direction, key, val));
} // Apply vendor prefixes and format declaration(s)
return options.vendor && vendorPrefixer ? objectReduce(vendorPrefixer.prefix(key, val), (v, k) => formatDeclaration(k, v)) : formatDeclaration(key, val);
}
function createDeclarationBlock(properties, options, engine) {
return objectReduce(properties, (value, key) => createDeclaration(key, value, options, engine));
}
/* eslint-disable prefer-template, no-console */
const CHARS = 'abcdefghijklmnopqrstuvwxyz';
const CHARS_LENGTH = CHARS.length;
function generateClassName(key, options, engine) {
// Avoid hashes that start with an invalid number
if (options.deterministic) {
return `c${generateHash(key)}`;
}
engine.nameIndex += 1;
const index = engine.nameIndex;
if (index < CHARS_LENGTH) {
return CHARS[index];
}
return CHARS[index % CHARS_LENGTH] + String(Math.floor(index / CHARS_LENGTH));
}
function insertAtRule(cacheKey, rule, options, engine) {
const {
cacheManager,
sheetManager
} = engine;
let item = cacheManager.read(cacheKey);
if (!item) {
engine.ruleCount += 1;
item = {
result: ''
};
sheetManager.insertRule(rule, { ...options,
type: 'global'
});
cacheManager.write(cacheKey, item);
}
return item;
}
function insertStyles(cacheKey, render, options, engine, minimumRank) {
const {
cacheManager,
sheetManager,
vendorPrefixer
} = engine;
let item = cacheManager.read(cacheKey, minimumRank);
if (!item) {
engine.ruleCount += 1; // Generate class name and format CSS rule with class name
const className = options.className ?? generateClassName(cacheKey, options, engine);
const css = render(className); // Insert rule and return a rank (insert index)
const rank = sheetManager.insertRule(options.selector && options.vendor && vendorPrefixer ? vendorPrefixer.prefixSelector(options.selector, css) : css, options); // Cache the results for subsequent performance
item = {
rank,
result: className
};
cacheManager.write(cacheKey, item);
}
return item;
}
function renderProperty(engine, property, value, options) {
const key = formatProperty(property);
const {
rankings
} = options;
const {
result,
rank
} = insertStyles(createCacheKey(key, value, options), name => formatRule(name, createDeclaration(key, value, options, engine), options), options, engine, rankings?.[key]); // Persist the rank for specificity guarantees
if (rankings && rank !== undefined && (rankings[key] === undefined || rank > rankings[key])) {
rankings[key] = rank;
}
return result;
}
function renderDeclaration(engine, property, value, options) {
const {
customProperties
} = engine;
let className = '';
const handler = (prop, val) => {
if (isValidValue(prop, val)) {
className += renderProperty(engine, prop, val, options) + ' ';
}
};
if (customProperties && property in customProperties) {
// @ts-expect-error Value is a complex union
customProperties[property](value, handler, engine);
} else {
handler(property, value);
}
return className.trim();
}
function renderFontFace(engine, fontFace, options) {
let name = fontFace.fontFamily;
let block = createDeclarationBlock(formatFontFace(fontFace), options, engine);
if (!name) {
name = `ff${generateHash(block)}`;
block += formatDeclaration('font-family', name);
}
insertAtRule(createCacheKey('@font-face', name, options), `@font-face { ${block} }`, options, engine);
return name;
}
function renderImport(engine, value, options) {
const path = formatImport(value);
insertAtRule(createCacheKey('@import', path, options), `@import ${path};`, options, engine);
return path;
}
function renderKeyframes(engine, keyframes, animationName, options) {
const block = objectReduce(keyframes, (keyframe, step) => `${step} { ${createDeclarationBlock(keyframe, options, engine)} } `);
const name = animationName || `kf${generateHash(block)}`;
insertAtRule(createCacheKey('@keyframes', name, options), `@keyframes ${name} { ${block} }`, options, engine);
return name;
}
function renderVariable(engine, name, value, options) {
const key = formatVariable(name);
return insertStyles(createCacheKey(key, value, options), className => formatRule(className, formatDeclaration(key, value), options), options, engine).result;
} // It's much faster to set and unset options (conditions and selector) than it is
// to spread and clone the options object. Since rendering is synchronous, it just works!
function renderAtRules(engine, rule, options, render) {
const {
className: originalClassName,
media: originalMedia,
selector: originalSelector,
supports: originalSupports
} = options;
const variants = [];
let className = '';
objectLoop(rule['@media'], (condition, query) => {
options.media = joinQueries(options.media, query);
className += render(engine, condition, options).result + ' ';
options.media = originalMedia;
});
objectLoop(rule['@selectors'], (nestedRule, selectorGroup) => {
arrayLoop(selectorGroup.split(','), selector => {
if (originalSelector === undefined) {
options.selector = '';
} // eslint-disable-next-line @typescript-eslint/restrict-plus-operands
options.selector += selector.trim();
className += render(engine, nestedRule, options).result + ' ';
options.selector = originalSelector;
});
});
objectLoop(rule['@supports'], (condition, query) => {
options.supports = joinQueries(options.supports, query);
className += render(engine, condition, options).result + ' ';
options.supports = originalSupports;
});
objectLoop(rule['@variables'], (value, name) => {
className += renderVariable(engine, formatVariable(name), value, options) + ' ';
});
objectLoop(rule['@variants'], (nestedRule, variant) => {
if (process.env.NODE_ENV !== "production" && !VARIANT_COMBO_PATTERN.test(variant)) {
throw new Error(`Invalid variant "${variant}". Type and enumeration must be separated with a ":", and each part may only contain a-z, 0-9, -, _.`);
}
options.className = undefined;
variants.push({
result: render(engine, nestedRule, options).result,
types: variant.split('+').map(v => v.trim())
});
options.className = originalClassName;
});
return {
result: className.trim(),
variants
};
}
function renderRule(engine, rule, options) {
let className = '';
objectLoop(rule, (value, property) => {
if (isObject(value)) {
if (isNestedSelector(property)) {
var _selectors;
(rule[_selectors = '@selectors'] || (rule[_selectors] = {}))[property] = value;
} else if (!isAtRule(property) && process.env.NODE_ENV !== "production") {
console.warn(`Unknown property selector or nested block "${property}".`);
}
} else if (isValidValue(property, value)) {
className += renderDeclaration(engine, property, value, options) + ' ';
}
}); // Render at-rules last to somewhat ensure specificity
const atResult = renderAtRules(engine, rule, options, renderRule);
return {
result: (className + atResult.result).trim(),
variants: atResult.variants
};
}
function renderRuleGrouped(engine, rule, options) {
const atRules = {};
let variables = '';
let properties = ''; // Extract all nested rules first as we need to process them *after* properties
objectLoop(rule, (value, property) => {
if (isObject(value)) {
// Extract and include variables in the top level class
if (property === '@variables') {
variables += formatVariableBlock(value); // Extract all other at-rules
} else if (isAtRule(property)) {
atRules[property] = value; // Merge local selectors into the selectors at-rule
} else if (isNestedSelector(property)) {
var _selectors2;
(atRules[_selectors2 = '@selectors'] || (atRules[_selectors2] = {}))[property] = value; // Log for invalid value
} else if (process.env.NODE_ENV !== "production") {
console.warn(`Unknown property selector or nested block "${property}".`);
}
} else if (isValidValue(property, value)) {
properties += createDeclaration(property, value, options, engine);
}
}); // Always use deterministic classes for grouped rules
options.deterministic = true; // Insert rule styles only once
const block = variables + properties;
const {
result
} = insertStyles(createCacheKey(block, '', options), name => formatRule(name, block, options), options, engine); // Render all at/nested rules with the parent class name
options.className = result;
const {
variants
} = renderAtRules(engine, atRules, options, renderRuleGrouped);
return {
result: result.trim(),
variants
};
}
const noop = () => {};
function createStyleEngine(engineOptions) {
const renderOptions = {};
const engine = {
cacheManager: createCacheManager(),
direction: 'ltr',
name: 'style',
nameIndex: -1,
ruleCount: -1,
...engineOptions,
prefersColorScheme: () => false,
prefersContrastLevel: () => false,
renderDeclaration: (property, value, options = renderOptions) => renderDeclaration(engine, property, value, options),
renderFontFace: (fontFace, options = renderOptions) => renderFontFace(engine, fontFace, options),
renderImport: (path, options = renderOptions) => renderImport(engine, path, options),
renderKeyframes: (keyframes, animationName = '', options = renderOptions) => renderKeyframes(engine, keyframes, animationName, options),
renderRule: (rule, options = renderOptions) => renderRule(engine, rule, options),
renderRuleGrouped: (rule, options = renderOptions) => renderRuleGrouped(engine, rule, options),
renderVariable: (name, value, options = renderOptions) => renderVariable(engine, name, value, options),
setDirection: noop,
setRootVariables: noop,
setTheme: noop
};
return engine;
}
export { FONT_FACE_RULE as F, IMPORT_RULE as I, KEYFRAMES_RULE as K, MEDIA_RULE as M, STYLE_RULE as S, VARIANT_PATTERN as V, SUPPORTS_RULE as a, createStyleEngine as b, createCacheKey as c, createCacheManager as d, KEYFRAME_RULE as e, formatVariable as f, VARIANT_COMBO_PATTERN as g, insertAtRule$1 as h, insertRule as i, insertImportRule as j, isAtRule as k, isImportRule as l, isNestedSelector as m, isUnitlessProperty as n, isValidValue as o, isVariable as p, formatProperty as q, formatValue as r, formatDeclaration as s, formatFontFace as t, formatImport as u, formatRule as v, formatVariableBlock as w, createDeclaration as x, createDeclarationBlock as y };
//# sourceMappingURL=bundle-30cd3868.js.map