From 1c22625fc0f31a383b68441ecd134d60a3f10242 Mon Sep 17 00:00:00 2001 From: Thierry Bela Date: Fri, 11 Aug 2023 01:48:08 -0400 Subject: [PATCH] add web export | add stats data to transform,, parse and render functions | fix escape sequence parsing bug #4 --- README.md | 16 ++-- dist/index-umd-web.js | 115 +++++++++++++++---------- dist/index.cjs | 115 +++++++++++++++---------- dist/index.d.ts | 11 ++- dist/lib/ast/minify.js | 26 ++++-- dist/lib/parser/parse.js | 21 +++-- dist/lib/parser/tokenize.js | 24 +++--- dist/lib/parser/utils/syntax.js | 5 +- dist/lib/renderer/render.js | 15 ++-- dist/lib/transform.js | 24 +++--- package.json | 3 +- src/@types/index.d.ts | 11 ++- src/lib/ast/minify.ts | 57 +++++++----- src/lib/parser/parse.ts | 27 +++--- src/lib/parser/tokenize.ts | 30 ++++--- src/lib/parser/utils/syntax.ts | 7 +- src/lib/renderer/render.ts | 18 ++-- src/lib/transform.ts | 23 +++-- test/files/result/font-awesome-all.css | 6 +- test/specs/block.spec.js | 61 +++++++++++++ 20 files changed, 393 insertions(+), 222 deletions(-) diff --git a/README.md b/README.md index 60bbc36..b4c3913 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -[![cov](https://tbela99.github.io/css-parser/badges/coverage.svg)](https://github.com/tbela99/css-parser/actions) +[![npm](https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fraw.githubusercontent.com%2Ftbela99%2Fcss-parser%2Fmaster%2Fpackage.json&query=version&logo=npm&label=npm&link=https%3A%2F%2Fwww.npmjs.com%2Fpackage%2F%40tbela99%2Fcss-parser)](https://www.npmjs.com/package/@tbela99/css-parser) [![cov](https://tbela99.github.io/css-parser/badges/coverage.svg)](https://github.com/tbela99/css-parser/actions) # css-parser @@ -12,15 +12,15 @@ $ npm install @tbela99/css-parser ### Features -- [x] fault tolerant parser, will try to fix invalid tokens according to the CSS syntax module 3 recommendations. -- [x] efficient minification, see benchmark -- [x] replace @import at-rules with actual css content of the imported rule -- [x] automatically create nested css rules -- [x] works the same way in node and web browser +- fault tolerant parser, will try to fix invalid tokens according to the CSS syntax module 3 recommendations. +- efficient minification, see [benchmark](https://tbela99.github.io/css-parser/benchmark/index.html) +- replace @import at-rules with actual css content of the imported rule +- automatically create nested css rules +- works the same way in node and web browser ### Performance -- [x] flatten @import +- flatten @import ## Transform @@ -50,7 +50,7 @@ Include ParseOptions and RenderOptions - src: string, optional. css file location to be used with sourcemap. - minify: boolean, optional. default to _true_. optimize ast. -- nestingRules: boolean, optional. automatically nest rules. +- nestingRules: boolean, optional. automatically generated nested rules. - removeEmpty: boolean, remove empty nodes from the ast. - location: boolean, optional. includes node location in the ast, required for sourcemap generation. - cwd: string, optional. the current working directory. when specified url() are resolved using this value diff --git a/dist/index-umd-web.js b/dist/index-umd-web.js index 225ff70..d57ed2a 100644 --- a/dist/index-umd-web.js +++ b/dist/index-umd-web.js @@ -93,10 +93,7 @@ if (name.charAt(0) != '#') { return false; } - if (isIdent(name.charAt(1))) { - return true; - } - return true; + return isIdent(name.charAt(1)); } function isNumber(name) { if (name.length == 0) { @@ -1566,6 +1563,7 @@ } function render(data, opt = {}) { + const startTime = performance.now(); const options = Object.assign(opt.minify ?? true ? { indent: '', newLine: '', @@ -1584,7 +1582,9 @@ } return acc + renderToken(curr, options); } - return { code: doRender(data, options, reducer, 0) }; + return { code: doRender(data, options, reducer, 0), stats: { + total: `${(performance.now() - startTime).toFixed(2)}ms` + } }; } // @ts-ignore function doRender(data, options, reducer, level = 0, indents = []) { @@ -1613,7 +1613,7 @@ case 'AtRule': case 'Rule': if (data.typ == 'AtRule' && !('chi' in data)) { - return `${indent}@${data.nam} ${data.val};`; + return `${indent}@${data.nam}${data.val === '' ? '' : options.indent || ' '}${data.val};`; } // @ts-ignore let children = data.chi.reduce((css, node) => { @@ -1625,7 +1625,7 @@ str = `${node.nam}:${options.indent}${node.val.reduce(reducer, '').trimEnd()};`; } else if (node.typ == 'AtRule' && !('chi' in node)) { - str = `@${node.nam} ${node.val};`; + str = `${data.val === '' ? '' : options.indent || ' '}${data.val};`; } else { str = doRender(node, options, reducer, level + 1, indents); @@ -1642,7 +1642,7 @@ children = children.slice(0, -1); } if (data.typ == 'AtRule') { - return `@${data.nam}${data.val ? ' ' + data.val + options.indent : ''}{${options.newLine}` + (children === '' ? '' : indentSub + children + options.newLine) + indent + `}`; + return `@${data.nam}${data.val === '' ? '' : options.indent || ' '}${data.val}${options.indent}{${options.newLine}` + (children === '' ? '' : indentSub + children + options.newLine) + indent + `}`; } return data.sel + `${options.indent}{${options.newLine}` + (children === '' ? '' : indentSub + children + options.newLine) + indent + `}`; } @@ -1792,7 +1792,9 @@ case 'Delim': return /* options.minify && 'Pseudo-class' == token.typ && '::' == token.val.slice(0, 2) ? token.val.slice(1) : */ token.val; } - throw new Error(`unexpected token ${JSON.stringify(token, null, 1)}`); + console.error(`unexpected token ${JSON.stringify(token, null, 1)}`); + // throw new Error(`unexpected token ${JSON.stringify(token, null, 1)}`); + return ''; } function eq(a, b) { @@ -2581,7 +2583,7 @@ // @ts-ignore return null; } - return { result, node1: exchanged ? node2 : node1, node2: exchanged ? node2 : node2 }; + return { result, node1: exchanged ? node2 : node1, node2: exchanged ? node1 : node2 }; } function matchSelectors(selector1, selector2, parentType) { let match = [[]]; @@ -2769,11 +2771,27 @@ if (node.typ == 'AtRule' && node.nam == 'font-face') { continue; } - if (node.typ == 'AtRule' && node.val == 'all') { + if (node.typ == 'AtRule') { + if (node.nam == 'media' && node.val == 'all') { + // @ts-ignore + ast.chi?.splice(i, 1, ...node.chi); + i--; + continue; + } + // console.debug({previous, node}); // @ts-ignore - ast.chi?.splice(i, 1, ...node.chi); - i--; - continue; + if (previous?.typ == 'AtRule' && + previous.nam == node.nam && + previous.val == node.val) { + if ('chi' in node) { + // @ts-ignore + previous.chi.push(...node.chi); + } + // else { + ast?.chi?.splice(i--, 1); + continue; + // } + } } // @ts-ignore if (node.typ == 'Rule') { @@ -3288,10 +3306,11 @@ } buffer += quoteStr; while (value = peek()) { - if (ind >= iterator.length) { - yield pushToken(buffer, hasNewLine ? 'Bad-string' : 'Unclosed-string'); - break; - } + // if (ind >= iterator.length) { + // + // yield pushToken(buffer, hasNewLine ? 'Bad-string' : 'Unclosed-string'); + // break; + // } if (value == '\\') { const sequence = peek(6); let escapeSequence = ''; @@ -3314,7 +3333,7 @@ // not hex or new line // @ts-ignore if (i == 1 && !isNewLine(codepoint)) { - buffer += sequence[i]; + buffer += value + sequence[i]; next(2); continue; } @@ -3334,11 +3353,12 @@ continue; } // buffer += value; - if (ind >= iterator.length) { - // drop '\\' at the end - yield pushToken(buffer); - break; - } + // if (ind >= iterator.length) { + // + // // drop '\\' at the end + // yield pushToken(buffer); + // break; + // } buffer += next(2); continue; } @@ -3507,7 +3527,7 @@ buffer = ''; break; } - buffer += value; + buffer += prev() + value; break; case '"': case "'": @@ -3709,6 +3729,7 @@ * @param opt */ async function parse$1(iterator, opt = {}) { + const startTime = performance.now(); const errors = []; const options = { src: '', @@ -3847,7 +3868,7 @@ src: options.resolve(url, options.src).absolute })); }); - bytesIn += root.bytesIn; + bytesIn += root.stats.bytesIn; if (root.ast.chi.length > 0) { context.chi.push(...root.ast.chi); } @@ -3893,13 +3914,6 @@ // rule if (delim.typ == 'Block-start') { const position = map.get(tokens[0]); - // if (context.typ == 'Rule') { - // - // if (tokens[0]?.typ == 'Iden') { - // errors.push({action: 'drop', message: 'invalid nesting rule', location: {src, ...position}}); - // return null; - // } - // } const uniq = new Map; parseTokens(tokens, { minify: options.minify }).reduce((acc, curr, index, array) => { if (curr.typ == 'Whitespace') { @@ -4075,12 +4089,21 @@ if (tokens.length > 0) { await parseNode(tokens); } + const endParseTime = performance.now(); if (options.minify) { if (ast.chi.length > 0) { minify(ast, options, true); } } - return { ast, errors, bytesIn }; + const endTime = performance.now(); + return { + ast, errors, stats: { + bytesIn, + parse: `${(endParseTime - startTime).toFixed(2)}ms`, + minify: `${(endTime - endParseTime).toFixed(2)}ms`, + total: `${(endTime - startTime).toFixed(2)}ms` + } + }; } function parseString(src, options = { location: false }) { return [...tokenize(src)].map(t => { @@ -4431,19 +4454,17 @@ async function transform$1(css, options = {}) { options = { minify: true, removeEmpty: true, ...options }; const startTime = performance.now(); - const parseResult = await parse$1(css, options); - const renderTime = performance.now(); - const rendered = render(parseResult.ast, options); - const endTime = performance.now(); - return { - ...parseResult, ...rendered, stats: { - bytesIn: parseResult.bytesIn, - bytesOut: rendered.code.length, - parse: `${(renderTime - startTime).toFixed(2)}ms`, - render: `${(endTime - renderTime).toFixed(2)}ms`, - total: `${(endTime - startTime).toFixed(2)}ms` - } - }; + return parse$1(css, options).then((parseResult) => { + const rendered = render(parseResult.ast, options); + return { + ...parseResult, ...rendered, stats: { + bytesOut: rendered.code.length, + ...parseResult.stats, + render: rendered.stats.total, + total: `${(performance.now() - startTime).toFixed(2)}ms` + } + }; + }); } const matchUrl = /^(https?:)?\/\//; diff --git a/dist/index.cjs b/dist/index.cjs index 8827622..a773a82 100644 --- a/dist/index.cjs +++ b/dist/index.cjs @@ -91,10 +91,7 @@ function isHash(name) { if (name.charAt(0) != '#') { return false; } - if (isIdent(name.charAt(1))) { - return true; - } - return true; + return isIdent(name.charAt(1)); } function isNumber(name) { if (name.length == 0) { @@ -1564,6 +1561,7 @@ function hsl2rgb(h, s, l, a = null) { } function render(data, opt = {}) { + const startTime = performance.now(); const options = Object.assign(opt.minify ?? true ? { indent: '', newLine: '', @@ -1582,7 +1580,9 @@ function render(data, opt = {}) { } return acc + renderToken(curr, options); } - return { code: doRender(data, options, reducer, 0) }; + return { code: doRender(data, options, reducer, 0), stats: { + total: `${(performance.now() - startTime).toFixed(2)}ms` + } }; } // @ts-ignore function doRender(data, options, reducer, level = 0, indents = []) { @@ -1611,7 +1611,7 @@ function doRender(data, options, reducer, level = 0, indents = []) { case 'AtRule': case 'Rule': if (data.typ == 'AtRule' && !('chi' in data)) { - return `${indent}@${data.nam} ${data.val};`; + return `${indent}@${data.nam}${data.val === '' ? '' : options.indent || ' '}${data.val};`; } // @ts-ignore let children = data.chi.reduce((css, node) => { @@ -1623,7 +1623,7 @@ function doRender(data, options, reducer, level = 0, indents = []) { str = `${node.nam}:${options.indent}${node.val.reduce(reducer, '').trimEnd()};`; } else if (node.typ == 'AtRule' && !('chi' in node)) { - str = `@${node.nam} ${node.val};`; + str = `${data.val === '' ? '' : options.indent || ' '}${data.val};`; } else { str = doRender(node, options, reducer, level + 1, indents); @@ -1640,7 +1640,7 @@ function doRender(data, options, reducer, level = 0, indents = []) { children = children.slice(0, -1); } if (data.typ == 'AtRule') { - return `@${data.nam}${data.val ? ' ' + data.val + options.indent : ''}{${options.newLine}` + (children === '' ? '' : indentSub + children + options.newLine) + indent + `}`; + return `@${data.nam}${data.val === '' ? '' : options.indent || ' '}${data.val}${options.indent}{${options.newLine}` + (children === '' ? '' : indentSub + children + options.newLine) + indent + `}`; } return data.sel + `${options.indent}{${options.newLine}` + (children === '' ? '' : indentSub + children + options.newLine) + indent + `}`; } @@ -1790,7 +1790,9 @@ function renderToken(token, options = {}) { case 'Delim': return /* options.minify && 'Pseudo-class' == token.typ && '::' == token.val.slice(0, 2) ? token.val.slice(1) : */ token.val; } - throw new Error(`unexpected token ${JSON.stringify(token, null, 1)}`); + console.error(`unexpected token ${JSON.stringify(token, null, 1)}`); + // throw new Error(`unexpected token ${JSON.stringify(token, null, 1)}`); + return ''; } function eq(a, b) { @@ -2579,7 +2581,7 @@ function minify(ast, options = {}, recursive = false) { // @ts-ignore return null; } - return { result, node1: exchanged ? node2 : node1, node2: exchanged ? node2 : node2 }; + return { result, node1: exchanged ? node2 : node1, node2: exchanged ? node1 : node2 }; } function matchSelectors(selector1, selector2, parentType) { let match = [[]]; @@ -2767,11 +2769,27 @@ function minify(ast, options = {}, recursive = false) { if (node.typ == 'AtRule' && node.nam == 'font-face') { continue; } - if (node.typ == 'AtRule' && node.val == 'all') { + if (node.typ == 'AtRule') { + if (node.nam == 'media' && node.val == 'all') { + // @ts-ignore + ast.chi?.splice(i, 1, ...node.chi); + i--; + continue; + } + // console.debug({previous, node}); // @ts-ignore - ast.chi?.splice(i, 1, ...node.chi); - i--; - continue; + if (previous?.typ == 'AtRule' && + previous.nam == node.nam && + previous.val == node.val) { + if ('chi' in node) { + // @ts-ignore + previous.chi.push(...node.chi); + } + // else { + ast?.chi?.splice(i--, 1); + continue; + // } + } } // @ts-ignore if (node.typ == 'Rule') { @@ -3286,10 +3304,11 @@ function* tokenize(iterator) { } buffer += quoteStr; while (value = peek()) { - if (ind >= iterator.length) { - yield pushToken(buffer, hasNewLine ? 'Bad-string' : 'Unclosed-string'); - break; - } + // if (ind >= iterator.length) { + // + // yield pushToken(buffer, hasNewLine ? 'Bad-string' : 'Unclosed-string'); + // break; + // } if (value == '\\') { const sequence = peek(6); let escapeSequence = ''; @@ -3312,7 +3331,7 @@ function* tokenize(iterator) { // not hex or new line // @ts-ignore if (i == 1 && !isNewLine(codepoint)) { - buffer += sequence[i]; + buffer += value + sequence[i]; next(2); continue; } @@ -3332,11 +3351,12 @@ function* tokenize(iterator) { continue; } // buffer += value; - if (ind >= iterator.length) { - // drop '\\' at the end - yield pushToken(buffer); - break; - } + // if (ind >= iterator.length) { + // + // // drop '\\' at the end + // yield pushToken(buffer); + // break; + // } buffer += next(2); continue; } @@ -3505,7 +3525,7 @@ function* tokenize(iterator) { buffer = ''; break; } - buffer += value; + buffer += prev() + value; break; case '"': case "'": @@ -3707,6 +3727,7 @@ const funcLike = ['Start-parens', 'Func', 'UrlFunc', 'Pseudo-class-func']; * @param opt */ async function parse$1(iterator, opt = {}) { + const startTime = performance.now(); const errors = []; const options = { src: '', @@ -3845,7 +3866,7 @@ async function parse$1(iterator, opt = {}) { src: options.resolve(url, options.src).absolute })); }); - bytesIn += root.bytesIn; + bytesIn += root.stats.bytesIn; if (root.ast.chi.length > 0) { context.chi.push(...root.ast.chi); } @@ -3891,13 +3912,6 @@ async function parse$1(iterator, opt = {}) { // rule if (delim.typ == 'Block-start') { const position = map.get(tokens[0]); - // if (context.typ == 'Rule') { - // - // if (tokens[0]?.typ == 'Iden') { - // errors.push({action: 'drop', message: 'invalid nesting rule', location: {src, ...position}}); - // return null; - // } - // } const uniq = new Map; parseTokens(tokens, { minify: options.minify }).reduce((acc, curr, index, array) => { if (curr.typ == 'Whitespace') { @@ -4073,12 +4087,21 @@ async function parse$1(iterator, opt = {}) { if (tokens.length > 0) { await parseNode(tokens); } + const endParseTime = performance.now(); if (options.minify) { if (ast.chi.length > 0) { minify(ast, options, true); } } - return { ast, errors, bytesIn }; + const endTime = performance.now(); + return { + ast, errors, stats: { + bytesIn, + parse: `${(endParseTime - startTime).toFixed(2)}ms`, + minify: `${(endTime - endParseTime).toFixed(2)}ms`, + total: `${(endTime - startTime).toFixed(2)}ms` + } + }; } function parseString(src, options = { location: false }) { return [...tokenize(src)].map(t => { @@ -4429,19 +4452,17 @@ function parseTokens(tokens, options = {}) { async function transform$1(css, options = {}) { options = { minify: true, removeEmpty: true, ...options }; const startTime = performance.now(); - const parseResult = await parse$1(css, options); - const renderTime = performance.now(); - const rendered = render(parseResult.ast, options); - const endTime = performance.now(); - return { - ...parseResult, ...rendered, stats: { - bytesIn: parseResult.bytesIn, - bytesOut: rendered.code.length, - parse: `${(renderTime - startTime).toFixed(2)}ms`, - render: `${(endTime - renderTime).toFixed(2)}ms`, - total: `${(endTime - startTime).toFixed(2)}ms` - } - }; + return parse$1(css, options).then((parseResult) => { + const rendered = render(parseResult.ast, options); + return { + ...parseResult, ...rendered, stats: { + bytesOut: rendered.code.length, + ...parseResult.stats, + render: rendered.stats.total, + total: `${(performance.now() - startTime).toFixed(2)}ms` + } + }; + }); } const matchUrl = /^(https?:)?\/\//; diff --git a/dist/index.d.ts b/dist/index.d.ts index 749146e..08d3d3d 100644 --- a/dist/index.d.ts +++ b/dist/index.d.ts @@ -487,11 +487,19 @@ interface TransformOptions extends ParserOptions, RenderOptions { interface ParseResult { ast: AstRuleStyleSheet; errors: ErrorDescription[]; - bytesIn: number; + stats: { + bytesIn: number; + parse: string; + minify: string; + total: string; + } } interface RenderResult { code: string ; + stats: { + total: string; + } } interface TransformResult extends ParseResult, RenderResult { @@ -500,6 +508,7 @@ interface TransformResult extends ParseResult, RenderResult { bytesIn: number; bytesOut: number; parse: string; + minify: string; render: string; total: string; } diff --git a/dist/lib/ast/minify.js b/dist/lib/ast/minify.js index 746c7ee..36d2bc3 100644 --- a/dist/lib/ast/minify.js +++ b/dist/lib/ast/minify.js @@ -135,7 +135,7 @@ function minify(ast, options = {}, recursive = false) { // @ts-ignore return null; } - return { result, node1: exchanged ? node2 : node1, node2: exchanged ? node2 : node2 }; + return { result, node1: exchanged ? node2 : node1, node2: exchanged ? node1 : node2 }; } function matchSelectors(selector1, selector2, parentType) { let match = [[]]; @@ -323,11 +323,27 @@ function minify(ast, options = {}, recursive = false) { if (node.typ == 'AtRule' && node.nam == 'font-face') { continue; } - if (node.typ == 'AtRule' && node.val == 'all') { + if (node.typ == 'AtRule') { + if (node.nam == 'media' && node.val == 'all') { + // @ts-ignore + ast.chi?.splice(i, 1, ...node.chi); + i--; + continue; + } + // console.debug({previous, node}); // @ts-ignore - ast.chi?.splice(i, 1, ...node.chi); - i--; - continue; + if (previous?.typ == 'AtRule' && + previous.nam == node.nam && + previous.val == node.val) { + if ('chi' in node) { + // @ts-ignore + previous.chi.push(...node.chi); + } + // else { + ast?.chi?.splice(i--, 1); + continue; + // } + } } // @ts-ignore if (node.typ == 'Rule') { diff --git a/dist/lib/parser/parse.js b/dist/lib/parser/parse.js index b786b1f..bb2ecf9 100644 --- a/dist/lib/parser/parse.js +++ b/dist/lib/parser/parse.js @@ -12,6 +12,7 @@ const funcLike = ['Start-parens', 'Func', 'UrlFunc', 'Pseudo-class-func']; * @param opt */ async function parse(iterator, opt = {}) { + const startTime = performance.now(); const errors = []; const options = { src: '', @@ -150,7 +151,7 @@ async function parse(iterator, opt = {}) { src: options.resolve(url, options.src).absolute })); }); - bytesIn += root.bytesIn; + bytesIn += root.stats.bytesIn; if (root.ast.chi.length > 0) { context.chi.push(...root.ast.chi); } @@ -196,13 +197,6 @@ async function parse(iterator, opt = {}) { // rule if (delim.typ == 'Block-start') { const position = map.get(tokens[0]); - // if (context.typ == 'Rule') { - // - // if (tokens[0]?.typ == 'Iden') { - // errors.push({action: 'drop', message: 'invalid nesting rule', location: {src, ...position}}); - // return null; - // } - // } const uniq = new Map; parseTokens(tokens, { minify: options.minify }).reduce((acc, curr, index, array) => { if (curr.typ == 'Whitespace') { @@ -378,12 +372,21 @@ async function parse(iterator, opt = {}) { if (tokens.length > 0) { await parseNode(tokens); } + const endParseTime = performance.now(); if (options.minify) { if (ast.chi.length > 0) { minify(ast, options, true); } } - return { ast, errors, bytesIn }; + const endTime = performance.now(); + return { + ast, errors, stats: { + bytesIn, + parse: `${(endParseTime - startTime).toFixed(2)}ms`, + minify: `${(endTime - endParseTime).toFixed(2)}ms`, + total: `${(endTime - startTime).toFixed(2)}ms` + } + }; } function parseString(src, options = { location: false }) { return [...tokenize(src)].map(t => { diff --git a/dist/lib/parser/tokenize.js b/dist/lib/parser/tokenize.js index 5d832e8..69d0795 100644 --- a/dist/lib/parser/tokenize.js +++ b/dist/lib/parser/tokenize.js @@ -36,10 +36,11 @@ function* tokenize(iterator) { } buffer += quoteStr; while (value = peek()) { - if (ind >= iterator.length) { - yield pushToken(buffer, hasNewLine ? 'Bad-string' : 'Unclosed-string'); - break; - } + // if (ind >= iterator.length) { + // + // yield pushToken(buffer, hasNewLine ? 'Bad-string' : 'Unclosed-string'); + // break; + // } if (value == '\\') { const sequence = peek(6); let escapeSequence = ''; @@ -62,7 +63,7 @@ function* tokenize(iterator) { // not hex or new line // @ts-ignore if (i == 1 && !isNewLine(codepoint)) { - buffer += sequence[i]; + buffer += value + sequence[i]; next(2); continue; } @@ -82,11 +83,12 @@ function* tokenize(iterator) { continue; } // buffer += value; - if (ind >= iterator.length) { - // drop '\\' at the end - yield pushToken(buffer); - break; - } + // if (ind >= iterator.length) { + // + // // drop '\\' at the end + // yield pushToken(buffer); + // break; + // } buffer += next(2); continue; } @@ -255,7 +257,7 @@ function* tokenize(iterator) { buffer = ''; break; } - buffer += value; + buffer += prev() + value; break; case '"': case "'": diff --git a/dist/lib/parser/utils/syntax.js b/dist/lib/parser/utils/syntax.js index 585b708..0636048 100644 --- a/dist/lib/parser/utils/syntax.js +++ b/dist/lib/parser/utils/syntax.js @@ -87,10 +87,7 @@ function isHash(name) { if (name.charAt(0) != '#') { return false; } - if (isIdent(name.charAt(1))) { - return true; - } - return true; + return isIdent(name.charAt(1)); } function isNumber(name) { if (name.length == 0) { diff --git a/dist/lib/renderer/render.js b/dist/lib/renderer/render.js index 886116f..f486846 100644 --- a/dist/lib/renderer/render.js +++ b/dist/lib/renderer/render.js @@ -1,6 +1,7 @@ import { COLORS_NAMES, rgb2Hex, hsl2Hex, hwb2hex, cmyk2hex, NAMES_COLORS } from './utils/color.js'; function render(data, opt = {}) { + const startTime = performance.now(); const options = Object.assign(opt.minify ?? true ? { indent: '', newLine: '', @@ -19,7 +20,9 @@ function render(data, opt = {}) { } return acc + renderToken(curr, options); } - return { code: doRender(data, options, reducer, 0) }; + return { code: doRender(data, options, reducer, 0), stats: { + total: `${(performance.now() - startTime).toFixed(2)}ms` + } }; } // @ts-ignore function doRender(data, options, reducer, level = 0, indents = []) { @@ -48,7 +51,7 @@ function doRender(data, options, reducer, level = 0, indents = []) { case 'AtRule': case 'Rule': if (data.typ == 'AtRule' && !('chi' in data)) { - return `${indent}@${data.nam} ${data.val};`; + return `${indent}@${data.nam}${data.val === '' ? '' : options.indent || ' '}${data.val};`; } // @ts-ignore let children = data.chi.reduce((css, node) => { @@ -60,7 +63,7 @@ function doRender(data, options, reducer, level = 0, indents = []) { str = `${node.nam}:${options.indent}${node.val.reduce(reducer, '').trimEnd()};`; } else if (node.typ == 'AtRule' && !('chi' in node)) { - str = `@${node.nam} ${node.val};`; + str = `${data.val === '' ? '' : options.indent || ' '}${data.val};`; } else { str = doRender(node, options, reducer, level + 1, indents); @@ -77,7 +80,7 @@ function doRender(data, options, reducer, level = 0, indents = []) { children = children.slice(0, -1); } if (data.typ == 'AtRule') { - return `@${data.nam}${data.val ? ' ' + data.val + options.indent : ''}{${options.newLine}` + (children === '' ? '' : indentSub + children + options.newLine) + indent + `}`; + return `@${data.nam}${data.val === '' ? '' : options.indent || ' '}${data.val}${options.indent}{${options.newLine}` + (children === '' ? '' : indentSub + children + options.newLine) + indent + `}`; } return data.sel + `${options.indent}{${options.newLine}` + (children === '' ? '' : indentSub + children + options.newLine) + indent + `}`; } @@ -227,7 +230,9 @@ function renderToken(token, options = {}) { case 'Delim': return /* options.minify && 'Pseudo-class' == token.typ && '::' == token.val.slice(0, 2) ? token.val.slice(1) : */ token.val; } - throw new Error(`unexpected token ${JSON.stringify(token, null, 1)}`); + console.error(`unexpected token ${JSON.stringify(token, null, 1)}`); + // throw new Error(`unexpected token ${JSON.stringify(token, null, 1)}`); + return ''; } export { render, renderToken }; diff --git a/dist/lib/transform.js b/dist/lib/transform.js index 31ea354..3b2815f 100644 --- a/dist/lib/transform.js +++ b/dist/lib/transform.js @@ -4,19 +4,17 @@ import { render } from './renderer/render.js'; async function transform(css, options = {}) { options = { minify: true, removeEmpty: true, ...options }; const startTime = performance.now(); - const parseResult = await parse(css, options); - const renderTime = performance.now(); - const rendered = render(parseResult.ast, options); - const endTime = performance.now(); - return { - ...parseResult, ...rendered, stats: { - bytesIn: parseResult.bytesIn, - bytesOut: rendered.code.length, - parse: `${(renderTime - startTime).toFixed(2)}ms`, - render: `${(endTime - renderTime).toFixed(2)}ms`, - total: `${(endTime - startTime).toFixed(2)}ms` - } - }; + return parse(css, options).then((parseResult) => { + const rendered = render(parseResult.ast, options); + return { + ...parseResult, ...rendered, stats: { + bytesOut: rendered.code.length, + ...parseResult.stats, + render: rendered.stats.total, + total: `${(performance.now() - startTime).toFixed(2)}ms` + } + }; + }); } export { transform }; diff --git a/package.json b/package.json index 666cfae..67350f2 100644 --- a/package.json +++ b/package.json @@ -1,9 +1,10 @@ { "name": "@tbela99/css-parser", "description": "CSS parser for node and the browser", - "version": "0.0.1-rc1", + "version": "0.0.1-rc2", "exports": { ".": "./dist/index.js", + "./umd": "./dist/index-umd-web.js", "./web": "./dist/web/index.js", "./cjs": "./dist/index.cjs" }, diff --git a/src/@types/index.d.ts b/src/@types/index.d.ts index 48377c2..08a0708 100644 --- a/src/@types/index.d.ts +++ b/src/@types/index.d.ts @@ -58,11 +58,19 @@ export interface TransformOptions extends ParserOptions, RenderOptions { export interface ParseResult { ast: AstRuleStyleSheet; errors: ErrorDescription[]; - bytesIn: number; + stats: { + bytesIn: number; + parse: string; + minify: string; + total: string; + } } export interface RenderResult { code: string ; + stats: { + total: string; + } } export interface TransformResult extends ParseResult, RenderResult { @@ -71,6 +79,7 @@ export interface TransformResult extends ParseResult, RenderResult { bytesIn: number; bytesOut: number; parse: string; + minify: string; render: string; total: string; } diff --git a/src/lib/ast/minify.ts b/src/lib/ast/minify.ts index 1a33e5c..638c9fc 100644 --- a/src/lib/ast/minify.ts +++ b/src/lib/ast/minify.ts @@ -90,15 +90,11 @@ export function minify(ast: AstNode, options: ParserOptions = {}, recursive: boo if (curr[1] == ' ' && !isIdent(curr[2]) && !isFunction(curr[2])) { curr.splice(0, 2); - } - - else if (combinators.includes(curr[1])) { + } else if (combinators.includes(curr[1])) { curr.splice(0, 1); } - } - - else if (ast.typ == 'Rule' && (isIdent(curr[0]) || isFunction(curr[0]))) { + } else if (ast.typ == 'Rule' && (isIdent(curr[0]) || isFunction(curr[0]))) { curr.unshift('&', ' '); } @@ -186,7 +182,8 @@ export function minify(ast: AstNode, options: ParserOptions = {}, recursive: boo // @ts-ignore return null; } - return {result, node1: exchanged ? node2 : node1, node2: exchanged ? node2 : node2}; + + return {result, node1: exchanged ? node2 : node1, node2: exchanged ? node1 : node2}; } function matchSelectors(selector1: string[][], selector2: string[][], parentType: NodeType): null | MatchedSelector { @@ -454,12 +451,37 @@ export function minify(ast: AstNode, options: ParserOptions = {}, recursive: boo if (node.typ == 'AtRule' && (node).nam == 'font-face') { continue; } - if (node.typ == 'AtRule' && (node).val == 'all') { + if (node.typ == 'AtRule') { + + if ((node).nam == 'media' && (node).val == 'all') { + + // @ts-ignore + ast.chi?.splice(i, 1, ...node.chi); + i--; + continue; + } + + // console.debug({previous, node}); + // @ts-ignore - ast.chi?.splice(i, 1, ...node.chi); - i--; - continue; + if (previous?.typ == 'AtRule' && + (previous).nam == (node).nam && + (previous).val == (node).val) { + + if ('chi' in node) { + + // @ts-ignore + previous.chi.push(...node.chi); + } + + // else { + + ast?.chi?.splice(i--, 1); + continue; + // } + } } + // @ts-ignore if (node.typ == 'Rule') { @@ -568,23 +590,17 @@ export function minify(ast: AstNode, options: ParserOptions = {}, recursive: boo if (curr[1] == ' ') { curr.splice(0, 2); - } - - else { + } else { if (ast.typ != 'Rule' && combinators.includes(curr[1])) { wrap = false; - } - - else { + } else { curr.splice(0, 1); } } - } - - else if (combinators.includes(curr[0])) { + } else if (combinators.includes(curr[0])) { curr.unshift('&'); wrap = false; @@ -814,6 +830,7 @@ export function reduceSelector(selector: string[][]) { reducible: selector.every((selector) => !['>', '+', '~', '&'].includes(selector[0])) }; } + function hasOnlyDeclarations(node: AstRule): boolean { let k: number = node.chi.length; diff --git a/src/lib/parser/parse.ts b/src/lib/parser/parse.ts index f168d50..8d88842 100644 --- a/src/lib/parser/parse.ts +++ b/src/lib/parser/parse.ts @@ -44,6 +44,8 @@ const funcLike = ['Start-parens', 'Func', 'UrlFunc', 'Pseudo-class-func']; * @param opt */ export async function parse(iterator: string, opt: ParserOptions = {}): Promise { + + const startTime: number = performance.now(); const errors: ErrorDescription[] = []; const options: ParserOptions = { src: '', @@ -215,7 +217,7 @@ export async function parse(iterator: string, opt: ParserOptions = {}): Promise< })) }); - bytesIn += root.bytesIn; + bytesIn += root.stats.bytesIn; if (root.ast.chi.length > 0) { context.chi.push(...root.ast.chi); @@ -274,14 +276,6 @@ export async function parse(iterator: string, opt: ParserOptions = {}): Promise< const position: Position = map.get(tokens[0]); - // if (context.typ == 'Rule') { - // - // if (tokens[0]?.typ == 'Iden') { - // errors.push({action: 'drop', message: 'invalid nesting rule', location: {src, ...position}}); - // return null; - // } - // } - const uniq = new Map; parseTokens(tokens, {minify: options.minify}).reduce((acc: string[][], curr: Token, index: number, array: Token[]) => { @@ -506,6 +500,8 @@ export async function parse(iterator: string, opt: ParserOptions = {}): Promise< } + const endParseTime: number = performance.now(); + if (options.minify) { if (ast.chi.length > 0) { @@ -514,7 +510,16 @@ export async function parse(iterator: string, opt: ParserOptions = {}): Promise< } } - return {ast, errors, bytesIn}; + const endTime: number = performance.now(); + + return { + ast, errors, stats: { + bytesIn, + parse: `${(endParseTime - startTime).toFixed(2)}ms`, + minify: `${(endTime - endParseTime).toFixed(2)}ms`, + total: `${(endTime - startTime).toFixed(2)}ms` + } + }; } export function parseString(src: string, options = {location: false}): Token[] { @@ -679,7 +684,7 @@ function getTokenType(val: string, hint?: string): Token { return { typ: 'Color', val, - kin : 'hex' + kin: 'hex' }; } diff --git a/src/lib/parser/tokenize.ts b/src/lib/parser/tokenize.ts index ffced9a..7bfd0d6 100644 --- a/src/lib/parser/tokenize.ts +++ b/src/lib/parser/tokenize.ts @@ -60,11 +60,11 @@ export function* tokenize(iterator: string): Generator { while (value = peek()) { - if (ind >= iterator.length) { - - yield pushToken(buffer, hasNewLine ? 'Bad-string' : 'Unclosed-string'); - break; - } + // if (ind >= iterator.length) { + // + // yield pushToken(buffer, hasNewLine ? 'Bad-string' : 'Unclosed-string'); + // break; + // } if (value == '\\') { @@ -98,7 +98,7 @@ export function* tokenize(iterator: string): Generator { // @ts-ignore if (i == 1 && !isNewLine(codepoint)) { - buffer += sequence[i]; + buffer += value + sequence[i]; next(2); continue; } @@ -123,12 +123,12 @@ export function* tokenize(iterator: string): Generator { } // buffer += value; - if (ind >= iterator.length) { - - // drop '\\' at the end - yield pushToken(buffer); - break; - } + // if (ind >= iterator.length) { + // + // // drop '\\' at the end + // yield pushToken(buffer); + // break; + // } buffer += next(2); continue; @@ -340,7 +340,9 @@ export function* tokenize(iterator: string): Generator { break; case '\\': + value = next(); + // EOF if (ind + 1 >= iterator.length) { // end of stream ignore \\ @@ -348,7 +350,9 @@ export function* tokenize(iterator: string): Generator { buffer = ''; break; } - buffer += value; + + buffer += prev() + value; + break; case '"': case "'": diff --git a/src/lib/parser/utils/syntax.ts b/src/lib/parser/utils/syntax.ts index 38442ba..5ba969e 100644 --- a/src/lib/parser/utils/syntax.ts +++ b/src/lib/parser/utils/syntax.ts @@ -139,12 +139,7 @@ export function isHash(name: string): boolean { return false; } - if (isIdent(name.charAt(1))) { - - return true; - } - - return true; + return isIdent(name.charAt(1)); } export function isNumber(name: string): boolean { diff --git a/src/lib/renderer/render.ts b/src/lib/renderer/render.ts index 08a4702..f214e91 100644 --- a/src/lib/renderer/render.ts +++ b/src/lib/renderer/render.ts @@ -12,6 +12,8 @@ import {cmyk2hex, COLORS_NAMES, hsl2Hex, hwb2hex, NAMES_COLORS, rgb2Hex} from ". export function render(data: AstNode, opt: RenderOptions = {}): RenderResult { + const startTime: number = performance.now(); + const options = Object.assign(opt.minify ?? true ? { indent: '', newLine: '', @@ -37,7 +39,10 @@ export function render(data: AstNode, opt: RenderOptions = {}): RenderResult { return acc + renderToken(curr, options); } - return {code: doRender(data, options, reducer, 0)}; + return {code: doRender(data, options, reducer, 0), stats: { + + total: `${(performance.now() - startTime).toFixed(2)}ms` + }}; } // @ts-ignore @@ -87,7 +92,7 @@ function doRender(data: AstNode, options: RenderOptions, reducer: Function, leve if (data.typ == 'AtRule' && !('chi' in data)) { - return `${indent}@${(data).nam} ${(data).val};`; + return `${indent}@${(data).nam}${(data).val === '' ? '' : options.indent || ' '}${(data).val};`; } // @ts-ignore @@ -103,7 +108,7 @@ function doRender(data: AstNode, options: RenderOptions, reducer: Function, leve str = `${(node).nam}:${options.indent}${(node).val.reduce(<() => string>reducer, '').trimEnd()};`; } else if (node.typ == 'AtRule' && !('chi' in node)) { - str = `@${(node).nam} ${(node).val};`; + str = `${(data).val === '' ? '' : options.indent || ' '}${(data).val};`; } else { str = doRender(node, options, reducer, level + 1, indents); @@ -129,7 +134,7 @@ function doRender(data: AstNode, options: RenderOptions, reducer: Function, leve if (data.typ == 'AtRule') { - return `@${(data).nam}${(data).val ? ' ' + (data).val + options.indent : ''}{${options.newLine}` + (children === '' ? '' : indentSub + children + options.newLine) + indent + `}` + return `@${(data).nam}${(data).val === '' ? '' : options.indent || ' '}${(data).val}${options.indent}{${options.newLine}` + (children === '' ? '' : indentSub + children + options.newLine) + indent + `}` } return (data).sel + `${options.indent}{${options.newLine}` + (children === '' ? '' : indentSub + children + options.newLine) + indent + `}` @@ -358,5 +363,8 @@ export function renderToken(token: Token, options: RenderOptions = {}): string { return /* options.minify && 'Pseudo-class' == token.typ && '::' == token.val.slice(0, 2) ? token.val.slice(1) : */token.val; } - throw new Error(`unexpected token ${JSON.stringify(token, null, 1)}`); + console.error(`unexpected token ${JSON.stringify(token, null, 1)}`); + // throw new Error(`unexpected token ${JSON.stringify(token, null, 1)}`); + + return ''; } \ No newline at end of file diff --git a/src/lib/transform.ts b/src/lib/transform.ts index 5389ac9..13a8065 100644 --- a/src/lib/transform.ts +++ b/src/lib/transform.ts @@ -7,19 +7,18 @@ export async function transform(css: string, options: TransformOptions = {}): Pr options = {minify: true, removeEmpty: true, ...options}; const startTime: number = performance.now(); - const parseResult: ParseResult = await parse(css, options); - const renderTime: number = performance.now(); - const rendered: RenderResult = render(parseResult.ast, options); - const endTime: number = performance.now(); + return parse(css, options).then((parseResult: ParseResult) => { - return { - ...parseResult, ...rendered, stats: { - bytesIn: parseResult.bytesIn, - bytesOut: rendered.code.length, - parse: `${(renderTime - startTime).toFixed(2)}ms`, - render: `${(endTime - renderTime).toFixed(2)}ms`, - total: `${(endTime - startTime).toFixed(2)}ms` + const rendered: RenderResult = render(parseResult.ast, options); + + return { + ...parseResult, ...rendered, stats: { + bytesOut: rendered.code.length, + ...parseResult.stats, + render: rendered.stats.total, + total: `${(performance.now() - startTime).toFixed(2)}ms` + } } - } + }); } diff --git a/test/files/result/font-awesome-all.css b/test/files/result/font-awesome-all.css index ac58751..36d101a 100644 --- a/test/files/result/font-awesome-all.css +++ b/test/files/result/font-awesome-all.css @@ -4537,7 +4537,7 @@ readers do not read off random characters that represent icons */ position: static; width: auto } -@font-face{ +@font-face { font-family: 'Font Awesome 5 Brands'; font-style: normal; font-weight: 400; @@ -4549,7 +4549,7 @@ readers do not read off random characters that represent icons */ font-family: 'Font Awesome 5 Brands'; font-weight: 400 } -@font-face{ +@font-face { font-family: 'Font Awesome 5 Free'; font-style: normal; font-weight: 400; @@ -4561,7 +4561,7 @@ readers do not read off random characters that represent icons */ font-family: 'Font Awesome 5 Free'; font-weight: 400 } -@font-face{ +@font-face { font-family: 'Font Awesome 5 Free'; font-style: normal; font-weight: 900; diff --git a/test/specs/block.spec.js b/test/specs/block.spec.js index 3adc06e..bdfe44f 100644 --- a/test/specs/block.spec.js +++ b/test/specs/block.spec.js @@ -4,6 +4,7 @@ import { readFile } from 'fs/promises'; import { transform } from '../../dist/node/index.js'; import { dirname } from 'path'; import { readFileSync } from 'fs'; +import {render} from "../../src/index.js"; const dir = dirname(new URL(import.meta.url).pathname) + '/../files'; describe('parse block', function () { @@ -198,4 +199,64 @@ abbr[title], abbr[data-original-title], abbr>[data-original-title] { minify: true }).then(result => f(result.code).equals(`abbr:is([title],[data-original-title],abbr>[data-original-title]){text-decoration:underline dotted;-webkit-text-decoration:underline dotted;cursor:help;border-bottom:0;-webkit-text-decoration-skip-ink:none;text-decoration-skip-ink:none}`)); }); + + it('merge at-rule & escape sequence #14', function () { + const file = ` + +@media (max-width: 767px) {.main-heading { font-size: 32px; font-weight: 300; }} +@media (max-width: 767px) {.section { max-width: 100vw; padding-left: 16px; padding-right: 16px; }} +@media (max-width: 767px) {.hero-cta-form, .sign-in-form__third-party-container, .google-sign-in-cta-widget { margin-top: 0px; width: 100%; }} +@media (max-width: 767px) {.babybear\\:z-0 { z-index: 0; }} +@media (max-width: 767px) {.babybear\\:mr-0 { margin-right: 0px; }} +@media (max-width: 767px) {.babybear\\:hidden { display: none; }} +@media (max-width: 767px) {.babybear\\:min-h-\\[0\\] { min-height: 0px; }} + +`; + return transform(file, { + minify: true + }).then(result => f(result.code).equals(`@media (max-width:767px){.main-heading{font-size:32px;font-weight:300}.section{max-width:100vw;padding-left:16px;padding-right:16px}.hero-cta-form,.sign-in-form__third-party-container,.google-sign-in-cta-widget{margin-top:0;width:100%}.babybear\\:z-0{z-index:0}.babybear\\:mr-0{margin-right:0}.babybear\\:hidden{display:none}.babybear\\:min-h-\\[0\\]{min-height:0}}`)); + }); + + it('merge at-rule & escape sequence #15', function () { + const file = ` + +@media (max-width: 767px) {.main-heading { font-size: 32px; font-weight: 300; }} +@media (max-width: 767px) {.section { max-width: 100vw; padding-left: 16px; padding-right: 16px; }} +@media (max-width: 767px) {.hero-cta-form, .sign-in-form__third-party-container, .google-sign-in-cta-widget { margin-top: 0px; width: 100%; }} +@media (max-width: 767px) {.babybear\\:z-0 { z-index: 0; }} +@media (max-width: 767px) {.babybear\\:mr-0 { margin-right: 0px; }} +@media (max-width: 767px) {.babybear\\:hidden { display: none; }} +@media (max-width: 767px) {.babybear\\:min-h-\\[0\\] { min-height: 0px; }} + +`; + return transform(file, { + minify: true + }).then(result => f(render(result.ast, {minify: false}).code).equals(`@media (max-width:767px) { + .main-heading { + font-size: 32px; + font-weight: 300 + } + .section { + max-width: 100vw; + padding-left: 16px; + padding-right: 16px + } + .hero-cta-form,.sign-in-form__third-party-container,.google-sign-in-cta-widget { + margin-top: 0; + width: 100% + } + .babybear\\:z-0 { + z-index: 0 + } + .babybear\\:mr-0 { + margin-right: 0 + } + .babybear\\:hidden { + display: none + } + .babybear\\:min-h-\\[0\\] { + min-height: 0 + } +}`)); + }); });