pleroma-fe/src/services/theme_data/theme_data_3.service.js

775 lines
24 KiB
JavaScript
Raw Normal View History

import { convert, brightness } from 'chromatism'
import sum from 'hash-sum'
2024-05-31 14:33:44 -04:00
import { flattenDeep, sortBy } from 'lodash'
import {
alphaBlend,
getTextColor,
rgba2css,
mixrgb,
2026-01-06 16:22:52 +02:00
relativeLuminance,
} from '../color_convert/color_convert.js'
import {
colorFunctions,
shadowFunctions,
2026-01-06 16:22:52 +02:00
process,
} from './theme3_slot_functions.js'
2024-02-19 20:05:49 +02:00
import {
unroll,
getAllPossibleCombinations,
genericRuleToSelector,
normalizeCombination,
2026-01-06 16:22:52 +02:00
findRules,
2024-02-19 20:05:49 +02:00
} from './iss_utils.js'
import { deserializeShadow } from './iss_deserializer.js'
2024-02-19 20:05:49 +02:00
// Ensuring the order of components
2024-01-18 14:35:25 +02:00
const components = {
Root: null,
Text: null,
FunText: null,
Link: null,
Icon: null,
Border: null,
2025-02-12 15:45:46 +02:00
PanelHeader: null,
2024-02-19 15:11:59 +02:00
Panel: null,
Chat: null,
2025-02-18 00:07:45 +02:00
ChatMessage: null,
2026-01-06 16:22:52 +02:00
Button: null,
}
export const findShadow = (shadows, { dynamicVars, staticVars }) => {
2026-01-06 16:22:52 +02:00
return (shadows || []).map((shadow) => {
2024-02-21 22:18:56 +02:00
let targetShadow
if (typeof shadow === 'string') {
if (shadow.startsWith('$')) {
2026-01-06 16:22:52 +02:00
targetShadow = process(
shadow,
shadowFunctions,
{ findColor, findShadow },
{ dynamicVars, staticVars },
)
2024-02-21 22:18:56 +02:00
} else if (shadow.startsWith('--')) {
// modifiers are completely unsupported here
const variableSlot = shadow.substring(2)
2024-02-21 22:18:56 +02:00
return findShadow(staticVars[variableSlot], { dynamicVars, staticVars })
} else {
targetShadow = deserializeShadow(shadow)
2024-02-21 22:18:56 +02:00
}
} else {
targetShadow = shadow
}
2026-01-06 16:22:52 +02:00
const shadowArray = Array.isArray(targetShadow)
? targetShadow
: [targetShadow]
return shadowArray.map((s) => ({
2024-02-21 22:18:56 +02:00
...s,
2026-01-06 16:22:52 +02:00
color: findColor(s.color, { dynamicVars, staticVars }),
2024-02-21 22:18:56 +02:00
}))
})
}
2024-10-11 20:48:46 +03:00
export const findColor = (color, { dynamicVars, staticVars }) => {
2024-10-25 16:39:37 +03:00
try {
2026-01-06 16:22:52 +02:00
if (
typeof color !== 'string' ||
(!color.startsWith('--') && !color.startsWith('$'))
)
return color
2024-10-25 16:39:37 +03:00
let targetColor = null
if (color.startsWith('--')) {
// Modifier support is pretty much for v2 themes only
2026-01-06 16:22:52 +02:00
const [variable, modifier] = color.split(/,/g).map((str) => str.trim())
2024-10-25 16:39:37 +03:00
const variableSlot = variable.substring(2)
if (variableSlot === 'stack') {
const { r, g, b } = dynamicVars.stacked
targetColor = { r, g, b }
2024-10-25 16:39:37 +03:00
} else if (variableSlot.startsWith('parent')) {
if (variableSlot === 'parent') {
const { r, g, b } = dynamicVars.lowerLevelBackground ?? {}
2024-10-25 16:39:37 +03:00
targetColor = { r, g, b }
} else {
const virtualSlot = variableSlot.replace(/^parent/, '')
2026-01-06 16:22:52 +02:00
targetColor = convert(
dynamicVars.lowerLevelVirtualDirectivesRaw[virtualSlot],
).rgb
2024-10-25 16:39:37 +03:00
}
} else {
const staticVar = staticVars[variableSlot]
const dynamicVar = dynamicVars[variableSlot]
if (!staticVar && !dynamicVar) {
console.warn(dynamicVars, variableSlot, dynamicVars[variableSlot])
console.warn(`Couldn't find variable "${variableSlot}", falling back to magenta. Variables are:
Static:
${JSON.stringify(staticVars, null, 2)}
Dynamic:
${JSON.stringify(dynamicVars, null, 2)}`)
2024-10-25 16:39:37 +03:00
}
targetColor = convert(staticVar ?? dynamicVar ?? '#FF00FF').rgb
}
2024-10-25 16:39:37 +03:00
if (modifier) {
2026-01-06 16:22:52 +02:00
const effectiveBackground =
dynamicVars.lowerLevelBackground ?? targetColor
const isLightOnDark =
relativeLuminance(convert(effectiveBackground).rgb) < 0.5
2024-10-25 16:39:37 +03:00
const mod = isLightOnDark ? 1 : -1
2026-01-06 16:22:52 +02:00
targetColor = brightness(
Number.parseFloat(modifier) * mod,
targetColor,
).rgb
2024-10-25 16:39:37 +03:00
}
}
2024-10-25 16:39:37 +03:00
if (color.startsWith('$')) {
try {
2026-01-06 16:22:52 +02:00
targetColor = process(
color,
colorFunctions,
{ findColor },
{ dynamicVars, staticVars },
)
2024-10-25 16:39:37 +03:00
} catch (e) {
2026-01-06 16:22:52 +02:00
console.error(
'Failure executing color function',
e,
'\n Function: ' + color,
)
2024-10-25 16:39:37 +03:00
targetColor = '#FF00FF'
}
}
2024-10-25 16:39:37 +03:00
// Color references other color
return targetColor
} catch (e) {
throw new Error(`Couldn't find color "${color}", variables are:
Static:
${JSON.stringify(staticVars, null, 2)}
Dynamic:
${JSON.stringify(dynamicVars, null, 2)}\nError: ${e}`)
}
}
2026-01-06 16:22:52 +02:00
const getTextColorAlpha = (
directives,
intendedTextColor,
dynamicVars,
staticVars,
) => {
const opacity = directives.textOpacity
const backgroundColor = convert(dynamicVars.lowerLevelBackground).rgb
2026-01-06 16:22:52 +02:00
const textColor = convert(
findColor(intendedTextColor, { dynamicVars, staticVars }),
).rgb
if (opacity === null || opacity === undefined || opacity >= 1) {
return convert(textColor).hex
}
if (opacity === 0) {
return convert(backgroundColor).hex
}
const opacityMode = directives.textOpacityMode
switch (opacityMode) {
case 'fake':
return convert(alphaBlend(textColor, opacity, backgroundColor)).hex
case 'mixrgb':
return convert(mixrgb(backgroundColor, textColor)).hex
default:
return rgba2css({ a: opacity, ...textColor })
}
}
// Loading all style.js[on] files dynamically
2025-02-28 10:52:04 -05:00
const componentsContext = import.meta.glob(
['/src/**/*.style.js', '/src/**/*.style.json'],
2026-01-06 16:22:52 +02:00
{ eager: true },
2025-02-28 10:52:04 -05:00
)
2026-01-06 16:22:52 +02:00
Object.keys(componentsContext).forEach((key) => {
2025-02-28 10:52:04 -05:00
const component = componentsContext[key].default
if (components[component.name] != null) {
2026-01-06 16:22:52 +02:00
console.warn(
`Component in file ${key} is trying to override existing component ${component.name}! You have collisions/duplicates!`,
)
}
components[component.name] = component
})
2025-02-18 18:30:08 +02:00
2026-01-06 16:22:52 +02:00
Object.keys(components).forEach((key) => {
2025-02-12 15:45:46 +02:00
if (key === 'Root') return
2026-01-06 16:22:52 +02:00
components.Root.validInnerComponents =
components.Root.validInnerComponents || []
2025-02-12 15:45:46 +02:00
components.Root.validInnerComponents.push(key)
})
2026-01-06 16:22:52 +02:00
Object.keys(components).forEach((key) => {
2025-02-12 15:45:46 +02:00
const component = components[key]
const { validInnerComponents = [] } = component
2026-01-06 16:22:52 +02:00
validInnerComponents.forEach((inner) => {
2025-02-12 15:45:46 +02:00
const child = components[inner]
component.possibleChildren = component.possibleChildren || []
component.possibleChildren.push(child)
child.possibleParents = child.possibleParents || []
child.possibleParents.push(component)
})
})
2024-04-24 15:09:52 +03:00
const engineChecksum = sum(components)
2024-02-19 20:05:49 +02:00
const ruleToSelector = genericRuleToSelector(components)
2024-04-24 15:09:52 +03:00
export const getEngineChecksum = () => engineChecksum
2024-07-17 17:19:57 +03:00
/**
* Initializes and compiles the theme according to the ruleset
*
* @param {Object[]} inputRuleset - set of rules to compile theme against. Acts as an override to
* component default rulesets
* @param {string} ultimateBackgroundColor - Color that will be the "final" background for
* calculating contrast ratios and making text automatically accessible. Really used for cases when
* stuff is transparent.
* @param {boolean} debug - print out debug information in console, mostly just performance stuff
* @param {boolean} liteMode - use validInnerComponentsLite instead of validInnerComponents, meant to
* generatate theme previews and such that need to be compiled faster and don't require a lot of other
* components present in "normal" mode
* @param {boolean} onlyNormalState - only use components 'normal' states, meant for generating theme
* previews since states are the biggest factor for compilation time and are completely unnecessary
* when previewing multiple themes at same time
*/
2024-07-04 03:20:26 +03:00
export const init = ({
inputRuleset,
2024-07-04 03:20:26 +03:00
ultimateBackgroundColor,
debug = false,
liteMode = false,
editMode = false,
2024-07-17 17:19:57 +03:00
onlyNormalState = false,
2026-01-06 16:22:52 +02:00
initialStaticVars = {},
2024-07-04 03:20:26 +03:00
}) => {
const rootComponentName = 'Root'
if (!inputRuleset) throw new Error('Ruleset is null or undefined!')
const staticVars = { ...initialStaticVars }
2024-02-08 18:18:01 +02:00
const stacked = {}
const computed = {}
2024-02-11 23:11:28 +02:00
const rulesetUnsorted = [
...Object.values(components)
2026-01-06 16:22:52 +02:00
.map((c) =>
(c.defaultRules || []).map((r) => ({
source: 'Built-in',
component: c.name,
...r,
})),
)
2024-02-11 23:11:28 +02:00
.reduce((acc, arr) => [...acc, ...arr], []),
2026-01-06 16:22:52 +02:00
...inputRuleset,
].map((rule) => {
2024-02-11 23:11:28 +02:00
normalizeCombination(rule)
let currentParent = rule.parent
while (currentParent) {
normalizeCombination(currentParent)
currentParent = currentParent.parent
}
return rule
})
2024-01-31 17:39:51 +02:00
2024-02-11 23:11:28 +02:00
const ruleset = rulesetUnsorted
.map((data, index) => ({ data, index }))
2024-09-12 12:46:47 +03:00
.toSorted(({ data: a, index: ai }, { data: b, index: bi }) => {
2024-02-11 23:11:28 +02:00
const parentsA = unroll(a).length
const parentsB = unroll(b).length
2024-09-12 12:46:47 +03:00
let aScore = 0
let bScore = 0
aScore += parentsA * 1000
bScore += parentsB * 1000
aScore += a.variant !== 'normal' ? 100 : 0
bScore += b.variant !== 'normal' ? 100 : 0
2026-01-06 16:22:52 +02:00
aScore += a.state.filter((x) => x !== 'normal').length * 1000
bScore += b.state.filter((x) => x !== 'normal').length * 1000
2024-09-12 12:46:47 +03:00
aScore += a.component === 'Text' ? 1 : 0
bScore += b.component === 'Text' ? 1 : 0
// Debug
2024-11-12 23:24:28 +02:00
a._specificityScore = aScore
b._specificityScore = bScore
2024-09-12 12:46:47 +03:00
if (aScore === bScore) {
return ai - bi
}
2024-09-12 12:46:47 +03:00
return aScore - bScore
2024-02-11 23:11:28 +02:00
})
.map(({ data }) => data)
2024-10-02 16:22:28 +03:00
if (!ultimateBackgroundColor) {
2026-01-06 16:22:52 +02:00
console.warn(
'No ultimate background color provided, falling back to panel color',
)
const rootRule = ruleset.findLast(
(x) => x.component === 'Root' && x.directives?.['--bg'],
)
2024-10-02 16:22:28 +03:00
ultimateBackgroundColor = rootRule.directives['--bg'].split('|')[1].trim()
}
2026-01-06 16:22:52 +02:00
const virtualComponents = new Set(
Object.values(components)
.filter((c) => c.virtual)
.map((c) => c.name),
)
const transparentComponents = new Set(
Object.values(components)
.filter((c) => c.transparent)
.map((c) => c.name),
)
const nonEditableComponents = new Set(
Object.values(components)
.filter((c) => c.notEditable)
.map((c) => c.name),
)
2025-02-12 15:45:46 +02:00
const extraCompileComponents = new Set([])
2026-01-06 16:22:52 +02:00
Object.values(components).forEach((component) => {
const relevantRules = ruleset.filter((r) => r.component === component.name)
const backgrounds = relevantRules
.map((r) => r.directives.background)
.filter((x) => x)
const opacities = relevantRules
.map((r) => r.directives.opacity)
.filter((x) => x)
2025-02-12 15:45:46 +02:00
if (
2026-01-06 16:22:52 +02:00
backgrounds.some((x) => x.match(/--parent/)) ||
opacities.some((x) => x != null && x < 1)
) {
2025-02-12 15:45:46 +02:00
extraCompileComponents.add(component.name)
}
})
const processCombination = (combination) => {
2024-10-25 16:39:37 +03:00
try {
const selector = ruleToSelector(combination, true)
const cssSelector = ruleToSelector(combination)
2024-10-25 16:39:37 +03:00
const parentSelector = selector.split(/ /g).slice(0, -1).join(' ')
const soloSelector = selector.split(/ /g).slice(-1)[0]
2024-10-25 16:39:37 +03:00
const lowerLevelSelector = parentSelector
let lowerLevelBackground = computed[lowerLevelSelector]?.background
if (editMode && !lowerLevelBackground) {
// FIXME hack for editor until it supports handling component backgrounds
lowerLevelBackground = '#00FFFF'
}
2026-01-06 16:22:52 +02:00
const lowerLevelVirtualDirectives =
computed[lowerLevelSelector]?.virtualDirectives
const lowerLevelVirtualDirectivesRaw =
computed[lowerLevelSelector]?.virtualDirectivesRaw
2024-10-25 16:39:37 +03:00
const dynamicVars = computed[selector] || {
lowerLevelSelector,
2024-10-25 16:39:37 +03:00
lowerLevelBackground,
lowerLevelVirtualDirectives,
2026-01-06 16:22:52 +02:00
lowerLevelVirtualDirectivesRaw,
}
2024-10-25 16:39:37 +03:00
// Inheriting all of the applicable rules
const existingRules = ruleset.filter(findRules(combination))
2026-01-06 16:22:52 +02:00
const computedDirectives = existingRules
.map((r) => r.directives)
.reduce((acc, directives) => ({ ...acc, ...directives }), {})
2024-10-25 16:39:37 +03:00
const computedRule = {
...combination,
2026-01-06 16:22:52 +02:00
directives: computedDirectives,
}
2024-10-25 16:39:37 +03:00
computed[selector] = computed[selector] || {}
2024-10-25 16:39:37 +03:00
computed[selector].computedRule = computedRule
computed[selector].dynamicVars = dynamicVars
2024-12-30 16:40:29 +02:00
// avoid putting more stuff into actual CSS
computed[selector].virtualDirectives = {}
2024-12-30 16:40:29 +02:00
// but still be able to access it i.e. from --parent
2026-01-06 16:22:52 +02:00
computed[selector].virtualDirectivesRaw =
computed[lowerLevelSelector]?.virtualDirectivesRaw || {}
2024-10-25 16:39:37 +03:00
if (virtualComponents.has(combination.component)) {
const virtualName = [
'--',
combination.component.toLowerCase(),
combination.variant === 'normal'
? ''
2026-01-06 16:22:52 +02:00
: combination.variant[0].toUpperCase() +
combination.variant.slice(1).toLowerCase(),
...sortBy(combination.state.filter((x) => x !== 'normal')).map(
(state) => state[0].toUpperCase() + state.slice(1).toLowerCase(),
),
2024-10-25 16:39:37 +03:00
].join('')
let inheritedTextColor = computedDirectives.textColor
let inheritedTextAuto = computedDirectives.textAuto
let inheritedTextOpacity = computedDirectives.textOpacity
let inheritedTextOpacityMode = computedDirectives.textOpacityMode
2026-01-06 16:22:52 +02:00
const lowerLevelTextSelector = [
...selector.split(/ /g).slice(0, -1),
soloSelector,
].join(' ')
2024-10-25 16:39:37 +03:00
const lowerLevelTextRule = computed[lowerLevelTextSelector]
2026-01-06 16:22:52 +02:00
if (
inheritedTextColor == null ||
inheritedTextOpacity == null ||
inheritedTextOpacityMode == null
) {
inheritedTextColor =
computedDirectives.textColor ?? lowerLevelTextRule.textColor
inheritedTextAuto =
computedDirectives.textAuto ?? lowerLevelTextRule.textAuto
inheritedTextOpacity =
computedDirectives.textOpacity ?? lowerLevelTextRule.textOpacity
inheritedTextOpacityMode =
computedDirectives.textOpacityMode ??
lowerLevelTextRule.textOpacityMode
2024-10-25 16:39:37 +03:00
}
2024-10-25 16:39:37 +03:00
const newTextRule = {
...computedRule,
directives: {
...computedRule.directives,
textColor: inheritedTextColor,
textAuto: inheritedTextAuto ?? 'preserve',
textOpacity: inheritedTextOpacity,
2026-01-06 16:22:52 +02:00
textOpacityMode: inheritedTextOpacityMode,
},
}
2024-10-25 16:39:37 +03:00
dynamicVars.inheritedBackground = lowerLevelBackground
dynamicVars.stacked = convert(stacked[lowerLevelSelector]).rgb
2026-01-06 16:22:52 +02:00
const intendedTextColor = convert(
findColor(inheritedTextColor, { dynamicVars, staticVars }),
).rgb
const textColor =
newTextRule.directives.textAuto === 'no-auto'
? intendedTextColor
: getTextColor(
convert(stacked[lowerLevelSelector]).rgb,
intendedTextColor,
newTextRule.directives.textAuto === 'preserve',
)
const virtualDirectives = {
...(computed[lowerLevelSelector].virtualDirectives || {}),
}
const virtualDirectivesRaw = {
...(computed[lowerLevelSelector].virtualDirectivesRaw || {}),
}
2024-10-25 16:39:37 +03:00
// Storing color data in lower layer to use as custom css properties
2026-01-06 16:22:52 +02:00
virtualDirectives[virtualName] = getTextColorAlpha(
newTextRule.directives,
textColor,
dynamicVars,
)
2024-10-25 16:39:37 +03:00
virtualDirectivesRaw[virtualName] = textColor
computed[lowerLevelSelector].virtualDirectives = virtualDirectives
computed[lowerLevelSelector].virtualDirectivesRaw = virtualDirectivesRaw
return {
dynamicVars,
selector: cssSelector.split(/ /g).slice(0, -1).join(' '),
...combination,
directives: {},
virtualDirectives,
2026-01-06 16:22:52 +02:00
virtualDirectivesRaw,
2024-10-25 16:39:37 +03:00
}
} else {
computed[selector] = computed[selector] || {}
// TODO: DEFAULT TEXT COLOR
2026-01-06 16:22:52 +02:00
const lowerLevelStackedBackground =
stacked[lowerLevelSelector] || convert(ultimateBackgroundColor).rgb
2024-10-25 16:39:37 +03:00
if (computedDirectives.background) {
let inheritRule = null
const variantRules = ruleset.filter(
findRules({
component: combination.component,
variant: combination.variant,
2026-01-06 16:22:52 +02:00
parent: combination.parent,
}),
2024-10-25 16:39:37 +03:00
)
const lastVariantRule = variantRules[variantRules.length - 1]
if (lastVariantRule) {
inheritRule = lastVariantRule
} else {
2026-01-06 16:22:52 +02:00
const normalRules = ruleset.filter(
findRules({
component: combination.component,
parent: combination.parent,
}),
)
2024-10-25 16:39:37 +03:00
const lastNormalRule = normalRules[normalRules.length - 1]
inheritRule = lastNormalRule
}
2026-01-06 16:22:52 +02:00
const inheritSelector = ruleToSelector(
{ ...inheritRule, parent: combination.parent },
true,
)
2024-10-25 16:39:37 +03:00
const inheritedBackground = computed[inheritSelector].background
2024-10-25 16:39:37 +03:00
dynamicVars.inheritedBackground = inheritedBackground
2026-01-06 16:22:52 +02:00
const rgb = convert(
findColor(computedDirectives.background, {
dynamicVars,
staticVars,
}),
).rgb
2024-10-25 16:39:37 +03:00
if (!stacked[selector]) {
let blend
const alpha = computedDirectives.opacity ?? 1
if (alpha >= 1) {
blend = rgb
} else if (alpha <= 0) {
blend = lowerLevelStackedBackground
} else {
2026-01-06 16:22:52 +02:00
blend = alphaBlend(
rgb,
computedDirectives.opacity,
lowerLevelStackedBackground,
)
2024-10-25 16:39:37 +03:00
}
stacked[selector] = blend
2026-01-06 16:22:52 +02:00
computed[selector].background = {
...rgb,
a: computedDirectives.opacity ?? 1,
}
}
}
2024-10-25 16:39:37 +03:00
if (computedDirectives.shadow) {
2026-01-06 16:22:52 +02:00
dynamicVars.shadow = flattenDeep(
findShadow(flattenDeep(computedDirectives.shadow), {
dynamicVars,
staticVars,
}),
)
2024-10-25 16:39:37 +03:00
}
2024-10-25 16:39:37 +03:00
if (!stacked[selector]) {
computedDirectives.background = 'transparent'
computedDirectives.opacity = 0
stacked[selector] = lowerLevelStackedBackground
2026-01-06 16:22:52 +02:00
computed[selector].background = {
...lowerLevelStackedBackground,
a: 0,
}
2024-10-25 16:39:37 +03:00
}
2024-10-25 16:39:37 +03:00
dynamicVars.stacked = stacked[selector]
dynamicVars.background = computed[selector].background
2026-01-06 16:22:52 +02:00
const dynamicSlots = Object.entries(computedDirectives).filter(([k]) =>
k.startsWith('--'),
)
2024-10-25 16:39:37 +03:00
dynamicSlots.forEach(([k, v]) => {
2026-01-06 16:22:52 +02:00
const [type, value] = v.split('|').map((x) => x.trim()) // woah, Extreme!
2024-10-25 16:39:37 +03:00
switch (type) {
case 'color': {
const color = findColor(value, { dynamicVars, staticVars })
dynamicVars[k] = color
if (combination.component === rootComponentName) {
staticVars[k.substring(2)] = color
}
break
}
2024-10-25 16:39:37 +03:00
case 'shadow': {
2026-01-06 16:22:52 +02:00
const shadow = value
.split(/,/g)
.map((s) => s.trim())
.filter((x) => x)
2024-10-25 16:39:37 +03:00
dynamicVars[k] = shadow
if (combination.component === rootComponentName) {
staticVars[k.substring(2)] = shadow
}
break
}
2024-10-25 16:39:37 +03:00
case 'generic': {
dynamicVars[k] = value
if (combination.component === rootComponentName) {
staticVars[k.substring(2)] = value
}
break
}
}
2024-10-25 16:39:37 +03:00
})
const rule = {
dynamicVars,
selector: cssSelector,
...combination,
2026-01-06 16:22:52 +02:00
directives: computedDirectives,
}
2024-10-25 16:39:37 +03:00
return rule
}
2024-10-25 16:39:37 +03:00
} catch (e) {
const { component, variant, state } = combination
2026-01-06 16:22:52 +02:00
throw new Error(
`Error processing combination ${component}.${variant}:${state.join(':')}: ${e}`,
)
}
}
const processInnerComponent = (component, parent) => {
const combinations = []
2026-01-06 16:22:52 +02:00
const { states: originalStates = {}, variants: originalVariants = {} } =
component
2024-01-18 14:35:25 +02:00
let validInnerComponents
if (editMode) {
2026-01-06 16:22:52 +02:00
const temp =
component.validInnerComponentsLite ||
component.validInnerComponents ||
[]
validInnerComponents = temp.filter(
(c) => virtualComponents.has(c) && !nonEditableComponents.has(c),
)
} else if (liteMode) {
2026-01-06 16:22:52 +02:00
validInnerComponents =
component.validInnerComponentsLite ||
component.validInnerComponents ||
[]
} else if (
component.name === 'Root' ||
component.states != null ||
component.background?.includes('--parent')
) {
validInnerComponents = component.validInnerComponents || []
2025-02-12 15:45:46 +02:00
} else {
2026-01-06 16:22:52 +02:00
validInnerComponents =
component.validInnerComponents?.filter(
(c) =>
virtualComponents.has(c) ||
transparentComponents.has(c) ||
extraCompileComponents.has(c),
) || []
}
2024-07-17 17:19:57 +03:00
// Normalizing states and variants to always include "normal"
2024-01-18 14:35:25 +02:00
const states = { normal: '', ...originalStates }
const variants = { normal: '', ...originalVariants }
2026-01-06 16:22:52 +02:00
const innerComponents = validInnerComponents.map((name) => {
const result = components[name]
2026-01-06 16:22:52 +02:00
if (result === undefined)
console.error(
`Component ${component.name} references a component ${name} which does not exist!`,
)
return result
})
2024-01-18 14:35:25 +02:00
// Optimization: we only really need combinations without "normal" because all states implicitly have it
2026-01-06 16:22:52 +02:00
const permutationStateKeys = Object.keys(states).filter(
(s) => s !== 'normal',
)
const stateCombinations =
onlyNormalState && !virtualComponents.has(component.name)
? [['normal']]
: [
['normal'],
...getAllPossibleCombinations(permutationStateKeys)
.map((combination) => ['normal', ...combination])
.filter((combo) => {
// Optimization: filter out some hard-coded combinations that don't make sense
if (combo.indexOf('disabled') >= 0) {
return !(
combo.indexOf('hover') >= 0 ||
2024-07-17 17:19:57 +03:00
combo.indexOf('focused') >= 0 ||
combo.indexOf('pressed') >= 0
2026-01-06 16:22:52 +02:00
)
}
return true
}),
]
const stateVariantCombination = Object.keys(variants)
.map((variant) => {
return stateCombinations.map((state) => ({ variant, state }))
})
.reduce((acc, x) => [...acc, ...x], [])
2024-01-18 14:35:25 +02:00
2026-01-06 16:22:52 +02:00
stateVariantCombination.forEach((combination) => {
combination.component = component.name
combination.lazy = component.lazy || parent?.lazy
combination.parent = parent
if (!liteMode && combination.state.indexOf('hover') >= 0) {
combination.lazy = true
}
if (
2025-02-18 18:30:08 +02:00
!liteMode &&
parent?.component !== 'Root' &&
2026-01-06 16:22:52 +02:00
!virtualComponents.has(component.name) &&
!transparentComponents.has(component.name) &&
extraCompileComponents.has(component.name)
) {
2025-02-12 15:54:30 +02:00
combination.lazy = true
}
combinations.push(combination)
2024-02-08 18:18:01 +02:00
2026-01-06 16:22:52 +02:00
innerComponents.forEach((innerComponent) => {
combinations.push(...processInnerComponent(innerComponent, combination))
})
2024-01-18 14:35:25 +02:00
})
2024-02-08 18:18:01 +02:00
return combinations
2024-01-18 14:35:25 +02:00
}
const t0 = performance.now()
2026-01-06 16:22:52 +02:00
const combinations = processInnerComponent(
components[rootComponentName] ?? components.Root,
)
const t1 = performance.now()
2024-07-17 17:19:57 +03:00
if (debug) {
console.debug('Tree traveral took ' + (t1 - t0) + ' ms')
}
2026-01-06 16:22:52 +02:00
const result = combinations
.map((combination) => {
if (combination.lazy) {
return async () => processCombination(combination)
} else {
return processCombination(combination)
}
})
.filter((x) => x)
const t2 = performance.now()
2024-07-17 17:19:57 +03:00
if (debug) {
console.debug('Eager processing took ' + (t2 - t1) + ' ms')
}
2024-09-19 20:37:14 +03:00
// optimization to traverse big-ass array only once instead of twice
const eager = []
const lazy = []
2026-01-06 16:22:52 +02:00
result.forEach((x) => {
2024-09-19 20:37:14 +03:00
if (typeof x === 'function') {
lazy.push(x)
} else {
eager.push(x)
}
})
return {
2024-09-19 20:37:14 +03:00
lazy,
eager,
staticVars,
2024-12-12 15:07:12 +02:00
engineChecksum,
2026-01-06 16:22:52 +02:00
themeChecksum: sum([lazy, eager]),
}
2024-01-18 14:35:25 +02:00
}