logo

pleroma-fe

My custom branche(s) on git.pleroma.social/pleroma/pleroma-fe git clone https://anongit.hacktivis.me/git/pleroma-fe.git/

theme_data_3.service.js (22775B)


  1. import { convert, brightness } from 'chromatism'
  2. import sum from 'hash-sum'
  3. import { flattenDeep, sortBy } from 'lodash'
  4. import {
  5. alphaBlend,
  6. getTextColor,
  7. rgba2css,
  8. mixrgb,
  9. relativeLuminance
  10. } from '../color_convert/color_convert.js'
  11. import {
  12. colorFunctions,
  13. shadowFunctions,
  14. process
  15. } from './theme3_slot_functions.js'
  16. import {
  17. unroll,
  18. getAllPossibleCombinations,
  19. genericRuleToSelector,
  20. normalizeCombination,
  21. findRules
  22. } from './iss_utils.js'
  23. import { deserializeShadow } from './iss_deserializer.js'
  24. // Ensuring the order of components
  25. const components = {
  26. Root: null,
  27. Text: null,
  28. FunText: null,
  29. Link: null,
  30. Icon: null,
  31. Border: null,
  32. PanelHeader: null,
  33. Attachment: null,
  34. Panel: null,
  35. Chat: null,
  36. ChatMessage: null,
  37. Button: null
  38. }
  39. export const findShadow = (shadows, { dynamicVars, staticVars }) => {
  40. return (shadows || []).map(shadow => {
  41. let targetShadow
  42. if (typeof shadow === 'string') {
  43. if (shadow.startsWith('$')) {
  44. targetShadow = process(shadow, shadowFunctions, { findColor, findShadow }, { dynamicVars, staticVars })
  45. } else if (shadow.startsWith('--')) {
  46. // modifiers are completely unsupported here
  47. const variableSlot = shadow.substring(2)
  48. return findShadow(staticVars[variableSlot], { dynamicVars, staticVars })
  49. } else {
  50. targetShadow = deserializeShadow(shadow)
  51. }
  52. } else {
  53. targetShadow = shadow
  54. }
  55. const shadowArray = Array.isArray(targetShadow) ? targetShadow : [targetShadow]
  56. return shadowArray.map(s => ({
  57. ...s,
  58. color: findColor(s.color, { dynamicVars, staticVars })
  59. }))
  60. })
  61. }
  62. export const findColor = (color, { dynamicVars, staticVars }) => {
  63. try {
  64. if (typeof color !== 'string' || (!color.startsWith('--') && !color.startsWith('$'))) return color
  65. let targetColor = null
  66. if (color.startsWith('--')) {
  67. // Modifier support is pretty much for v2 themes only
  68. const [variable, modifier] = color.split(/,/g).map(str => str.trim())
  69. const variableSlot = variable.substring(2)
  70. if (variableSlot === 'stack') {
  71. const { r, g, b } = dynamicVars.stacked
  72. targetColor = { r, g, b }
  73. } else if (variableSlot.startsWith('parent')) {
  74. if (variableSlot === 'parent') {
  75. const { r, g, b } = dynamicVars.lowerLevelBackground
  76. targetColor = { r, g, b }
  77. } else {
  78. const virtualSlot = variableSlot.replace(/^parent/, '')
  79. targetColor = convert(dynamicVars.lowerLevelVirtualDirectivesRaw[virtualSlot]).rgb
  80. }
  81. } else {
  82. switch (variableSlot) {
  83. case 'inheritedBackground':
  84. targetColor = convert(dynamicVars.inheritedBackground).rgb
  85. break
  86. case 'background':
  87. targetColor = convert(dynamicVars.background).rgb
  88. break
  89. default:
  90. targetColor = convert(staticVars[variableSlot]).rgb
  91. }
  92. }
  93. if (modifier) {
  94. const effectiveBackground = dynamicVars.lowerLevelBackground ?? targetColor
  95. const isLightOnDark = relativeLuminance(convert(effectiveBackground).rgb) < 0.5
  96. const mod = isLightOnDark ? 1 : -1
  97. targetColor = brightness(Number.parseFloat(modifier) * mod, targetColor).rgb
  98. }
  99. }
  100. if (color.startsWith('$')) {
  101. try {
  102. targetColor = process(color, colorFunctions, { findColor }, { dynamicVars, staticVars })
  103. } catch (e) {
  104. console.error('Failure executing color function', e)
  105. targetColor = '#FF00FF'
  106. }
  107. }
  108. // Color references other color
  109. return targetColor
  110. } catch (e) {
  111. throw new Error(`Couldn't find color "${color}", variables are:
  112. Static:
  113. ${JSON.stringify(staticVars, null, 2)}
  114. Dynamic:
  115. ${JSON.stringify(dynamicVars, null, 2)}`, e)
  116. }
  117. }
  118. const getTextColorAlpha = (directives, intendedTextColor, dynamicVars, staticVars) => {
  119. const opacity = directives.textOpacity
  120. const backgroundColor = convert(dynamicVars.lowerLevelBackground).rgb
  121. const textColor = convert(findColor(intendedTextColor, { dynamicVars, staticVars })).rgb
  122. if (opacity === null || opacity === undefined || opacity >= 1) {
  123. return convert(textColor).hex
  124. }
  125. if (opacity === 0) {
  126. return convert(backgroundColor).hex
  127. }
  128. const opacityMode = directives.textOpacityMode
  129. switch (opacityMode) {
  130. case 'fake':
  131. return convert(alphaBlend(textColor, opacity, backgroundColor)).hex
  132. case 'mixrgb':
  133. return convert(mixrgb(backgroundColor, textColor)).hex
  134. default:
  135. return rgba2css({ a: opacity, ...textColor })
  136. }
  137. }
  138. // Loading all style.js[on] files dynamically
  139. const componentsContext = require.context('src', true, /\.style.js(on)?$/)
  140. componentsContext.keys().forEach(key => {
  141. const component = componentsContext(key).default
  142. if (components[component.name] != null) {
  143. console.warn(`Component in file ${key} is trying to override existing component ${component.name}! You have collisions/duplicates!`)
  144. }
  145. components[component.name] = component
  146. })
  147. Object.keys(components).forEach(key => {
  148. if (key === 'Root') return
  149. components.Root.validInnerComponents = components.Root.validInnerComponents || []
  150. components.Root.validInnerComponents.push(key)
  151. })
  152. Object.keys(components).forEach(key => {
  153. const component = components[key]
  154. const { validInnerComponents = [] } = component
  155. validInnerComponents.forEach(inner => {
  156. const child = components[inner]
  157. component.possibleChildren = component.possibleChildren || []
  158. component.possibleChildren.push(child)
  159. child.possibleParents = child.possibleParents || []
  160. child.possibleParents.push(component)
  161. })
  162. })
  163. const engineChecksum = sum(components)
  164. const ruleToSelector = genericRuleToSelector(components)
  165. export const getEngineChecksum = () => engineChecksum
  166. /**
  167. * Initializes and compiles the theme according to the ruleset
  168. *
  169. * @param {Object[]} inputRuleset - set of rules to compile theme against. Acts as an override to
  170. * component default rulesets
  171. * @param {string} ultimateBackgroundColor - Color that will be the "final" background for
  172. * calculating contrast ratios and making text automatically accessible. Really used for cases when
  173. * stuff is transparent.
  174. * @param {boolean} debug - print out debug information in console, mostly just performance stuff
  175. * @param {boolean} liteMode - use validInnerComponentsLite instead of validInnerComponents, meant to
  176. * generatate theme previews and such that need to be compiled faster and don't require a lot of other
  177. * components present in "normal" mode
  178. * @param {boolean} onlyNormalState - only use components 'normal' states, meant for generating theme
  179. * previews since states are the biggest factor for compilation time and are completely unnecessary
  180. * when previewing multiple themes at same time
  181. */
  182. export const init = ({
  183. inputRuleset,
  184. ultimateBackgroundColor,
  185. debug = false,
  186. liteMode = false,
  187. editMode = false,
  188. onlyNormalState = false,
  189. initialStaticVars = {}
  190. }) => {
  191. const rootComponentName = 'Root'
  192. if (!inputRuleset) throw new Error('Ruleset is null or undefined!')
  193. const staticVars = { ...initialStaticVars }
  194. const stacked = {}
  195. const computed = {}
  196. const rulesetUnsorted = [
  197. ...Object.values(components)
  198. .map(c => (c.defaultRules || []).map(r => ({ source: 'Built-in', component: c.name, ...r })))
  199. .reduce((acc, arr) => [...acc, ...arr], []),
  200. ...inputRuleset
  201. ].map(rule => {
  202. normalizeCombination(rule)
  203. let currentParent = rule.parent
  204. while (currentParent) {
  205. normalizeCombination(currentParent)
  206. currentParent = currentParent.parent
  207. }
  208. return rule
  209. })
  210. const ruleset = rulesetUnsorted
  211. .map((data, index) => ({ data, index }))
  212. .toSorted(({ data: a, index: ai }, { data: b, index: bi }) => {
  213. const parentsA = unroll(a).length
  214. const parentsB = unroll(b).length
  215. let aScore = 0
  216. let bScore = 0
  217. aScore += parentsA * 1000
  218. bScore += parentsB * 1000
  219. aScore += a.variant !== 'normal' ? 100 : 0
  220. bScore += b.variant !== 'normal' ? 100 : 0
  221. aScore += a.state.filter(x => x !== 'normal').length * 1000
  222. bScore += b.state.filter(x => x !== 'normal').length * 1000
  223. aScore += a.component === 'Text' ? 1 : 0
  224. bScore += b.component === 'Text' ? 1 : 0
  225. // Debug
  226. a._specificityScore = aScore
  227. b._specificityScore = bScore
  228. if (aScore === bScore) {
  229. return ai - bi
  230. }
  231. return aScore - bScore
  232. })
  233. .map(({ data }) => data)
  234. if (!ultimateBackgroundColor) {
  235. console.warn('No ultimate background color provided, falling back to panel color')
  236. const rootRule = ruleset.findLast((x) => (x.component === 'Root' && x.directives?.['--bg']))
  237. ultimateBackgroundColor = rootRule.directives['--bg'].split('|')[1].trim()
  238. }
  239. const virtualComponents = new Set(Object.values(components).filter(c => c.virtual).map(c => c.name))
  240. const transparentComponents = new Set(Object.values(components).filter(c => c.transparent).map(c => c.name))
  241. const nonEditableComponents = new Set(Object.values(components).filter(c => c.notEditable).map(c => c.name))
  242. const extraCompileComponents = new Set([])
  243. Object.values(components).forEach(component => {
  244. const relevantRules = ruleset.filter(r => r.component === component.name)
  245. const backgrounds = relevantRules.map(r => r.directives.background).filter(x => x)
  246. const opacities = relevantRules.map(r => r.directives.opacity).filter(x => x)
  247. if (
  248. backgrounds.some(x => x.match(/--parent/)) ||
  249. opacities.some(x => x != null && x < 1))
  250. {
  251. extraCompileComponents.add(component.name)
  252. }
  253. })
  254. const processCombination = (combination) => {
  255. try {
  256. const selector = ruleToSelector(combination, true)
  257. const cssSelector = ruleToSelector(combination)
  258. const parentSelector = selector.split(/ /g).slice(0, -1).join(' ')
  259. const soloSelector = selector.split(/ /g).slice(-1)[0]
  260. const lowerLevelSelector = parentSelector
  261. let lowerLevelBackground = computed[lowerLevelSelector]?.background
  262. if (editMode && !lowerLevelBackground) {
  263. // FIXME hack for editor until it supports handling component backgrounds
  264. lowerLevelBackground = '#00FFFF'
  265. }
  266. const lowerLevelVirtualDirectives = computed[lowerLevelSelector]?.virtualDirectives
  267. const lowerLevelVirtualDirectivesRaw = computed[lowerLevelSelector]?.virtualDirectivesRaw
  268. const dynamicVars = computed[selector] || {
  269. lowerLevelSelector,
  270. lowerLevelBackground,
  271. lowerLevelVirtualDirectives,
  272. lowerLevelVirtualDirectivesRaw
  273. }
  274. // Inheriting all of the applicable rules
  275. const existingRules = ruleset.filter(findRules(combination))
  276. const computedDirectives =
  277. existingRules
  278. .map(r => r.directives)
  279. .reduce((acc, directives) => ({ ...acc, ...directives }), {})
  280. const computedRule = {
  281. ...combination,
  282. directives: computedDirectives
  283. }
  284. computed[selector] = computed[selector] || {}
  285. computed[selector].computedRule = computedRule
  286. computed[selector].dynamicVars = dynamicVars
  287. // avoid putting more stuff into actual CSS
  288. computed[selector].virtualDirectives = {}
  289. // but still be able to access it i.e. from --parent
  290. computed[selector].virtualDirectivesRaw = computed[lowerLevelSelector]?.virtualDirectivesRaw || {}
  291. if (virtualComponents.has(combination.component)) {
  292. const virtualName = [
  293. '--',
  294. combination.component.toLowerCase(),
  295. combination.variant === 'normal'
  296. ? ''
  297. : combination.variant[0].toUpperCase() + combination.variant.slice(1).toLowerCase(),
  298. ...sortBy(combination.state.filter(x => x !== 'normal')).map(state => state[0].toUpperCase() + state.slice(1).toLowerCase())
  299. ].join('')
  300. let inheritedTextColor = computedDirectives.textColor
  301. let inheritedTextAuto = computedDirectives.textAuto
  302. let inheritedTextOpacity = computedDirectives.textOpacity
  303. let inheritedTextOpacityMode = computedDirectives.textOpacityMode
  304. const lowerLevelTextSelector = [...selector.split(/ /g).slice(0, -1), soloSelector].join(' ')
  305. const lowerLevelTextRule = computed[lowerLevelTextSelector]
  306. if (inheritedTextColor == null || inheritedTextOpacity == null || inheritedTextOpacityMode == null) {
  307. inheritedTextColor = computedDirectives.textColor ?? lowerLevelTextRule.textColor
  308. inheritedTextAuto = computedDirectives.textAuto ?? lowerLevelTextRule.textAuto
  309. inheritedTextOpacity = computedDirectives.textOpacity ?? lowerLevelTextRule.textOpacity
  310. inheritedTextOpacityMode = computedDirectives.textOpacityMode ?? lowerLevelTextRule.textOpacityMode
  311. }
  312. const newTextRule = {
  313. ...computedRule,
  314. directives: {
  315. ...computedRule.directives,
  316. textColor: inheritedTextColor,
  317. textAuto: inheritedTextAuto ?? 'preserve',
  318. textOpacity: inheritedTextOpacity,
  319. textOpacityMode: inheritedTextOpacityMode
  320. }
  321. }
  322. dynamicVars.inheritedBackground = lowerLevelBackground
  323. dynamicVars.stacked = convert(stacked[lowerLevelSelector]).rgb
  324. const intendedTextColor = convert(findColor(inheritedTextColor, { dynamicVars, staticVars })).rgb
  325. const textColor = newTextRule.directives.textAuto === 'no-auto'
  326. ? intendedTextColor
  327. : getTextColor(
  328. convert(stacked[lowerLevelSelector]).rgb,
  329. intendedTextColor,
  330. newTextRule.directives.textAuto === 'preserve'
  331. )
  332. const virtualDirectives = { ...(computed[lowerLevelSelector].virtualDirectives || {}) }
  333. const virtualDirectivesRaw = { ...(computed[lowerLevelSelector].virtualDirectivesRaw || {}) }
  334. // Storing color data in lower layer to use as custom css properties
  335. virtualDirectives[virtualName] = getTextColorAlpha(newTextRule.directives, textColor, dynamicVars)
  336. virtualDirectivesRaw[virtualName] = textColor
  337. computed[lowerLevelSelector].virtualDirectives = virtualDirectives
  338. computed[lowerLevelSelector].virtualDirectivesRaw = virtualDirectivesRaw
  339. return {
  340. dynamicVars,
  341. selector: cssSelector.split(/ /g).slice(0, -1).join(' '),
  342. ...combination,
  343. directives: {},
  344. virtualDirectives,
  345. virtualDirectivesRaw
  346. }
  347. } else {
  348. computed[selector] = computed[selector] || {}
  349. // TODO: DEFAULT TEXT COLOR
  350. const lowerLevelStackedBackground = stacked[lowerLevelSelector] || convert(ultimateBackgroundColor).rgb
  351. if (computedDirectives.background) {
  352. let inheritRule = null
  353. const variantRules = ruleset.filter(
  354. findRules({
  355. component: combination.component,
  356. variant: combination.variant,
  357. parent: combination.parent
  358. })
  359. )
  360. const lastVariantRule = variantRules[variantRules.length - 1]
  361. if (lastVariantRule) {
  362. inheritRule = lastVariantRule
  363. } else {
  364. const normalRules = ruleset.filter(findRules({
  365. component: combination.component,
  366. parent: combination.parent
  367. }))
  368. const lastNormalRule = normalRules[normalRules.length - 1]
  369. inheritRule = lastNormalRule
  370. }
  371. const inheritSelector = ruleToSelector({ ...inheritRule, parent: combination.parent }, true)
  372. const inheritedBackground = computed[inheritSelector].background
  373. dynamicVars.inheritedBackground = inheritedBackground
  374. const rgb = convert(findColor(computedDirectives.background, { dynamicVars, staticVars })).rgb
  375. if (!stacked[selector]) {
  376. let blend
  377. const alpha = computedDirectives.opacity ?? 1
  378. if (alpha >= 1) {
  379. blend = rgb
  380. } else if (alpha <= 0) {
  381. blend = lowerLevelStackedBackground
  382. } else {
  383. blend = alphaBlend(rgb, computedDirectives.opacity, lowerLevelStackedBackground)
  384. }
  385. stacked[selector] = blend
  386. computed[selector].background = { ...rgb, a: computedDirectives.opacity ?? 1 }
  387. }
  388. }
  389. if (computedDirectives.shadow) {
  390. dynamicVars.shadow = flattenDeep(findShadow(flattenDeep(computedDirectives.shadow), { dynamicVars, staticVars }))
  391. }
  392. if (!stacked[selector]) {
  393. computedDirectives.background = 'transparent'
  394. computedDirectives.opacity = 0
  395. stacked[selector] = lowerLevelStackedBackground
  396. computed[selector].background = { ...lowerLevelStackedBackground, a: 0 }
  397. }
  398. dynamicVars.stacked = stacked[selector]
  399. dynamicVars.background = computed[selector].background
  400. const dynamicSlots = Object.entries(computedDirectives).filter(([k]) => k.startsWith('--'))
  401. dynamicSlots.forEach(([k, v]) => {
  402. const [type, value] = v.split('|').map(x => x.trim()) // woah, Extreme!
  403. switch (type) {
  404. case 'color': {
  405. const color = findColor(value, { dynamicVars, staticVars })
  406. dynamicVars[k] = color
  407. if (combination.component === rootComponentName) {
  408. staticVars[k.substring(2)] = color
  409. }
  410. break
  411. }
  412. case 'shadow': {
  413. const shadow = value.split(/,/g).map(s => s.trim()).filter(x => x)
  414. dynamicVars[k] = shadow
  415. if (combination.component === rootComponentName) {
  416. staticVars[k.substring(2)] = shadow
  417. }
  418. break
  419. }
  420. case 'generic': {
  421. dynamicVars[k] = value
  422. if (combination.component === rootComponentName) {
  423. staticVars[k.substring(2)] = value
  424. }
  425. break
  426. }
  427. }
  428. })
  429. const rule = {
  430. dynamicVars,
  431. selector: cssSelector,
  432. ...combination,
  433. directives: computedDirectives
  434. }
  435. return rule
  436. }
  437. } catch (e) {
  438. const { component, variant, state } = combination
  439. throw new Error(`Error processing combination ${component}.${variant}:${state.join(':')}: ${e}`)
  440. }
  441. }
  442. const processInnerComponent = (component, parent) => {
  443. const combinations = []
  444. const {
  445. states: originalStates = {},
  446. variants: originalVariants = {}
  447. } = component
  448. let validInnerComponents
  449. if (editMode) {
  450. const temp = (component.validInnerComponentsLite || component.validInnerComponents || [])
  451. validInnerComponents = temp
  452. .filter(c => virtualComponents.has(c) && !nonEditableComponents.has(c))
  453. } else if (liteMode) {
  454. validInnerComponents = (component.validInnerComponentsLite || component.validInnerComponents || [])
  455. } else if (component.name === 'Root') {
  456. validInnerComponents = component.validInnerComponents || []
  457. } else {
  458. validInnerComponents = component
  459. .validInnerComponents
  460. ?.filter(
  461. c => virtualComponents.has(c)
  462. || transparentComponents.has(c)
  463. || extraCompileComponents.has(c)
  464. )
  465. || []
  466. }
  467. // Normalizing states and variants to always include "normal"
  468. const states = { normal: '', ...originalStates }
  469. const variants = { normal: '', ...originalVariants }
  470. const innerComponents = (validInnerComponents).map(name => {
  471. const result = components[name]
  472. if (result === undefined) console.error(`Component ${component.name} references a component ${name} which does not exist!`)
  473. return result
  474. })
  475. // Optimization: we only really need combinations without "normal" because all states implicitly have it
  476. const permutationStateKeys = Object.keys(states).filter(s => s !== 'normal')
  477. const stateCombinations = (onlyNormalState && !virtualComponents.has(component.name))
  478. ? [
  479. ['normal']
  480. ]
  481. : [
  482. ['normal'],
  483. ...getAllPossibleCombinations(permutationStateKeys)
  484. .map(combination => ['normal', ...combination])
  485. .filter(combo => {
  486. // Optimization: filter out some hard-coded combinations that don't make sense
  487. if (combo.indexOf('disabled') >= 0) {
  488. return !(
  489. combo.indexOf('hover') >= 0 ||
  490. combo.indexOf('focused') >= 0 ||
  491. combo.indexOf('pressed') >= 0
  492. )
  493. }
  494. return true
  495. })
  496. ]
  497. const stateVariantCombination = Object.keys(variants).map(variant => {
  498. return stateCombinations.map(state => ({ variant, state }))
  499. }).reduce((acc, x) => [...acc, ...x], [])
  500. stateVariantCombination.forEach(combination => {
  501. combination.component = component.name
  502. combination.lazy = component.lazy || parent?.lazy
  503. combination.parent = parent
  504. if (!liteMode && combination.state.indexOf('hover') >= 0) {
  505. combination.lazy = true
  506. }
  507. if (
  508. !liteMode &&
  509. parent?.component !== 'Root' &&
  510. !virtualComponents.has(component.name) &&
  511. !transparentComponents.has(component.name) &&
  512. extraCompileComponents.has(component.name)
  513. ) {
  514. combination.lazy = true
  515. }
  516. combinations.push(combination)
  517. innerComponents.forEach(innerComponent => {
  518. combinations.push(...processInnerComponent(innerComponent, combination))
  519. })
  520. })
  521. return combinations
  522. }
  523. const t0 = performance.now()
  524. const combinations = processInnerComponent(components[rootComponentName] ?? components.Root)
  525. const t1 = performance.now()
  526. if (debug) {
  527. console.debug('Tree traveral took ' + (t1 - t0) + ' ms')
  528. }
  529. const result = combinations.map((combination) => {
  530. if (combination.lazy) {
  531. return async () => processCombination(combination)
  532. } else {
  533. return processCombination(combination)
  534. }
  535. }).filter(x => x)
  536. const t2 = performance.now()
  537. if (debug) {
  538. console.debug('Eager processing took ' + (t2 - t1) + ' ms')
  539. }
  540. // optimization to traverse big-ass array only once instead of twice
  541. const eager = []
  542. const lazy = []
  543. result.forEach(x => {
  544. if (typeof x === 'function') {
  545. lazy.push(x)
  546. } else {
  547. eager.push(x)
  548. }
  549. })
  550. return {
  551. lazy,
  552. eager,
  553. staticVars,
  554. engineChecksum,
  555. themeChecksum: sum([lazy, eager])
  556. }
  557. }