Created
March 7, 2023 18:49
-
-
Save prontiol/2a68a21838eff442475efd0e7e6e44f1 to your computer and use it in GitHub Desktop.
jss.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
interface IStyle { | |
[key: string]: string | IStyle, | |
} | |
interface IStyleSet { | |
[key: string]: IStyle, | |
} | |
type TClasses<T extends IStyleSet> = { | |
[key in keyof T]: string | |
} | |
const id = 'wm-style' | |
let cache: IStyle = {} | |
let rules: string[] = [] | |
const toBase26 = (n: number) => { | |
const result = [] | |
do { | |
result.unshift(String.fromCharCode('a'.charCodeAt(0) + (n % 26))) | |
n /= 26 | |
} while (n >= 1) | |
return result.join('') | |
} | |
const toKebabCase = (text: string) => { | |
return text.replace(/([A-Z])/g, '-$1').toLowerCase() | |
} | |
const normalize = (text: string) => { | |
return text.replace(/[\s\n;]/g, '').replace(/\.([\w\d]+)/, '._') | |
} | |
const ruleset = (rule: string, selector?: string) => { | |
return (selector ? `${selector} { ${rule}; }` : rule) | |
} | |
const getStyleNode = () => { | |
let style = document.getElementById(id) | |
if (style instanceof HTMLStyleElement) { | |
if (process.env.NODE_ENV === 'development') { | |
// @ts-ignore | |
// tslint:disable-next-line | |
console.log(`JSS: Existing <style id="${id}"> found. Total rules: ${style.sheet.cssRules.length}`) | |
} | |
return style as HTMLStyleElement | |
} | |
if (process.env.NODE_ENV === 'development') { | |
// @ts-ignore | |
// tslint:disable-next-line | |
console.log(`JSS: Existing <style id="${id}"> not found, creating a new one.`) | |
} | |
style = document.createElement('style') | |
style.id = id | |
document.head.appendChild(style) | |
return style as HTMLStyleElement | |
} | |
const hydrate = (style: HTMLStyleElement) => { | |
if (process.env.NODE_ENV === 'development') { | |
// @ts-ignore | |
// tslint:disable-next-line | |
console.groupCollapsed('JSS: Hydrating') | |
// @ts-ignore | |
// tslint:disable-next-line | |
console.log(`JSS: hydrate() started: ${Object.keys(cache).length} rules in cache, ${rules.length} rules in stack, ${style.sheet && style.sheet.cssRules.length} rules in <style>`) | |
} | |
const sheet = style.sheet | |
if (sheet instanceof CSSStyleSheet) { | |
const { cssRules } = sheet | |
// tslint:disable-next-line:prefer-for-of | |
for (let i = 0; i < cssRules.length; i++) { | |
const { cssText } = cssRules[i] | |
const match = cssText.match(/\.([\w\d]+)/) | |
if (match) { | |
const className = match[1] | |
const rule = normalize(cssText) | |
if (process.env.NODE_ENV === 'development') { | |
// @ts-ignore | |
// tslint:disable-next-line | |
console.log(`JSS: hydrating: ${className}: ${rule}`) | |
} | |
rules.push(cssText) | |
cache[rule] = className | |
} | |
} | |
} | |
if (process.env.NODE_ENV === 'development') { | |
// @ts-ignore | |
// tslint:disable-next-line | |
console.log(`JSS: hydrate() done: ${Object.keys(cache).length} rules in cache, ${rules.length} rules in stack, ${style.sheet && style.sheet.cssRules.length} rules in <style>`) | |
// @ts-ignore | |
// tslint:disable-next-line | |
console.log(`JSS: Cache:`, cache) | |
// @ts-ignore | |
// tslint:disable-next-line | |
console.groupEnd() | |
} | |
} | |
let injectRule = (rule: string) => { | |
rules.push(rule) | |
} | |
if (typeof document !== 'undefined') { | |
const style = getStyleNode() | |
// @ts-ignore | |
cache = (style.cache = style.cache || {}) | |
hydrate(style) | |
injectRule = (rule: string) => { | |
try { | |
const sheet = style.sheet as CSSStyleSheet | |
const n = sheet.insertRule(rule, sheet.cssRules.length) | |
if (process.env.NODE_ENV === 'development') { | |
// @ts-ignore | |
// tslint:disable-next-line | |
console.log(`JSS: inserting rule: ${rule}, ${sheet.cssRules.length} rules total in <style>`) | |
} | |
if (process.env.NODE_ENV === 'development') { | |
if (normalize(sheet.cssRules[n].cssText) !== normalize(rule)) { | |
// @ts-ignore | |
// tslint:disable-next-line | |
console.warn(`JSS: Rule mismatch. Provided: "${normalize(rule)}", Parsed: ${normalize(sheet.cssRules[n].cssText)}`) | |
} | |
} | |
} catch (exception) { | |
if (process.env.NODE_ENV === 'development') { | |
console.warn(exception) | |
} | |
} | |
rules.push(rule) | |
} | |
} | |
const generateRule = (className: string, declaration: string, child: string, media?: string) => { | |
return ruleset(ruleset(declaration, `.${className}${child}`), media) | |
} | |
const insertRule = (declaration: string, child: string, media?: string) => { | |
const className = toBase26(rules.length) | |
const rule = generateRule(className, declaration, child, media) | |
injectRule(rule) | |
return className | |
} | |
const insert = (key: string, value: string, child: string, media?: string) => { | |
const property = toKebabCase(key) | |
const declaration = `${property}: ${value}` | |
const identity = normalize(generateRule('_', declaration, child, media)) | |
return (cache[identity] = cache[identity] || insertRule(declaration, child, media)) | |
} | |
const render = (obj: IStyle, child: string = '', media?: string): string => { | |
return ( | |
Object.keys(obj).map((key) => { | |
const val = obj[key] | |
if (typeof val === 'object') { | |
if (/^@media/.test(key)) { | |
return render(val, child, key) | |
} else { | |
return render(val, child + key, media) | |
} | |
} else { | |
return insert(key, val, child, media) | |
} | |
}) | |
.sort() | |
.join(' ') | |
) | |
} | |
const css = (obj: IStyleSet): TClasses<typeof obj> => { | |
return ( | |
Object.keys(obj).reduce( | |
(acc: TClasses<typeof obj>, key: string) => { | |
acc[key] = render(obj[key]) | |
return acc | |
}, | |
{}, | |
) | |
) | |
} | |
css.reset = () => { | |
cache = {} | |
rules = [] | |
} | |
css.toString = () => { | |
return rules.sort().join('\n') | |
} | |
export default css |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment