From de70a7e1c1ff8af0a3336583bf3873ff13998042 Mon Sep 17 00:00:00 2001 From: Thierry Bela Date: Sat, 12 Aug 2023 15:24:55 -0400 Subject: [PATCH 1/3] invalid shorthand value --- README.md | 6 ++--- dist/index-umd-web.js | 38 +++++++++++++++++++----------- dist/index.cjs | 38 +++++++++++++++++++----------- dist/index.d.ts | 28 +++++++++++++++++++++- dist/index.js | 1 + dist/lib/parser/declaration/map.js | 7 ++++-- dist/lib/parser/parse.js | 4 ++-- dist/lib/parser/utils/type.js | 4 ++++ dist/lib/renderer/render.js | 2 ++ dist/web/index.js | 1 + package.json | 20 ++++++++-------- src/lib/parser/declaration/list.ts | 2 +- src/lib/parser/declaration/map.ts | 12 ++++++---- src/lib/parser/parse.ts | 4 ++-- src/lib/parser/utils/index.ts | 3 ++- src/lib/parser/utils/type.ts | 11 +++++++-- src/lib/renderer/render.ts | 4 ++++ test/specs/shorthand.spec.js | 13 ++++++++++ 18 files changed, 142 insertions(+), 56 deletions(-) diff --git a/README.md b/README.md index 5af5b9f..d950af5 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ $ npm install @tbela99/css-parser - 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 +- automatically generate nested css rules - works the same way in node and web browser ### Performance @@ -79,7 +79,7 @@ parse(css, parseOptions = {}) ````javascript -const {ast, errors} = await parse(css); +const {ast, errors, stats} = await parse(css); ```` ## Rendering @@ -96,7 +96,7 @@ render(ast, RenderOptions = {}); import {render} from '@tbela99/css-parser'; // minified -const {code} = render(ast, {minify: true}); +const {code, stats} = render(ast, {minify: true}); console.log(code); ``` diff --git a/dist/index-umd-web.js b/dist/index-umd-web.js index d57ed2a..15e17ea 100644 --- a/dist/index-umd-web.js +++ b/dist/index-umd-web.js @@ -1069,6 +1069,21 @@ const getConfig = () => config$1; + const funcList = ['clamp', 'calc']; + function matchType(val, properties) { + if (val.typ == 'Iden' && properties.keywords.includes(val.val) || + (properties.types.includes(val.typ))) { + return true; + } + if (val.typ == 'Number' && val.val == '0') { + return properties.types.some(type => type == 'Length' || type == 'Angle'); + } + if (val.typ == 'Func' && funcList.includes(val.val)) { + return val.chi.every((t => ['Literal', 'Comma', 'Whitespace', 'Start-parens', 'End-parens'].includes(t.typ) || matchType(t, properties))); + } + return false; + } + // name to color const COLORS_NAMES = Object.seal({ 'aliceblue': '#f0f8ff', @@ -1597,6 +1612,8 @@ const indent = indents[level]; const indentSub = indents[level + 1]; switch (data.typ) { + case 'Declaration': + return `${data.nam}:${options.indent}${data.val.reduce((acc, curr) => acc + renderToken(curr), '')}`; case 'Comment': return options.removeComments ? '' : data.val; case 'StyleSheet': @@ -2000,17 +2017,6 @@ } } - function matchType(val, properties) { - if (val.typ == 'Iden' && properties.keywords.includes(val.val) || - (properties.types.includes(val.typ))) { - return true; - } - if (val.typ == 'Number' && val.val == '0') { - return properties.types.some(type => type == 'Length' || type == 'Angle'); - } - return false; - } - const propertiesConfig = getConfig(); class PropertyMap { config; @@ -2239,7 +2245,10 @@ return acc; }, []); count++; - if (!Object.values(tokens).every(v => v.length == count)) { + if (Object.entries(this.config.properties).some(entry => { + // missing required property + return entry[1].required && !(entry[0] in tokens); + }) || !Object.values(tokens).every(v => v.length == count)) { // @ts-ignore iterable = this.declarations.values(); } @@ -4106,13 +4115,13 @@ }; } function parseString(src, options = { location: false }) { - return [...tokenize(src)].map(t => { + return parseTokens([...tokenize(src)].map(t => { const token = getTokenType(t.token, t.hint); if (options.location) { Object.assign(token, { loc: t.position }); } return token; - }); + })); } function getTokenType(val, hint) { if (val === '' && hint == null) { @@ -4623,6 +4632,7 @@ exports.isTime = isTime; exports.isWhiteSpace = isWhiteSpace; exports.load = load; + exports.matchType = matchType; exports.matchUrl = matchUrl; exports.minify = minify; exports.minifyRule = minifyRule; diff --git a/dist/index.cjs b/dist/index.cjs index a773a82..888abf0 100644 --- a/dist/index.cjs +++ b/dist/index.cjs @@ -1067,6 +1067,21 @@ var config$1 = { const getConfig = () => config$1; +const funcList = ['clamp', 'calc']; +function matchType(val, properties) { + if (val.typ == 'Iden' && properties.keywords.includes(val.val) || + (properties.types.includes(val.typ))) { + return true; + } + if (val.typ == 'Number' && val.val == '0') { + return properties.types.some(type => type == 'Length' || type == 'Angle'); + } + if (val.typ == 'Func' && funcList.includes(val.val)) { + return val.chi.every((t => ['Literal', 'Comma', 'Whitespace', 'Start-parens', 'End-parens'].includes(t.typ) || matchType(t, properties))); + } + return false; +} + // name to color const COLORS_NAMES = Object.seal({ 'aliceblue': '#f0f8ff', @@ -1595,6 +1610,8 @@ function doRender(data, options, reducer, level = 0, indents = []) { const indent = indents[level]; const indentSub = indents[level + 1]; switch (data.typ) { + case 'Declaration': + return `${data.nam}:${options.indent}${data.val.reduce((acc, curr) => acc + renderToken(curr), '')}`; case 'Comment': return options.removeComments ? '' : data.val; case 'StyleSheet': @@ -1998,17 +2015,6 @@ class PropertySet { } } -function matchType(val, properties) { - if (val.typ == 'Iden' && properties.keywords.includes(val.val) || - (properties.types.includes(val.typ))) { - return true; - } - if (val.typ == 'Number' && val.val == '0') { - return properties.types.some(type => type == 'Length' || type == 'Angle'); - } - return false; -} - const propertiesConfig = getConfig(); class PropertyMap { config; @@ -2237,7 +2243,10 @@ class PropertyMap { return acc; }, []); count++; - if (!Object.values(tokens).every(v => v.length == count)) { + if (Object.entries(this.config.properties).some(entry => { + // missing required property + return entry[1].required && !(entry[0] in tokens); + }) || !Object.values(tokens).every(v => v.length == count)) { // @ts-ignore iterable = this.declarations.values(); } @@ -4104,13 +4113,13 @@ async function parse$1(iterator, opt = {}) { }; } function parseString(src, options = { location: false }) { - return [...tokenize(src)].map(t => { + return parseTokens([...tokenize(src)].map(t => { const token = getTokenType(t.token, t.hint); if (options.location) { Object.assign(token, { loc: t.position }); } return token; - }); + })); } function getTokenType(val, hint) { if (val === '' && hint == null) { @@ -4607,6 +4616,7 @@ exports.isResolution = isResolution; exports.isTime = isTime; exports.isWhiteSpace = isWhiteSpace; exports.load = load; +exports.matchType = matchType; exports.matchUrl = matchUrl; exports.minify = minify; exports.minifyRule = minifyRule; diff --git a/dist/index.d.ts b/dist/index.d.ts index 08d3d3d..83e3c05 100644 --- a/dist/index.d.ts +++ b/dist/index.d.ts @@ -173,6 +173,30 @@ interface AttrToken { } declare type Token = LiteralToken | IdentToken | CommaToken | ColonToken | SemiColonToken | NumberToken | AtRuleToken | PercentageToken | FunctionURLToken | FunctionToken | DimensionToken | LengthToken | AngleToken | StringToken | TimeToken | FrequencyToken | ResolutionToken | UnclosedStringToken | HashToken | BadStringToken | BlockStartToken | BlockEndToken | AttrStartToken | AttrEndToken | ParensStartToken | ParensEndToken | CDOCommentToken | BadCDOCommentToken | CommentToken | BadCommentToken | WhitespaceToken | IncludesToken | DashMatchToken | LessThanToken | GreaterThanToken | PseudoClassToken | PseudoClassFunctionToken | DelimToken | BadUrlToken | UrlToken | ImportantToken | ColorToken | AttrToken | EOFToken; +interface PropertyMapType { + default: string[]; + types: string[]; + keywords: string[]; + required?: boolean; + multiple?: boolean; + prefix?: { + typ: 'Literal'; + val: string; + }; + previous?: string; + separator?: { + typ: 'Comma'; + }; + constraints?: { + [key: string]: { + [key: string]: any; + }; + }; + mapping?: { + [key: string]: any; + }; +} + interface PropertiesConfig { properties: PropertiesConfigProperties; map: Map; @@ -629,6 +653,8 @@ declare function isWhiteSpace(codepoint: number): boolean; declare const getConfig: () => PropertiesConfig; +declare function matchType(val: Token, properties: PropertyMapType): boolean; + declare function render(data: AstNode, opt?: RenderOptions): RenderResult; declare function renderToken(token: Token, options?: RenderOptions): string; @@ -661,4 +687,4 @@ declare function resolve(url: string, currentDirectory: string, cwd?: string): { declare function parse(iterator: string, opt?: ParserOptions): Promise; declare function transform(css: string, options?: TransformOptions): Promise; -export { combinators, dirname, getConfig, hasDeclaration, isAngle, isAtKeyword, isDigit, isDimension, isFrequency, isFunction, isHash, isHexColor, isHexDigit, isIdent, isIdentCodepoint, isIdentStart, isLength, isNewLine, isNumber, isPercentage, isPseudo, isResolution, isTime, isWhiteSpace, load, matchUrl, minify, minifyRule, parse, parseDimension, parseString, reduceSelector, render, renderToken, resolve, tokenize, transform, urlTokenMatcher, walk }; +export { combinators, dirname, getConfig, hasDeclaration, isAngle, isAtKeyword, isDigit, isDimension, isFrequency, isFunction, isHash, isHexColor, isHexDigit, isIdent, isIdentCodepoint, isIdentStart, isLength, isNewLine, isNumber, isPercentage, isPseudo, isResolution, isTime, isWhiteSpace, load, matchType, matchUrl, minify, minifyRule, parse, parseDimension, parseString, reduceSelector, render, renderToken, resolve, tokenize, transform, urlTokenMatcher, walk }; diff --git a/dist/index.js b/dist/index.js index 7c48004..1ccc579 100644 --- a/dist/index.js +++ b/dist/index.js @@ -3,6 +3,7 @@ export { parseString, urlTokenMatcher } from './lib/parser/parse.js'; export { tokenize } from './lib/parser/tokenize.js'; export { isAngle, isAtKeyword, isDigit, isDimension, isFrequency, isFunction, isHash, isHexColor, isHexDigit, isIdent, isIdentCodepoint, isIdentStart, isLength, isNewLine, isNumber, isPercentage, isPseudo, isResolution, isTime, isWhiteSpace, parseDimension } from './lib/parser/utils/syntax.js'; export { getConfig } from './lib/parser/utils/config.js'; +export { matchType } from './lib/parser/utils/type.js'; export { render, renderToken } from './lib/renderer/render.js'; export { combinators, hasDeclaration, minify, minifyRule, reduceSelector } from './lib/ast/minify.js'; export { walk } from './lib/ast/walk.js'; diff --git a/dist/lib/parser/declaration/map.js b/dist/lib/parser/declaration/map.js index f1272d3..527955c 100644 --- a/dist/lib/parser/declaration/map.js +++ b/dist/lib/parser/declaration/map.js @@ -1,7 +1,7 @@ import { eq } from '../utils/eq.js'; import { getConfig } from '../utils/config.js'; -import { renderToken } from '../../renderer/render.js'; import { matchType } from '../utils/type.js'; +import { renderToken } from '../../renderer/render.js'; import { parseString } from '../parse.js'; import { PropertySet } from './set.js'; @@ -233,7 +233,10 @@ class PropertyMap { return acc; }, []); count++; - if (!Object.values(tokens).every(v => v.length == count)) { + if (Object.entries(this.config.properties).some(entry => { + // missing required property + return entry[1].required && !(entry[0] in tokens); + }) || !Object.values(tokens).every(v => v.length == count)) { // @ts-ignore iterable = this.declarations.values(); } diff --git a/dist/lib/parser/parse.js b/dist/lib/parser/parse.js index bb2ecf9..b0d4d3f 100644 --- a/dist/lib/parser/parse.js +++ b/dist/lib/parser/parse.js @@ -389,13 +389,13 @@ async function parse(iterator, opt = {}) { }; } function parseString(src, options = { location: false }) { - return [...tokenize(src)].map(t => { + return parseTokens([...tokenize(src)].map(t => { const token = getTokenType(t.token, t.hint); if (options.location) { Object.assign(token, { loc: t.position }); } return token; - }); + })); } function getTokenType(val, hint) { if (val === '' && hint == null) { diff --git a/dist/lib/parser/utils/type.js b/dist/lib/parser/utils/type.js index e45ff5e..f6a03c9 100644 --- a/dist/lib/parser/utils/type.js +++ b/dist/lib/parser/utils/type.js @@ -1,3 +1,4 @@ +const funcList = ['clamp', 'calc']; function matchType(val, properties) { if (val.typ == 'Iden' && properties.keywords.includes(val.val) || (properties.types.includes(val.typ))) { @@ -6,6 +7,9 @@ function matchType(val, properties) { if (val.typ == 'Number' && val.val == '0') { return properties.types.some(type => type == 'Length' || type == 'Angle'); } + if (val.typ == 'Func' && funcList.includes(val.val)) { + return val.chi.every((t => ['Literal', 'Comma', 'Whitespace', 'Start-parens', 'End-parens'].includes(t.typ) || matchType(t, properties))); + } return false; } diff --git a/dist/lib/renderer/render.js b/dist/lib/renderer/render.js index f486846..06544fe 100644 --- a/dist/lib/renderer/render.js +++ b/dist/lib/renderer/render.js @@ -35,6 +35,8 @@ function doRender(data, options, reducer, level = 0, indents = []) { const indent = indents[level]; const indentSub = indents[level + 1]; switch (data.typ) { + case 'Declaration': + return `${data.nam}:${options.indent}${data.val.reduce((acc, curr) => acc + renderToken(curr), '')}`; case 'Comment': return options.removeComments ? '' : data.val; case 'StyleSheet': diff --git a/dist/web/index.js b/dist/web/index.js index ff4c2f8..c0b2dfe 100644 --- a/dist/web/index.js +++ b/dist/web/index.js @@ -3,6 +3,7 @@ export { parseString, urlTokenMatcher } from '../lib/parser/parse.js'; export { tokenize } from '../lib/parser/tokenize.js'; export { isAngle, isAtKeyword, isDigit, isDimension, isFrequency, isFunction, isHash, isHexColor, isHexDigit, isIdent, isIdentCodepoint, isIdentStart, isLength, isNewLine, isNumber, isPercentage, isPseudo, isResolution, isTime, isWhiteSpace, parseDimension } from '../lib/parser/utils/syntax.js'; export { getConfig } from '../lib/parser/utils/config.js'; +export { matchType } from '../lib/parser/utils/type.js'; export { render, renderToken } from '../lib/renderer/render.js'; import { transform as transform$1 } from '../lib/transform.js'; export { combinators, hasDeclaration, minify, minifyRule, reduceSelector } from '../lib/ast/minify.js'; diff --git a/package.json b/package.json index 67350f2..19e862c 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@tbela99/css-parser", "description": "CSS parser for node and the browser", - "version": "0.0.1-rc2", + "version": "0.0.1-rc3", "exports": { ".": "./dist/index.js", "./umd": "./dist/index-umd-web.js", @@ -41,20 +41,20 @@ "homepage": "https://github.com/tbela99/css-parser#readme", "devDependencies": { "@esm-bundle/chai": "^4.3.4-fix.0", - "@rollup/plugin-commonjs": "^25.0.2", + "@rollup/plugin-commonjs": "^25.0.4", "@rollup/plugin-json": "^6.0.0", - "@rollup/plugin-node-resolve": "^15.0.1", + "@rollup/plugin-node-resolve": "^15.1.0", "@rollup/plugin-terser": "^0.4.3", - "@rollup/plugin-typescript": "^11.0.0", + "@rollup/plugin-typescript": "^11.1.2", "@types/chai": "^4.3.5", "@types/mocha": "^10.0.1", - "@types/node": "^18.15.10", - "@web/test-runner": "^0.16.1", - "@webref/css": "^6.5.9", + "@types/node": "^20.4.10", + "@web/test-runner": "^0.17.0", + "@webref/css": "^6.6.2", "c8": "^8.0.1", "mocha": "^10.2.0", - "rollup": "^3.20.1", - "rollup-plugin-dts": "^5.3.0", - "tslib": "^2.5.0" + "rollup": "^3.28.0", + "rollup-plugin-dts": "^5.3.1", + "tslib": "^2.6.1" } } diff --git a/src/lib/parser/declaration/list.ts b/src/lib/parser/declaration/list.ts index c3fb317..9193d70 100644 --- a/src/lib/parser/declaration/list.ts +++ b/src/lib/parser/declaration/list.ts @@ -125,7 +125,7 @@ export class PropertyList { } } - return value; + return <{value: AstNode, done: boolean}>value; } } } diff --git a/src/lib/parser/declaration/map.ts b/src/lib/parser/declaration/map.ts index a5da198..7759170 100644 --- a/src/lib/parser/declaration/map.ts +++ b/src/lib/parser/declaration/map.ts @@ -1,13 +1,12 @@ import { - AstDeclaration, + AstDeclaration, IdentToken, PropertyMapType, ShorthandPropertyType, Token } from "../../../@types"; import {ShorthandMapType} from "../../../@types"; import {eq} from "../utils/eq"; -import {getConfig} from "../utils"; +import {getConfig, matchType} from "../utils"; import {renderToken} from "../../renderer"; -import {matchType} from "../utils/type"; import {parseString} from "../parse"; import {PropertySet} from "./set"; @@ -342,7 +341,12 @@ export class PropertyMap { count++; - if (!Object.values(tokens).every(v => v.length == count)) { + if (Object.entries(this.config.properties).some(entry => { + + // missing required property + return entry[1].required && !(entry[0] in tokens); + + }) || !Object.values(tokens).every(v => v.length == count)) { // @ts-ignore iterable = this.declarations.values(); diff --git a/src/lib/parser/parse.ts b/src/lib/parser/parse.ts index 8d88842..13cb1d1 100644 --- a/src/lib/parser/parse.ts +++ b/src/lib/parser/parse.ts @@ -524,7 +524,7 @@ export async function parse(iterator: string, opt: ParserOptions = {}): Promise< export function parseString(src: string, options = {location: false}): Token[] { - return [...tokenize(src)].map(t => { + return parseTokens([...tokenize(src)].map(t => { const token = getTokenType(t.token, t.hint); @@ -534,7 +534,7 @@ export function parseString(src: string, options = {location: false}): Token[] { } return token; - }); + })); } function getTokenType(val: string, hint?: string): Token { diff --git a/src/lib/parser/utils/index.ts b/src/lib/parser/utils/index.ts index 67cc48d..534fd91 100644 --- a/src/lib/parser/utils/index.ts +++ b/src/lib/parser/utils/index.ts @@ -1,3 +1,4 @@ export * from './syntax'; -export * from './config'; \ No newline at end of file +export * from './config'; +export * from './type'; \ No newline at end of file diff --git a/src/lib/parser/utils/type.ts b/src/lib/parser/utils/type.ts index 0a40eb4..3faa986 100644 --- a/src/lib/parser/utils/type.ts +++ b/src/lib/parser/utils/type.ts @@ -1,6 +1,8 @@ -import {IdentToken, PropertyMapType, Token} from "../../../@types"; +import {FunctionToken, IdentToken, PropertyMapType, Token} from "../../../@types"; -export function matchType(val: Token, properties: PropertyMapType) { +const funcList = ['clamp', 'calc']; + +export function matchType(val: Token, properties: PropertyMapType): boolean { if (val.typ == 'Iden' && properties.keywords.includes((val).val) || (properties.types.includes(val.typ))) { @@ -13,5 +15,10 @@ export function matchType(val: Token, properties: PropertyMapType) { return properties.types.some(type => type == 'Length' || type == 'Angle') } + if (val.typ == 'Func' && funcList.includes((val).val)) { + + return val.chi.every((t => ['Literal', 'Comma', 'Whitespace', 'Start-parens', 'End-parens'].includes(t.typ) || matchType(t, properties))); + } + return false; } \ No newline at end of file diff --git a/src/lib/renderer/render.ts b/src/lib/renderer/render.ts index f214e91..acc1f23 100644 --- a/src/lib/renderer/render.ts +++ b/src/lib/renderer/render.ts @@ -63,6 +63,10 @@ function doRender(data: AstNode, options: RenderOptions, reducer: Function, leve switch (data.typ) { + case 'Declaration': + + return `${(data).nam}:${options.indent}${(data).val.reduce((acc, curr) => acc + renderToken(curr), '')}`; + case 'Comment': return options.removeComments ? '' : (data).val; diff --git a/test/specs/shorthand.spec.js b/test/specs/shorthand.spec.js index 3226f81..c3aa3b2 100644 --- a/test/specs/shorthand.spec.js +++ b/test/specs/shorthand.spec.js @@ -221,4 +221,17 @@ border: #333 solid 1px; }`, options).then(result => f(result.code).equals('.test input[type=text]{border:#333 solid}')); }); + + it('clamp & calc #15', function () { + return transform(` +@media all { + html { + font-family: Blanco, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"; + font-size: clamp(12px, 0.8rem + 0.25vw, 20px); + font-weight: 400; + line-height: 1.7; + } +} +`, options).then(result => f(result.code).equals('html{font:clamp(12px,.8rem + .25vw,20px)/1.7 Blanco,system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol"}')); + }); }); From e8dc908f803f22e4ccda609d1f2669c1154579e8 Mon Sep 17 00:00:00 2001 From: Thierry Bela Date: Sun, 13 Aug 2023 03:38:22 -0400 Subject: [PATCH 2/3] incorrect shorthand parsing #6 --- dist/index-umd-web.js | 71 ++++++++++++++++----------- dist/index.cjs | 71 ++++++++++++++++----------- dist/lib/parser/declaration/map.js | 67 +++++++++++++++----------- dist/lib/renderer/render.js | 4 ++ src/lib/parser/declaration/map.ts | 77 ++++++++++++++++++------------ src/lib/renderer/render.ts | 6 +++ 6 files changed, 181 insertions(+), 115 deletions(-) diff --git a/dist/index-umd-web.js b/dist/index-umd-web.js index 15e17ea..062dd78 100644 --- a/dist/index-umd-web.js +++ b/dist/index-umd-web.js @@ -1639,6 +1639,10 @@ str = options.removeComments ? '' : node.val; } else if (node.typ == 'Declaration') { + if (node.val.length == 0) { + console.error(`invalid declaration`, node); + return ''; + } str = `${node.nam}:${options.indent}${node.val.reduce(reducer, '').trimEnd()};`; } else if (node.typ == 'AtRule' && !('chi' in node)) { @@ -2031,6 +2035,9 @@ this.pattern = config.pattern.split(/\s/); } add(declaration) { + for (const val of declaration.val) { + Object.defineProperty(val, 'propertyName', { enumerable: false, writable: true, value: declaration.nam }); + } if (declaration.nam == this.config.shorthand) { this.declarations = new Map; this.declarations.set(declaration.nam, declaration); @@ -2064,7 +2071,7 @@ i--; continue; } - if (matchType(acc[i], props)) { + if (('propertyName' in acc[i] && acc[i].propertyName == property) || matchType(acc[i], props)) { if ('prefix' in props && props.previous != null && !(props.previous in tokens)) { return acc; } @@ -2198,10 +2205,12 @@ } else { let count = 0; + let match; const separator = this.config.separator; const tokens = {}; // @ts-ignore - /* const valid: string[] =*/ Object.entries(this.config.properties).reduce((acc, curr) => { + /* const valid: string[] =*/ + Object.entries(this.config.properties).reduce((acc, curr) => { if (!this.declarations.has(curr[0])) { if (curr[1].required) { acc.push(curr[0]); @@ -2210,33 +2219,39 @@ } let current = 0; const props = this.config.properties[curr[0]]; - const declaration = this.declarations.get(curr[0]); - // @ts-ignore - for (const val of (declaration instanceof PropertySet ? [...declaration][0] : declaration).val) { - if (separator != null && separator.typ == val.typ && eq(separator, val)) { - current++; - if (tokens[curr[0]].length == current) { - tokens[curr[0]].push([]); + const properties = this.declarations.get(curr[0]); + for (const declaration of [(properties instanceof PropertySet ? [...properties][0] : properties)]) { + // @ts-ignore + for (const val of declaration.val) { + if (separator != null && separator.typ == val.typ && eq(separator, val)) { + current++; + if (tokens[curr[0]].length == current) { + tokens[curr[0]].push([]); + } + continue; } - continue; - } - if (val.typ == 'Whitespace' || val.typ == 'Comment') { - continue; - } - if (props.multiple && props.separator != null && props.separator.typ == val.typ && eq(props.separator, val)) { - continue; - } - if (matchType(val, curr[1])) { - if (!(curr[0] in tokens)) { - tokens[curr[0]] = [[]]; + if (val.typ == 'Whitespace' || val.typ == 'Comment') { + continue; + } + if (props.multiple && props.separator != null && props.separator.typ == val.typ && eq(props.separator, val)) { + continue; + } + match = matchType(val, curr[1]); + if (isShorthand) { + isShorthand = match; + } + if (('propertyName' in val && val.propertyName == property) || match) { + if (!(curr[0] in tokens)) { + tokens[curr[0]] = [[]]; + } + // is default value + tokens[curr[0]][current].push(val); + // continue; + } + else { + acc.push(curr[0]); + break; } - // is default value - tokens[curr[0]][current].push(val); - // continue; - } - else { - acc.push(curr[0]); - break; } } if (count == 0) { @@ -2245,7 +2260,7 @@ return acc; }, []); count++; - if (Object.entries(this.config.properties).some(entry => { + if (!isShorthand || Object.entries(this.config.properties).some(entry => { // missing required property return entry[1].required && !(entry[0] in tokens); }) || !Object.values(tokens).every(v => v.length == count)) { diff --git a/dist/index.cjs b/dist/index.cjs index 888abf0..e0f5e27 100644 --- a/dist/index.cjs +++ b/dist/index.cjs @@ -1637,6 +1637,10 @@ function doRender(data, options, reducer, level = 0, indents = []) { str = options.removeComments ? '' : node.val; } else if (node.typ == 'Declaration') { + if (node.val.length == 0) { + console.error(`invalid declaration`, node); + return ''; + } str = `${node.nam}:${options.indent}${node.val.reduce(reducer, '').trimEnd()};`; } else if (node.typ == 'AtRule' && !('chi' in node)) { @@ -2029,6 +2033,9 @@ class PropertyMap { this.pattern = config.pattern.split(/\s/); } add(declaration) { + for (const val of declaration.val) { + Object.defineProperty(val, 'propertyName', { enumerable: false, writable: true, value: declaration.nam }); + } if (declaration.nam == this.config.shorthand) { this.declarations = new Map; this.declarations.set(declaration.nam, declaration); @@ -2062,7 +2069,7 @@ class PropertyMap { i--; continue; } - if (matchType(acc[i], props)) { + if (('propertyName' in acc[i] && acc[i].propertyName == property) || matchType(acc[i], props)) { if ('prefix' in props && props.previous != null && !(props.previous in tokens)) { return acc; } @@ -2196,10 +2203,12 @@ class PropertyMap { } else { let count = 0; + let match; const separator = this.config.separator; const tokens = {}; // @ts-ignore - /* const valid: string[] =*/ Object.entries(this.config.properties).reduce((acc, curr) => { + /* const valid: string[] =*/ + Object.entries(this.config.properties).reduce((acc, curr) => { if (!this.declarations.has(curr[0])) { if (curr[1].required) { acc.push(curr[0]); @@ -2208,33 +2217,39 @@ class PropertyMap { } let current = 0; const props = this.config.properties[curr[0]]; - const declaration = this.declarations.get(curr[0]); - // @ts-ignore - for (const val of (declaration instanceof PropertySet ? [...declaration][0] : declaration).val) { - if (separator != null && separator.typ == val.typ && eq(separator, val)) { - current++; - if (tokens[curr[0]].length == current) { - tokens[curr[0]].push([]); + const properties = this.declarations.get(curr[0]); + for (const declaration of [(properties instanceof PropertySet ? [...properties][0] : properties)]) { + // @ts-ignore + for (const val of declaration.val) { + if (separator != null && separator.typ == val.typ && eq(separator, val)) { + current++; + if (tokens[curr[0]].length == current) { + tokens[curr[0]].push([]); + } + continue; } - continue; - } - if (val.typ == 'Whitespace' || val.typ == 'Comment') { - continue; - } - if (props.multiple && props.separator != null && props.separator.typ == val.typ && eq(props.separator, val)) { - continue; - } - if (matchType(val, curr[1])) { - if (!(curr[0] in tokens)) { - tokens[curr[0]] = [[]]; + if (val.typ == 'Whitespace' || val.typ == 'Comment') { + continue; + } + if (props.multiple && props.separator != null && props.separator.typ == val.typ && eq(props.separator, val)) { + continue; + } + match = matchType(val, curr[1]); + if (isShorthand) { + isShorthand = match; + } + if (('propertyName' in val && val.propertyName == property) || match) { + if (!(curr[0] in tokens)) { + tokens[curr[0]] = [[]]; + } + // is default value + tokens[curr[0]][current].push(val); + // continue; + } + else { + acc.push(curr[0]); + break; } - // is default value - tokens[curr[0]][current].push(val); - // continue; - } - else { - acc.push(curr[0]); - break; } } if (count == 0) { @@ -2243,7 +2258,7 @@ class PropertyMap { return acc; }, []); count++; - if (Object.entries(this.config.properties).some(entry => { + if (!isShorthand || Object.entries(this.config.properties).some(entry => { // missing required property return entry[1].required && !(entry[0] in tokens); }) || !Object.values(tokens).every(v => v.length == count)) { diff --git a/dist/lib/parser/declaration/map.js b/dist/lib/parser/declaration/map.js index 527955c..07bef83 100644 --- a/dist/lib/parser/declaration/map.js +++ b/dist/lib/parser/declaration/map.js @@ -19,6 +19,9 @@ class PropertyMap { this.pattern = config.pattern.split(/\s/); } add(declaration) { + for (const val of declaration.val) { + Object.defineProperty(val, 'propertyName', { enumerable: false, writable: true, value: declaration.nam }); + } if (declaration.nam == this.config.shorthand) { this.declarations = new Map; this.declarations.set(declaration.nam, declaration); @@ -52,7 +55,7 @@ class PropertyMap { i--; continue; } - if (matchType(acc[i], props)) { + if (('propertyName' in acc[i] && acc[i].propertyName == property) || matchType(acc[i], props)) { if ('prefix' in props && props.previous != null && !(props.previous in tokens)) { return acc; } @@ -186,10 +189,12 @@ class PropertyMap { } else { let count = 0; + let match; const separator = this.config.separator; const tokens = {}; // @ts-ignore - /* const valid: string[] =*/ Object.entries(this.config.properties).reduce((acc, curr) => { + /* const valid: string[] =*/ + Object.entries(this.config.properties).reduce((acc, curr) => { if (!this.declarations.has(curr[0])) { if (curr[1].required) { acc.push(curr[0]); @@ -198,33 +203,39 @@ class PropertyMap { } let current = 0; const props = this.config.properties[curr[0]]; - const declaration = this.declarations.get(curr[0]); - // @ts-ignore - for (const val of (declaration instanceof PropertySet ? [...declaration][0] : declaration).val) { - if (separator != null && separator.typ == val.typ && eq(separator, val)) { - current++; - if (tokens[curr[0]].length == current) { - tokens[curr[0]].push([]); + const properties = this.declarations.get(curr[0]); + for (const declaration of [(properties instanceof PropertySet ? [...properties][0] : properties)]) { + // @ts-ignore + for (const val of declaration.val) { + if (separator != null && separator.typ == val.typ && eq(separator, val)) { + current++; + if (tokens[curr[0]].length == current) { + tokens[curr[0]].push([]); + } + continue; } - continue; - } - if (val.typ == 'Whitespace' || val.typ == 'Comment') { - continue; - } - if (props.multiple && props.separator != null && props.separator.typ == val.typ && eq(props.separator, val)) { - continue; - } - if (matchType(val, curr[1])) { - if (!(curr[0] in tokens)) { - tokens[curr[0]] = [[]]; + if (val.typ == 'Whitespace' || val.typ == 'Comment') { + continue; + } + if (props.multiple && props.separator != null && props.separator.typ == val.typ && eq(props.separator, val)) { + continue; + } + match = matchType(val, curr[1]); + if (isShorthand) { + isShorthand = match; + } + if (('propertyName' in val && val.propertyName == property) || match) { + if (!(curr[0] in tokens)) { + tokens[curr[0]] = [[]]; + } + // is default value + tokens[curr[0]][current].push(val); + // continue; + } + else { + acc.push(curr[0]); + break; } - // is default value - tokens[curr[0]][current].push(val); - // continue; - } - else { - acc.push(curr[0]); - break; } } if (count == 0) { @@ -233,7 +244,7 @@ class PropertyMap { return acc; }, []); count++; - if (Object.entries(this.config.properties).some(entry => { + if (!isShorthand || Object.entries(this.config.properties).some(entry => { // missing required property return entry[1].required && !(entry[0] in tokens); }) || !Object.values(tokens).every(v => v.length == count)) { diff --git a/dist/lib/renderer/render.js b/dist/lib/renderer/render.js index 06544fe..5bb510f 100644 --- a/dist/lib/renderer/render.js +++ b/dist/lib/renderer/render.js @@ -62,6 +62,10 @@ function doRender(data, options, reducer, level = 0, indents = []) { str = options.removeComments ? '' : node.val; } else if (node.typ == 'Declaration') { + if (node.val.length == 0) { + console.error(`invalid declaration`, node); + return ''; + } str = `${node.nam}:${options.indent}${node.val.reduce(reducer, '').trimEnd()};`; } else if (node.typ == 'AtRule' && !('chi' in node)) { diff --git a/src/lib/parser/declaration/map.ts b/src/lib/parser/declaration/map.ts index 7759170..50404a2 100644 --- a/src/lib/parser/declaration/map.ts +++ b/src/lib/parser/declaration/map.ts @@ -1,6 +1,5 @@ import { - AstDeclaration, IdentToken, - PropertyMapType, ShorthandPropertyType, + AstDeclaration, PropertyMapType, ShorthandPropertyType, Token } from "../../../@types"; import {ShorthandMapType} from "../../../@types"; @@ -30,6 +29,11 @@ export class PropertyMap { add(declaration: AstDeclaration) { + for (const val of declaration.val) { + + Object.defineProperty(val, 'propertyName', {enumerable: false, writable: true, value: declaration.nam}); + } + if (declaration.nam == this.config.shorthand) { this.declarations = new Map; @@ -78,7 +82,7 @@ export class PropertyMap { continue; } - if (matchType(acc[i], props)) { + if (('propertyName' in acc[i] && acc[i].propertyName == property) || matchType(acc[i], props)) { if ('prefix' in props && props.previous != null && !(props.previous in tokens)) { @@ -267,11 +271,13 @@ export class PropertyMap { } else { let count = 0; + let match: boolean; const separator = this.config.separator; const tokens = <{ [key: string]: Token[][] }>{}; // @ts-ignore - /* const valid: string[] =*/ Object.entries(this.config.properties).reduce((acc, curr) => { + /* const valid: string[] =*/ + Object.entries(this.config.properties).reduce((acc, curr) => { if (!this.declarations.has(curr[0])) { @@ -286,48 +292,56 @@ export class PropertyMap { let current = 0; const props = this.config.properties[curr[0]]; - const declaration = this.declarations.get(curr[0]); + const properties = this.declarations.get(curr[0]); - // @ts-ignore - for (const val of (declaration instanceof PropertySet ? [...declaration][0] : declaration).val) { + for (const declaration of [(properties instanceof PropertySet ? [...properties][0] : properties)]) { - if (separator != null && separator.typ == val.typ && eq(separator, val)) { + // @ts-ignore + for (const val of declaration.val) { - current++; + if (separator != null && separator.typ == val.typ && eq(separator, val)) { - if (tokens[curr[0]].length == current) { + current++; - tokens[curr[0]].push([]); - } + if (tokens[curr[0]].length == current) { - continue; - } + tokens[curr[0]].push([]); + } - if (val.typ == 'Whitespace' || val.typ == 'Comment') { + continue; + } - continue; - } + if (val.typ == 'Whitespace' || val.typ == 'Comment') { - if (props.multiple && props.separator != null && props.separator.typ == val.typ && eq(props.separator, val)) { + continue; + } - continue; - } + if (props.multiple && props.separator != null && props.separator.typ == val.typ && eq(props.separator, val)) { - if (matchType(val, curr[1])) { + continue; + } - if (!(curr[0] in tokens)) { + match = matchType(val, curr[1]); - tokens[curr[0]] = [[]]; + if (isShorthand) { + + isShorthand = match; } - // is default value + if (('propertyName' in val && val.propertyName == property) || match) { - tokens[curr[0]][current].push(val); - // continue; - } else { + if (!(curr[0] in tokens)) { - acc.push(curr[0]); - break; + tokens[curr[0]] = [[]]; + } + + // is default value + tokens[curr[0]][current].push(val); + } else { + + acc.push(curr[0]); + break; + } } } @@ -341,7 +355,7 @@ export class PropertyMap { count++; - if (Object.entries(this.config.properties).some(entry => { + if (!isShorthand || Object.entries(this.config.properties).some(entry => { // missing required property return entry[1].required && !(entry[0] in tokens); @@ -350,7 +364,8 @@ export class PropertyMap { // @ts-ignore iterable = this.declarations.values(); - } else { + } + else { const values: Token[] = Object.entries(tokens).reduce((acc, curr) => { diff --git a/src/lib/renderer/render.ts b/src/lib/renderer/render.ts index acc1f23..a76bedf 100644 --- a/src/lib/renderer/render.ts +++ b/src/lib/renderer/render.ts @@ -109,6 +109,12 @@ function doRender(data: AstNode, options: RenderOptions, reducer: Function, leve str = options.removeComments ? '' : (node).val; } else if (node.typ == 'Declaration') { + if ((node).val.length == 0) { + + console.error(`invalid declaration`, node); + return ''; + } + str = `${(node).nam}:${options.indent}${(node).val.reduce(<() => string>reducer, '').trimEnd()};`; } else if (node.typ == 'AtRule' && !('chi' in node)) { From 997ff5f7b4ea586fc310fafcceb2a36ea62610be Mon Sep 17 00:00:00 2001 From: Thierry Bela Date: Sun, 13 Aug 2023 03:44:23 -0400 Subject: [PATCH 3/3] incorrectly parse shorthand #6 --- .npmignore | 3 +- README.md | 56 ++++++++++++++++++++++ package.json | 1 - src/lib/parser/declaration/map.ts | 1 + test/specs/shorthand.spec.js | 80 +++++++++++++++++++++++++++++++ 5 files changed, 139 insertions(+), 2 deletions(-) diff --git a/.npmignore b/.npmignore index b90411f..d47bc21 100644 --- a/.npmignore +++ b/.npmignore @@ -9,4 +9,5 @@ /package-lock.json /node_modules /coverage -/.github \ No newline at end of file +/.github +/.gitattributes \ No newline at end of file diff --git a/README.md b/README.md index d950af5..4049dbb 100644 --- a/README.md +++ b/README.md @@ -160,6 +160,62 @@ Single JavaScript file ``` +## Example + +### Automatic CSS Nesting + +CSS + +```css + +table.colortable td { + text-align:center; +} +table.colortable td.c { + text-transform:uppercase; +} +table.colortable td:first-child, table.colortable td:first-child+td { + border:1px solid black; +} +table.colortable th { + text-align:center; + background:black; + color:white; +} +``` + +Javascript +```javascript +import {parse, render} from '@tbela99/css-parser'; + + +const options = {minify: true, nestingRules: true}; + +const {code} = await parse(css, options).then(result => render(result.ast, {minify: false})); +// +console.debug(code); +``` + +Result +```css +table.colortable { + & td { + text-align: center; + &.c { + text-transform: uppercase + } + &:first-child,&:first-child+td { + border: 1px solid #000 + } + } + & th { + text-align: center; + background: #000; + color: #fff + } +} +``` + ## AST ### Comment diff --git a/package.json b/package.json index 19e862c..0132fc7 100644 --- a/package.json +++ b/package.json @@ -50,7 +50,6 @@ "@types/mocha": "^10.0.1", "@types/node": "^20.4.10", "@web/test-runner": "^0.17.0", - "@webref/css": "^6.6.2", "c8": "^8.0.1", "mocha": "^10.2.0", "rollup": "^3.28.0", diff --git a/src/lib/parser/declaration/map.ts b/src/lib/parser/declaration/map.ts index 50404a2..82a6882 100644 --- a/src/lib/parser/declaration/map.ts +++ b/src/lib/parser/declaration/map.ts @@ -82,6 +82,7 @@ export class PropertyMap { continue; } + // @ts-ignore if (('propertyName' in acc[i] && acc[i].propertyName == property) || matchType(acc[i], props)) { if ('prefix' in props && props.previous != null && !(props.previous in tokens)) { diff --git a/test/specs/shorthand.spec.js b/test/specs/shorthand.spec.js index c3aa3b2..1284af0 100644 --- a/test/specs/shorthand.spec.js +++ b/test/specs/shorthand.spec.js @@ -1,6 +1,7 @@ /* generate from test/specs/shorthand.spec.ts */ import { expect as f } from '../../node_modules/@esm-bundle/chai/esm/chai.js'; import { transform } from '../../dist/node/index.js'; +import {render} from "../../dist/index.js"; const options = { minify: true, @@ -234,4 +235,83 @@ border: #333 solid 1px; } `, options).then(result => f(result.code).equals('html{font:clamp(12px,.8rem + .25vw,20px)/1.7 Blanco,system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol"}')); }); + + it('shorthand parsing #16', function () { + return transform(` +@media all { + html { + font-family: Blanco, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"; + font-size: clamp(12px, 0.8rem + 0.25vw, 20px); + font-weight: 400; + line-height: 1.7; + } +} + +button.jetpack-instant-search__overlay-close { + align-items: center; + appearance: none; + background-image: none; + background-position: initial; + background-size: initial; + background-repeat: initial; + background-attachment: initial; + background-origin: initial; + background-clip: initial; + border-top: none; + border-right: none; + border-left: none; + border-image: initial; + border-bottom: 1px solid rgb(230, 241, 245); + border-radius: 0px; + box-shadow: none; + cursor: pointer; + display: flex; + height: 61px; + justify-content: center; + line-height: 1; + margin: 0px; + outline: none; + padding: 0px; + text-decoration: none; + text-shadow: none; + text-transform: none; + width: 60px; + background-color: transparent !important; +} + +`, options).then(result => f(render(result.ast, {minify: false}).code).equals(`html { + font: clamp(12px,.8rem + .25vw,20px)/1.7 Blanco,system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol" +} +button.jetpack-instant-search__overlay-close { + align-items: center; + appearance: none; + background-image: none; + background-position: initial; + background-size: initial; + background-repeat: initial; + background-attachment: initial; + background-origin: initial; + background-clip: initial; + background-color: #0000 !important; + border-top: none; + border-right: none; + border-left: none; + border-image: initial; + border-bottom: 1px solid #e6f1f5; + border-radius: 0; + box-shadow: none; + cursor: pointer; + display: flex; + height: 61px; + justify-content: center; + line-height: 1; + margin: 0; + outline: none; + padding: 0; + text-decoration: none; + text-shadow: none; + text-transform: none; + width: 60px +}`)); + }); });