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/services/theme_data/iss_deserializer.js b/src/services/theme_data/iss_deserializer.js index 431e1b94..3cd2f15f 100644 --- a/src/services/theme_data/iss_deserializer.js +++ b/src/services/theme_data/iss_deserializer.js @@ -1,24 +1,51 @@ +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 { + return Object.fromEntries(modes.map((mode, i) => [mode, result[i]])) + } +} // this works nearly the same as HTML tree converter -export const deserialize = (input) => { - const buffer = [] +const parseIss = (input) => { + const buffer = [{ selector: null, content: [] }] let textBuffer = '' const getCurrentBuffer = () => { - let current = buffer[buffer.length - 1][1] + let current = buffer[buffer.length - 1] if (current == null) { - current = { name: null, content: [] } + current = { selector: null, content: [] } } - buffer.push(current) return current } // Processes current line buffer, adds it to output buffer and clears line buffer - const flushText = (content) => { + const flushText = (kind) => { if (textBuffer === '') return - if (content) { - getCurrentBuffer().content.push(textBuffer) + if (kind === 'content') { + getCurrentBuffer().content.push(textBuffer.trim()) } else { - getCurrentBuffer().name = textBuffer + getCurrentBuffer().selector = textBuffer.trim() } textBuffer = '' } @@ -27,17 +54,90 @@ export const deserialize = (input) => { const char = input[i] if (char === ';') { - flushText(true) + flushText('content') } else if (char === '{') { - flushText(false) + flushText('header') } else if (char === '}') { - buffer.push({ name: null, content: [] }) + flushText('content') + buffer.push({ selector: null, content: [] }) textBuffer = '' } else { textBuffer += char } } - flushText() 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(':') + console.log(property, value) + let realValue = value.trim() + if (property === 'shadow') { + realValue = parseShadow(value.split(',').map(v => 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 8d6e9333..6bba85e4 100644 --- a/src/services/theme_data/iss_serializer.js +++ b/src/services/theme_data/iss_serializer.js @@ -1,6 +1,12 @@ import { unroll } from './iss_utils.js' -const serializeShadow = s => `{${s.inset ? 'inset ' : ''}${s.x} ${s.y} ${s.blur} ${s.spread} ${s.color} / ${s.alpha}}` +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 s + } +} export const serialize = (ruleset) => { return ruleset.map((rule) => { @@ -8,8 +14,8 @@ export const serialize = (ruleset) => { const header = unroll(rule).reverse().map(rule => { const { component } = rule - const newVariant = rule.variant === 'normal' ? '' : ('.' + rule.variant) - const newState = rule.state.filter(st => st !== 'normal') + 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(' ') @@ -19,9 +25,9 @@ export const serialize = (ruleset) => { const [valType, newValue] = value.split('|') // only first one! intentional! switch (valType) { case 'shadow': - return ` ${directive}: ${newValue.map(serializeShadow).join(', ')}` + return ` ${directive}: ${valType.trim()} | ${newValue.map(serializeShadow).map(s => s.trim()).join(', ')}` default: - return ` ${directive}: ${newValue}` + return ` ${directive}: ${valType.trim()} | ${newValue.trim()}` } } else { switch (directive) { diff --git a/src/services/theme_data/theme_data_3.service.js b/src/services/theme_data/theme_data_3.service.js index b98cbb98..39c8b74f 100644 --- a/src/services/theme_data/theme_data_3.service.js +++ b/src/services/theme_data/theme_data_3.service.js @@ -23,9 +23,6 @@ import { findRules } from './iss_utils.js' import { parseCssShadow } from './css_utils.js' -import { - serialize -} from './iss_serializer.js' // Ensuring the order of components const components = { diff --git a/test/unit/specs/services/theme_data/iss_deserializer.spec.js b/test/unit/specs/services/theme_data/iss_deserializer.spec.js index f1967144..3488801c 100644 --- a/test/unit/specs/services/theme_data/iss_deserializer.spec.js +++ b/test/unit/specs/services/theme_data/iss_deserializer.spec.js @@ -1,47 +1,16 @@ import { deserialize } from 'src/services/theme_data/iss_deserializer.js' +import { serialize } from 'src/services/theme_data/iss_serializer.js' +import Button from 'src/components/button.style.js' -/* eslint-disable quotes */ -const testData = ``` - Root { - --accent: color | #e2b188; - --badgeNotification: color | #e15932; - --bg: color | #0f161e; - --cBlue: color | #81beea; - --cGreen: color | #5dc94a; - --cOrange: color | #ffc459; - --cRed: color | #d31014; - --defaultButtonBevel: shadow | $borderSide(#FFFFFF, top, 0.2) | $borderSide(#000000, bottom, 0.2); - --defaultButtonHoverGlow: shadow | 0 0 4 --text; - --defaultButtonShadow: shadow | 0 0 2 #000000; - --defaultInputBevel: shadow | $borderSide(#FFFFFF, bottom, 0.2)| $borderSide(#000000, top, 0.2); - --fg: color | #151e2b; - --font: generic | sans-serif; - --link: color | #e2b188; - --monoFont: generic | monospace; - --pressedButtonBevel: shadow | $borderSide(#FFFFFF, bottom, 0.2)| $borderSide(#000000, top, 0.2); - --selectionBackground: color | --accent; - --selectionText: color | $textColor(--accent, --text, no-preserve); - --text: color | #b9b9ba; - --wallpaper: color | #0c1118; - background: transparent; - opacity: 0; - } +describe.only('ISS (de)serialization', () => { + describe('ISS deserialization', () => { + it('Output should = input', () => { + const normalized = Button.defaultRules.map(x => ({ component: 'Button', ...x })) + const serialized = serialize(normalized) + const deserialized = deserialize(serialized) + // deserialized.toString() - Root Underlay { - background: #000000; - opacity: 0.6; - } - - Root Underlay, test { - background: #000000; - opacity: 0.6; - } - ``` - -describe.only('html_tree_converter', () => { - describe('convertHtmlToTree', () => { - it('should parse ISS correctly', () => { - console.log(deserialize(testData)) + expect(JSON.stringify(deserialized)).to.equal(JSON.stringify(normalized)) }) }) })