diff --git a/src/js/cache.ts b/src/js/cache.ts index 8642113..1bbf1ed 100644 --- a/src/js/cache.ts +++ b/src/js/cache.ts @@ -4,7 +4,6 @@ import { LRUCache } from 'lru-cache'; import { Options } from './typedef'; -import { valueToJsonString } from './util'; /* numeric constants */ const MAX_CACHE = 4096; @@ -100,15 +99,28 @@ export const createCacheKey = ( opt: Options = {} ): string => { const { customProperty = {}, dimension = {} } = opt; - let cacheKey = ''; if ( - keyData && - Object.keys(keyData).length && - typeof customProperty.callback !== 'function' && - typeof dimension.callback !== 'function' + !keyData || + Object.keys(keyData).length === 0 || + typeof customProperty.callback === 'function' || + typeof dimension.callback === 'function' ) { - keyData.opt = valueToJsonString(opt); - cacheKey = valueToJsonString(keyData); + return ''; } - return cacheKey; + const baseKey = `${keyData.namespace || ''}:${keyData.name || ''}:${keyData.value || ''}`; + const optStr = [ + opt.format || '', + opt.colorSpace || '', + opt.colorScheme || '', + opt.currentColor || '', + opt.d50 ? '1' : '0', + opt.nullable ? '1' : '0', + opt.preserveComment ? '1' : '0', + String(opt.delimiter || '') + ].join('|'); + const customPropStr = Object.keys(customProperty).length + ? JSON.stringify(customProperty) + : ''; + const dimStr = Object.keys(dimension).length ? JSON.stringify(dimension) : ''; + return `${baseKey}::${optStr}::${customPropStr}::${dimStr}`; }; diff --git a/src/js/css-calc.ts b/src/js/css-calc.ts index fca84c2..ba52fce 100644 --- a/src/js/css-calc.ts +++ b/src/js/css-calc.ts @@ -793,8 +793,7 @@ export const parseTokens = ( const mathFunc = new Set(); let nest = 0; const res: string[] = []; - while (tokens.length) { - const token = tokens.shift(); + for (const token of tokens) { if (!Array.isArray(token)) { throw new TypeError(`${token} is not an array.`); } diff --git a/src/js/css-gradient.ts b/src/js/css-gradient.ts index 4f57567..18801f8 100644 --- a/src/js/css-gradient.ts +++ b/src/js/css-gradient.ts @@ -62,6 +62,26 @@ const FROM_ANGLE = `from\\s+${DIM_ANGLE}`; const AT_POSITION = `at\\s+(?:${POS_1}|${POS_2}|${POS_4})`; const TO_SIDE_CORNER = `to\\s+(?:(?:${L_R})(?:\\s(?:${T_B}))?|(?:${T_B})(?:\\s(?:${L_R}))?)`; const IN_COLOR_SPACE = `in\\s+(?:${CS_RECT}|${CS_HUE})`; +const LINE_SYNTAX_LINEAR = [ + `(?:${DIM_ANGLE}|${TO_SIDE_CORNER})(?:\\s+${IN_COLOR_SPACE})?`, + `${IN_COLOR_SPACE}(?:\\s+(?:${DIM_ANGLE}|${TO_SIDE_CORNER}))?` +].join('|'); +const LINE_SYNTAX_RADIAL = [ + `(?:${RAD_SHAPE})(?:\\s+(?:${RAD_SIZE}))?(?:\\s+${AT_POSITION})?(?:\\s+${IN_COLOR_SPACE})?`, + `(?:${RAD_SIZE})(?:\\s+(?:${RAD_SHAPE}))?(?:\\s+${AT_POSITION})?(?:\\s+${IN_COLOR_SPACE})?`, + `${AT_POSITION}(?:\\s+${IN_COLOR_SPACE})?`, + `${IN_COLOR_SPACE}(?:\\s+${RAD_SHAPE})(?:\\s+(?:${RAD_SIZE}))?(?:\\s+${AT_POSITION})?`, + `${IN_COLOR_SPACE}(?:\\s+${RAD_SIZE})(?:\\s+(?:${RAD_SHAPE}))?(?:\\s+${AT_POSITION})?`, + `${IN_COLOR_SPACE}(?:\\s+${AT_POSITION})?` +].join('|'); +const LINE_SYNTAX_CONIC = [ + `${FROM_ANGLE}(?:\\s+${AT_POSITION})?(?:\\s+${IN_COLOR_SPACE})?`, + `${AT_POSITION}(?:\\s+${IN_COLOR_SPACE})?`, + `${IN_COLOR_SPACE}(?:\\s+${FROM_ANGLE})?(?:\\s+${AT_POSITION})?` +].join('|'); +const REG_LINE_LINEAR = new RegExp(`^(?:${LINE_SYNTAX_LINEAR})$`); +const REG_LINE_RADIAL = new RegExp(`^(?:${LINE_SYNTAX_RADIAL})$`); +const REG_LINE_CONIC = new RegExp(`^(?:${LINE_SYNTAX_CONIC})$`); /* type definitions */ /** @@ -136,51 +156,19 @@ export const validateGradientLine = ( if (isString(value) && isString(type)) { value = value.trim(); type = type.trim(); - let lineSyntax = ''; - const defaultValues = []; + let reg: RegExp | null = null; + let defaultValues: RegExp[] = []; if (/^(?:repeating-)?linear-gradient$/.test(type)) { - /* - * = [ - * [ | to ] || - * - * ] - */ - lineSyntax = [ - `(?:${DIM_ANGLE}|${TO_SIDE_CORNER})(?:\\s+${IN_COLOR_SPACE})?`, - `${IN_COLOR_SPACE}(?:\\s+(?:${DIM_ANGLE}|${TO_SIDE_CORNER}))?` - ].join('|'); - defaultValues.push(/to\s+bottom/); + reg = REG_LINE_LINEAR; + defaultValues = [/to\s+bottom/]; } else if (/^(?:repeating-)?radial-gradient$/.test(type)) { - /* - * = [ - * [ [ || ]? [ at ]? ] || - * ]? - */ - lineSyntax = [ - `(?:${RAD_SHAPE})(?:\\s+(?:${RAD_SIZE}))?(?:\\s+${AT_POSITION})?(?:\\s+${IN_COLOR_SPACE})?`, - `(?:${RAD_SIZE})(?:\\s+(?:${RAD_SHAPE}))?(?:\\s+${AT_POSITION})?(?:\\s+${IN_COLOR_SPACE})?`, - `${AT_POSITION}(?:\\s+${IN_COLOR_SPACE})?`, - `${IN_COLOR_SPACE}(?:\\s+${RAD_SHAPE})(?:\\s+(?:${RAD_SIZE}))?(?:\\s+${AT_POSITION})?`, - `${IN_COLOR_SPACE}(?:\\s+${RAD_SIZE})(?:\\s+(?:${RAD_SHAPE}))?(?:\\s+${AT_POSITION})?`, - `${IN_COLOR_SPACE}(?:\\s+${AT_POSITION})?` - ].join('|'); - defaultValues.push(/ellipse/, /farthest-corner/, /at\s+center/); + reg = REG_LINE_RADIAL; + defaultValues = [/ellipse/, /farthest-corner/, /at\s+center/]; } else if (/^(?:repeating-)?conic-gradient$/.test(type)) { - /* - * = [ - * [ [ from ]? [ at ]? ] || - * - * ] - */ - lineSyntax = [ - `${FROM_ANGLE}(?:\\s+${AT_POSITION})?(?:\\s+${IN_COLOR_SPACE})?`, - `${AT_POSITION}(?:\\s+${IN_COLOR_SPACE})?`, - `${IN_COLOR_SPACE}(?:\\s+${FROM_ANGLE})?(?:\\s+${AT_POSITION})?` - ].join('|'); - defaultValues.push(/at\s+center/); + reg = REG_LINE_CONIC; + defaultValues = [/at\s+center/]; } - if (lineSyntax) { - const reg = new RegExp(`^(?:${lineSyntax})$`); + if (reg) { const valid = reg.test(value); if (valid) { let line = value; @@ -188,21 +176,12 @@ export const validateGradientLine = ( line = line.replace(defaultValue, ''); } line = line.replace(/\s{2,}/g, ' ').trim(); - return { - line, - valid - }; + return { line, valid }; } - return { - valid, - line: value - }; + return { valid, line: value }; } } - return { - line: value, - valid: false - }; + return { line: value, valid: false }; }; /** diff --git a/src/js/relative-color.ts b/src/js/relative-color.ts index 9a20728..204366c 100644 --- a/src/js/relative-color.ts +++ b/src/js/relative-color.ts @@ -126,8 +126,7 @@ export function resolveColorChannels( let nest = 0; let func = ''; let precededPct = false; - while (tokens.length) { - const token = tokens.shift(); + for (const token of tokens) { if (!Array.isArray(token)) { throw new TypeError(`${token} is not an array.`); } @@ -390,8 +389,9 @@ export function extractOriginColor( const tokens = tokenize({ css: restValue }); const originColor: string[] = []; let nest = 0; - while (tokens.length) { - const [type, tokenValue] = tokens.shift() as [TokenType, string]; + let tokenIndex = 0; + for (const [type, tokenValue] of tokens) { + tokenIndex++; switch (type) { case FUNC: case PAREN_OPEN: { @@ -438,7 +438,7 @@ export function extractOriginColor( setCache(cacheKey, null); return resolvedOriginColor; } - const channelValues = resolveColorChannels(tokens, opt); + const channelValues = resolveColorChannels(tokens.slice(tokenIndex), opt); if (channelValues instanceof NullObject) { setCache(cacheKey, null); return channelValues; diff --git a/src/js/util.ts b/src/js/util.ts index aba9ee8..ff356e0 100644 --- a/src/js/util.ts +++ b/src/js/util.ts @@ -18,7 +18,6 @@ const { Delim: DELIM, EOF, Function: FUNC, - Ident: IDENT, OpenParen: PAREN_OPEN, Whitespace: W_SPACE } = TokenType; @@ -32,6 +31,7 @@ const DEG_HALF = 180; /* regexp */ const REG_COLOR = new RegExp(`^(?:${SYN_COLOR_TYPE})$`); +const REG_DIMENSION = /^([+-]?(?:\d+(?:\.\d+)?|\.\d+)(?:e[+-]?\d+)?)([a-z]*)$/i; const REG_FN_COLOR = /^(?:(?:ok)?l(?:ab|ch)|color(?:-mix)?|hsla?|hwb|rgba?|var)\(/; const REG_MIX = new RegExp(SYN_MIX); @@ -78,8 +78,7 @@ export const splitValue = (value: string, opt: Options = {}): string[] => { let nest = 0; let str = ''; const res: string[] = []; - while (tokens.length) { - const [type, value] = tokens.shift() as [TokenType, string]; + for (const [type, value] of tokens) { switch (type) { case COMMA: { if (regDelimiter.test(value)) { @@ -173,15 +172,8 @@ export const extractDashedIdent = (value: string): string[] => { if (cachedResult instanceof CacheItem) { return cachedResult.item as string[]; } - const tokens = tokenize({ css: value }); - const items = new Set(); - while (tokens.length) { - const [type, value] = tokens.shift() as [TokenType, string]; - if (type === IDENT && value.startsWith('--')) { - items.add(value); - } - } - const res = [...items] as string[]; + const matches = value.match(/--[\w-]+/g); + const res = matches ? [...new Set(matches)] : []; setCache(cacheKey, res); return res; }; @@ -220,41 +212,6 @@ export const isColor = (value: unknown, opt: Options = {}): boolean => { return false; }; -/** - * value to JSON string - * @param value - CSS value - * @param [func] - stringify function - * @returns stringified value in JSON notation - */ -export const valueToJsonString = ( - value: unknown, - func: boolean = false -): string => { - if (typeof value === 'undefined') { - return ''; - } - const res = JSON.stringify(value, (_key, val) => { - let replacedValue; - if (typeof val === 'undefined') { - replacedValue = null; - } else if (typeof val === 'function') { - if (func) { - replacedValue = val.toString().replace(/\s/g, '').substring(0, HEX); - } else { - replacedValue = val.name; - } - } else if (val instanceof Map || val instanceof Set) { - replacedValue = [...val]; - } else if (typeof val === 'bigint') { - replacedValue = val.toString(); - } else { - replacedValue = val; - } - return replacedValue; - }); - return res; -}; - /** * round to specified precision * @param value - numeric value @@ -469,14 +426,14 @@ export const isAbsoluteSizeOrLength = ( */ export const isAbsoluteFontSize = (css: unknown): boolean => { if (isString(css)) { - const [token] = tokenize({ css }); - if (Array.isArray(token)) { - const [, , , , detail = {}] = token; - const { unit, value } = detail as { - unit: string; - value: number; - }; - return isAbsoluteSizeOrLength(value, unit); + const str = css.trim(); + if (isAbsoluteSizeOrLength(str, undefined)) { + return true; + } + const match = str.match(REG_DIMENSION); + if (match) { + const [, value, unit] = match; + return isAbsoluteSizeOrLength(Number(value), unit || undefined); } } return false; diff --git a/test/cache.test.ts b/test/cache.test.ts index 03cd5dc..e1e7ac3 100644 --- a/test/cache.test.ts +++ b/test/cache.test.ts @@ -163,7 +163,40 @@ describe('create cache key', () => { foo: 'foo', bar: 'bar' }); - assert.strictEqual(res, '{"foo":"foo","bar":"bar","opt":"{}"}', 'result'); + assert.strictEqual(res, '::::||||0|0|0|::::', 'result'); + }); + + it('should get value', () => { + const res = func( + { + namespace: 'foo', + name: 'bar', + value: 'baz' + }, + { + format: 'computedValue', + colorSpace: 'srgb', + colorScheme: 'normal', + currentColor: 'black', + d50: false, + nullable: false, + preserveComment: true, + delimiter: ' ', + customProperty: { + '--foo': 'foo', + '--bar': 'bar' + }, + dimension: { + em: 12, + rem: 16 + } + } + ); + assert.strictEqual( + res, + 'foo:bar:baz::computedValue|srgb|normal|black|0|0|1| ::{"--foo":"foo","--bar":"bar"}::{"em":12,"rem":16}', + 'result' + ); }); it('should get empty string', () => { diff --git a/test/util.test.ts b/test/util.test.ts index 9fcb384..f1cfb6c 100644 --- a/test/util.test.ts +++ b/test/util.test.ts @@ -300,137 +300,6 @@ describe('is color', () => { }); }); -describe('value to JSON string', () => { - const func = util.valueToJsonString; - - it('should get result', () => { - const res = func(); - assert.strictEqual(res, '', 'result'); - }); - - it('should get result', () => { - const res = func(null); - assert.strictEqual(res, 'null', 'result'); - }); - - it('should get result', () => { - const res = func('foo'); - assert.strictEqual(res, '"foo"', 'result'); - }); - - it('should get result', () => { - const res = func({ - foo: 'bar', - baz: undefined - }); - assert.strictEqual(res, '{"foo":"bar","baz":null}', 'result'); - }); - - it('should get result', () => { - const res = func({ - foo: 'bar', - map: new Map([ - ['key1', 1], - ['key2', true], - ['key1', 3] - ]), - set: new Set([1, 'baz', 3, 2, 3, 'baz']) - }); - assert.strictEqual( - res, - '{"foo":"bar","map":[["key1",3],["key2",true]],"set":[1,"baz",3,2]}', - 'result' - ); - }); - - it('should get result', () => { - const res = func({ - foo: 'bar', - func: () => {} - }); - assert.strictEqual(res, '{"foo":"bar","func":"func"}', 'result'); - }); - - it('should get result', () => { - const res = func( - { - foo: 'bar', - func: () => {} - }, - true - ); - assert.strictEqual(res, '{"foo":"bar","func":"()=>{}"}', 'result'); - }); - - it('should get result', () => { - const res = func( - { - foo: 'bar', - func: () => { - const l = 100; - for (let i = 0; i < l; i++) { - i++; - } - } - }, - true - ); - assert.strictEqual( - res, - '{"foo":"bar","func":"()=>{constl=100;"}', - 'result' - ); - }); - - it('should get result', () => { - const myCallback = () => {}; - const res = func({ - foo: 'bar', - func: myCallback - }); - assert.strictEqual(res, '{"foo":"bar","func":"myCallback"}', 'result'); - }); - - it('should get result', () => { - const myCallback = () => {}; - const res = func( - { - foo: 'bar', - func: myCallback - }, - true - ); - assert.strictEqual(res, '{"foo":"bar","func":"()=>{}"}', 'result'); - }); - - it('should get result', () => { - const res = func({ - foo: 'bar', - big: 1n - }); - assert.strictEqual(res, '{"foo":"bar","big":"1"}', 'result'); - }); - - it('should get result', () => { - const opt = { - foo: 'bar', - cssCalc: { - globals: new Map([ - ['bar', 'baz'], - ['qux', 1] - ]) - } - }; - const res = func(opt); - assert.strictEqual(opt.cssCalc.globals instanceof Map, true, 'map'); - assert.strictEqual( - res, - '{"foo":"bar","cssCalc":{"globals":[["bar","baz"],["qux",1]]}}', - 'result' - ); - }); -}); - describe('round to specified precision', () => { const func = util.roundToPrecision;