logo

pleroma-fe

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

theme_data_3.service.js (16543B)


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