Преглед изворни кода

Merge branch 'piss-serialization' into 'develop'

Pleroma ISS (interface stylesheets) implementation

See merge request pleroma/pleroma-fe!1943
HJ пре 2 месеци
родитељ
комит
f127ae307b

+ 0 - 0
changelog.d/piss-serialization.skip


+ 3 - 1
src/components/alert.style.js

@@ -27,7 +27,9 @@ export default {
         component: 'Alert'
       },
       component: 'Border',
-      textColor: '--parent'
+      directives: {
+        textColor: '--parent'
+      }
     },
     {
       variant: 'error',

+ 2 - 2
src/components/button.style.js

@@ -34,8 +34,8 @@ export default {
       directives: {
         '--defaultButtonHoverGlow': 'shadow | 0 0 4 --text',
         '--defaultButtonShadow': 'shadow | 0 0 2 #000000',
-        '--defaultButtonBevel': 'shadow | $borderSide(#FFFFFF, top, 0.2) | $borderSide(#000000, bottom, 0.2)',
-        '--pressedButtonBevel': 'shadow | $borderSide(#FFFFFF, bottom, 0.2)| $borderSide(#000000, top, 0.2)'
+        '--defaultButtonBevel': 'shadow | $borderSide(#FFFFFF, top, 0.2), $borderSide(#000000, bottom, 0.2)',
+        '--pressedButtonBevel': 'shadow | $borderSide(#FFFFFF, bottom, 0.2), $borderSide(#000000, top, 0.2)'
       }
     },
     {

+ 1 - 2
src/components/button_unstyled.style.js

@@ -16,8 +16,7 @@ export default {
     {
       directives: {
         background: '#ffffff',
-        opacity: 0,
-        shadow: []
+        opacity: 0
       }
     },
     {

+ 1 - 1
src/components/input.style.js

@@ -26,7 +26,7 @@ export default {
     {
       component: 'Root',
       directives: {
-        '--defaultInputBevel': 'shadow | $borderSide(#FFFFFF, bottom, 0.2)| $borderSide(#000000, top, 0.2)'
+        '--defaultInputBevel': 'shadow | $borderSide(#FFFFFF, bottom, 0.2), $borderSide(#000000, top, 0.2)'
       }
     },
     {

+ 1 - 2
src/components/panel_header.style.js

@@ -16,8 +16,7 @@ export default {
       component: 'PanelHeader',
       directives: {
         backgroundNoCssColor: 'yes',
-        background: '--fg',
-        shadow: []
+        background: '--fg'
       }
     }
   ]

+ 153 - 0
src/services/theme_data/iss_deserializer.js

@@ -0,0 +1,153 @@
+import { flattenDeep } from 'lodash'
+
+const parseShadow = string => {
+  const modes = ['_full', 'inset', 'x', 'y', 'blur', 'spread', 'color', 'alpha']
+  const regexPrep = [
+    // inset keyword (optional)
+    '^(?:(inset)\\s+)?',
+    // x
+    '(?:(-?[0-9]+(?:\\.[0-9]+)?)\\s+)',
+    // y
+    '(?:(-?[0-9]+(?:\\.[0-9]+)?)\\s+)',
+    // blur (optional)
+    '(?:(-?[0-9]+(?:\\.[0-9]+)?)\\s+)?',
+    // spread (optional)
+    '(?:(-?[0-9]+(?:\\.[0-9]+)?)\\s+)?',
+    // either hex, variable or function
+    '(#[0-9a-f]{6}|--[a-z\\-_]+|\\$[a-z\\-()_]+)',
+    // opacity (optional)
+    '(?:\\s+\\/\\s+([0-9]+(?:\\.[0-9]+)?)\\s*)?$'
+  ].join('')
+  const regex = new RegExp(regexPrep, 'gis') // global, (stable) indices, single-string
+  const result = regex.exec(string)
+  if (result == null) {
+    return string
+  } else {
+    const numeric = new Set(['x', 'y', 'blur', 'spread', 'alpha'])
+    const { x, y, blur, spread, alpha, inset, color } = Object.fromEntries(modes.map((mode, i) => {
+      if (numeric.has(mode)) {
+        return [mode, Number(result[i])]
+      } else if (mode === 'inset') {
+        return [mode, !!result[i]]
+      } else {
+        return [mode, result[i]]
+      }
+    }).filter(([k, v]) => v !== false).slice(1))
+
+    return { x, y, blur, spread, color, alpha, inset }
+  }
+}
+// this works nearly the same as HTML tree converter
+const parseIss = (input) => {
+  const buffer = [{ selector: null, content: [] }]
+  let textBuffer = ''
+
+  const getCurrentBuffer = () => {
+    let current = buffer[buffer.length - 1]
+    if (current == null) {
+      current = { selector: null, content: [] }
+    }
+    return current
+  }
+
+  // Processes current line buffer, adds it to output buffer and clears line buffer
+  const flushText = (kind) => {
+    if (textBuffer === '') return
+    if (kind === 'content') {
+      getCurrentBuffer().content.push(textBuffer.trim())
+    } else {
+      getCurrentBuffer().selector = textBuffer.trim()
+    }
+    textBuffer = ''
+  }
+
+  for (let i = 0; i < input.length; i++) {
+    const char = input[i]
+
+    if (char === ';') {
+      flushText('content')
+    } else if (char === '{') {
+      flushText('header')
+    } else if (char === '}') {
+      flushText('content')
+      buffer.push({ selector: null, content: [] })
+      textBuffer = ''
+    } else {
+      textBuffer += char
+    }
+  }
+
+  return buffer
+}
+export const deserialize = (input) => {
+  const ast = parseIss(input)
+  const finalResult = ast.filter(i => i.selector != null).map(item => {
+    const { selector, content } = item
+    let stateCount = 0
+    const selectors = selector.split(/,/g)
+    const result = selectors.map(selector => {
+      const output = { component: '' }
+      let currentDepth = null
+
+      selector.split(/ /g).reverse().forEach((fragment, index, arr) => {
+        const fragmentObject = { component: '' }
+
+        let mode = 'component'
+        for (let i = 0; i < fragment.length; i++) {
+          const char = fragment[i]
+          switch (char) {
+            case '.': {
+              mode = 'variant'
+              fragmentObject.variant = ''
+              break
+            }
+            case ':': {
+              mode = 'state'
+              fragmentObject.state = fragmentObject.state || []
+              stateCount++
+              break
+            }
+            default: {
+              if (mode === 'state') {
+                const currentState = fragmentObject.state[stateCount - 1]
+                if (currentState == null) {
+                  fragmentObject.state.push('')
+                }
+                fragmentObject.state[stateCount - 1] += char
+              } else {
+                fragmentObject[mode] += char
+              }
+            }
+          }
+        }
+        if (currentDepth !== null) {
+          currentDepth.parent = { ...fragmentObject }
+          currentDepth = currentDepth.parent
+        } else {
+          Object.keys(fragmentObject).forEach(key => {
+            output[key] = fragmentObject[key]
+          })
+          if (index !== (arr.length - 1)) {
+            output.parent = { component: '' }
+          }
+          currentDepth = output
+        }
+      })
+
+      output.directives = Object.fromEntries(content.map(d => {
+        const [property, value] = d.split(':')
+        let realValue = value.trim()
+        if (property === 'shadow') {
+          realValue = value.split(',').map(v => parseShadow(v.trim()))
+        } if (!Number.isNaN(Number(value))) {
+          realValue = Number(value)
+        }
+        return [property, realValue]
+      }))
+
+      return output
+    })
+    return result
+  })
+  return flattenDeep(finalResult)
+}

+ 37 - 55
src/services/theme_data/iss_serializer.js

@@ -1,62 +1,44 @@
-import { unroll } from './iss_utils'
+import { unroll } from './iss_utils.js'
 
-const getCanonicState = (state) => {
-  if (state) {
-    return ['normal', ...state.filter(x => x !== 'normal')]
+const serializeShadow = s => {
+  if (typeof s === 'object') {
+    return `${s.inset ? 'inset ' : ''}${s.x} ${s.y} ${s.blur} ${s.spread} ${s.color} / ${s.alpha}`
   } else {
-    return ['normal']
+    return s
   }
 }
 
-const getCanonicRuleHeader = ({
-  component,
-  variant = 'normal',
-  parent,
-  state
-}) => ({
-  component,
-  variant,
-  parent,
-  state: getCanonicState(state)
-})
-
-const prepareRule = (rule) => {
-  const { parent } = rule
-  const chain = [...unroll(parent), rule].map(getCanonicRuleHeader)
-  const header = chain.map(({ component, variant, state }) => [
-    component,
-    variant === 'normal' ? '' : ('.' + variant),
-    state.filter(s => s !== 'normal').map(s => ':' + s).join('')
-  ].join('')).join(' ')
-
-  console.log(header, rule.directives)
-  const content = Object.entries(rule.directives).map(([key, value]) => {
-    let realValue = value
-
-    switch (key) {
-      case 'shadow':
-        realValue = realValue.map(v => `${v.inset ? 'inset ' : ''}${v.x} ${v.y} ${v.blur} ${v.spread} ${v.color} / ${v.alpha}`)
-    }
-
-    if (Array.isArray(realValue)) {
-      realValue = realValue.join(', ')
-    }
-
-    return `  ${key}: ${realValue};`
-  }).sort().join('\n')
-
-  return [
-    header,
-    content
-  ]
-}
-
 export const serialize = (ruleset) => {
-  // Scrapped idea: automatically combine same-set directives
-  // problem: might violate the order rules
-
-  return ruleset.filter(r => Object.keys(r.directives).length > 0).map(r => {
-    const [header, content] = prepareRule(r)
-    return `${header} {\n${content}\n}\n\n`
-  })
+  return ruleset.map((rule) => {
+    if (Object.keys(rule.directives || {}).length === 0) return false
+
+    const header = unroll(rule).reverse().map(rule => {
+      const { component } = rule
+      const newVariant = (rule.variant == null || rule.variant === 'normal') ? '' : ('.' + rule.variant)
+      const newState = (rule.state || []).filter(st => st !== 'normal')
+
+      return `${component}${newVariant}${newState.map(st => ':' + st).join('')}`
+    }).join(' ')
+
+    const content = Object.entries(rule.directives).map(([directive, value]) => {
+      if (directive.startsWith('--')) {
+        const [valType, newValue] = value.split('|') // only first one! intentional!
+        switch (valType) {
+          case 'shadow':
+            return `  ${directive}: ${valType.trim()} | ${newValue.map(serializeShadow).map(s => s.trim()).join(', ')}`
+          default:
+            return `  ${directive}: ${valType.trim()} | ${newValue.trim()}`
+        }
+      } else {
+        switch (directive) {
+          case 'shadow':
+            return `  ${directive}: ${value.map(serializeShadow).join(', ')}`
+          default:
+            return `  ${directive}: ${value}`
+        }
+      }
+    })
+
+    return `${header} {\n${content.join(';\n')}\n}`
+  }).filter(x => x).join('\n\n')
 }

+ 14 - 2
src/services/theme_data/theme_data_3.service.js

@@ -504,9 +504,21 @@ export const init = ({
     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: result.filter(x => typeof x === 'function'),
-    eager: result.filter(x => typeof x !== 'function'),
+    lazy,
+    eager,
     staticVars,
     engineChecksum
   }

+ 40 - 0
test/unit/specs/services/theme_data/iss_deserializer.spec.js

@@ -0,0 +1,40 @@
+import { deserialize } from 'src/services/theme_data/iss_deserializer.js'
+import { serialize } from 'src/services/theme_data/iss_serializer.js'
+const componentsContext = require.context('src', true, /\.style.js(on)?$/)
+
+describe('ISS (de)serialization', () => {
+  componentsContext.keys().forEach(key => {
+    const component = componentsContext(key).default
+
+    it(`(De)serialization of component ${component.name} works`, () => {
+      const normalized = component.defaultRules.map(x => ({ component: component.name, ...x }))
+      const serialized = serialize(normalized)
+      const deserialized = deserialize(serialized)
+
+      // for some reason comparing objects directly fails the assert
+      expect(JSON.stringify(deserialized, null, 2)).to.equal(JSON.stringify(normalized, null, 2))
+    })
+  })
+
+  /*
+  // Debug snippet
+  const onlyComponent = componentsContext('./components/panel_header.style.js').default
+  it(`(De)serialization of component ${onlyComponent.name} works`, () => {
+    const normalized = onlyComponent.defaultRules.map(x => ({ component: onlyComponent.name, ...x }))
+    console.log('BEGIN INPUT ================')
+    console.log(normalized)
+    console.log('END INPUT ==================')
+    const serialized = serialize(normalized)
+    console.log('BEGIN SERIAL ===============')
+    console.log(serialized)
+    console.log('END SERIAL =================')
+    const deserialized = deserialize(serialized)
+    console.log('BEGIN DESERIALIZED =========')
+    console.log(serialized)
+    console.log('END DESERIALIZED ===========')
+
+    // for some reason comparing objects directly fails the assert
+    expect(JSON.stringify(deserialized, null, 2)).to.equal(JSON.stringify(normalized, null, 2))
+  })
+  */
+})