// ...................................... //// compose // ...................................... const compose = (...fns) => (arg) => fns.reduceRight((acc, fn) => (fn ? fn(acc) : acc), arg); // ...................................... //// partition // ...................................... const accept = ([x, ...xs], fn, index = 0) => { if (undef(x)) return []; if (fn(x, index)) { return [x, ...accept(xs, fn, (index += 1))]; } else { return [...accept(xs, fn, (index += 1))]; } }; const reject = ([x, ...xs], fn, index = 0) => { if (undef(x)) return []; if (!fn(x, index)) { return [x, ...reject(xs, fn, (index += 1))]; } else { return [...reject(xs, fn, (index += 1))]; } }; // ...................................... //// partition // ...................................... const partition = (xs, fn) => [accept(xs, fn), reject(xs, fn)]; const def = (x) => typeof x !== 'undefined' && x !== null; const undef = (x) => !def(x); const lineBreakPattern = /(\r\n|\n|\r)/gm; //--- regex const newRegexExp = (pattern, flag = '') => { const regex = new RegExp(pattern, flag); regex.lastIndex = 0; return regex; }; const regExp = { regex: (pattern, flag) => newRegexExp(pattern, flag), match: (str, pattern, flag) => str.match(regExp.regex(pattern, flag)) || [], test: (str, pattern, flag) => regExp.regex(pattern, flag).test(str), exec: (str, pattern, flag) => regExp.regex(pattern, flag).exec(str) || [], }; const arrayOwnProperties = (obj) => isArray(obj) ? obj : Object.keys(obj).map((m) => ({ [m]: obj[m] })); //--- remove const removeDoubleSpace = (x) => isArray(x) ? x.map((str) => str.replace(/\s{2,}/g, ' ')) : x.replace(/\s{2,}/g, ' '); const removeSpace = (str) => str.split(/\s+/).join(''); const removeLineBreak = (str) => str.replaceAll(lineBreakPattern, ''); //--- is const isArray = (x) => Array.isArray(x); const isFunc = (x) => def(x) && typeof x === 'function'; const isPlainObject = (x) => x !== null && typeof x === 'object' && x.constructor.name === Object.name && !('props' in x && x.$$typeof); const isFalsish = (x) => x === undefined || x === null || x === false || x === ''; //--- Memoize const memoize = (func, src) => { const cache = {}; return (...args) => { const key = JSON.stringify(args); if (!cache[key]) { cache[key] = func(...args); } console.log('memodized...', `[ ${src} ]`); return cache[key]; }; }; const parseCssArray = (fn) => ([head, ...tail], result = []) => { let index = 0; if (!head) { return result; } let array = [head, ...tail]; const current = head; //.trim() const next = tail[0]; const [cssString, nextArray] = fn(current, next, index, array); // console.log({ current, next, array, cssString, nextArray }); result.push(cssString); tail = nextArray ? nextArray : tail; return parseCssArray(fn)(tail, result); }; // ...................................... //// accumulateMultValues // ...................................... const accumulateMultValues = ([head, ...tail], fn, result = []) => { if (!head) { return result; } if (fn(head)) { return accumulateMultValues([], fn, [...result, head]); } return accumulateMultValues(tail, fn, [...result, head]); }; // ...................................... //// declFn // ...................................... const filterArray = (index, limiter, array) => { return array.filter((_, i) => i < index || i > index + limiter); }; /*--- declarations with multiple values e.g. border: 2mm rgba(211, 220,50,.8);*/ const declFn = (current, next, index, array) => { if (next && current.endsWith(':')) { const slice = array.slice(index + 1); const accumulated = accumulateMultValues(slice, (x) => x.trim().endsWith(';') ); const cssString = `${current} ${accumulated.join(' ')}`; const filteredArray = filterArray(index, accumulated.length, array); return [cssString, filteredArray]; } return [current]; }; // ...................................... //// classnamesFn // ...................................... /*--- classnames with multiple names e.g. .class-1 .class-2 , element attribules .... || @keyframes */ const classnamesFn = (current, next, index, array) => { //--- ignore atrules for example @media because it doesn't have multiple classes if ( current.startsWith('.') || current.startsWith('&.') || current.startsWith('&:') || current.includes('@') ) { if (next && next !== '{') { const slice = array.slice(index + 1); const accumulated = accumulateMultValues(slice, (x) => x.trim().endsWith('{') ).slice(0, -1); const cssString = `${current} ${accumulated.join(' ')}`; const filteredArray = filterArray(index, accumulated.length, array); return [cssString, filteredArray]; } //--- single clasname return [current]; } return [current]; }; // ...................................... //// stringToArray // ...................................... const stringToArray = (str) => str .split(' ') .filter(Boolean) .filter((f) => !f.includes('undefined')); // ...................................... //// sanitizeArray // ...................................... const sanitizeArray = compose( parseCssArray(classnamesFn), parseCssArray(declFn), stringToArray ); // ...................................... //// css To Array // ...................................... const cssToArray = (str) => { return sanitizeArray(str); }; const unitlessKeys = { animationIterationCount: 1, aspectRatio: 1, borderImageOutset: 1, borderImageSlice: 1, borderImageWidth: 1, boxFlex: 1, boxFlexGroup: 1, boxOrdinalGroup: 1, columnCount: 1, columns: 1, flex: 1, flexGrow: 1, flexPositive: 1, flexShrink: 1, flexNegative: 1, flexOrder: 1, gridRow: 1, gridRowEnd: 1, gridRowSpan: 1, gridRowStart: 1, gridColumn: 1, gridColumnEnd: 1, gridColumnSpan: 1, gridColumnStart: 1, msGridRow: 1, msGridRowSpan: 1, msGridColumn: 1, msGridColumnSpan: 1, fontWeight: 1, lineHeight: 1, opacity: 1, order: 1, orphans: 1, tabSize: 1, widows: 1, zIndex: 1, zoom: 1, WebkitLineClamp: 1, // SVG-related properties fillOpacity: 1, floodOpacity: 1, stopOpacity: 1, strokeDasharray: 1, strokeDashoffset: 1, strokeMiterlimit: 1, strokeOpacity: 1, strokeWidth: 1, }; // ...................................... //// critical Decl Char // ...................................... /*--- prevents replacing class names with colons e.g. &.classname p:not(.classic) */ const preventClassnameColons = (str) => { const index = str.indexOf('{'); const noReplaceable = str.substring(0, index); const replaceable = str.substring(index); return `${noReplaceable}${replaceable .replaceAll(':', ': ') .replaceAll(';', '; ')}`; }; const replacementCriticalDecl = (match, p1, p2, p3) => { if (match.includes('{')) { return preventClassnameColons(match); } return match.replaceAll(':', ': ').replaceAll(';', '; '); }; // ...................................... //// critical Class Char // ...................................... const criticalDeclChar = (str) => { return str.replaceAll(/[a-z\-\s]+:.*?;/gm, replacementCriticalDecl); }; // ...................................... //// critical Class Char // ...................................... const criticalClassChar = (str) => { return str .replaceAll(/\{|\}/g, ' $& ') .replaceAll('&', ' &'); }; // ...................................... //// sanitize Critical Chars // ...................................... const sanitizeCriticalChars = (str) => { const string = removeDoubleSpace( removeLineBreak(str).trim() ); return criticalDeclChar(criticalClassChar(string)); }; // ...................................... //// prettify Attributes // ...................................... const attributePattern = /\(.*?\)|\[.*?\]/gm; const replacementPrettifyAttrs = (match, p1, p2) => { if (match.includes('[')) { return removeSpace(match); //--- match.includes('(') } else { //--- hyphenated compound words e.g. min-width: if (regExp.test(match, /[a-z]-[a-z]/gm)) { return removeSpace(match); } // no replace return match; } }; /*--- prevents substitution in case of declaration value e.g. translate3d(0, 100, 0) , calc(100% - 30px) and in case of classes with attributes or atrules e.g. @media (min-width:760px) */ const prettifyAttributes = (str) => { return str.replaceAll(attributePattern, replacementPrettifyAttrs); }; // ...................................... //// prettify Atrules // ...................................... const replacementPrettifyAtrules = (match, p1, p2) => { return removeSpace(match).replace('{', ' {') }; const prettifyAtrules = (str) => { return str.replaceAll(/@.*?\{/gm, replacementPrettifyAtrules); }; // ...................................... //// prettify // ...................................... const prettify = (str) => { const string = removeDoubleSpace(str) .replaceAll(' ;', ';') .replaceAll(' :', ':') .replaceAll(' (', '(') .replaceAll('( ', '(') .replaceAll(' )', ')') .replaceAll(' [', '[') .replaceAll('[ ', '[') .replaceAll(' ]', ']') .replaceAll(/(;){1,}/gm, '; ') .replaceAll(/;\s{1,};/gm, '; ') .replaceAll('};', '}'); const attrPrettified = prettifyAttributes(string); const atrulesPrettified =prettifyAtrules(attrPrettified); return removeDoubleSpace(atrulesPrettified).trim(); }; // ...................................... //// sanitizeString // ...................................... const sanitizeString = (str) => { const sanitized = sanitizeCriticalChars(str); const string = prettify(sanitized); return string }; // ...................................... //// sanitize // ...................................... const sanitize = (x) => { if (!x) { return isArray(x) ? [] : ''; } return isArray(x) ? x.map((_x) => sanitizeString(_x)) : sanitizeString(x); }; // ...................................... //// Mult Slectors // ...................................... const multSlectors = (props) => { // console.log({props}); const [classnameRoot, nameRoot, ...ignore] = props; //--- multiSelectors if (nameRoot.includes(',')) { const split = nameRoot.split(','); const [h, ...t] = split; const replaceH = h.includes('&') ? `${h.replaceAll('&', classnameRoot)}` : `${classnameRoot} ${h}`; const replaceT = t.map((m) => m.includes('&') ? `, ${m.replaceAll('&', classnameRoot)}` : `, ${classnameRoot} ${m}` ); const multiSelectors = removeDoubleSpace( `${replaceH}${replaceT.join(' ')}` ).trim(); return multiSelectors; } }; // ...................................... //// replace Props Mult Slectors // ...................................... const replacePropsMultSlectors = (props) => { const [propsRoot, propsMult] = partition( props, (elel, indx) => !elel.includes(',') ); const propsRootReplaced = replaceProps.sigleSelectors(propsRoot).join(' '); const propsMultReplaced = [multSlectors([propsRootReplaced, ...propsMult])]; return [propsRootReplaced, propsMultReplaced]; }; // ...................................... //// replace Props // ...................................... let count = 0; const sigleSelectors = (props, result = '') => { count += 1; if (props.length === 1) { return props; } const [a, b, ...tail] = props; if (count === 20 || !a || !b) { return [result]; } const replace = b.includes('&') ? b.replaceAll('&', a) : `${a} ${b}`; return sigleSelectors([replace, ...tail], replace); }; // ...................................... //// replace Props // ...................................... const replaceSigleSelectors = (props) => { return sigleSelectors(props); }; const replaceProps = { sigleSelectors: (props) => replaceSigleSelectors(props), multSlectors: (props) => replacePropsMultSlectors(props), }; // ...................................... //// replaceName // ...................................... const replaceName = (name, classname) => { const ampersandName = Array.isArray(classname) ? classname.join(' ') : classname; if (name.includes('&:')) { return `${ampersandName.replaceAll('&', '')}${name.replace( '&', '' )}`.trim(); } return name.replaceAll('&', '').trim(); }; // ...................................... //// extract To Chunks // ...................................... const extractToChunks = (array, fn) => { let nameIndex = 0; let name = undefined; let open = []; let close = []; let closure = false; let indexArray = 0; array.forEach((element, index) => { if (!closure) { if (fn(element, index, array)) { nameIndex = index; name = element; } if (name) { if (element.includes('{')) { open.push(index); } if (element.includes('}')) { close.push(index); } } //--- closure class if (open.length > 0) { if (open.length === close.length) { closure = true; indexArray = index; } } } }); open = open[0]; close = close[close.length - 1]; return { array, index: indexArray, name, nameIndex, open, close, props: [name], value: array.filter((_, index) => index > open && index < close), remaining: array.filter((_, index) => index < nameIndex || index > close), }; }; // ...................................... //// create Tokens // ...................................... const createTokens = (array, indexArray = -1, result = [], cssArray) => { indexArray += 1; const [head, ...tail] = array; if (!head) { return result; } const next = tail[0] ? tail[0] : ''; if (next.includes('{')) { const classname = head; const chunks = extractToChunks( array, (element, index, array) => element === classname ); result = [...result, chunks]; return createTokens(chunks.remaining, -1, result); } return createTokens(tail, indexArray, result); }; // ...................................... //// tokenizer // ...................................... const tokenizer = (cssArray) => { const tokens = createTokens(cssArray, -1, []); return tokens; }; // ...................................... //// hasChildren // ...................................... const hasChildren = (obj) => { return obj.children.length ? true : false; }; const tokenizerRootChildrenChild = (child, props, result = []) => { const value = child.value; const tokenized = tokenizer(value); const rootChildren = createRoot(tokenized, value, props); props = [...props, child.name].flat(); //--- children if (hasChildren(rootChildren)) { return tokenized .map((m) => { return tokenizerRootChildrenChild( m, props, [ ...result, { ...rootChildren, type: '', name: child.name, props: props, }, ].flat() ); }) .flat(); } else { //--- root return [ ...result.flat(), { ...rootChildren, type: '', name: child.name, props: props, }, ].flat(); } }; const getNameAtrules = (props) => { return props.filter((f) => f.includes('@'))[0]; }; // ...................................... //// tokenizerRootChildren // ...................................... const tokenizerRootChildren = (child, tokenized, rootChildren) => { const children = tokenized .map((m) => tokenizerRootChildrenChild(m, rootChildren.props)) .flat(); return [ ...children.map((child) => { const props = child.props.flat(); const name = props.join(' ').includes('@') ? getNameAtrules(props) : child.name; return { ...child, props, name: name, type: 'children-root-children', }; }), ]; }; // ...................................... //// processChildren // ...................................... const processChildren = (child, classnameRoot) => { const { value, name } = child; const classname = [`${classnameRoot}`, name]; if ( name.includes('@') && !name.includes('@media') && !name.includes('@document') ) { return [ { children: [], name: name, props: classname, //[classnameRoot], type: 'children-root-atrule', value: child.value, }, ]; } //--- root with children const tokenized = tokenizer(value); const rootChildren = createRoot(tokenized, value, classname); // console.log({child, tokenized, rootChildren, value}); if (hasChildren(rootChildren)) { const rootChildrenChild = tokenizerRootChildren( child, tokenized, rootChildren ); const props = rootChildren.props.flat(); const name = child.name; return [ { ...rootChildren, children: [], name: name, props: props, type: 'children-root', }, ...rootChildrenChild, ].flat(); } else { //--- root without children const props = rootChildren.props.flat(); const _name = child.name; return [ { ...rootChildren, name: _name, props: props, type: 'children-root', }, ].flat(); } }; // ...................................... //// value Root // ...................................... const valueRoot = (tokenized, init) => tokenized.reduce((acc, tokens) => { acc = acc.replaceAll(`${tokens.name} { ${tokens.value.join(' ')} }`, ''); return acc; }, init); // ...................................... //// create Root // ...................................... const createRoot = (tokenized, array, props) => { const init = array.join(' '); props = props.flat(); const name = props[props.length - 1]; const value = valueRoot(tokenized, init).split(' ').filter(Boolean); return { children: tokenized.map((child) => processChildren(child, name)).flat(), name: name, props: props, type: 'root', value: value, }; }; // ...................................... //// parseProps // ...................................... const parseProps = (childrens, classnameRoot) => { return childrens.reduce((acc, child) => { const propsArray = child.props.map((str) => str.trim()); if (propsArray.length === 1) { return [...acc, child]; } if (child.name.includes('@')) { const props = replaceProps.sigleSelectors( propsArray.filter((f) => !f.includes('@')) ); const name = child.name; //replaceName(child.name, classnameRoot); return [...acc, { ...child, props, name }]; } //--- mult selectors if (child.name.includes(',')) { const [propsRoot, propsMult] = replaceProps.multSlectors(propsArray); const props = propsMult; const name = replaceName(child.name, propsRoot); return [...acc, { ...child, props, name }]; } //--- sigle selectors const props = replaceProps.sigleSelectors(propsArray); const name = replaceName(child.name, classnameRoot); return [...acc, { ...child, props, name }]; }, []); }; // ...................................... //// flat Atrules // ...................................... const flatAtrules = (atrules) => { const childrenValue = atrules.reduce((acc, prev) => { const name = prev.name; const classname = prev.props.join(' '); const value = prev.value.join(' '); const str = `${classname} { ${value} } `; acc[name] = acc[name] ? (acc[name] += [...new Set([str])]) : (acc[name] = [...new Set([str])]); return acc; }, {}); return Object.entries(childrenValue).map(([name, value]) => { const atruleValue = Array.isArray(value) ? value.map((v) => v.trim()) : [value.trim()]; return { children: [], name, props: [], type: '@media', value: atruleValue, }; }); }; // ...................................... //// flat Atrules // ...................................... const compiledChildren = (children, classnameRoot) => { const compiled = parseProps(children, classnameRoot); const [atrulesClassnames, regularClassnames] = partition( compiled, (elel, indx) => elel.name.includes('@') && !elel.name.includes('@keyframes') ); return [...regularClassnames, ...flatAtrules(atrulesClassnames)]; }; // ...................................... //// compile // ...................................... const compile = (cssArray, classnameRoot) => { //--- an array of existing classes in css template strings const tokenized = tokenizer(cssArray); //--- root class created with classnameRoot const root = createRoot(tokenized, cssArray, [`.${classnameRoot}`]); //--- classes belonging to root class const childrens = compiledChildren(root.children, classnameRoot); const compiled = [{ ...root, children: [] }, ...childrens]; return compiled; }; // ...................................... //// compiled (memoize) // ...................................... const compiled = memoize((cssString, classnameRoot) => { const string = sanitize(cssString); const cssArray = cssToArray(string); const _compile = compile(cssArray, [classnameRoot]); return _compile; }, 'compile'); // ...................................... //// serialize String // ...................................... const serializeString = (cssString, classnameRoot = 'css') => { return compiled(cssString, classnameRoot); }; const isUpper = (c) => c >= 'A' && c <= 'Z'; // ...................................... //// add Unit If Needed // ...................................... function addUnitIfNeeded(name, value) { if (value == null || typeof value === 'boolean' || value === '') { return ''; } if ( typeof value === 'number' && value !== 0 && !(name in unitlessKeys) && !name.startsWith('--') ) { return `${value}px`; } return String(value).trim(); } // ...................................... //// hyphenate // ...................................... function hyphenate(string) { let output = ''; for (let i = 0; i < string.length; i++) { const c = string[i]; // Check for CSS variable prefix if (i === 1 && c === '-' && string[0] === '-') { return string; } if (isUpper(c)) { output += '-' + c.toLowerCase(); } else { output += c; } } return output.startsWith('ms-') ? '-' + output : output; } // ...................................... //// obj To CssString // ...................................... const objToCssString = (obj) => { const rules = []; for (const key in obj) { const val = obj[key]; if (!obj.hasOwnProperty(key) || isFalsish(val)) continue; if ((Array.isArray(val) ) || isFunc(val)) { rules.push(`${hyphenate(key)}:`, val, ';'); } else if (isPlainObject(val)) { rules.push(`${key} {`, ...objToCssString(val), '}'); } else { rules.push(`${hyphenate(key)}: ${addUnitIfNeeded(key, val)};`); } } return rules.join(''); }; // ...................................... //// serialize Object // ...................................... const serializeObject = (args, classnameRoot) => { return serializeString (objToCssString(args), classnameRoot) }; // ...................................... //// prettier // ...................................... const prettierString = (str) => removeDoubleSpace(str).trim(); const prettier = (obj) => { return Object.entries(obj).reduce((acc, [key, value]) => { return { ...acc, [prettierString(key)]: prettierString(value), }; }, {}); }; const getLabelHash = (str) => { const match = str.split(' '); return match[0]; }; // ...................................... //// resolver Atrules // ...................................... // const _resolverAtrules = ([current, ...tail], result = {}) => { // if (!current) { // return result; // } // const [key, value] = current; // const match = regExp.match(key, /@(.*?)\)/gm, ''); // const matchString = key.replace(match[0], '').replaceAll(' :', ':'); // const stringKey = removeDoubleSpace(matchString.trim()); // if (stringKey.includes('@')) { // return _resolverAtrules([[stringKey, value], ...tail], result); // } else { // const labelHash = getLabelHash(key); // const str = ` ${stringKey} { label: ${labelHash}; ${value} } `; // result[match[0]] = result[match[0]] // ? (result[match[0]] += str) // : (result[match[0]] = str); // return _resolverAtrules(tail, result); // } // }; // ...................................... //// resolver Keyframes // ...................................... const resolverKeyframes = (array) => { return array.reduce((acc, [key, value]) => { if (!value) { return acc; } const propArray = key.split(' '); `${propArray[propArray.length - 2]}`; const rulename = `${propArray[propArray.length - 1]}`; const labelHash = getLabelHash(key); const str = `label: ${labelHash}; ${value} `; acc[`@keyframes ${rulename}`] = acc[`@keyframes ${rulename}`] ? (acc[`@keyframes ${rulename}`] += str) : (acc[`@keyframes ${rulename}`] = str); return acc; }, {}); }; // ...................................... //// resolver Rulename // ...................................... const resolverRulename = (array) => { return array.reduce((acc, [key, value]) => { const labelHash = getLabelHash(key); const k = key.includes('@') ? key.match(/@.*/gm)[0].trim() : key; return { ...acc, [k]: `label: ${labelHash}; ${value}` }; }, {}); }; const resolverAtrules = (atrules) => { return atrules.reduce((acc, [key, value]) => { const labelHash = getLabelHash(value); return { ...acc, [key]: `label: ${labelHash}; ${value}`, }; }, {}); }; // ...................................... //// resolver // ...................................... const resolver = (styles) => { const serialized = styles.reduce((acc, prev) => { const key = prev.name.includes('@') ? prev.name : prev.props.join(' '); const value = prev.value.join(' '); return { ...acc, [key]: value, }; }, {}); //--- rules const rules = Object.entries(serialized).reduce( (acc, [key, value]) => { const prop = regExp.test(key, /@(.*?)\)/gm, '') ? 'atrule' : regExp.test(key, /@keyframes/gm, '') ? 'keyframes' : 'rulename'; acc[prop] = { ...acc[prop], [key]: value }; return acc; }, { rulename: {}, atrule: {}, keyframes: {}, } ); const rulenames = resolverRulename(Object.entries(rules.rulename)); const atrules = resolverAtrules(Object.entries(rules.atrule)); const keyframes = resolverKeyframes(Object.entries(rules.keyframes)); return prettier({ ...rulenames, ...atrules, ...keyframes }); }; // ...................................... //// serializeResolver // ...................................... const serializeResolver = (styles) => arrayOwnProperties(resolver(styles)); // ...................................... //// serialize // ...................................... const serialize = (args, classnameRoot) => { return typeof args === 'string' ? { name: classnameRoot, styles: serializeResolver(serializeString(args, classnameRoot)), } : { name: classnameRoot, styles: serializeResolver(serializeObject(args, classnameRoot)), }; }; export { cssToArray, replaceName, replaceProps, serialize, serializeObject, tokenizer };