Merge branch 'piss-serialization' into 'develop'
Pleroma ISS (interface stylesheets) implementation See merge request pleroma/pleroma-fe!1943
This commit is contained in:
commit
f127ae307b
0
changelog.d/piss-serialization.skip
Normal file
0
changelog.d/piss-serialization.skip
Normal file
|
@ -27,7 +27,9 @@ export default {
|
|||
component: 'Alert'
|
||||
},
|
||||
component: 'Border',
|
||||
directives: {
|
||||
textColor: '--parent'
|
||||
}
|
||||
},
|
||||
{
|
||||
variant: 'error',
|
||||
|
|
|
@ -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)'
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
|
@ -16,8 +16,7 @@ export default {
|
|||
{
|
||||
directives: {
|
||||
background: '#ffffff',
|
||||
opacity: 0,
|
||||
shadow: []
|
||||
opacity: 0
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
|
@ -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)'
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
|
@ -16,8 +16,7 @@ export default {
|
|||
component: 'PanelHeader',
|
||||
directives: {
|
||||
backgroundNoCssColor: 'yes',
|
||||
background: '--fg',
|
||||
shadow: []
|
||||
background: '--fg'
|
||||
}
|
||||
}
|
||||
]
|
||||
|
|
153
src/services/theme_data/iss_deserializer.js
Normal file
153
src/services/theme_data/iss_deserializer.js
Normal file
|
@ -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)
|
||||
}
|
|
@ -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.map((rule) => {
|
||||
if (Object.keys(rule.directives || {}).length === 0) return false
|
||||
|
||||
return ruleset.filter(r => Object.keys(r.directives).length > 0).map(r => {
|
||||
const [header, content] = prepareRule(r)
|
||||
return `${header} {\n${content}\n}\n\n`
|
||||
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')
|
||||
}
|
||||
|
|
|
@ -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
test/unit/specs/services/theme_data/iss_deserializer.spec.js
Normal file
40
test/unit/specs/services/theme_data/iss_deserializer.spec.js
Normal file
|
@ -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))
|
||||
})
|
||||
*/
|
||||
})
|
Loading…
Reference in a new issue