Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
103 changes: 94 additions & 9 deletions src/helper/css/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,29 @@ const toHash = (str: string): string => {
return 'css-' + out
}

const normalizeLabel = (label: string): string => {
return label.trim().replace(/\s+/g, '-')
}

const isValidClassName = (name: string): boolean => /^-?[_a-zA-Z][_a-zA-Z0-9-]*$/.test(name)

// CSS-wide keywords that are invalid as @keyframes names per the spec
const RESERVED_KEYFRAME_NAMES = new Set([
'default',
'inherit',
'initial',
'none',
'revert',
'revert-layer',
'unset',
])
const isValidKeyframeName = (name: string): boolean =>
isValidClassName(name) && !RESERVED_KEYFRAME_NAMES.has(name.toLowerCase())

const defaultOnInvalidSlug = (slug: string) => {
console.warn(`Invalid slug: ${slug}`)
}

const cssStringReStr: string = [
'"(?:(?:\\\\[\\s\\S]|[^"\\\\])*)"', // double quoted string

Expand Down Expand Up @@ -90,6 +113,25 @@ type CssVariableAsyncType = Promise<CssVariableBasicType>
type CssVariableArrayType = (CssVariableBasicType | CssVariableAsyncType)[]
export type CssVariableType = CssVariableBasicType | CssVariableAsyncType | CssVariableArrayType

/**
* A function that customizes generated CSS class names.
*
* @param hash - The default hash-based class name (e.g. `css-1234567890`)
* @param label - The comment label extracted from the CSS template, may be empty.
* Whitespace is trimmed and inner spaces are replaced with hyphens.
* @param styleString - The minified CSS style string
* @returns The custom class name to use. Must be a safe CSS identifier;
* otherwise, the default hash is used as a fallback.
*/
export type ClassNameSlug = (hash: string, label: string, styleString: string) => string

/**
* A callback function called when an invalid slug is returned from ClassNameSlug.
*
* @param slug - The invalid slug
*/
export type OnInvalidSlug = (slug: string) => void

export const buildStyleString = (
strings: TemplateStringsArray,
values: CssVariableType[]
Expand Down Expand Up @@ -154,14 +196,30 @@ export const buildStyleString = (

export const cssCommon = (
strings: TemplateStringsArray,
values: CssVariableType[]
values: CssVariableType[],
classNameSlug?: ClassNameSlug,
onInvalidSlug?: OnInvalidSlug
): CssClassName => {
let [label, thisStyleString, selectors, externalClassNames] = buildStyleString(strings, values)
const isPseudoGlobal = isPseudoGlobalSelectorRe.exec(thisStyleString)
if (isPseudoGlobal) {
thisStyleString = isPseudoGlobal[1]
}
const selector = (isPseudoGlobal ? PSEUDO_GLOBAL_SELECTOR : '') + toHash(label + thisStyleString)
const hash = toHash(label + thisStyleString)

let customSlug: string | undefined
if (classNameSlug) {
const slug = classNameSlug(hash, normalizeLabel(label), thisStyleString)
if (slug) {
if (isValidClassName(slug)) {
customSlug = slug
} else {
;(onInvalidSlug || defaultOnInvalidSlug)(slug)
}
}
}

const selector = (isPseudoGlobal ? PSEUDO_GLOBAL_SELECTOR : '') + (customSlug || hash)
const className = (
isPseudoGlobal ? selectors.map((s) => s[CLASS_NAME]) : [selector, ...externalClassNames]
).join(' ')
Expand Down Expand Up @@ -196,40 +254,67 @@ export const cxCommon = (

export const keyframesCommon = (
strings: TemplateStringsArray,
...values: CssVariableType[]
values: CssVariableType[],
classNameSlug?: ClassNameSlug,
onInvalidSlug?: OnInvalidSlug
): CssClassName => {
const [label, styleString] = buildStyleString(strings, values)
const hash = toHash(label + styleString)

let customSlug: string | undefined
if (classNameSlug) {
const slug = classNameSlug(hash, normalizeLabel(label), styleString)
if (slug) {
if (isValidKeyframeName(slug)) {
customSlug = slug
} else {
;(onInvalidSlug || defaultOnInvalidSlug)(slug)
}
}
}

return {
[SELECTOR]: '',
[CLASS_NAME]: `@keyframes ${toHash(label + styleString)}`,
[CLASS_NAME]: `@keyframes ${customSlug || hash}`,
[STYLE_STRING]: styleString,
[SELECTORS]: [],
[EXTERNAL_CLASS_NAMES]: [],
}
}

type ViewTransitionType = {
(strings: TemplateStringsArray, values: CssVariableType[]): CssClassName
(
strings: TemplateStringsArray,
values: CssVariableType[],
classNameSlug?: ClassNameSlug,
onInvalidSlug?: OnInvalidSlug
): CssClassName
(content: CssClassName): CssClassName
(): CssClassName
}

let viewTransitionNameIndex = 0
export const viewTransitionCommon: ViewTransitionType = ((
strings: TemplateStringsArray | CssClassName | undefined,
values: CssVariableType[]
values: CssVariableType[],
classNameSlug?: ClassNameSlug,
onInvalidSlug?: OnInvalidSlug
): CssClassName => {
if (!strings) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
strings = [`/* h-v-t ${viewTransitionNameIndex++} */`] as any
}
const content = Array.isArray(strings)
? cssCommon(strings as TemplateStringsArray, values)
? cssCommon(strings as TemplateStringsArray, values, classNameSlug, onInvalidSlug)
: (strings as CssClassName)

const transitionName = content[CLASS_NAME]
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const res = cssCommon(['view-transition-name:', ''] as any, [transitionName])
const res = cssCommon(
['view-transition-name:', ''] as any, // eslint-disable-line @typescript-eslint/no-explicit-any
[transitionName],
classNameSlug,
onInvalidSlug
)

content[CLASS_NAME] = PSEUDO_GLOBAL_SELECTOR + content[CLASS_NAME]
content[STYLE_STRING] = content[STYLE_STRING].replace(
Expand Down
Loading
Loading