UNPKG

css-doodle

Version:

A web component for drawing patterns with CSS

854 lines (802 loc) 19.3 kB
// I need to rewrite this import parse_var from './parse-var.js'; import parse_svg from './parse-svg.js'; import generate_svg_extended from '../generator/svg-extended.js'; import { first, last } from '../utils/index.js'; const Tokens = { func(name = '') { return { type: 'func', name, arguments: [] }; }, argument() { return { type: 'argument', value: [] }; }, text(value = '') { return { type: 'text', value }; }, pseudo(selector = '') { return { type: 'pseudo', selector, styles: [] }; }, cond(name = '') { return { type: 'cond', name, styles: [], arguments: [] }; }, rule(property = '') { return { type: 'rule', property, value: [] }; }, keyframes(name = '') { return { type: 'keyframes', name, steps: [] } }, step(name = '') { return { type: 'step', name, styles: [] } } }; const is = { white_space(c) { return /[\s\n\t]/.test(c); }, line_break(c) { return /\n/.test(c); }, number(n) { return !isNaN(n); }, pair(n) { return ['"', '(', ')', "'"].includes(n); }, pair_of(c, n) { return ({ '"': '"', "'": "'", '(': ')' })[c] == n; }, selector(it) { let index = it.index(); let c; let stack_paren = []; let stack_quote = []; let result = false; while (!it.end()) { c = it.next(); if (c === '"' || c === "'" || c === '`') { let quote = last(stack_quote); if (c === quote) { stack_quote.pop(); } else if (!stack_quote.length) { stack_quote.push(c); } } if (c == '(') { stack_paren.push(c); } if (c == ')') { stack_paren.pop(); } if (!stack_paren.length && !stack_quote.length) { if (c === '{') { result = true; break; } if (c === ';' || c === '}') { result = false; break; } } } it.index(index); return result; } }; // This should not be in the parser // but I'll leave it here until the rewriting const symbols = { 'π': Math.PI, '∏': Math.PI }; function composible(name) { return /^@(canvas|shaders|doodle)/.test(name); } function iterator(input = '') { let index = 0, col = 1, line = 1; return { curr(n = 0) { return input[index + n]; }, end() { return input.length <= index; }, info() { return { index, col, line }; }, index(n) { return (n === undefined ? index : index = n); }, range(start, end) { return input.substring(start, end); }, next() { let next = input[index++]; if (next == '\n') line++, col = 0; else col++; return next; } }; } function throw_error(msg, { col, line }) { console.warn( `(at line ${ line }, column ${ col }) ${ msg }` ); } function get_text_value(input) { if (input.trim().length) { return is.number(+input) ? +input : input.trim() } else { return input; } } function read_until(fn, include) { return function(it, reset) { let index = it.index(); let word = ''; while (!it.end()) { let c = it.next(); if (fn(c)) { if (include) word += c; break; } else { word += c; } } if (reset) { it.index(index); } return word; } } function read_word(it, reset) { let check = c => /[^\w@]/.test(c); return read_until(check)(it, reset); } function read_keyframe_name(it) { return read_until(c => /[\s\{]/.test(c))(it); } function read_line(it, reset) { let check = c => is.line_break(c) || c == '{'; return read_until(check, true)(it, reset); } function read_step(it, extra) { let c, step = Tokens.step(); while (!it.end()) { if ((c = it.curr()) == '}') break; if (is.white_space(c)) { it.next(); continue; } else if (!step.name.length) { step.name = read_value(it, c => c === '{'); } else { step.styles.push(read_rule(it, extra)); if (it.curr() == '}') break; } it.next(); } return step; } function read_steps(it, extra) { const steps = []; let c; while (!it.end()) { if ((c = it.curr()) == '}') break; else if (is.white_space(c)) { it.next(); continue; } else { steps.push(read_step(it, extra)); } it.next(); } return steps; } function read_keyframes(it, extra) { let keyframes = Tokens.keyframes(), c; while (!it.end()) { if ((c = it.curr()) == '}') break; else if (!keyframes.name.length) { read_word(it); keyframes.name = read_keyframe_name(it); if (!keyframes.name.length) { throw_error('missing keyframes name', it.info()); break; } continue; } else if (c == '{' || it.curr(-1) == '{') { it.next(); keyframes.steps = read_steps(it, extra); break; } it.next(); } return keyframes; } function read_comments(it, flag = {}) { it.next(); while (!it.end()) { let c = it.curr(); if (flag.inline) { if (c == '\n') break; } else { if ((c = it.curr()) == '*' && it.curr(1) == '/') break; } it.next(); } if (!flag.inline) { it.next(); it.next(); } } function skip_tag(it) { it.next(); while(!it.end()) { let c = it.curr(); if (c == '>') break; it.next(); } } function read_property(it) { let prop = '', c; while (!it.end()) { if ((c = it.curr()) == ':') break; else if (!is.white_space(c)) prop += c; it.next(); } return prop; } function read_arguments(it, composition, doodle, variables = {}) { let args = [], group = [], stack = [], arg = '', c; let raw = ''; while (!it.end()) { c = it.curr(); let prev = it.curr(-1); let start = it.index(); if ((/[\('"`]/.test(c) && prev !== '\\')) { if (stack.length) { /* if ((c !== '(') && last(stack) === '(') { stack.pop(); } */ if (c !== '(' && c === last(stack)) { stack.pop(); } else { stack.push(c); } } else { stack.push(c); } arg += c; } else if (!doodle && ((c == '@' || c === '$') || (prev === '.' && composition))) { if (!group.length) { arg = arg.trimLeft(); } if (arg.length) { group.push(Tokens.text(arg)); arg = ''; } group.push(read_func(it, variables)); } else if (doodle && /[)]/.test(c) || (!doodle && /[,)]/.test(c))) { if (stack.length) { if (c == ')' && last(stack) === '(') { stack.pop(); } arg += c; } else { if (arg.length) { if (!group.length) { group.push(Tokens.text(get_text_value(arg))); } else if (/\S/.test(arg)) { group.push(Tokens.text(arg)); } if (arg.trim().startsWith('±') && !doodle) { let raw = arg.trim().substr(1); let cloned = structuredClone(group); last(cloned).value = '-' + raw; args.push(normalize_argument(cloned)); last(group).value = raw; } } args.push(normalize_argument(group)); [group, arg] = [[], '']; if (c == ')') break; } } else { if (symbols[c] && !/[0-9]/.test(it.curr(-1))) { c = symbols[c]; } arg += c; } if (composition && ((it.curr(1) == ')' || it.curr(1) == ';') || !/[0-9a-zA-Z_\-.]/.test(it.curr())) && !stack.length) { if (group.length) { args.push(normalize_argument(group)); } break; } else { raw += it.range(start, it.index() + 1); it.next(); } } return [skip_last_empty_args(args), raw]; } function skip_last_empty_args(args) { let arg = last(args[0]); if (arg && arg.type === 'text' && !String(arg.value).trim().length) { args[0] = args[0].slice(0, -1); } return args; } function normalize_argument(group) { let result = group.map(arg => { if (arg.type == 'text' && typeof arg.value == 'string') { let value = String(arg.value); if (value.includes('`')) { arg.value = value = value.replace(/`/g, '"'); } arg.value = value; } return arg; }); let ft = first(result) || {}; let ed = last(result) || {}; if (ft.type == 'text' && ed.type == 'text') { let cf = first(ft.value); let ce = last(ed.value); if (typeof ft.value == 'string' && typeof ed.value == 'string') { if (is.pair_of(cf, ce)) { ft.value = ft.value.slice(1); ed.value = ed.value.slice(0, ed.value.length - 1); result.cluster = true; } } } return result; } function seperate_func_name(name) { let fname = '', extra = ''; if ((/\D$/.test(name) && !/\d+[x-]\d+/.test(name)) || Math[name.substr(1)]) { return { fname: name, extra } } for (let i = name.length - 1; i >= 0; i--) { let c = name[i]; let prev = name[i - 1]; let next = name[i + 1]; if (/[\d.]/.test(c) || ((c == 'x' || c == '-') && /\d/.test(prev) && /\d/.test(next))) { extra = c + extra; } else { fname = name.substring(0, i + 1); break; } } return { fname, extra }; } function has_times_syntax(token) { let str = JSON.stringify(token); return str.includes('pureName') && str.includes('times'); } function is_svg(name) { return /^@svg$/i.test(name); } function read_func(it, variables = {}) { let func = Tokens.func(); let name = it.curr(), c; let has_argument = false; let is_calc = name === '$';; if (name === '@') { it.next(); } else { name = '@'; } while (!it.end()) { c = it.curr(); let next = it.curr(1); let composition = (c == '.' && (/[a-zA-Z@$]/.test(next))); if (c == '(' || composition) { has_argument = true; it.next(); let [args, raw_args] = read_arguments(it, composition, composible(name), variables); if (is_svg(name)) { let parsed_svg = parse_svg(raw_args); let line = 0; for (let item of parsed_svg.value) { if (item.variable) { variables[item.name] = (parse(`${'\n'.repeat(line++)} ${item.name}: ${item.value}`))[0].value; } } if (/\d\s*{/.test(raw_args) && has_times_syntax(parsed_svg)) { let svg = generate_svg_extended(parsed_svg); // compatible with old iterator svg += ')'; let extended = read_arguments(iterator(svg), composition, composible(name), variables); args = extended[0]; } } func.arguments = args; func.variables = variables; break; } else if (/[0-9a-zA-Z_\-.%]/.test(c)) { name += c; } if (!has_argument && next !== '(' && !/[0-9a-zA-Z_\-.%]/.test(next)) { break; } it.next(); } let { fname, extra } = seperate_func_name(name); func.name = is_calc ? '@$' + name.substr(1) : fname; if (extra.length) { func.arguments.unshift([{ type: 'text', value: extra }]); } if (is_calc && func.name.length > 2) { if (!func.arguments.length) { let name = func.name.substring(0, 2); let value = func.name.substring(2); func.name = name; func.arguments.push( [{ type: 'text', value: value }] ); } if (/\d$/.test(func.name)) { let name = func.name.substring(0, 2); let value = func.name.substring(2); func.name = name; func.arguments[0][0].value = value; } } func.position = it.info().index; return func; } function read_value(it, check_break = () => {}) { let text = Tokens.text(), idx = 0, skip = true, c; const value = []; value[idx] = []; let stack = [], quote_stack = []; while (!it.end()) { c = it.curr(); if (skip && is.white_space(c)) { it.next(); continue; } else { skip = false; } if (c == '\n' && !is.white_space(it.curr(-1))) { text.value += ' '; } else if (c == ',' && !stack.length) { if (text.value.length) { value[idx].push(text); text = Tokens.text(); } value[++idx] = []; skip = true; } else if ((/[;}<]/.test(c) || check_break(c)) && !quote_stack.length) { if (text.value.length) { value[idx].push(text); text = Tokens.text(); } break; } else if ((c === '@' || c === '$') && /[\w-\(%]/.test(it.curr(1))) { if (text.value.length) { value[idx].push(text); text = Tokens.text(); } value[idx].push(read_func(it)); } else if (c === '"' || c === "'") { let quote = last(quote_stack); if (c === quote) { quote_stack.pop(); } else if (!quote_stack.length) { quote_stack.push(c); } text.value += c; } else if (!is.white_space(c) || !is.white_space(it.curr(-1))) { if (c == '(') stack.push(c); if (c == ')') stack.pop(); if (symbols[c] && !/[0-9]/.test(it.curr(-1))) { c = symbols[c]; } text.value += c; } let cc = it.curr(); if ((cc === ';' || cc == '}' || check_break(cc)) && !quote_stack.length) { break; } it.next(); } if (text.value.length) { value[idx].push(text); } return value; } function read_selector(it) { let selector = '', c; while (!it.end()) { if ((c = it.curr()) == '{') break; else { selector += c; } it.next(); } selector = selector.trim(); return selector; } function read_cond_selector(it) { let keyword = '', c; let segments = []; let selector = {}; while (!it.end()) { if ((c = it.curr()) == '(') { if (keyword.length) { if (selector.name) { segments.push({ keyword }); } else { selector.name = keyword; } keyword = ''; } it.next(); let args = read_arguments(it)[0]; segments.push({ arguments: args }); } else if (!is.white_space(c)) { if (c == '{' || c == ')') { if (keyword.length) { if (selector.name) { segments.push({ keyword }); } else { selector.name = keyword; } } break; } else { keyword += c; } } else if (is.white_space(c) && !selector.name) { selector.name = keyword; keyword = ''; } else if (is.white_space(c) && keyword.length) { segments.push({ keyword }); keyword = ''; } it.next(); } let [name, ...addition] = (selector.name || '').trim().split(/\s+/); return { name, addition, segments } } function read_pseudo(it, extra) { let pseudo = Tokens.pseudo(), c; while (!it.end()) { c = it.curr(); if (c == '/' && it.curr(1) == '*') { read_comments(it); } else if (c == '}') { break; } else if (is.white_space(c)) { it.next(); continue; } else if (!pseudo.selector) { pseudo.selector = read_selector(it); } else if (c == ':') { let nested = read_pseudo(it, extra); if (nested.selector) pseudo.styles.push(nested); } else if (c == '&') { pseudo.styles.push(read_cond(it, extra)); } else { let rule = read_rule(it, extra); if (rule.property == '@use') { pseudo.styles = pseudo.styles.concat( rule.value ); } else if (rule.property) { pseudo.styles.push(rule); } if (it.curr() == '}') break; } it.next(); } return pseudo; } function read_rule(it, extra) { let rule = Tokens.rule(), c; let start = it.index(); while (!it.end()) { c = it.curr(); if (c == '/' && it.curr(1) == '*') { read_comments(it); } else if (c == ';') { break; } else if (!rule.property.length) { rule.property = read_property(it); if (rule.property == '@use') { rule.value = read_var(it, extra); break; } } else { rule.value = read_value(it); break; } it.next(); } let end = it.index(); rule.raw = () => it.range(start, end).trim(); return rule; } function read_cond(it, extra) { let cond = Tokens.cond(), c; while (!it.end()) { c = it.curr(); if (c == '/' && it.curr(1) == '*') { read_comments(it); } else if (c == '}') { break; } else if (!cond.name.length) { Object.assign(cond, read_cond_selector(it)); } else if (c == ':') { let pseudo = read_pseudo(it); if (pseudo.selector) cond.styles.push(pseudo); } else if (c == '&') { cond.styles.push(read_cond(it)); } else if (c == '@' && !read_line(it, true).includes(':')) { cond.styles.push(read_cond(it)); } else if (!is.white_space(c)) { let rule = read_rule(it, extra); if (rule.property) cond.styles.push(rule); if (it.curr() == '}') break; } it.next(); } return cond; } function read_variable(extra, name) { let rule = ''; if (extra && extra.get_variable) { rule = extra.get_variable(name); } return rule; } function evaluate_value(values, extra) { values.forEach && values.forEach(v => { if (v.type == 'text' && v.value) { let vars = parse_var(v.value); v.value = vars.reduce((ret, p) => { let rule = '', other = '', parsed; rule = read_variable(extra, p.name); if (!rule && p.fallback) { p.fallback.every(n => { other = read_variable(extra, n.name); if (other) { rule = other; return false; } }); } try { parsed = parse(rule, extra); } catch (e) { } if (parsed) { ret.push.apply(ret, parsed); } return ret; }, []); } if (v.type == 'func' && v.arguments) { v.arguments.forEach(arg => { evaluate_value(arg, extra); }); } }); } function read_var(it, extra) { it.next(); let groups = read_value(it) || []; return groups.reduce((ret, group) => { evaluate_value(group, extra); let [token] = group; if (token.value && token.value.length) { ret.push(...token.value); } return ret; }, []); } export default function parse(input, extra) { const it = iterator(input); const Tokens = []; while (!it.end()) { let c = it.curr(); if (is.white_space(c)) { it.next(); continue; } else if (c == '/' && it.curr(1) == '*') { read_comments(it); } else if (c == ':') { let pseudo = read_pseudo(it, extra); if (pseudo.selector) Tokens.push(pseudo); } else if (c == '@' && read_word(it, true) === '@keyframes') { let keyframes = read_keyframes(it, extra); Tokens.push(keyframes); } else if (c == '<') { skip_tag(it); } else if (is.selector(it)) { let cond = read_cond(it, extra); if (cond.name.length) Tokens.push(cond); } else if (!is.white_space(c)) { let rule = read_rule(it, extra); if (rule.property) Tokens.push(rule); } it.next(); } return Tokens; }