UNPKG

free-style

Version:

Make CSS easier and more maintainable by using JavaScript

374 lines 10.4 kB
/** * The unique id is used for unique hashes. */ let uniqueId = 0; /** * Quick dictionary lookup for unit-less numbers. */ const CSS_NUMBER = new Set(); /** * CSS properties that are valid unit-less numbers. * * Ref: https://github.com/facebook/react/blob/master/packages/react-dom/src/shared/CSSProperty.js */ const CSS_NUMBER_KEYS = [ "animation-iteration-count", "border-image-outset", "border-image-slice", "border-image-width", "box-flex", "box-flex-group", "box-ordinal-group", "column-count", "columns", "counter-increment", "counter-reset", "flex", "flex-grow", "flex-positive", "flex-shrink", "flex-negative", "flex-order", "font-weight", "grid-area", "grid-column", "grid-column-end", "grid-column-span", "grid-column-start", "grid-row", "grid-row-end", "grid-row-span", "grid-row-start", "line-clamp", "line-height", "opacity", "order", "orphans", "tab-size", "widows", "z-index", "zoom", // SVG properties. "fill-opacity", "flood-opacity", "stop-opacity", "stroke-dasharray", "stroke-dashoffset", "stroke-miterlimit", "stroke-opacity", "stroke-width", ]; // Add vendor prefixes to all unit-less properties. for (const property of CSS_NUMBER_KEYS) { for (const prefix of ["-webkit-", "-ms-", "-moz-", "-o-", ""]) { CSS_NUMBER.add(prefix + property); } } /** * Escape a CSS class name. */ function escape(str) { return str.replace(/[ !#$%&()*+,./;<=>?@[\]^`{|}~"'\\]/g, "\\$&"); } /** * Interpolate the `&` with style name. */ function interpolate(selector, styleName) { return selector.replace(/&/g, styleName); } /** * Transform a JavaScript property into a CSS property. */ function hyphenate(propertyName) { return propertyName .replace(/[A-Z]/g, (m) => `-${m.toLowerCase()}`) .replace(/^ms-/, "-ms-"); // Internet Explorer vendor prefix. } /** * Generate a hash value from a string. */ function stringHash(str) { let value = 5381; let len = str.length; while (len--) value = (value * 33) ^ str.charCodeAt(len); return (value >>> 0).toString(36); } /** * Interpolate CSS selectors. */ function child(selector, parent) { if (selector.indexOf("&") === -1) return `${parent} ${selector}`; return interpolate(selector, parent); } /** * Transform a style string to a CSS string. */ function tupleToStyle([name, value]) { if (typeof value === "number" && value && !CSS_NUMBER.has(name)) { return `${name}:${value}px`; } return `${name}:${String(value)}`; } /** * Recursive loop building styles with deferred selectors. */ function stylize(rulesList, stylesList, key, styles, parentClassName) { const properties = []; const nestedStyles = []; // Sort keys before adding to styles. for (const [key, value] of Object.entries(styles)) { if (key.charCodeAt(0) !== 36 /* $ */ && value != null) { if (Array.isArray(value)) { const name = hyphenate(key); for (let i = 0; i < value.length; i++) { const style = value[i]; if (style != null) properties.push([name, style]); } } else if (typeof value === "object") { nestedStyles.push([key, value]); } else { properties.push([hyphenate(key), value]); } } } const isUnique = !!styles.$unique; const parent = styles.$global ? "" : parentClassName; const style = properties.map(tupleToStyle).join(";"); let pid = style; let selector = parent; let childRules = rulesList; let childStyles = stylesList; if (key.charCodeAt(0) === 64 /* @ */) { childRules = []; childStyles = []; // Nested styles support (e.g. `.foo > @media`). if (parent && style) { childStyles.push({ selector, style, isUnique }); } // Add new rule to parent. rulesList.push({ selector: key, style: parent ? "" : style, rules: childRules, styles: childStyles, }); } else { selector = parent ? (key ? child(key, parent) : parent) : key; if (style) { stylesList.push({ selector, style, isUnique }); } } for (const [name, value] of nestedStyles) { pid += `|${name}#${stylize(childRules, childStyles, name, value, selector)}`; } return pid; } /** * Transform `stylize` tree into style objects. */ function compose(cache, rulesList, stylesList, id, name) { for (const { selector, style, isUnique } of stylesList) { const key = interpolate(selector, name); const item = new Style(style, isUnique ? String(++uniqueId) : id); item.add(new Selector(key)); cache.add(item); } for (const { selector, style, rules, styles } of rulesList) { const key = interpolate(selector, name); const item = new Rule(key, style, id); compose(item, rules, styles, id, name); cache.add(item); } } /** * Cache to list to styles. */ function join(arr) { let res = ""; for (let i = 0; i < arr.length; i++) res += arr[i]; return res; } /** * Implement a cache/event emitter. */ export class Cache { constructor(changes) { this.changes = changes; this.changeId = 0; this.sheet = []; this.children = []; this.counters = new Map(); } add(style) { const id = style.cid(); const count = this.counters.get(id) ?? 0; this.counters.set(id, count + 1); if (count === 0) { const item = style.clone(); const index = this.children.push(item); this.sheet.push(item.getStyles()); this.changeId++; if (this.changes) this.changes.add(item, index); } else if (style instanceof Cache) { const index = this.children.findIndex((x) => x.cid() === id); const item = this.children[index]; const prevChangeId = item.changeId; item.merge(style); if (item.changeId !== prevChangeId) { this.sheet[index] = item.getStyles(); this.changeId++; if (this.changes) this.changes.change(item, index); } } } remove(style) { const id = style.cid(); const count = this.counters.get(id); if (count) { const index = this.children.findIndex((x) => x.cid() === id); if (count === 1) { const item = this.children[index]; this.counters.delete(id); this.children.splice(index, 1); this.sheet.splice(index, 1); this.changeId++; if (this.changes) this.changes.remove(item, index); } else if (style instanceof Cache) { const item = this.children[index]; const prevChangeId = item.changeId; this.counters.set(id, count - 1); item.unmerge(style); if (item.changeId !== prevChangeId) { this.sheet[index] = item.getStyles(); this.changeId++; if (this.changes) this.changes.change(item, index); } } } } merge(cache) { for (const item of cache.children) this.add(item); return this; } unmerge(cache) { for (const item of cache.children) this.remove(item); return this; } } /** * Selector is a dumb class made to represent nested CSS selectors. */ export class Selector { constructor(selector) { this.selector = selector; } cid() { return this.selector; } getStyles() { return this.selector; } clone() { return this; } } /** * The style container registers a style string with selectors. */ export class Style extends Cache { constructor(style, id) { super(); this.style = style; this.id = id; } cid() { return `${this.id}|${this.style}`; } getStyles() { return `${this.sheet.join(",")}{${this.style}}`; } clone() { return new Style(this.style, this.id).merge(this); } } /** * Implement rule logic for style output. */ export class Rule extends Cache { constructor(rule, style, id) { super(); this.rule = rule; this.style = style; this.id = id; } cid() { return `${this.id}|${this.rule}|${this.style}`; } getStyles() { return `${this.rule}{${this.style}${join(this.sheet)}}`; } clone() { return new Rule(this.rule, this.style, this.id).merge(this); } } /** * The FreeStyle class implements the API for everything else. */ export class Sheet extends Cache { constructor(prefix, changes) { super(changes); this.prefix = prefix; } register(compiled) { const className = `${this.prefix}${compiled.id}`; if (process.env.NODE_ENV !== "production" && compiled.displayName) { const name = `${compiled.displayName}_${className}`; compose(this, compiled.rules, compiled.styles, compiled.id, escape(name)); return name; } compose(this, compiled.rules, compiled.styles, compiled.id, className); return className; } registerStyle(styles) { return this.register(compile(styles)); } getStyles() { return join(this.sheet); } } /** * Exports a simple function to create a new instance. */ export function create(changes, prefix = "") { return new Sheet(prefix, changes); } /** * Compile styles into a registerable object. */ export function compile(styles) { const ruleList = []; const styleList = []; const pid = stylize(ruleList, styleList, "", styles, ".&"); return { id: stringHash(pid), rules: ruleList, styles: styleList, displayName: styles.$displayName, }; } //# sourceMappingURL=index.js.map