From b6f367b5a168b04c89f21c80cf2063e51a95d040 Mon Sep 17 00:00:00 2001 From: Andrey Yamanov Date: Wed, 23 Nov 2022 17:48:34 +0100 Subject: [PATCH 1/2] feat: new parser initial implementation --- src/tasty/parser/ast.ts | 64 ++++++++++++++++++++++++++++++ src/tasty/parser/index.ts | 1 + src/tasty/parser/renderer.ts | 56 +++++++++++++++++++++++++++ src/tasty/parser/tokenizer.ts | 73 +++++++++++++++++++++++++++++++++++ 4 files changed, 194 insertions(+) create mode 100644 src/tasty/parser/ast.ts create mode 100644 src/tasty/parser/index.ts create mode 100644 src/tasty/parser/renderer.ts create mode 100644 src/tasty/parser/tokenizer.ts diff --git a/src/tasty/parser/ast.ts b/src/tasty/parser/ast.ts new file mode 100644 index 00000000..c141eea7 --- /dev/null +++ b/src/tasty/parser/ast.ts @@ -0,0 +1,64 @@ +import { RawStyleToken } from './tokenizer'; + +export type StyleToken = RawStyleToken & { + children?: StyleToken[]; +}; + +const COLOR_FUNCTIONS = ['rgb', 'rgba', 'hsl', 'hsla']; + +export function createAST( + tokens: StyleToken[], + startIndex = 0, +): [StyleToken[], number] { + const ast: StyleToken[] = []; + + for (let i: number = startIndex; i < tokens.length; i++) { + let token = tokens[i]; + + switch (token.type) { + case 'property': + const propertyName = `--${token.value.slice(1)}`; + + token.type = 'func'; + token.value = 'var'; + token.children = [ + { + type: 'propertyName', + value: propertyName, + }, + ]; + break; + case 'func': + if (COLOR_FUNCTIONS.includes(token.value)) { + token.type = 'color'; + + do { + i++; + token.value += tokens[i].value; + } while (tokens[i] && tokens[i].value !== ')'); + } else { + [token.children, i] = createAST(tokens, i + 2); + } + break; + case 'bracket': + if (token.value === '(') { + token.type = 'func'; + token.value = 'calc'; + [token.children, i] = createAST(tokens, i + 1); + } else if (token.value === ')') { + return [ast, i + 1]; + } + break; + default: + break; + } + + if (token.type !== 'space') { + ast.push(token); + } + + if (i === -1) break; + } + + return [ast, -1]; +} diff --git a/src/tasty/parser/index.ts b/src/tasty/parser/index.ts new file mode 100644 index 00000000..911a1a65 --- /dev/null +++ b/src/tasty/parser/index.ts @@ -0,0 +1 @@ +export function parseStyle(val) {} diff --git a/src/tasty/parser/renderer.ts b/src/tasty/parser/renderer.ts new file mode 100644 index 00000000..813f6d54 --- /dev/null +++ b/src/tasty/parser/renderer.ts @@ -0,0 +1,56 @@ +import { StyleToken } from './ast'; + +export const CUSTOM_UNITS: Record string)> = + { + r: 'var(--radius)', + bw: 'var(--border-width)', + ow: 'var(--outline-width)', + x: 'var(--gap)', + fs: 'var(--font-size)', + lh: 'var(--line-height)', + rp: 'var(--rem-pixel)', + gp: 'var(--column-gap)', + }; + +export function renderCustomUnit(token: StyleToken) { + const converter = + CUSTOM_UNITS[token.unit as unknown as keyof typeof CUSTOM_UNITS]; + + if (!converter) { + return token.value; + } + + if (typeof converter === 'function') { + return converter(token.value); + } + + if (token.value === '1') { + return converter; + } + + return `calc(${token.amount} * ${converter})`; +} + +const INSTANT_VALUES = [')', ',']; + +export function renderStyleTokens(tokens: StyleToken[]) { + let result = ''; + + tokens.forEach((token) => { + if (INSTANT_VALUES.includes(token.value)) { + result += token.value; + } else if (token.type === 'func') { + result += token.children + ? `${token.value}(${renderStyleTokens(token.children)})` + : ''; + } else if (token.type === 'value') { + result += renderCustomUnit(token); + } else if (token.type === 'property') { + result += `var(--${token.value.slice(1)})`; + } else { + result += token.value; + } + }); + + return result; +} diff --git a/src/tasty/parser/tokenizer.ts b/src/tasty/parser/tokenizer.ts new file mode 100644 index 00000000..7b41f873 --- /dev/null +++ b/src/tasty/parser/tokenizer.ts @@ -0,0 +1,73 @@ +export type RawStyleToken = { + value: string; + type: typeof TOKEN_MAP[number]; + negative?: boolean; + amount?: number; + unit?: string; +}; + +const CACHE = new Map(); +const MAX_CACHE = 1000; +const REGEXP = + /((|-)([0-9]+|[0-9]+\.[0-9]+|\.[0-9]+)([a-z]+|%|))|([a-z][a-z0-9-]*(?=\())|([a-z][a-z0-9-]*)|(#[a-z][a-z0-9-]{2,}|#[a-f0-9]{3}|[a-f0-9]{6})|(@[a-z][a-z0-9-]*)|(--[a-z][a-z0-9-]*)|([+*/-])|([()])|("[^"]*"|'[^']*')|(,)|(\\|\|)|(\s+)/gi; +const TOKEN_MAP = [ + '', + 'value', + 'sign', + 'number', + 'unit', + 'func', + 'mod', + 'color', + 'property', + 'propertyName', + 'operator', + 'bracket', + 'text', + 'comma', + 'delimiter', + 'space', +]; + +export function tokenize(value: string) { + value = value.trim(); + + if (!value) { + return []; + } + + if (CACHE.size > MAX_CACHE) { + CACHE.clear(); + } + + let tokens: RawStyleToken[] = []; + + if (!CACHE.has(value)) { + REGEXP.lastIndex = 0; + + let rawToken; + + while ((rawToken = REGEXP.exec(value))) { + const token: RawStyleToken = rawToken[1] + ? { + type: 'value', + value: rawToken[1], + negative: !!rawToken[2], + amount: parseFloat(rawToken[3]), + unit: rawToken[4], + } + : { + type: TOKEN_MAP[rawToken.indexOf(rawToken[0], 1)], + value: rawToken[0], + }; + + if (token.type != 'space') { + tokens.push(token); + } + } + + CACHE.set(value, tokens); + } + + return CACHE.get(value); +} From 8472753bc339bf727d7b838a7313290a911422c0 Mon Sep 17 00:00:00 2001 From: Andrey Yamanov Date: Thu, 5 Jan 2023 18:05:08 +0100 Subject: [PATCH 2/2] feat: parser api and units option --- src/tasty/parser/ast.ts | 5 ++++- src/tasty/parser/index.ts | 37 ++++++++++++++++++++++++++++++++++- src/tasty/parser/renderer.ts | 33 ++++++++++++++----------------- src/tasty/parser/tokenizer.ts | 9 +++++++-- 4 files changed, 62 insertions(+), 22 deletions(-) diff --git a/src/tasty/parser/ast.ts b/src/tasty/parser/ast.ts index c141eea7..60fc15d9 100644 --- a/src/tasty/parser/ast.ts +++ b/src/tasty/parser/ast.ts @@ -4,8 +4,11 @@ export type StyleToken = RawStyleToken & { children?: StyleToken[]; }; -const COLOR_FUNCTIONS = ['rgb', 'rgba', 'hsl', 'hsla']; +const COLOR_FUNCTIONS = ['rgb', 'rgba', 'hsl', 'hsla', 'lch', 'oklch']; +/** + * Create an AST from a flat array of style tokens. + */ export function createAST( tokens: StyleToken[], startIndex = 0, diff --git a/src/tasty/parser/index.ts b/src/tasty/parser/index.ts index 911a1a65..c8f802fa 100644 --- a/src/tasty/parser/index.ts +++ b/src/tasty/parser/index.ts @@ -1 +1,36 @@ -export function parseStyle(val) {} +import { tokenize } from './tokenizer'; +import { renderStyleTokens, CustomUnitMap } from './renderer'; +import { createAST, StyleToken } from './ast'; + +interface StyleParserProps { + units: CustomUnitMap; +} + +export function CreateStyleParser({ units }: StyleParserProps) { + return { + parse(value) { + return createAST(tokenize(value)); + }, + render(tokens) { + return renderStyleTokens(tokens, { units }); + }, + excludeMods(tokens: StyleToken[], allMods: string[]) { + const mods: string[] = []; + + for (let i = 0; i < tokens.length; i++) { + const token = tokens[i]; + + if (token.type === 'text') { + const mod = token.value; + + if (allMods.includes(mod)) { + mods.push(mod); + tokens.splice(i--, 1); + } + } + } + + return mods; + }, + }; +} diff --git a/src/tasty/parser/renderer.ts b/src/tasty/parser/renderer.ts index 813f6d54..ebe2749e 100644 --- a/src/tasty/parser/renderer.ts +++ b/src/tasty/parser/renderer.ts @@ -1,20 +1,11 @@ import { StyleToken } from './ast'; -export const CUSTOM_UNITS: Record string)> = - { - r: 'var(--radius)', - bw: 'var(--border-width)', - ow: 'var(--outline-width)', - x: 'var(--gap)', - fs: 'var(--font-size)', - lh: 'var(--line-height)', - rp: 'var(--rem-pixel)', - gp: 'var(--column-gap)', - }; - -export function renderCustomUnit(token: StyleToken) { - const converter = - CUSTOM_UNITS[token.unit as unknown as keyof typeof CUSTOM_UNITS]; +export type CustomUnitMap = Record string)>; + +export function renderCustomUnit(token: StyleToken, units?: CustomUnitMap) { + units = units || {}; + + const converter = token.unit ? units[token.unit] : undefined; if (!converter) { return token.value; @@ -33,7 +24,13 @@ export function renderCustomUnit(token: StyleToken) { const INSTANT_VALUES = [')', ',']; -export function renderStyleTokens(tokens: StyleToken[]) { +/** + * Render a style tokens to a string. + */ +export function renderStyleTokens( + tokens: StyleToken[], + { units }: { units?: CustomUnitMap } = {}, +) { let result = ''; tokens.forEach((token) => { @@ -41,10 +38,10 @@ export function renderStyleTokens(tokens: StyleToken[]) { result += token.value; } else if (token.type === 'func') { result += token.children - ? `${token.value}(${renderStyleTokens(token.children)})` + ? `${token.value}(${renderStyleTokens(token.children, units)})` : ''; } else if (token.type === 'value') { - result += renderCustomUnit(token); + result += renderCustomUnit(token, units); } else if (token.type === 'property') { result += `var(--${token.value.slice(1)})`; } else { diff --git a/src/tasty/parser/tokenizer.ts b/src/tasty/parser/tokenizer.ts index 7b41f873..1fc2ad96 100644 --- a/src/tasty/parser/tokenizer.ts +++ b/src/tasty/parser/tokenizer.ts @@ -29,7 +29,10 @@ const TOKEN_MAP = [ 'space', ]; -export function tokenize(value: string) { +/** + * Tokenize a style value into a flat array of tokens. + */ +export function tokenize(value: string): RawStyleToken[] { value = value.trim(); if (!value) { @@ -69,5 +72,7 @@ export function tokenize(value: string) { CACHE.set(value, tokens); } - return CACHE.get(value); + const cachedTokens = CACHE.get(value); + + return cachedTokens ? cachedTokens : []; }