import { flattenDeep } from 'lodash' export const deserializeShadow = string => { const modes = ['_full', 'inset', 'x', 'y', 'blur', 'spread', 'color', 'alpha', 'name'] 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-z0-9\\-_]+|\\$[a-z0-9\\-()_]+)', // opacity (optional) '(?:\\s+\\/\\s+([0-9]+(?:\\.[0-9]+)?)\\s*)?', // name '(?:\\s+#(\\w+)\\s*)?', '$' ].join('') const regex = new RegExp(regexPrep, 'gis') // global, (stable) indices, single-string const result = regex.exec(string) if (result == null) { if (string.startsWith('$') || string.startsWith('--')) { return string } else { throw new Error(`Invalid shadow definition: '${string}'`) } } else { const numeric = new Set(['x', 'y', 'blur', 'spread', 'alpha']) const { x, y, blur, spread, alpha, inset, color, name } = Object.fromEntries(modes.map((mode, i) => { if (numeric.has(mode)) { const number = Number(result[i]) if (Number.isNaN(number)) { if (mode === 'alpha') return [mode, 1] return [mode, 0] } return [mode, number] } 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, name } } } // 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') { if (realValue === 'none') { realValue = [] } else { realValue = value.split(',').map(v => deserializeShadow(v.trim())) } } if (!Number.isNaN(Number(value))) { realValue = Number(value) } return [property, realValue] })) return output }) return result }) return flattenDeep(finalResult) }