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 (18429B)


  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 { parseCssShadow } from './css_utils.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. Panel: null,
  33. Chat: null,
  34. ChatMessage: null
  35. }
  36. const findShadow = (shadows, { dynamicVars, staticVars }) => {
  37. return (shadows || []).map(shadow => {
  38. let targetShadow
  39. if (typeof shadow === 'string') {
  40. if (shadow.startsWith('$')) {
  41. targetShadow = process(shadow, shadowFunctions, { findColor, findShadow }, { dynamicVars, staticVars })
  42. } else if (shadow.startsWith('--')) {
  43. const [variable] = shadow.split(/,/g).map(str => str.trim()) // discarding modifier since it's not supported
  44. const variableSlot = variable.substring(2)
  45. return findShadow(staticVars[variableSlot], { dynamicVars, staticVars })
  46. } else {
  47. targetShadow = parseCssShadow(shadow)
  48. }
  49. } else {
  50. targetShadow = shadow
  51. }
  52. const shadowArray = Array.isArray(targetShadow) ? targetShadow : [targetShadow]
  53. return shadowArray.map(s => ({
  54. ...s,
  55. color: findColor(s.color, { dynamicVars, staticVars })
  56. }))
  57. })
  58. }
  59. const findColor = (color, { dynamicVars, staticVars }) => {
  60. if (typeof color !== 'string' || (!color.startsWith('--') && !color.startsWith('$'))) return color
  61. let targetColor = null
  62. if (color.startsWith('--')) {
  63. const [variable, modifier] = color.split(/,/g).map(str => str.trim())
  64. const variableSlot = variable.substring(2)
  65. if (variableSlot === 'stack') {
  66. const { r, g, b } = dynamicVars.stacked
  67. targetColor = { r, g, b }
  68. } else if (variableSlot.startsWith('parent')) {
  69. if (variableSlot === 'parent') {
  70. const { r, g, b } = dynamicVars.lowerLevelBackground
  71. targetColor = { r, g, b }
  72. } else {
  73. const virtualSlot = variableSlot.replace(/^parent/, '')
  74. targetColor = convert(dynamicVars.lowerLevelVirtualDirectivesRaw[virtualSlot]).rgb
  75. }
  76. } else {
  77. switch (variableSlot) {
  78. case 'inheritedBackground':
  79. targetColor = convert(dynamicVars.inheritedBackground).rgb
  80. break
  81. case 'background':
  82. targetColor = convert(dynamicVars.background).rgb
  83. break
  84. default:
  85. targetColor = convert(staticVars[variableSlot]).rgb
  86. }
  87. }
  88. if (modifier) {
  89. const effectiveBackground = dynamicVars.lowerLevelBackground ?? targetColor
  90. const isLightOnDark = relativeLuminance(convert(effectiveBackground).rgb) < 0.5
  91. const mod = isLightOnDark ? 1 : -1
  92. targetColor = brightness(Number.parseFloat(modifier) * mod, targetColor).rgb
  93. }
  94. }
  95. if (color.startsWith('$')) {
  96. try {
  97. targetColor = process(color, colorFunctions, { findColor }, { dynamicVars, staticVars })
  98. } catch (e) {
  99. console.error('Failure executing color function', e)
  100. targetColor = '#FF00FF'
  101. }
  102. }
  103. // Color references other color
  104. return targetColor
  105. }
  106. const getTextColorAlpha = (directives, intendedTextColor, dynamicVars, staticVars) => {
  107. const opacity = directives.textOpacity
  108. const backgroundColor = convert(dynamicVars.lowerLevelBackground).rgb
  109. const textColor = convert(findColor(intendedTextColor, { dynamicVars, staticVars })).rgb
  110. if (opacity === null || opacity === undefined || opacity >= 1) {
  111. return convert(textColor).hex
  112. }
  113. if (opacity === 0) {
  114. return convert(backgroundColor).hex
  115. }
  116. const opacityMode = directives.textOpacityMode
  117. switch (opacityMode) {
  118. case 'fake':
  119. return convert(alphaBlend(textColor, opacity, backgroundColor)).hex
  120. case 'mixrgb':
  121. return convert(mixrgb(backgroundColor, textColor)).hex
  122. default:
  123. return rgba2css({ a: opacity, ...textColor })
  124. }
  125. }
  126. // Loading all style.js[on] files dynamically
  127. const componentsContext = require.context('src', true, /\.style.js(on)?$/)
  128. componentsContext.keys().forEach(key => {
  129. const component = componentsContext(key).default
  130. if (components[component.name] != null) {
  131. console.warn(`Component in file ${key} is trying to override existing component ${component.name}! You have collisions/duplicates!`)
  132. }
  133. components[component.name] = component
  134. })
  135. const engineChecksum = sum(components)
  136. const ruleToSelector = genericRuleToSelector(components)
  137. export const getEngineChecksum = () => engineChecksum
  138. /**
  139. * Initializes and compiles the theme according to the ruleset
  140. *
  141. * @param {Object[]} inputRuleset - set of rules to compile theme against. Acts as an override to
  142. * component default rulesets
  143. * @param {string} ultimateBackgroundColor - Color that will be the "final" background for
  144. * calculating contrast ratios and making text automatically accessible. Really used for cases when
  145. * stuff is transparent.
  146. * @param {boolean} debug - print out debug information in console, mostly just performance stuff
  147. * @param {boolean} liteMode - use validInnerComponentsLite instead of validInnerComponents, meant to
  148. * generatate theme previews and such that need to be compiled faster and don't require a lot of other
  149. * components present in "normal" mode
  150. * @param {boolean} onlyNormalState - only use components 'normal' states, meant for generating theme
  151. * previews since states are the biggest factor for compilation time and are completely unnecessary
  152. * when previewing multiple themes at same time
  153. * @param {string} rootComponentName - [UNTESTED] which component to start from, meant for previewing a
  154. * part of the theme (i.e. just the button) for themes 3 editor.
  155. */
  156. export const init = ({
  157. inputRuleset,
  158. ultimateBackgroundColor,
  159. debug = false,
  160. liteMode = false,
  161. onlyNormalState = false,
  162. rootComponentName = 'Root'
  163. }) => {
  164. if (!inputRuleset) throw new Error('Ruleset is null or undefined!')
  165. const staticVars = {}
  166. const stacked = {}
  167. const computed = {}
  168. const rulesetUnsorted = [
  169. ...Object.values(components)
  170. .map(c => (c.defaultRules || []).map(r => ({ component: c.name, ...r, source: 'Built-in' })))
  171. .reduce((acc, arr) => [...acc, ...arr], []),
  172. ...inputRuleset
  173. ].map(rule => {
  174. normalizeCombination(rule)
  175. let currentParent = rule.parent
  176. while (currentParent) {
  177. normalizeCombination(currentParent)
  178. currentParent = currentParent.parent
  179. }
  180. return rule
  181. })
  182. const ruleset = rulesetUnsorted
  183. .map((data, index) => ({ data, index }))
  184. .sort(({ data: a, index: ai }, { data: b, index: bi }) => {
  185. const parentsA = unroll(a).length
  186. const parentsB = unroll(b).length
  187. if (parentsA === parentsB) {
  188. if (a.component === 'Text') return -1
  189. if (b.component === 'Text') return 1
  190. return ai - bi
  191. }
  192. if (parentsA === 0 && parentsB !== 0) return -1
  193. if (parentsB === 0 && parentsA !== 0) return 1
  194. return parentsA - parentsB
  195. })
  196. .map(({ data }) => data)
  197. const virtualComponents = new Set(Object.values(components).filter(c => c.virtual).map(c => c.name))
  198. const processCombination = (combination) => {
  199. const selector = ruleToSelector(combination, true)
  200. const cssSelector = ruleToSelector(combination)
  201. const parentSelector = selector.split(/ /g).slice(0, -1).join(' ')
  202. const soloSelector = selector.split(/ /g).slice(-1)[0]
  203. const lowerLevelSelector = parentSelector
  204. const lowerLevelBackground = computed[lowerLevelSelector]?.background
  205. const lowerLevelVirtualDirectives = computed[lowerLevelSelector]?.virtualDirectives
  206. const lowerLevelVirtualDirectivesRaw = computed[lowerLevelSelector]?.virtualDirectivesRaw
  207. const dynamicVars = computed[selector] || {
  208. lowerLevelBackground,
  209. lowerLevelVirtualDirectives,
  210. lowerLevelVirtualDirectivesRaw
  211. }
  212. // Inheriting all of the applicable rules
  213. const existingRules = ruleset.filter(findRules(combination))
  214. const computedDirectives = existingRules.map(r => r.directives).reduce((acc, directives) => ({ ...acc, ...directives }), {})
  215. const computedRule = {
  216. ...combination,
  217. directives: computedDirectives
  218. }
  219. computed[selector] = computed[selector] || {}
  220. computed[selector].computedRule = computedRule
  221. computed[selector].dynamicVars = dynamicVars
  222. if (virtualComponents.has(combination.component)) {
  223. const virtualName = [
  224. '--',
  225. combination.component.toLowerCase(),
  226. combination.variant === 'normal'
  227. ? ''
  228. : combination.variant[0].toUpperCase() + combination.variant.slice(1).toLowerCase(),
  229. ...sortBy(combination.state.filter(x => x !== 'normal')).map(state => state[0].toUpperCase() + state.slice(1).toLowerCase())
  230. ].join('')
  231. let inheritedTextColor = computedDirectives.textColor
  232. let inheritedTextAuto = computedDirectives.textAuto
  233. let inheritedTextOpacity = computedDirectives.textOpacity
  234. let inheritedTextOpacityMode = computedDirectives.textOpacityMode
  235. const lowerLevelTextSelector = [...selector.split(/ /g).slice(0, -1), soloSelector].join(' ')
  236. const lowerLevelTextRule = computed[lowerLevelTextSelector]
  237. if (inheritedTextColor == null || inheritedTextOpacity == null || inheritedTextOpacityMode == null) {
  238. inheritedTextColor = computedDirectives.textColor ?? lowerLevelTextRule.textColor
  239. inheritedTextAuto = computedDirectives.textAuto ?? lowerLevelTextRule.textAuto
  240. inheritedTextOpacity = computedDirectives.textOpacity ?? lowerLevelTextRule.textOpacity
  241. inheritedTextOpacityMode = computedDirectives.textOpacityMode ?? lowerLevelTextRule.textOpacityMode
  242. }
  243. const newTextRule = {
  244. ...computedRule,
  245. directives: {
  246. ...computedRule.directives,
  247. textColor: inheritedTextColor,
  248. textAuto: inheritedTextAuto ?? 'preserve',
  249. textOpacity: inheritedTextOpacity,
  250. textOpacityMode: inheritedTextOpacityMode
  251. }
  252. }
  253. dynamicVars.inheritedBackground = lowerLevelBackground
  254. dynamicVars.stacked = convert(stacked[lowerLevelSelector]).rgb
  255. const intendedTextColor = convert(findColor(inheritedTextColor, { dynamicVars, staticVars })).rgb
  256. const textColor = newTextRule.directives.textAuto === 'no-auto'
  257. ? intendedTextColor
  258. : getTextColor(
  259. convert(stacked[lowerLevelSelector]).rgb,
  260. intendedTextColor,
  261. newTextRule.directives.textAuto === 'preserve'
  262. )
  263. const virtualDirectives = computed[lowerLevelSelector].virtualDirectives || {}
  264. const virtualDirectivesRaw = computed[lowerLevelSelector].virtualDirectivesRaw || {}
  265. // Storing color data in lower layer to use as custom css properties
  266. virtualDirectives[virtualName] = getTextColorAlpha(newTextRule.directives, textColor, dynamicVars)
  267. virtualDirectivesRaw[virtualName] = textColor
  268. computed[lowerLevelSelector].virtualDirectives = virtualDirectives
  269. computed[lowerLevelSelector].virtualDirectivesRaw = virtualDirectivesRaw
  270. return {
  271. dynamicVars,
  272. selector: cssSelector.split(/ /g).slice(0, -1).join(' '),
  273. ...combination,
  274. directives: {},
  275. virtualDirectives: {
  276. [virtualName]: getTextColorAlpha(newTextRule.directives, textColor, dynamicVars)
  277. },
  278. virtualDirectivesRaw: {
  279. [virtualName]: textColor
  280. }
  281. }
  282. } else {
  283. computed[selector] = computed[selector] || {}
  284. // TODO: DEFAULT TEXT COLOR
  285. const lowerLevelStackedBackground = stacked[lowerLevelSelector] || convert(ultimateBackgroundColor).rgb
  286. if (computedDirectives.background) {
  287. let inheritRule = null
  288. const variantRules = ruleset.filter(
  289. findRules({
  290. component: combination.component,
  291. variant: combination.variant,
  292. parent: combination.parent
  293. })
  294. )
  295. const lastVariantRule = variantRules[variantRules.length - 1]
  296. if (lastVariantRule) {
  297. inheritRule = lastVariantRule
  298. } else {
  299. const normalRules = ruleset.filter(findRules({
  300. component: combination.component,
  301. parent: combination.parent
  302. }))
  303. const lastNormalRule = normalRules[normalRules.length - 1]
  304. inheritRule = lastNormalRule
  305. }
  306. const inheritSelector = ruleToSelector({ ...inheritRule, parent: combination.parent }, true)
  307. const inheritedBackground = computed[inheritSelector].background
  308. dynamicVars.inheritedBackground = inheritedBackground
  309. const rgb = convert(findColor(computedDirectives.background, { dynamicVars, staticVars })).rgb
  310. if (!stacked[selector]) {
  311. let blend
  312. const alpha = computedDirectives.opacity ?? 1
  313. if (alpha >= 1) {
  314. blend = rgb
  315. } else if (alpha <= 0) {
  316. blend = lowerLevelStackedBackground
  317. } else {
  318. blend = alphaBlend(rgb, computedDirectives.opacity, lowerLevelStackedBackground)
  319. }
  320. stacked[selector] = blend
  321. computed[selector].background = { ...rgb, a: computedDirectives.opacity ?? 1 }
  322. }
  323. }
  324. if (computedDirectives.shadow) {
  325. dynamicVars.shadow = flattenDeep(findShadow(flattenDeep(computedDirectives.shadow), { dynamicVars, staticVars }))
  326. }
  327. if (!stacked[selector]) {
  328. computedDirectives.background = 'transparent'
  329. computedDirectives.opacity = 0
  330. stacked[selector] = lowerLevelStackedBackground
  331. computed[selector].background = { ...lowerLevelStackedBackground, a: 0 }
  332. }
  333. dynamicVars.stacked = stacked[selector]
  334. dynamicVars.background = computed[selector].background
  335. const dynamicSlots = Object.entries(computedDirectives).filter(([k, v]) => k.startsWith('--'))
  336. dynamicSlots.forEach(([k, v]) => {
  337. const [type, ...value] = v.split('|').map(x => x.trim()) // woah, Extreme!
  338. switch (type) {
  339. case 'color': {
  340. const color = findColor(value[0], { dynamicVars, staticVars })
  341. dynamicVars[k] = color
  342. if (combination.component === 'Root') {
  343. staticVars[k.substring(2)] = color
  344. }
  345. break
  346. }
  347. case 'shadow': {
  348. const shadow = value
  349. dynamicVars[k] = shadow
  350. if (combination.component === 'Root') {
  351. staticVars[k.substring(2)] = shadow
  352. }
  353. break
  354. }
  355. case 'generic': {
  356. dynamicVars[k] = value
  357. if (combination.component === 'Root') {
  358. staticVars[k.substring(2)] = value
  359. }
  360. break
  361. }
  362. }
  363. })
  364. const rule = {
  365. dynamicVars,
  366. selector: cssSelector,
  367. ...combination,
  368. directives: computedDirectives
  369. }
  370. return rule
  371. }
  372. }
  373. const processInnerComponent = (component, parent) => {
  374. const combinations = []
  375. const {
  376. states: originalStates = {},
  377. variants: originalVariants = {}
  378. } = component
  379. const validInnerComponents = (
  380. liteMode
  381. ? (component.validInnerComponentsLite || component.validInnerComponents)
  382. : component.validInnerComponents
  383. ) || []
  384. // Normalizing states and variants to always include "normal"
  385. const states = { normal: '', ...originalStates }
  386. const variants = { normal: '', ...originalVariants }
  387. const innerComponents = (validInnerComponents).map(name => {
  388. const result = components[name]
  389. if (result === undefined) console.error(`Component ${component.name} references a component ${name} which does not exist!`)
  390. return result
  391. })
  392. // Optimization: we only really need combinations without "normal" because all states implicitly have it
  393. const permutationStateKeys = Object.keys(states).filter(s => s !== 'normal')
  394. const stateCombinations = onlyNormalState
  395. ? [
  396. ['normal']
  397. ]
  398. : [
  399. ['normal'],
  400. ...getAllPossibleCombinations(permutationStateKeys)
  401. .map(combination => ['normal', ...combination])
  402. .filter(combo => {
  403. // Optimization: filter out some hard-coded combinations that don't make sense
  404. if (combo.indexOf('disabled') >= 0) {
  405. return !(
  406. combo.indexOf('hover') >= 0 ||
  407. combo.indexOf('focused') >= 0 ||
  408. combo.indexOf('pressed') >= 0
  409. )
  410. }
  411. return true
  412. })
  413. ]
  414. const stateVariantCombination = Object.keys(variants).map(variant => {
  415. return stateCombinations.map(state => ({ variant, state }))
  416. }).reduce((acc, x) => [...acc, ...x], [])
  417. stateVariantCombination.forEach(combination => {
  418. combination.component = component.name
  419. combination.lazy = component.lazy || parent?.lazy
  420. combination.parent = parent
  421. if (combination.state.indexOf('hover') >= 0) {
  422. combination.lazy = true
  423. }
  424. combinations.push(combination)
  425. innerComponents.forEach(innerComponent => {
  426. combinations.push(...processInnerComponent(innerComponent, combination))
  427. })
  428. })
  429. return combinations
  430. }
  431. const t0 = performance.now()
  432. const combinations = processInnerComponent(components[rootComponentName] ?? components.Root)
  433. const t1 = performance.now()
  434. if (debug) {
  435. console.debug('Tree traveral took ' + (t1 - t0) + ' ms')
  436. }
  437. const result = combinations.map((combination) => {
  438. if (combination.lazy) {
  439. return async () => processCombination(combination)
  440. } else {
  441. return processCombination(combination)
  442. }
  443. }).filter(x => x)
  444. const t2 = performance.now()
  445. if (debug) {
  446. console.debug('Eager processing took ' + (t2 - t1) + ' ms')
  447. }
  448. return {
  449. lazy: result.filter(x => typeof x === 'function'),
  450. eager: result.filter(x => typeof x !== 'function'),
  451. staticVars,
  452. engineChecksum
  453. }
  454. }