UNPKG

@aesthetic/style

Version:

A low-level, high-performance, atomic-based CSS-in-JS style engine.

624 lines (512 loc) 19.5 kB
// Bundled with Packemon: https://packemon.dev // Platform: browser, Support: stable, Format: lib 'use strict'; 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