@aesthetic/style
Version:
A low-level, high-performance, atomic-based CSS-in-JS style engine.
624 lines (512 loc) • 19.5 kB
JavaScript
// Bundled with Packemon: https://packemon.dev
// Platform: browser, Support: stable, Format: lib
;
const utils = require('@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) {
var _items$find;
const items = cache[key];
if (!items) {
return null;
}
if (minimumRank === undefined) {
return items[0];
}
return (_items$find = items.find(item => item.rank >= minimumRank)) !== null && _items$find !== void 0 ? _items$find : 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 !== null && index !== void 0 ? 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) {
var _sheet$cssRules$i;
index = i;
if (((_sheet$cssRules$i = sheet.cssRules[i]) === null || _sheet$cssRules$i === void 0 ? void 0 : _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) {
var _sheet$cssRules$i2;
index = i;
if (((_sheet$cssRules$i2 = sheet.cssRules[i]) === null || _sheet$cssRules$i2 === void 0 ? void 0 : _sheet$cssRules$i2.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(utils.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 = utils.hyphenate(name);
if (!isVariable(varName)) {
varName = `--${varName}`;
}
return varName;
}
function formatProperty(property) {
return utils.hyphenate(property);
}
function formatValue(property, value, options, engine) {
var _suffix;
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 = suffix) !== null && _suffix !== void 0 ? _suffix : 'px');
}
function formatDeclaration(key, value) {
if (Array.isArray(value)) {
return utils.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)) {
utils.arrayLoop(fontFace.local, alias => {
src.push(`local('${alias}')`);
});
delete fontFace.local;
}
if (Array.isArray(fontFace.srcPaths)) {
utils.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 utils.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 ? utils.objectReduce(vendorPrefixer.prefix(key, val), (v, k) => formatDeclaration(k, v)) : formatDeclaration(key, val);
}
function createDeclarationBlock(properties, options, engine) {
return utils.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${utils.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) {
var _options$className;
engine.ruleCount += 1; // Generate class name and format CSS rule with class name
const className = (_options$className = options.className) !== null && _options$className !== void 0 ? _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 === null || rankings === void 0 ? void 0 : 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${utils.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 = utils.objectReduce(keyframes, (keyframe, step) => `${step} { ${createDeclarationBlock(keyframe, options, engine)} } `);
const name = animationName || `kf${utils.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 = '';
utils.objectLoop(rule['@media'], (condition, query) => {
options.media = utils.joinQueries(options.media, query);
className += render(engine, condition, options).result + ' ';
options.media = originalMedia;
});
utils.objectLoop(rule['@selectors'], (nestedRule, selectorGroup) => {
utils.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;
});
});
utils.objectLoop(rule['@supports'], (condition, query) => {
options.supports = utils.joinQueries(options.supports, query);
className += render(engine, condition, options).result + ' ';
options.supports = originalSupports;
});
utils.objectLoop(rule['@variables'], (value, name) => {
className += renderVariable(engine, formatVariable(name), value, options) + ' ';
});
utils.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 = '';
utils.objectLoop(rule, (value, property) => {
if (utils.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
utils.objectLoop(rule, (value, property) => {
if (utils.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;
}
exports.FONT_FACE_RULE = FONT_FACE_RULE;
exports.IMPORT_RULE = IMPORT_RULE;
exports.KEYFRAMES_RULE = KEYFRAMES_RULE;
exports.KEYFRAME_RULE = KEYFRAME_RULE;
exports.MEDIA_RULE = MEDIA_RULE;
exports.STYLE_RULE = STYLE_RULE;
exports.SUPPORTS_RULE = SUPPORTS_RULE;
exports.VARIANT_COMBO_PATTERN = VARIANT_COMBO_PATTERN;
exports.VARIANT_PATTERN = VARIANT_PATTERN;
exports.createCacheKey = createCacheKey;
exports.createCacheManager = createCacheManager;
exports.createDeclaration = createDeclaration;
exports.createDeclarationBlock = createDeclarationBlock;
exports.createStyleEngine = createStyleEngine;
exports.formatDeclaration = formatDeclaration;
exports.formatFontFace = formatFontFace;
exports.formatImport = formatImport;
exports.formatProperty = formatProperty;
exports.formatRule = formatRule;
exports.formatValue = formatValue;
exports.formatVariable = formatVariable;
exports.formatVariableBlock = formatVariableBlock;
exports.insertAtRule = insertAtRule$1;
exports.insertImportRule = insertImportRule;
exports.insertRule = insertRule;
exports.isAtRule = isAtRule;
exports.isImportRule = isImportRule;
exports.isNestedSelector = isNestedSelector;
exports.isUnitlessProperty = isUnitlessProperty;
exports.isValidValue = isValidValue;
exports.isVariable = isVariable;
//# sourceMappingURL=bundle-cfb1e92a.js.map