free-style
Version:
Make CSS easier and more maintainable by using JavaScript
374 lines • 10.4 kB
JavaScript
/**
* 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