775 lines
24 KiB
JavaScript
775 lines
24 KiB
JavaScript
import { convert, brightness } from 'chromatism'
|
|
import sum from 'hash-sum'
|
|
import { flattenDeep, sortBy } from 'lodash'
|
|
import {
|
|
alphaBlend,
|
|
getTextColor,
|
|
rgba2css,
|
|
mixrgb,
|
|
relativeLuminance,
|
|
} from '../color_convert/color_convert.js'
|
|
|
|
import {
|
|
colorFunctions,
|
|
shadowFunctions,
|
|
process,
|
|
} from './theme3_slot_functions.js'
|
|
|
|
import {
|
|
unroll,
|
|
getAllPossibleCombinations,
|
|
genericRuleToSelector,
|
|
normalizeCombination,
|
|
findRules,
|
|
} from './iss_utils.js'
|
|
import { deserializeShadow } from './iss_deserializer.js'
|
|
|
|
// Ensuring the order of components
|
|
const components = {
|
|
Root: null,
|
|
Text: null,
|
|
FunText: null,
|
|
Link: null,
|
|
Icon: null,
|
|
Border: null,
|
|
PanelHeader: null,
|
|
Panel: null,
|
|
Chat: null,
|
|
ChatMessage: null,
|
|
Button: null,
|
|
}
|
|
|
|
export const findShadow = (shadows, { dynamicVars, staticVars }) => {
|
|
return (shadows || []).map((shadow) => {
|
|
let targetShadow
|
|
if (typeof shadow === 'string') {
|
|
if (shadow.startsWith('$')) {
|
|
targetShadow = process(
|
|
shadow,
|
|
shadowFunctions,
|
|
{ findColor, findShadow },
|
|
{ dynamicVars, staticVars },
|
|
)
|
|
} else if (shadow.startsWith('--')) {
|
|
// modifiers are completely unsupported here
|
|
const variableSlot = shadow.substring(2)
|
|
return findShadow(staticVars[variableSlot], { dynamicVars, staticVars })
|
|
} else {
|
|
targetShadow = deserializeShadow(shadow)
|
|
}
|
|
} else {
|
|
targetShadow = shadow
|
|
}
|
|
|
|
const shadowArray = Array.isArray(targetShadow)
|
|
? targetShadow
|
|
: [targetShadow]
|
|
return shadowArray.map((s) => ({
|
|
...s,
|
|
color: findColor(s.color, { dynamicVars, staticVars }),
|
|
}))
|
|
})
|
|
}
|
|
|
|
export const findColor = (color, { dynamicVars, staticVars }) => {
|
|
try {
|
|
if (
|
|
typeof color !== 'string' ||
|
|
(!color.startsWith('--') && !color.startsWith('$'))
|
|
)
|
|
return color
|
|
let targetColor = null
|
|
if (color.startsWith('--')) {
|
|
// Modifier support is pretty much for v2 themes only
|
|
const [variable, modifier] = color.split(/,/g).map((str) => str.trim())
|
|
const variableSlot = variable.substring(2)
|
|
if (variableSlot === 'stack') {
|
|
const { r, g, b } = dynamicVars.stacked
|
|
targetColor = { r, g, b }
|
|
} else if (variableSlot.startsWith('parent')) {
|
|
if (variableSlot === 'parent') {
|
|
const { r, g, b } = dynamicVars.lowerLevelBackground ?? {}
|
|
targetColor = { r, g, b }
|
|
} else {
|
|
const virtualSlot = variableSlot.replace(/^parent/, '')
|
|
targetColor = convert(
|
|
dynamicVars.lowerLevelVirtualDirectivesRaw[virtualSlot],
|
|
).rgb
|
|
}
|
|
} 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)}`)
|
|
}
|
|
targetColor = convert(staticVar ?? dynamicVar ?? '#FF00FF').rgb
|
|
}
|
|
|
|
if (modifier) {
|
|
const effectiveBackground =
|
|
dynamicVars.lowerLevelBackground ?? targetColor
|
|
const isLightOnDark =
|
|
relativeLuminance(convert(effectiveBackground).rgb) < 0.5
|
|
const mod = isLightOnDark ? 1 : -1
|
|
targetColor = brightness(
|
|
Number.parseFloat(modifier) * mod,
|
|
targetColor,
|
|
).rgb
|
|
}
|
|
}
|
|
|
|
if (color.startsWith('$')) {
|
|
try {
|
|
targetColor = process(
|
|
color,
|
|
colorFunctions,
|
|
{ findColor },
|
|
{ dynamicVars, staticVars },
|
|
)
|
|
} catch (e) {
|
|
console.error(
|
|
'Failure executing color function',
|
|
e,
|
|
'\n Function: ' + color,
|
|
)
|
|
targetColor = '#FF00FF'
|
|
}
|
|
}
|
|
// 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}`)
|
|
}
|
|
}
|
|
|
|
const getTextColorAlpha = (
|
|
directives,
|
|
intendedTextColor,
|
|
dynamicVars,
|
|
staticVars,
|
|
) => {
|
|
const opacity = directives.textOpacity
|
|
const backgroundColor = convert(dynamicVars.lowerLevelBackground).rgb
|
|
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
|
|
const componentsContext = import.meta.glob(
|
|
['/src/**/*.style.js', '/src/**/*.style.json'],
|
|
{ eager: true },
|
|
)
|
|
Object.keys(componentsContext).forEach((key) => {
|
|
const component = componentsContext[key].default
|
|
if (components[component.name] != null) {
|
|
console.warn(
|
|
`Component in file ${key} is trying to override existing component ${component.name}! You have collisions/duplicates!`,
|
|
)
|
|
}
|
|
components[component.name] = component
|
|
})
|
|
|
|
Object.keys(components).forEach((key) => {
|
|
if (key === 'Root') return
|
|
components.Root.validInnerComponents =
|
|
components.Root.validInnerComponents || []
|
|
components.Root.validInnerComponents.push(key)
|
|
})
|
|
|
|
Object.keys(components).forEach((key) => {
|
|
const component = components[key]
|
|
const { validInnerComponents = [] } = component
|
|
validInnerComponents.forEach((inner) => {
|
|
const child = components[inner]
|
|
component.possibleChildren = component.possibleChildren || []
|
|
component.possibleChildren.push(child)
|
|
child.possibleParents = child.possibleParents || []
|
|
child.possibleParents.push(component)
|
|
})
|
|
})
|
|
|
|
const engineChecksum = sum(components)
|
|
|
|
const ruleToSelector = genericRuleToSelector(components)
|
|
|
|
export const getEngineChecksum = () => engineChecksum
|
|
|
|
/**
|
|
* 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
|
|
*/
|
|
export const init = ({
|
|
inputRuleset,
|
|
ultimateBackgroundColor,
|
|
debug = false,
|
|
liteMode = false,
|
|
editMode = false,
|
|
onlyNormalState = false,
|
|
initialStaticVars = {},
|
|
}) => {
|
|
const rootComponentName = 'Root'
|
|
if (!inputRuleset) throw new Error('Ruleset is null or undefined!')
|
|
const staticVars = { ...initialStaticVars }
|
|
const stacked = {}
|
|
const computed = {}
|
|
|
|
const rulesetUnsorted = [
|
|
...Object.values(components)
|
|
.map((c) =>
|
|
(c.defaultRules || []).map((r) => ({
|
|
source: 'Built-in',
|
|
component: c.name,
|
|
...r,
|
|
})),
|
|
)
|
|
.reduce((acc, arr) => [...acc, ...arr], []),
|
|
...inputRuleset,
|
|
].map((rule) => {
|
|
normalizeCombination(rule)
|
|
let currentParent = rule.parent
|
|
while (currentParent) {
|
|
normalizeCombination(currentParent)
|
|
currentParent = currentParent.parent
|
|
}
|
|
|
|
return rule
|
|
})
|
|
|
|
const ruleset = rulesetUnsorted
|
|
.map((data, index) => ({ data, index }))
|
|
.toSorted(({ data: a, index: ai }, { data: b, index: bi }) => {
|
|
const parentsA = unroll(a).length
|
|
const parentsB = unroll(b).length
|
|
|
|
let aScore = 0
|
|
let bScore = 0
|
|
|
|
aScore += parentsA * 1000
|
|
bScore += parentsB * 1000
|
|
|
|
aScore += a.variant !== 'normal' ? 100 : 0
|
|
bScore += b.variant !== 'normal' ? 100 : 0
|
|
|
|
aScore += a.state.filter((x) => x !== 'normal').length * 1000
|
|
bScore += b.state.filter((x) => x !== 'normal').length * 1000
|
|
|
|
aScore += a.component === 'Text' ? 1 : 0
|
|
bScore += b.component === 'Text' ? 1 : 0
|
|
|
|
// Debug
|
|
a._specificityScore = aScore
|
|
b._specificityScore = bScore
|
|
|
|
if (aScore === bScore) {
|
|
return ai - bi
|
|
}
|
|
return aScore - bScore
|
|
})
|
|
.map(({ data }) => data)
|
|
|
|
if (!ultimateBackgroundColor) {
|
|
console.warn(
|
|
'No ultimate background color provided, falling back to panel color',
|
|
)
|
|
const rootRule = ruleset.findLast(
|
|
(x) => x.component === 'Root' && x.directives?.['--bg'],
|
|
)
|
|
ultimateBackgroundColor = rootRule.directives['--bg'].split('|')[1].trim()
|
|
}
|
|
|
|
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),
|
|
)
|
|
const extraCompileComponents = new Set([])
|
|
|
|
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)
|
|
if (
|
|
backgrounds.some((x) => x.match(/--parent/)) ||
|
|
opacities.some((x) => x != null && x < 1)
|
|
) {
|
|
extraCompileComponents.add(component.name)
|
|
}
|
|
})
|
|
|
|
const processCombination = (combination) => {
|
|
try {
|
|
const selector = ruleToSelector(combination, true)
|
|
const cssSelector = ruleToSelector(combination)
|
|
|
|
const parentSelector = selector.split(/ /g).slice(0, -1).join(' ')
|
|
const soloSelector = selector.split(/ /g).slice(-1)[0]
|
|
|
|
const lowerLevelSelector = parentSelector
|
|
let lowerLevelBackground = computed[lowerLevelSelector]?.background
|
|
if (editMode && !lowerLevelBackground) {
|
|
// FIXME hack for editor until it supports handling component backgrounds
|
|
lowerLevelBackground = '#00FFFF'
|
|
}
|
|
const lowerLevelVirtualDirectives =
|
|
computed[lowerLevelSelector]?.virtualDirectives
|
|
const lowerLevelVirtualDirectivesRaw =
|
|
computed[lowerLevelSelector]?.virtualDirectivesRaw
|
|
|
|
const dynamicVars = computed[selector] || {
|
|
lowerLevelSelector,
|
|
lowerLevelBackground,
|
|
lowerLevelVirtualDirectives,
|
|
lowerLevelVirtualDirectivesRaw,
|
|
}
|
|
|
|
// Inheriting all of the applicable rules
|
|
const existingRules = ruleset.filter(findRules(combination))
|
|
const computedDirectives = existingRules
|
|
.map((r) => r.directives)
|
|
.reduce((acc, directives) => ({ ...acc, ...directives }), {})
|
|
const computedRule = {
|
|
...combination,
|
|
directives: computedDirectives,
|
|
}
|
|
|
|
computed[selector] = computed[selector] || {}
|
|
computed[selector].computedRule = computedRule
|
|
computed[selector].dynamicVars = dynamicVars
|
|
|
|
// avoid putting more stuff into actual CSS
|
|
computed[selector].virtualDirectives = {}
|
|
// but still be able to access it i.e. from --parent
|
|
computed[selector].virtualDirectivesRaw =
|
|
computed[lowerLevelSelector]?.virtualDirectivesRaw || {}
|
|
|
|
if (virtualComponents.has(combination.component)) {
|
|
const virtualName = [
|
|
'--',
|
|
combination.component.toLowerCase(),
|
|
combination.variant === 'normal'
|
|
? ''
|
|
: 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(),
|
|
),
|
|
].join('')
|
|
|
|
let inheritedTextColor = computedDirectives.textColor
|
|
let inheritedTextAuto = computedDirectives.textAuto
|
|
let inheritedTextOpacity = computedDirectives.textOpacity
|
|
let inheritedTextOpacityMode = computedDirectives.textOpacityMode
|
|
const lowerLevelTextSelector = [
|
|
...selector.split(/ /g).slice(0, -1),
|
|
soloSelector,
|
|
].join(' ')
|
|
const lowerLevelTextRule = computed[lowerLevelTextSelector]
|
|
|
|
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
|
|
}
|
|
|
|
const newTextRule = {
|
|
...computedRule,
|
|
directives: {
|
|
...computedRule.directives,
|
|
textColor: inheritedTextColor,
|
|
textAuto: inheritedTextAuto ?? 'preserve',
|
|
textOpacity: inheritedTextOpacity,
|
|
textOpacityMode: inheritedTextOpacityMode,
|
|
},
|
|
}
|
|
|
|
dynamicVars.inheritedBackground = lowerLevelBackground
|
|
dynamicVars.stacked = convert(stacked[lowerLevelSelector]).rgb
|
|
|
|
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 || {}),
|
|
}
|
|
|
|
// Storing color data in lower layer to use as custom css properties
|
|
virtualDirectives[virtualName] = getTextColorAlpha(
|
|
newTextRule.directives,
|
|
textColor,
|
|
dynamicVars,
|
|
)
|
|
virtualDirectivesRaw[virtualName] = textColor
|
|
|
|
computed[lowerLevelSelector].virtualDirectives = virtualDirectives
|
|
computed[lowerLevelSelector].virtualDirectivesRaw = virtualDirectivesRaw
|
|
|
|
return {
|
|
dynamicVars,
|
|
selector: cssSelector.split(/ /g).slice(0, -1).join(' '),
|
|
...combination,
|
|
directives: {},
|
|
virtualDirectives,
|
|
virtualDirectivesRaw,
|
|
}
|
|
} else {
|
|
computed[selector] = computed[selector] || {}
|
|
|
|
// TODO: DEFAULT TEXT COLOR
|
|
const lowerLevelStackedBackground =
|
|
stacked[lowerLevelSelector] || convert(ultimateBackgroundColor).rgb
|
|
|
|
if (computedDirectives.background) {
|
|
let inheritRule = null
|
|
const variantRules = ruleset.filter(
|
|
findRules({
|
|
component: combination.component,
|
|
variant: combination.variant,
|
|
parent: combination.parent,
|
|
}),
|
|
)
|
|
const lastVariantRule = variantRules[variantRules.length - 1]
|
|
if (lastVariantRule) {
|
|
inheritRule = lastVariantRule
|
|
} else {
|
|
const normalRules = ruleset.filter(
|
|
findRules({
|
|
component: combination.component,
|
|
parent: combination.parent,
|
|
}),
|
|
)
|
|
const lastNormalRule = normalRules[normalRules.length - 1]
|
|
inheritRule = lastNormalRule
|
|
}
|
|
|
|
const inheritSelector = ruleToSelector(
|
|
{ ...inheritRule, parent: combination.parent },
|
|
true,
|
|
)
|
|
const inheritedBackground = computed[inheritSelector].background
|
|
|
|
dynamicVars.inheritedBackground = inheritedBackground
|
|
|
|
const rgb = convert(
|
|
findColor(computedDirectives.background, {
|
|
dynamicVars,
|
|
staticVars,
|
|
}),
|
|
).rgb
|
|
|
|
if (!stacked[selector]) {
|
|
let blend
|
|
const alpha = computedDirectives.opacity ?? 1
|
|
if (alpha >= 1) {
|
|
blend = rgb
|
|
} else if (alpha <= 0) {
|
|
blend = lowerLevelStackedBackground
|
|
} else {
|
|
blend = alphaBlend(
|
|
rgb,
|
|
computedDirectives.opacity,
|
|
lowerLevelStackedBackground,
|
|
)
|
|
}
|
|
stacked[selector] = blend
|
|
computed[selector].background = {
|
|
...rgb,
|
|
a: computedDirectives.opacity ?? 1,
|
|
}
|
|
}
|
|
}
|
|
|
|
if (computedDirectives.shadow) {
|
|
dynamicVars.shadow = flattenDeep(
|
|
findShadow(flattenDeep(computedDirectives.shadow), {
|
|
dynamicVars,
|
|
staticVars,
|
|
}),
|
|
)
|
|
}
|
|
|
|
if (!stacked[selector]) {
|
|
computedDirectives.background = 'transparent'
|
|
computedDirectives.opacity = 0
|
|
stacked[selector] = lowerLevelStackedBackground
|
|
computed[selector].background = {
|
|
...lowerLevelStackedBackground,
|
|
a: 0,
|
|
}
|
|
}
|
|
|
|
dynamicVars.stacked = stacked[selector]
|
|
dynamicVars.background = computed[selector].background
|
|
|
|
const dynamicSlots = Object.entries(computedDirectives).filter(([k]) =>
|
|
k.startsWith('--'),
|
|
)
|
|
|
|
dynamicSlots.forEach(([k, v]) => {
|
|
const [type, value] = v.split('|').map((x) => x.trim()) // woah, Extreme!
|
|
switch (type) {
|
|
case 'color': {
|
|
const color = findColor(value, { dynamicVars, staticVars })
|
|
dynamicVars[k] = color
|
|
if (combination.component === rootComponentName) {
|
|
staticVars[k.substring(2)] = color
|
|
}
|
|
break
|
|
}
|
|
case 'shadow': {
|
|
const shadow = value
|
|
.split(/,/g)
|
|
.map((s) => s.trim())
|
|
.filter((x) => x)
|
|
dynamicVars[k] = shadow
|
|
if (combination.component === rootComponentName) {
|
|
staticVars[k.substring(2)] = shadow
|
|
}
|
|
break
|
|
}
|
|
case 'generic': {
|
|
dynamicVars[k] = value
|
|
if (combination.component === rootComponentName) {
|
|
staticVars[k.substring(2)] = value
|
|
}
|
|
break
|
|
}
|
|
}
|
|
})
|
|
|
|
const rule = {
|
|
dynamicVars,
|
|
selector: cssSelector,
|
|
...combination,
|
|
directives: computedDirectives,
|
|
}
|
|
|
|
return rule
|
|
}
|
|
} catch (e) {
|
|
const { component, variant, state } = combination
|
|
throw new Error(
|
|
`Error processing combination ${component}.${variant}:${state.join(':')}: ${e}`,
|
|
)
|
|
}
|
|
}
|
|
|
|
const processInnerComponent = (component, parent) => {
|
|
const combinations = []
|
|
const { states: originalStates = {}, variants: originalVariants = {} } =
|
|
component
|
|
|
|
let validInnerComponents
|
|
if (editMode) {
|
|
const temp =
|
|
component.validInnerComponentsLite ||
|
|
component.validInnerComponents ||
|
|
[]
|
|
validInnerComponents = temp.filter(
|
|
(c) => virtualComponents.has(c) && !nonEditableComponents.has(c),
|
|
)
|
|
} else if (liteMode) {
|
|
validInnerComponents =
|
|
component.validInnerComponentsLite ||
|
|
component.validInnerComponents ||
|
|
[]
|
|
} else if (
|
|
component.name === 'Root' ||
|
|
component.states != null ||
|
|
component.background?.includes('--parent')
|
|
) {
|
|
validInnerComponents = component.validInnerComponents || []
|
|
} else {
|
|
validInnerComponents =
|
|
component.validInnerComponents?.filter(
|
|
(c) =>
|
|
virtualComponents.has(c) ||
|
|
transparentComponents.has(c) ||
|
|
extraCompileComponents.has(c),
|
|
) || []
|
|
}
|
|
|
|
// Normalizing states and variants to always include "normal"
|
|
const states = { normal: '', ...originalStates }
|
|
const variants = { normal: '', ...originalVariants }
|
|
const innerComponents = validInnerComponents.map((name) => {
|
|
const result = components[name]
|
|
if (result === undefined)
|
|
console.error(
|
|
`Component ${component.name} references a component ${name} which does not exist!`,
|
|
)
|
|
return result
|
|
})
|
|
|
|
// Optimization: we only really need combinations without "normal" because all states implicitly have it
|
|
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 ||
|
|
combo.indexOf('focused') >= 0 ||
|
|
combo.indexOf('pressed') >= 0
|
|
)
|
|
}
|
|
return true
|
|
}),
|
|
]
|
|
|
|
const stateVariantCombination = Object.keys(variants)
|
|
.map((variant) => {
|
|
return stateCombinations.map((state) => ({ variant, state }))
|
|
})
|
|
.reduce((acc, x) => [...acc, ...x], [])
|
|
|
|
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 (
|
|
!liteMode &&
|
|
parent?.component !== 'Root' &&
|
|
!virtualComponents.has(component.name) &&
|
|
!transparentComponents.has(component.name) &&
|
|
extraCompileComponents.has(component.name)
|
|
) {
|
|
combination.lazy = true
|
|
}
|
|
|
|
combinations.push(combination)
|
|
|
|
innerComponents.forEach((innerComponent) => {
|
|
combinations.push(...processInnerComponent(innerComponent, combination))
|
|
})
|
|
})
|
|
|
|
return combinations
|
|
}
|
|
|
|
const t0 = performance.now()
|
|
const combinations = processInnerComponent(
|
|
components[rootComponentName] ?? components.Root,
|
|
)
|
|
const t1 = performance.now()
|
|
if (debug) {
|
|
console.debug('Tree traveral took ' + (t1 - t0) + ' ms')
|
|
}
|
|
|
|
const result = combinations
|
|
.map((combination) => {
|
|
if (combination.lazy) {
|
|
return async () => processCombination(combination)
|
|
} else {
|
|
return processCombination(combination)
|
|
}
|
|
})
|
|
.filter((x) => x)
|
|
const t2 = performance.now()
|
|
if (debug) {
|
|
console.debug('Eager processing took ' + (t2 - t1) + ' ms')
|
|
}
|
|
|
|
// optimization to traverse big-ass array only once instead of twice
|
|
const eager = []
|
|
const lazy = []
|
|
|
|
result.forEach((x) => {
|
|
if (typeof x === 'function') {
|
|
lazy.push(x)
|
|
} else {
|
|
eager.push(x)
|
|
}
|
|
})
|
|
|
|
return {
|
|
lazy,
|
|
eager,
|
|
staticVars,
|
|
engineChecksum,
|
|
themeChecksum: sum([lazy, eager]),
|
|
}
|
|
}
|