diff --git a/changelog.d/piss-serialization.skip b/changelog.d/piss-serialization.skip new file mode 100644 index 00000000..e69de29b diff --git a/src/components/alert.style.js b/src/components/alert.style.js index 19bd4bbb..abbeb5ba 100644 --- a/src/components/alert.style.js +++ b/src/components/alert.style.js @@ -27,7 +27,9 @@ export default { component: 'Alert' }, component: 'Border', - textColor: '--parent' + directives: { + textColor: '--parent' + } }, { variant: 'error', diff --git a/src/components/button.style.js b/src/components/button.style.js index 6fec67a0..1bee8f8e 100644 --- a/src/components/button.style.js +++ b/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)' } }, { diff --git a/src/components/button_unstyled.style.js b/src/components/button_unstyled.style.js index 65b5c57b..435f9cc6 100644 --- a/src/components/button_unstyled.style.js +++ b/src/components/button_unstyled.style.js @@ -16,8 +16,7 @@ export default { { directives: { background: '#ffffff', - opacity: 0, - shadow: [] + opacity: 0 } }, { diff --git a/src/components/input.style.js b/src/components/input.style.js index 139a0034..7302cd6d 100644 --- a/src/components/input.style.js +++ b/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)' } }, { diff --git a/src/components/panel_header.style.js b/src/components/panel_header.style.js index 32464bc5..010e42cd 100644 --- a/src/components/panel_header.style.js +++ b/src/components/panel_header.style.js @@ -16,8 +16,7 @@ export default { component: 'PanelHeader', directives: { backgroundNoCssColor: 'yes', - background: '--fg', - shadow: [] + background: '--fg' } } ] diff --git a/src/services/theme_data/iss_deserializer.js b/src/services/theme_data/iss_deserializer.js new file mode 100644 index 00000000..5d71f35f --- /dev/null +++ b/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) +} diff --git a/src/services/theme_data/iss_serializer.js b/src/services/theme_data/iss_serializer.js index 39151e41..959852b7 100644 --- a/src/services/theme_data/iss_serializer.js +++ b/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.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') } diff --git a/src/services/theme_data/theme_data_3.service.js b/src/services/theme_data/theme_data_3.service.js index cf58da11..39c8b74f 100644 --- a/src/services/theme_data/theme_data_3.service.js +++ b/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 } diff --git a/test/unit/specs/services/theme_data/iss_deserializer.spec.js b/test/unit/specs/services/theme_data/iss_deserializer.spec.js new file mode 100644 index 00000000..01f8dacf --- /dev/null +++ b/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)) + }) + */ +})