Skip to content

Instantly share code, notes, and snippets.

@prontiol
Created March 7, 2023 18:49
Show Gist options
  • Save prontiol/2a68a21838eff442475efd0e7e6e44f1 to your computer and use it in GitHub Desktop.
Save prontiol/2a68a21838eff442475efd0e7e6e44f1 to your computer and use it in GitHub Desktop.
jss.ts
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