diff --git a/.gitattributes b/.gitattributes index d231fc0..145808a 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,5 +1,6 @@ /test/ export-ignore /docs/ export-ignore +/benchmark /tools/ export-ignore /package-lock.json export-ignore /.gitignore export-ignore diff --git a/.npmignore b/.npmignore index c44a2fa..b90411f 100644 --- a/.npmignore +++ b/.npmignore @@ -1,4 +1,5 @@ /test +/benchmark /tools /ROADMAP.md /rollup.config.mjs diff --git a/README.md b/README.md index 04d1994..60bbc36 100644 --- a/README.md +++ b/README.md @@ -8,9 +8,20 @@ CSS parser for node and the browser ```shell $ 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 + +### Performance + +- [x] flatten @import + ## Transform Parse and render css in a single pass. @@ -28,7 +39,7 @@ transform(css, transformOptions = {}) import {transform} from '@tbela99/css-parser'; -const {ast, code, errors, stats} = await transform(css, {compress: true, resolveImport: true, cwd: 'files/css'}); +const {ast, code, errors, stats} = await transform(css, {minify: true, resolveImport: true, cwd: 'files/css'}); ``` ### TransformOptions @@ -38,7 +49,8 @@ Include ParseOptions and RenderOptions #### ParseOptions - src: string, optional. css file location to be used with sourcemap. -- compress: boolean, optional. default to _true_. optimize ast and minify css. +- minify: boolean, optional. default to _true_. optimize ast. +- nestingRules: boolean, optional. automatically nest 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 @@ -46,11 +58,11 @@ Include ParseOptions and RenderOptions - resolveUrls: boolean, optional. resolve css url() according to the parameters 'src' and 'cwd' #### RenderOptions -- compress: boolean, optional. default to _true_. optimize ast and minify css. +- minify: boolean, optional. default to _true_. minify css output. - indent: string, optional. css indention string. uses space character by default. - newLine: string, new line character. - removeComments: boolean, remove comments in generated css. -- preserveLicense: boolean, force preserving comments starting with '/\*!' when compress is enabling. +- preserveLicense: boolean, force preserving comments starting with '/\*!' when minify is enabled. - colorConvert: boolean, convert colors to hex. @@ -84,7 +96,7 @@ render(ast, RenderOptions = {}); import {render} from '@tbela99/css-parser'; // minified -const {code} = render(ast, {compress: true}); +const {code} = render(ast, {minify: true}); console.log(code); ``` diff --git a/dist/config.json.js b/dist/config.json.js index a15cdd4..2a71745 100644 --- a/dist/config.json.js +++ b/dist/config.json.js @@ -117,6 +117,7 @@ var properties = { }, "border-width": { shorthand: "border-width", + map: "border", properties: [ "border-top-width", "border-right-width", @@ -127,6 +128,9 @@ var properties = { "Length", "Perc" ], + "default": [ + "medium" + ], keywords: [ "thin", "medium", @@ -134,19 +138,24 @@ var properties = { ] }, "border-top-width": { + map: "border", shorthand: "border-width" }, "border-right-width": { + map: "border", shorthand: "border-width" }, "border-bottom-width": { + map: "border", shorthand: "border-width" }, "border-left-width": { + map: "border", shorthand: "border-width" }, "border-style": { shorthand: "border-style", + map: "border", properties: [ "border-top-style", "border-right-style", @@ -155,6 +164,9 @@ var properties = { ], types: [ ], + "default": [ + "none" + ], keywords: [ "none", "hidden", @@ -169,19 +181,24 @@ var properties = { ] }, "border-top-style": { + map: "border", shorthand: "border-style" }, "border-right-style": { + map: "border", shorthand: "border-style" }, "border-bottom-style": { + map: "border", shorthand: "border-style" }, "border-left-style": { + map: "border", shorthand: "border-style" }, "border-color": { shorthand: "border-color", + map: "border", properties: [ "border-top-color", "border-right-color", @@ -191,23 +208,95 @@ var properties = { types: [ "Color" ], + "default": [ + "currentcolor" + ], keywords: [ ] }, "border-top-color": { + map: "border", shorthand: "border-color" }, "border-right-color": { + map: "border", shorthand: "border-color" }, "border-bottom-color": { + map: "border", shorthand: "border-color" }, "border-left-color": { + map: "border", shorthand: "border-color" } }; var map = { + border: { + shorthand: "border", + pattern: "border-color border-style border-width", + keywords: [ + "none" + ], + "default": [ + "0", + "none" + ], + properties: { + "border-color": { + types: [ + "Color" + ], + "default": [ + "currentcolor" + ], + keywords: [ + ] + }, + "border-style": { + types: [ + ], + "default": [ + "none" + ], + keywords: [ + "none", + "hidden", + "dotted", + "dashed", + "solid", + "double", + "groove", + "ridge", + "inset", + "outset" + ] + }, + "border-width": { + types: [ + "Length", + "Perc" + ], + "default": [ + "medium" + ], + keywords: [ + "thin", + "medium", + "thick" + ] + } + } + }, + "border-color": { + shorthand: "border" + }, + "border-style": { + shorthand: "border" + }, + "border-width": { + shorthand: "border" + }, outline: { shorthand: "outline", pattern: "outline-color outline-style outline-width", @@ -224,12 +313,10 @@ var map = { "Color" ], "default": [ - "currentColor", - "invert" + "currentColor" ], keywords: [ - "currentColor", - "invert" + "currentColor" ] }, "outline-style": { @@ -567,6 +654,7 @@ var map = { "default": [ "transparent" ], + multiple: true, keywords: [ ] }, @@ -587,6 +675,7 @@ var map = { "default": [ "scroll" ], + multiple: true, keywords: [ "scroll", "fixed", @@ -599,6 +688,7 @@ var map = { "default": [ "border-box" ], + multiple: true, keywords: [ "border-box", "padding-box", @@ -612,6 +702,7 @@ var map = { "default": [ "padding-box" ], + multiple: true, keywords: [ "border-box", "padding-box", diff --git a/dist/index-umd-web.js b/dist/index-umd-web.js index a1a74a6..225ff70 100644 --- a/dist/index-umd-web.js +++ b/dist/index-umd-web.js @@ -8,13 +8,14 @@ // https://www.w3.org/TR/2021/CRD-css-syntax-3-20211224/#typedef-ident-token // '\\' const REVERSE_SOLIDUS = 0x5c; + const dimensionUnits = [ + 'q', 'cap', 'ch', 'cm', 'cqb', 'cqh', 'cqi', 'cqmax', 'cqmin', 'cqw', 'dvb', + 'dvh', 'dvi', 'dvmax', 'dvmin', 'dvw', 'em', 'ex', 'ic', 'in', 'lh', 'lvb', + 'lvh', 'lvi', 'lvmax', 'lvw', 'mm', 'pc', 'pt', 'px', 'rem', 'rlh', 'svb', + 'svh', 'svi', 'svmin', 'svw', 'vb', 'vh', 'vi', 'vmax', 'vmin', 'vw' + ]; function isLength(dimension) { - return 'unit' in dimension && [ - 'q', 'cap', 'ch', 'cm', 'cqb', 'cqh', 'cqi', 'cqmax', 'cqmin', 'cqw', 'dvb', - 'dvh', 'dvi', 'dvmax', 'dvmin', 'dvw', 'em', 'ex', 'ic', 'in', 'lh', 'lvb', - 'lvh', 'lvi', 'lvmax', 'lvw', 'mm', 'pc', 'pt', 'px', 'rem', 'rlh', 'svb', - 'svh', 'svi', 'svmin', 'svw', 'vb', 'vh', 'vi', 'vmax', 'vmin', 'vw' - ].includes(dimension.unit.toLowerCase()); + return 'unit' in dimension && dimensionUnits.includes(dimension.unit.toLowerCase()); } function isResolution(dimension) { return 'unit' in dimension && ['dpi', 'dpcm', 'dppx', 'x'].includes(dimension.unit.toLowerCase()); @@ -239,6 +240,22 @@ } return true; } + function isHexDigit(name) { + if (name.length || name.length > 6) { + return false; + } + for (let chr of name) { + let codepoint = chr.charCodeAt(0); + if (!isDigit(codepoint) && + // A F + !(codepoint >= 0x41 && codepoint <= 0x46) && + // a f + !(codepoint >= 0x61 && codepoint <= 0x66)) { + return false; + } + } + return true; + } function isFunction(name) { return name.endsWith('(') && isIdent(name.slice(0, -1)); } @@ -374,6 +391,7 @@ }, "border-width": { shorthand: "border-width", + map: "border", properties: [ "border-top-width", "border-right-width", @@ -384,6 +402,9 @@ "Length", "Perc" ], + "default": [ + "medium" + ], keywords: [ "thin", "medium", @@ -391,19 +412,24 @@ ] }, "border-top-width": { + map: "border", shorthand: "border-width" }, "border-right-width": { + map: "border", shorthand: "border-width" }, "border-bottom-width": { + map: "border", shorthand: "border-width" }, "border-left-width": { + map: "border", shorthand: "border-width" }, "border-style": { shorthand: "border-style", + map: "border", properties: [ "border-top-style", "border-right-style", @@ -412,6 +438,9 @@ ], types: [ ], + "default": [ + "none" + ], keywords: [ "none", "hidden", @@ -426,19 +455,24 @@ ] }, "border-top-style": { + map: "border", shorthand: "border-style" }, "border-right-style": { + map: "border", shorthand: "border-style" }, "border-bottom-style": { + map: "border", shorthand: "border-style" }, "border-left-style": { + map: "border", shorthand: "border-style" }, "border-color": { shorthand: "border-color", + map: "border", properties: [ "border-top-color", "border-right-color", @@ -448,23 +482,95 @@ types: [ "Color" ], + "default": [ + "currentcolor" + ], keywords: [ ] }, "border-top-color": { + map: "border", shorthand: "border-color" }, "border-right-color": { + map: "border", shorthand: "border-color" }, "border-bottom-color": { + map: "border", shorthand: "border-color" }, "border-left-color": { + map: "border", shorthand: "border-color" } }; var map = { + border: { + shorthand: "border", + pattern: "border-color border-style border-width", + keywords: [ + "none" + ], + "default": [ + "0", + "none" + ], + properties: { + "border-color": { + types: [ + "Color" + ], + "default": [ + "currentcolor" + ], + keywords: [ + ] + }, + "border-style": { + types: [ + ], + "default": [ + "none" + ], + keywords: [ + "none", + "hidden", + "dotted", + "dashed", + "solid", + "double", + "groove", + "ridge", + "inset", + "outset" + ] + }, + "border-width": { + types: [ + "Length", + "Perc" + ], + "default": [ + "medium" + ], + keywords: [ + "thin", + "medium", + "thick" + ] + } + } + }, + "border-color": { + shorthand: "border" + }, + "border-style": { + shorthand: "border" + }, + "border-width": { + shorthand: "border" + }, outline: { shorthand: "outline", pattern: "outline-color outline-style outline-width", @@ -481,12 +587,10 @@ "Color" ], "default": [ - "currentColor", - "invert" + "currentColor" ], keywords: [ - "currentColor", - "invert" + "currentColor" ] }, "outline-style": { @@ -824,6 +928,7 @@ "default": [ "transparent" ], + multiple: true, keywords: [ ] }, @@ -844,6 +949,7 @@ "default": [ "scroll" ], + multiple: true, keywords: [ "scroll", "fixed", @@ -856,6 +962,7 @@ "default": [ "border-box" ], + multiple: true, keywords: [ "border-box", "padding-box", @@ -869,6 +976,7 @@ "default": [ "padding-box" ], + multiple: true, keywords: [ "border-box", "padding-box", @@ -1458,7 +1566,7 @@ } function render(data, opt = {}) { - const options = Object.assign(opt.compress ? { + const options = Object.assign(opt.minify ?? true ? { indent: '', newLine: '', removeComments: true @@ -1543,8 +1651,11 @@ function renderToken(token, options = {}) { switch (token.typ) { case 'Color': - if (options.compress || options.colorConvert) { - let value = token.kin == 'hex' ? token.val.toLowerCase() : ''; + if (options.minify || options.colorConvert) { + if (token.kin == 'lit' && token.val.toLowerCase() == 'currentcolor') { + return 'currentcolor'; + } + let value = token.kin == 'hex' ? token.val.toLowerCase() : (token.kin == 'lit' ? COLORS_NAMES[token.val.toLowerCase()] : ''); if (token.val == 'rgb' || token.val == 'rgba') { value = rgb2Hex(token); } @@ -1588,7 +1699,7 @@ case 'UrlFunc': case 'Pseudo-class-func': // @ts-ignore - return ( /* options.compress && 'Pseudo-class-func' == token.typ && token.val.slice(0, 2) == '::' ? token.val.slice(1) :*/token.val ?? '') + '(' + token.chi.reduce((acc, curr) => { + return ( /* options.minify && 'Pseudo-class-func' == token.typ && token.val.slice(0, 2) == '::' ? token.val.slice(1) :*/token.val ?? '') + '(' + token.chi.reduce((acc, curr) => { if (options.removeComments && curr.typ == 'Comment') { if (!options.preserveLicense || !curr.val.startsWith('/*!')) { return acc; @@ -1679,21 +1790,45 @@ case 'String': case 'Iden': case 'Delim': - return /* options.compress && 'Pseudo-class' == token.typ && '::' == token.val.slice(0, 2) ? token.val.slice(1) : */ token.val; + 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)}`); } function eq(a, b) { - if ((typeof a != 'object') || typeof b != 'object') { + if (a == null || b == null) { + return a == b; + } + if (typeof a != 'object' || typeof b != 'object') { return a === b; } + if (Object.getPrototypeOf(a) !== Object.getPrototypeOf(b)) { + return false; + } + if (Array.isArray(a)) { + if (a.length != b.length) { + return false; + } + let i = 0; + for (; i < a.length; i++) { + if (!eq(a[i], b[i])) { + return false; + } + } + return true; + } const k1 = Object.keys(a); const k2 = Object.keys(b); - return k1.length == k2.length && - k1.every((key) => { - return eq(a[key], b[key]); - }); + if (k1.length != k2.length) { + return false; + } + let key; + for (key of k1) { + if (!eq(a[key], b[key])) { + return false; + } + } + return true; } class PropertySet { @@ -1705,8 +1840,7 @@ } add(declaration) { if (declaration.nam == this.config.shorthand) { - this.declarations.clear(); - this.declarations.set(declaration.nam, declaration); + this.declarations = new Map; } else { // expand shorthand @@ -1729,6 +1863,10 @@ } if (token.typ != 'Whitespace' && token.typ != 'Comment') { if (token.typ == 'Iden' && this.config.keywords.includes(token.val)) { + if (tokens.length == 0) { + tokens.push([]); + current++; + } tokens[current].push(token); } if (token.typ == 'Literal' && token.val == this.config.separator) { @@ -1744,10 +1882,6 @@ this.declarations.delete(this.config.shorthand); for (const values of tokens) { this.config.properties.forEach((property, index) => { - // if (property == declaration.nam) { - // - // return; - // } if (!this.declarations.has(property)) { this.declarations.set(property, { typ: 'Declaration', @@ -1776,30 +1910,20 @@ this.declarations.set(declaration.nam, declaration); return this; } - // declaration.chi = declaration.chi.reduce((acc: Token[], token: Token) => { - // - // if (this.config.types.includes(token.typ) || ('0' == (token).chi && ( - // this.config.types.includes('Length') || - // this.config.types.includes('Angle') || - // this.config.types.includes('Dimension'))) || (token.typ == 'Iden' && this.config.keywords.includes(token.chi))) { - // - // acc.push(token); - // } - // - // return acc; - // }, []); - this.declarations.set(declaration.nam, declaration); } + this.declarations.set(declaration.nam, declaration); return this; } + isShortHand() { + if (this.declarations.has(this.config.shorthand)) { + return this.declarations.size == 1; + } + return this.config.properties.length == this.declarations.size; + } [Symbol.iterator]() { let iterator; const declarations = this.declarations; - if (declarations.size < this.config.properties.length || this.config.properties.some((property, index) => { - return !declarations.has(property) || (index > 0 && - // @ts-ignore - declarations.get(property).val.length != declarations.get(this.config.properties[Math.floor(index / 2)]).val.length); - })) { + if (declarations.size < this.config.properties.length) { iterator = declarations.values(); } else { @@ -1857,17 +1981,20 @@ return acc; }, []) }][Symbol.iterator](); - return { - next() { - return iterator.next(); - } - }; + // return { + // next() { + // + // return iterator.next(); + // } + // } } - return { - next() { - return iterator.next(); - } - }; + return iterator; + // return { + // next() { + // + // return iterator.next(); + // } + // } } } @@ -1882,34 +2009,7 @@ return false; } - function getTokenType(val) { - if (val == 'transparent' || val == 'currentcolor') { - return { - typ: 'Color', - val, - kin: 'lit' - }; - } - if (val.endsWith('%')) { - return { - typ: 'Perc', - val: val.slice(0, -1) - }; - } - return { - typ: isNumber(val) ? 'Number' : 'Iden', - val - }; - } - function parseString(val) { - return val.split(/\s/).map(getTokenType).reduce((acc, curr) => { - if (acc.length > 0) { - acc.push({ typ: 'Whitespace' }); - } - acc.push(curr); - return acc; - }, []); - } + const propertiesConfig = getConfig(); class PropertyMap { config; declarations; @@ -1924,7 +2024,7 @@ } add(declaration) { if (declaration.nam == this.config.shorthand) { - this.declarations.clear(); + this.declarations = new Map; this.declarations.set(declaration.nam, declaration); } else { @@ -2001,8 +2101,7 @@ const defaults = parseString(props.default[0]); if (!(property in tokens)) { tokens[property] = [ - [...defaults - ] + [...defaults] ]; } else { @@ -2036,145 +2135,229 @@ }, new Map); } } - this.declarations.set(declaration.nam, declaration); + // @ts-ignore + const config = propertiesConfig.properties[declaration.nam]; + let property = declaration.nam; + if (config != null) { + property = config.shorthand; + let value = this.declarations.get(property); + if (!(value instanceof PropertySet)) { + // @ts-ignore + this.declarations.set(property, new PropertySet(propertiesConfig.properties[config.shorthand])); + // Token[] + if (value != null) { + // @ts-ignore + this.declarations.get(property).add(value); + } + } + this.declarations.get(property).add(declaration); + } + else { + this.declarations.set(declaration.nam, declaration); + } } return this; } [Symbol.iterator]() { - let requiredCount = Object.keys(this.config.properties).reduce((acc, curr) => this.declarations.has(curr) && this.config.properties[curr].required ? ++acc : acc, 0); - if (requiredCount == 0) { - requiredCount = this.declarations.size; - } - if (requiredCount < this.requiredCount) { - // if (this.declarations.size == 1 && this.declarations.has(this.config.shorthand)) { - // - // this.declarations - // } - return this.declarations.values(); - } - let count = 0; - const separator = this.config.separator; - const tokens = {}; - // @ts-ignore - const valid = Object.entries(this.config.properties).reduce((acc, curr) => { - if (!this.declarations.has(curr[0])) { - if (curr[1].required) { - acc.push(curr[0]); + let iterable; + let requiredCount = 0; + let property; + let isShorthand = true; + for (property of Object.keys(this.config.properties)) { + if (this.config.properties[property].required) { + if (!this.declarations.has(property)) { + isShorthand = false; + break; } - return acc; - } - let current = 0; - const props = this.config.properties[curr[0]]; - // @ts-ignore - for (const val of this.declarations.get(curr[0]).val) { - if (separator != null && separator.typ == val.typ && eq(separator, val)) { - current++; - if (tokens[curr[0]].length == current) { - tokens[curr[0]].push([]); + else { + const val = this.declarations.get(property); + if (val instanceof PropertySet && !val.isShortHand()) { + isShorthand = false; + break; } - continue; - } - if (val.typ == 'Whitespace' || val.typ == 'Comment') { - continue; - } - if (props.multiple && props.separator != null && props.separator.typ == val.typ && eq(val, props.separator)) { - continue; - } - if (matchType(val, curr[1])) { - if (!(curr[0] in tokens)) { - tokens[curr[0]] = [[]]; + else { + requiredCount++; } - // is default value - tokens[curr[0]][current].push(val); - continue; } - acc.push(curr[0]); - break; } - if (count == 0) { - count = current; - } - return acc; - }, []); - if (valid.length > 0 || Object.values(tokens).every(v => v.every(v => v.length == count))) { - return this.declarations.values(); } - const values = Object.entries(tokens).reduce((acc, curr) => { - const props = this.config.properties[curr[0]]; - for (let i = 0; i < curr[1].length; i++) { - if (acc.length == i) { - acc.push([]); - } - let values = curr[1][i].reduce((acc, curr) => { - if (acc.length > 0) { - acc.push({ typ: 'Whitespace' }); + if (requiredCount == 0) { + requiredCount = this.declarations.size; + } + if (!isShorthand || requiredCount < this.requiredCount) { + // @ts-ignore + iterable = this.declarations.values(); + } + else { + let count = 0; + const separator = this.config.separator; + const tokens = {}; + // @ts-ignore + /* 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]); } - acc.push(curr); return acc; - }, []); - if (props.default.includes(curr[1][i].reduce((acc, curr) => acc + renderToken(curr) + ' ', '').trimEnd())) { - continue; } - values = values.filter((val) => { + 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([]); + } + continue; + } if (val.typ == 'Whitespace' || val.typ == 'Comment') { - return false; + continue; } - return !(val.typ == 'Iden' && props.default.includes(val.val)); - }); - if (values.length > 0) { - if ('mapping' in props) { - // @ts-ignore - if (!('constraints' in props) || !('max' in props.constraints) || values.length <= props.constraints.mapping.max) { - let i = values.length; - while (i--) { + 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]] = [[]]; + } + // is default value + tokens[curr[0]][current].push(val); + // continue; + } + else { + acc.push(curr[0]); + break; + } + } + if (count == 0) { + count = current; + } + return acc; + }, []); + count++; + if (!Object.values(tokens).every(v => v.length == count)) { + // @ts-ignore + iterable = this.declarations.values(); + } + else { + const values = Object.entries(tokens).reduce((acc, curr) => { + const props = this.config.properties[curr[0]]; + for (let i = 0; i < curr[1].length; i++) { + if (acc.length == i) { + acc.push([]); + } + let values = curr[1][i].reduce((acc, curr) => { + if (acc.length > 0) { + acc.push({ typ: 'Whitespace' }); + } + acc.push(curr); + return acc; + }, []); + // @todo remove renderToken call + if (props.default.includes(curr[1][i].reduce((acc, curr) => acc + renderToken(curr) + ' ', '').trimEnd())) { + continue; + } + let doFilterDefault = true; + if (curr[0] in propertiesConfig.properties) { + for (let v of values) { + if (!['Whitespace', 'Comment', 'Iden'].includes(v.typ) + || (v.typ == 'Iden' && !this.config.properties[curr[0]].default.includes(v.val))) { + doFilterDefault = false; + break; + } + } + } + // remove default values + values = values.filter((val) => { + if (val.typ == 'Whitespace' || val.typ == 'Comment') { + return false; + } + return !doFilterDefault || !(val.typ == 'Iden' && props.default.includes(val.val)); + }); + if (values.length > 0) { + if ('mapping' in props) { // @ts-ignore - if (values[i].typ == 'Iden' && values[i].val in props.mapping) { - // @ts-ignore - values.splice(i, 1, ...parseString(props.mapping[values[i].val])); + if (!('constraints' in props) || !('max' in props.constraints) || values.length <= props.constraints.mapping.max) { + let i = values.length; + while (i--) { + // @ts-ignore + if (values[i].typ == 'Iden' && values[i].val in props.mapping) { + // @ts-ignore + values.splice(i, 1, ...parseString(props.mapping[values[i].val])); + } + } } } + if ('prefix' in props) { + // @ts-ignore + acc[i].push({ ...props.prefix }); + } + else if (acc[i].length > 0) { + acc[i].push({ typ: 'Whitespace' }); + } + acc[i].push(...values.reduce((acc, curr) => { + if (acc.length > 0) { + // @ts-ignore + acc.push({ ...(props.separator ?? { typ: 'Whitespace' }) }); + } + // @ts-ignore + acc.push(curr); + return acc; + }, [])); } } - if ('prefix' in props) { - // @ts-ignore - acc[i].push({ ...props.prefix }); + return acc; + }, []).reduce((acc, curr) => { + if (acc.length > 0) { + acc.push({ ...separator }); } - else if (acc[i].length > 0) { - acc[i].push({ typ: 'Whitespace' }); + if (curr.length == 0 && this.config.default.length > 0) { + curr.push(...parseString(this.config.default[0]).reduce((acc, curr) => { + if (acc.length > 0) { + acc.push({ typ: 'Whitespace' }); + } + acc.push(curr); + return acc; + }, [])); } - acc[i].push(...values.reduce((acc, curr) => { - if (acc.length > 0) { + acc.push(...curr); + return acc; + }, []); + iterable = [{ + typ: 'Declaration', + nam: this.config.shorthand, + val: values + }][Symbol.iterator](); + } + } + const iterators = []; + return { + // @ts-ignore + next() { + let v = iterable.next(); + while (v.done || v.value instanceof PropertySet) { + if (v.value instanceof PropertySet) { + // @ts-ignore + iterators.push(iterable); + iterable = v.value[Symbol.iterator](); + v = iterable.next(); + } + if (v.done) { + if (iterators.length > 0) { // @ts-ignore - acc.push({ ...(props.separator ?? { typ: 'Whitespace' }) }); + iterable = iterators.pop(); + v = iterable.next(); + } + if (v.done && iterators.length == 0) { + break; } - // @ts-ignore - acc.push(curr); - return acc; - }, [])); - } - } - return acc; - }, []).reduce((acc, curr) => { - if (acc.length > 0) { - acc.push({ ...separator }); - } - if (curr.length == 0) { - curr.push(...this.config.default[0].split(/\s/).map(getTokenType).reduce((acc, curr) => { - if (acc.length > 0) { - acc.push({ typ: 'Whitespace' }); } - acc.push(curr); - return acc; - }, [])); + } + return v; } - acc.push(...curr); - return acc; - }, []); - return [{ - typ: 'Declaration', - nam: this.config.shorthand, - val: values - }][Symbol.iterator](); + }; } } @@ -2184,33 +2367,61 @@ constructor() { this.declarations = new Map; } + set(nam, value) { + return this.add({ typ: 'Declaration', nam, val: Array.isArray(value) ? value : parseString(String(value)) }); + } add(declaration) { if (declaration.typ != 'Declaration') { this.declarations.set(Number(Math.random().toString().slice(2)).toString(36), declaration); return this; } - const propertyName = declaration.nam; + let propertyName = declaration.nam; + let shortHandType; + let shorthand; if (propertyName in config.properties) { // @ts-ignore - const shorthand = config.properties[propertyName].shorthand; - if (!this.declarations.has(shorthand)) { + if ('map' in config.properties[propertyName]) { + shortHandType = 'map'; // @ts-ignore - this.declarations.set(shorthand, new PropertySet(config.properties[shorthand])); + shorthand = config.properties[propertyName].map; } - this.declarations.get(shorthand).add(declaration); - return this; + else { + shortHandType = 'set'; + // @ts-ignore + shorthand = config.properties[propertyName].shorthand; + } + } + else if (propertyName in config.map) { + shortHandType = 'map'; + // @ts-ignore + shorthand = config.map[propertyName].shorthand; } - if (propertyName in config.map) { + // @ts-ignore + if (shortHandType == 'map') { // @ts-ignore - const shorthand = config.map[propertyName].shorthand; if (!this.declarations.has(shorthand)) { // @ts-ignore this.declarations.set(shorthand, new PropertyMap(config.map[shorthand])); } + // @ts-ignore this.declarations.get(shorthand).add(declaration); - return this; + // return this; + } + // @ts-ignore + else if (shortHandType == 'set') { + // @ts-ignore + // const shorthand: string = config.properties[propertyName].shorthand; + if (!this.declarations.has(shorthand)) { + // @ts-ignore + this.declarations.set(shorthand, new PropertySet(config.properties[shorthand])); + } + // @ts-ignore + this.declarations.get(shorthand).add(declaration); + // return this; + } + else { + this.declarations.set(propertyName, declaration); } - this.declarations.set(propertyName, declaration); return this; } [Symbol.iterator]() { @@ -2239,56 +2450,300 @@ } } - const configuration = getConfig(); const combinators = ['+', '>', '~']; const notEndingWith = ['(', '['].concat(combinators); - function wrapNodes(previous, node, match, ast, i, nodeIndex) { - // @ts-ignore - let pSel = match.selector1.reduce(reducer, []).join(','); - // @ts-ignore - let nSel = match.selector2.reduce(reducer, []).join(','); - // @ts-ignore - const wrapper = { ...previous, chi: [], sel: match.match.reduce(reducer, []).join(',') }; - // @ts-ignore - Object.defineProperty(wrapper, 'raw', { - enumerable: false, - writable: true, + function minify(ast, options = {}, recursive = false) { + function wrapNodes(previous, node, match, ast, i, nodeIndex) { // @ts-ignore - value: match.match.map(t => t.slice()) - }); - if (pSel == '&' || pSel === '') { + let pSel = match.selector1.reduce(reducer, []).join(','); + // @ts-ignore + let nSel = match.selector2.reduce(reducer, []).join(','); // @ts-ignore - wrapper.chi.push(...previous.chi); + const wrapper = { ...previous, chi: [], sel: match.match.reduce(reducer, []).join(',') }; // @ts-ignore - if ((nSel == '&' || nSel === '') && hasOnlyDeclarations(previous)) { + Object.defineProperty(wrapper, 'raw', { + enumerable: false, + writable: true, // @ts-ignore - wrapper.chi.push(...node.chi); + value: match.match.map(t => t.slice()) + }); + if (pSel == '&' || pSel === '') { + // @ts-ignore + wrapper.chi.push(...previous.chi); + // @ts-ignore + if ((nSel == '&' || nSel === '') && hasOnlyDeclarations(previous)) { + // @ts-ignore + wrapper.chi.push(...node.chi); + } + else { + // @ts-ignore + wrapper.chi.push(node); + } } else { // @ts-ignore - wrapper.chi.push(node); + wrapper.chi.push(previous, node); } + // @ts-ignore + ast.chi.splice(i, 1, wrapper); + // @ts-ignore + ast.chi.splice(nodeIndex, 1); + // @ts-ignore + previous.sel = pSel; + // @ts-ignore + previous.raw = match.selector1; + // @ts-ignore + node.sel = nSel; + // @ts-ignore + node.raw = match.selector2; + reduceRuleSelector(wrapper); + return wrapper; } - else { + function reducer(acc, curr, index, array) { + // trim :is() + if (array.length == 1 && array[0][0] == ':is(' && array[0].at(-1) == ')') { + curr = curr.slice(1, -1); + } + if (curr[0] == '&') { + if (curr[1] == ' ' && !isIdent(curr[2]) && !isFunction(curr[2])) { + curr.splice(0, 2); + } + else if (combinators.includes(curr[1])) { + curr.splice(0, 1); + } + } + else if (ast.typ == 'Rule' && (isIdent(curr[0]) || isFunction(curr[0]))) { + curr.unshift('&', ' '); + } + acc.push(curr.join('')); + return acc; + } + function diff(n1, n2, options = {}) { + let node1 = n1; + let node2 = n2; + let exchanged = false; + if (node1.chi.length > node2.chi.length) { + const t = node1; + node1 = node2; + node2 = t; + exchanged = true; + } + let i = node1.chi.length; + let j = node2.chi.length; + if (i == 0 || j == 0) { + // @ts-ignore + return null; + } + // @ts-ignore + const raw1 = node1.raw; + // @ts-ignore + const raw2 = node2.raw; + // @ts-ignore + node1 = { ...node1, chi: node1.chi.slice() }; + node2 = { ...node2, chi: node2.chi.slice() }; + if (raw1 != null) { + Object.defineProperty(node1, 'raw', { enumerable: false, writable: true, value: raw1 }); + } + if (raw2 != null) { + Object.defineProperty(node2, 'raw', { enumerable: false, writable: true, value: raw2 }); + } + const intersect = []; + while (i--) { + if (node1.chi[i].typ == 'Comment') { + continue; + } + j = node2.chi.length; + if (j == 0) { + break; + } + while (j--) { + if (node2.chi[j].typ == 'Comment') { + continue; + } + if (node1.chi[i].nam == node2.chi[j].nam) { + if (eq(node1.chi[i], node2.chi[j])) { + intersect.push(node1.chi[i]); + node1.chi.splice(i, 1); + node2.chi.splice(j, 1); + break; + } + } + } + } // @ts-ignore - wrapper.chi.push(previous, node); + const result = (intersect.length == 0 ? null : { + ...node1, + // @ts-ignore + sel: [...new Set([...(n1?.raw?.reduce(reducer, []) || splitRule(n1.sel)).concat(n2?.raw?.reduce(reducer, []) || splitRule(n2.sel))])].join(','), + chi: intersect.reverse() + }); + if (result == null || [n1, n2].reduce((acc, curr) => curr.chi.length == 0 ? acc : acc + render(curr, options).code.length, 0) <= [node1, node2, result].reduce((acc, curr) => curr.chi.length == 0 ? acc : acc + render(curr, options).code.length, 0)) { + // @ts-ignore + return null; + } + return { result, node1: exchanged ? node2 : node1, node2: exchanged ? node2 : node2 }; + } + function matchSelectors(selector1, selector2, parentType) { + let match = [[]]; + const j = Math.min(selector1.reduce((acc, curr) => Math.min(acc, curr.length), selector1.length > 0 ? selector1[0].length : 0), selector2.reduce((acc, curr) => Math.min(acc, curr.length), selector2.length > 0 ? selector2[0].length : 0)); + let i = 0; + let k; + let l; + let token; + let matching = true; + let matchFunction = 0; + let inAttr = 0; + for (; i < j; i++) { + k = 0; + token = selector1[0][i]; + for (; k < selector1.length; k++) { + if (selector1[k][i] != token) { + matching = false; + break; + } + } + if (matching) { + l = 0; + for (; l < selector2.length; l++) { + if (selector2[l][i] != token) { + matching = false; + break; + } + } + } + if (!matching) { + break; + } + if (token == ',') { + match.push([]); + } + else { + if (token.endsWith('(')) { + matchFunction++; + } + if (token.endsWith('[')) { + inAttr++; + } + else if (token == ')') { + matchFunction--; + } + else if (token == ']') { + inAttr--; + } + match.at(-1).push(token); + } + } + // invalid function + if (matchFunction != 0 || inAttr != 0) { + return null; + } + if (parentType != 'Rule') { + for (const part of match) { + if (part.length > 0 && combinators.includes(part[0].charAt(0))) { + return null; + } + } + } + if (match.length > 1) { + console.error(`unsupported multilevel matching`); + console.error({ match, selector1, selector2 }); + return null; + } + for (const part of match) { + while (part.length > 0) { + const token = part.at(-1); + if (token == ' ' || combinators.includes(token) || notEndingWith.includes(token.at(-1))) { + part.pop(); + continue; + } + break; + } + } + if (match.every(t => t.length == 0)) { + return null; + } + if (eq([['&']], match)) { + return null; + } + function reduce(acc, curr) { + if (acc === null) { + return null; + } + let hasCompoundSelector = true; + curr = curr.slice(match[0].length); + while (curr.length > 0) { + if (curr[0] == ' ') { + hasCompoundSelector = false; + curr.unshift('&'); + continue; + } + break; + } + // invalid function match + if (curr.length > 0 && curr[0].endsWith('(') && curr.at(-1) != ')') { + return null; + } + if (curr.length == 1 && combinators.includes(curr[0].charAt(0))) { + return null; + } + if (hasCompoundSelector && curr.length > 0) { + hasCompoundSelector = !['&'].concat(combinators).includes(curr[0].charAt(0)); + } + if (curr[0] == ':is(') { + let inFunction = 0; + let canReduce = true; + const isCompound = curr.reduce((acc, token, index) => { + if (index == 0) { + inFunction++; + canReduce = curr[1] == '&'; + } + else if (token.endsWith('(')) { + if (inFunction == 0) { + canReduce = false; + } + inFunction++; + } + else if (token == ')') { + inFunction--; + } + else if (token == ',') { + if (!canReduce) { + canReduce = curr[index + 1] == '&'; + } + acc.push([]); + } + else + acc.at(-1)?.push(token); + return acc; + }, [[]]); + if (inFunction > 0) { + canReduce = false; + } + if (canReduce) { + curr = isCompound.reduce((acc, curr) => { + if (acc.length > 0) { + acc.push(','); + } + acc.push(...curr); + return acc; + }, []); + } + } + // @todo: check hasCompoundSelector && curr[0] == '&' && curr[1] == ' ' + acc.push(match.length == 0 ? ['&'] : (hasCompoundSelector && curr[0] != '&' && (curr.length == 0 || !combinators.includes(curr[0].charAt(0))) ? ['&'].concat(curr) : curr)); + return acc; + } + // @ts-ignore + selector1 = selector1.reduce(reduce, []); + // @ts-ignore + selector2 = selector2.reduce(reduce, []); + return selector1 == null || selector2 == null ? null : { + eq: eq(selector1, selector2), + match, + selector1, + selector2 + }; } - // @ts-ignore - ast.chi.splice(i, 1, wrapper); - // @ts-ignore - ast.chi.splice(nodeIndex, 1); - // @ts-ignore - previous.sel = pSel; - // @ts-ignore - previous.raw = match.selector1; - // @ts-ignore - node.sel = nSel; - // @ts-ignore - node.raw = match.selector2; - reduceRuleSelector(wrapper); - return wrapper; - } - function deduplicate(ast, options = {}, recursive = false) { // @ts-ignore if (('chi' in ast) && ast.chi?.length > 0) { let i = 0; @@ -2328,7 +2783,8 @@ // @ts-ignore if (options.nestingRules) { // @ts-ignore - if (previous != null && previous.typ == 'Rule') { + if (previous?.typ == 'Rule') { + // @ts-ignore reduceRuleSelector(previous); // @ts-ignore match = matchSelectors(previous.raw, node.raw, ast.typ); @@ -2367,7 +2823,7 @@ nodeIndex = --i; // @ts-ignore previous = ast.chi[nodeIndex]; - deduplicate(wrapper, options, recursive); + minify(wrapper, options, recursive); continue; } // @ts-ignore @@ -2416,13 +2872,24 @@ } else if (combinators.includes(curr[0])) { curr.unshift('&'); + wrap = false; } // @ts-ignore - acc.push(curr.map(t => t.replaceAll('&', node.optimized.optimized[0])).join('')); + acc.push(curr); return acc; }, []); + if (!wrap) { + wrap = selector.some(s => s[0] != '&'); + } + const rule = selector.map(s => { + if (s[0] == '&') { + // @ts-ignore + s[0] = node.optimized.optimized[0]; + } + return s.join(''); + }).join(','); // @ts-ignore - node.sel = (wrap ? node.optimized.optimized[0] : '') + `:is(${selector.join(',')})`; + node.sel = wrap ? node.optimized.optimized[0] + `:is(${rule})` : rule; } } // @ts-ignore @@ -2453,10 +2920,10 @@ // @ts-ignore if (hasDeclaration(node)) { // @ts-ignore - deduplicateRule(node); + minifyRule(node); } else { - deduplicate(node, options, recursive); + minify(node, options, recursive); } i--; previous = node; @@ -2498,10 +2965,10 @@ // @ts-ignore if (hasDeclaration(previous)) { // @ts-ignore - deduplicateRule(previous); + minifyRule(previous); } else { - deduplicate(previous, options, recursive); + minify(previous, options, recursive); } } } @@ -2512,18 +2979,97 @@ if (recursive && node != null && ('chi' in node)) { // @ts-ignore if (node.chi.some(n => n.typ == 'Declaration')) { - deduplicateRule(node); + minifyRule(node); } else { // @ts-ignore if (!(node.typ == 'AtRule' && node.nam != 'font-face')) { - deduplicate(node, options, recursive); + minify(node, options, recursive); } } } } return ast; } + function reduceSelector(selector) { + if (selector.length == 0) { + return null; + } + const optimized = []; + const k = selector.reduce((acc, curr) => acc == 0 ? curr.length : (curr.length == 0 ? acc : Math.min(acc, curr.length)), 0); + let i = 0; + let j; + let match; + for (; i < k; i++) { + const item = selector[0][i]; + match = true; + for (j = 1; j < selector.length; j++) { + if (item != selector[j][i]) { + match = false; + break; + } + } + if (!match) { + break; + } + optimized.push(item); + } + while (optimized.length > 0) { + const last = optimized.at(-1); + if ((last == ' ' || combinators.includes(last))) { + optimized.pop(); + continue; + } + break; + } + selector.forEach((selector) => selector.splice(0, optimized.length)); + // combinator + if (combinators.includes(optimized.at(-1))) { + const combinator = optimized.pop(); + selector.forEach(selector => selector.unshift(combinator)); + } + let reducible = optimized.length == 1; + if (optimized[0] == '&' && optimized[1] == ' ') { + optimized.splice(0, 2); + } + if (optimized.length == 0 || + (optimized[0].charAt(0) == '&' || + selector.length == 1)) { + return { + match: false, + optimized, + selector: selector.map(selector => selector[0] == '&' && selector[1] == ' ' ? selector.slice(2) : selector), + reducible: selector.length > 1 && selector.every((selector) => !combinators.includes(selector[0])) + }; + } + return { + match: true, + optimized, + selector: selector.reduce((acc, curr) => { + let hasCompound = true; + if (hasCompound && curr.length > 0) { + hasCompound = !['&'].concat(combinators).includes(curr[0].charAt(0)); + } + // @ts-ignore + if (hasCompound && curr[0] == ' ') { + hasCompound = false; + curr.unshift('&'); + } + if (curr.length == 0) { + curr.push('&'); + hasCompound = false; + } + if (reducible) { + const chr = curr[0].charAt(0); + // @ts-ignore + reducible = chr == '.' || chr == ':' || isIdentStart(chr.codePointAt(0)); + } + acc.push(hasCompound ? ['&'].concat(curr) : curr); + return acc; + }, []), + reducible: selector.every((selector) => !['>', '+', '~', '&'].includes(selector[0])) + }; + } function hasOnlyDeclarations(node) { let k = node.chi.length; while (k--) { @@ -2546,7 +3092,7 @@ } return true; } - function deduplicateRule(ast) { + function minifyRule(ast) { // @ts-ignore if (!('chi' in ast) || ast.chi?.length <= 1) { return ast; @@ -2554,45 +3100,19 @@ // @ts-ignore const j = ast.chi.length; let k = 0; - let map = new Map; + let properties = new PropertyList(); // @ts-ignore for (; k < j; k++) { // @ts-ignore const node = ast.chi[k]; - if (node.typ == 'Comment') { - // @ts-ignore - map.set(node, node); + if (node.typ == 'Comment' || node.typ == 'Declaration') { + properties.add(node); continue; } - else if (node.typ != 'Declaration') { - break; - } - if (node.nam in configuration.map || - node.nam in configuration.properties) { - // @ts-ignore - const shorthand = node.nam in configuration.map ? configuration.map[node.nam].shorthand : configuration.properties[node.nam].shorthand; - if (!map.has(shorthand)) { - map.set(shorthand, new PropertyList()); - } - map.get(shorthand).add(node); - } - else { - map.set(node.nam, node); - } - } - const children = []; - for (let child of map.values()) { - if (child instanceof PropertyList) { - // @ts-ignore - children.push(...child); - } - else { - // @ts-ignore - children.push(child); - } + break; } // @ts-ignore - ast.chi = children.concat(ast.chi?.slice(k)); + ast.chi = [...properties].concat(ast.chi.slice(k)); return ast; } function splitRule(buffer) { @@ -2718,345 +3238,482 @@ } // } } - function diff(n1, n2, options = {}) { - let node1 = n1; - let node2 = n2; - let exchanged = false; - if (node1.chi.length > node2.chi.length) { - const t = node1; - node1 = node2; - node2 = t; - exchanged = true; - } - let i = node1.chi.length; - let j = node2.chi.length; - if (i == 0 || j == 0) { - // @ts-ignore - return null; - } - // @ts-ignore - const raw1 = node1.raw; - // @ts-ignore - // const optimized1 = node1.optimized; - // @ts-ignore - const raw2 = node2.raw; + + function* walk(node) { // @ts-ignore - // const optimized2 = node2.optimized; - node1 = { ...node1, chi: node1.chi.slice() }; - node2 = { ...node2, chi: node2.chi.slice() }; - if (raw1 != null) { - Object.defineProperty(node1, 'raw', { enumerable: false, writable: true, value: raw1 }); - } - // if (optimized1 != null) { - // Object.defineProperty(node1, 'optimized', {enumerable: false, writable: true, value: optimized1}); - // } - if (raw2 != null) { - Object.defineProperty(node2, 'raw', { enumerable: false, writable: true, value: raw2 }); - } - // if (optimized2 != null) { - // Object.defineProperty(node2, 'optimized', {enumerable: false, writable: true, value: optimized2}); - // } - const intersect = []; - while (i--) { - if (node1.chi[i].typ == 'Comment') { - continue; - } - j = node2.chi.length; - if (j == 0) { - break; + yield* doWalk(node, null, null); + } + function* doWalk(node, parent, root) { + yield { node, parent, root }; + if ('chi' in node) { + for (const child of node.chi) { + yield* doWalk(child, node, (root ?? node)); } - while (j--) { - if (node2.chi[j].typ == 'Comment') { - continue; - } - if (node1.chi[i].nam == node2.chi[j].nam) { - if (eq(node1.chi[i], node2.chi[j])) { - intersect.push(node1.chi[i]); - node1.chi.splice(i, 1); - node2.chi.splice(j, 1); - break; - } - } + } + } + + function* tokenize(iterator) { + let ind = -1; + let lin = 1; + let col = 0; + const position = { + ind: Math.max(ind, 0), + lin: lin, + col: Math.max(col, 1) + }; + let value; + let buffer = ''; + function consumeWhiteSpace() { + let count = 0; + while (isWhiteSpace(iterator.charAt(count + ind + 1).charCodeAt(0))) { + count++; } + next(count); + return count; } - // @ts-ignore - const result = (intersect.length == 0 ? null : { - ...node1, - // @ts-ignore - sel: [...new Set([...(n1?.raw?.reduce(reducer, []) || splitRule(n1.sel)).concat(n2?.raw?.reduce(reducer, []) || splitRule(n2.sel))])].join(','), - chi: intersect.reverse() - }); - if (result == null || [n1, n2].reduce((acc, curr) => curr.chi.length == 0 ? acc : acc + render(curr, options).code.length, 0) <= [node1, node2, result].reduce((acc, curr) => curr.chi.length == 0 ? acc : acc + render(curr, options).code.length, 0)) { - // @ts-ignore - return null; + function pushToken(token, hint) { + const result = { token, hint, position: { ...position }, bytesIn: ind }; + position.ind = ind; + position.lin = lin; + position.col = col == 0 ? 1 : col; + return result; } - return { result, node1: exchanged ? node2 : node1, node2: exchanged ? node2 : node2 }; - } - function matchSelectors(selector1, selector2, parentType) { - let match = [[]]; - const j = Math.min(selector1.reduce((acc, curr) => Math.min(acc, curr.length), selector1.length > 0 ? selector1[0].length : 0), selector2.reduce((acc, curr) => Math.min(acc, curr.length), selector2.length > 0 ? selector2[0].length : 0)); - let i = 0; - let k; - let l; - let token; - let matching = true; - let matchFunction = 0; - let inAttr = 0; - for (; i < j; i++) { - k = 0; - token = selector1[0][i]; - for (; k < selector1.length; k++) { - if (selector1[k][i] != token) { - matching = false; + function* consumeString(quoteStr) { + const quote = quoteStr; + let value; + let hasNewLine = false; + if (buffer.length > 0) { + yield pushToken(buffer); + buffer = ''; + } + buffer += quoteStr; + while (value = peek()) { + if (ind >= iterator.length) { + yield pushToken(buffer, hasNewLine ? 'Bad-string' : 'Unclosed-string'); break; } - } - if (matching) { - l = 0; - for (; l < selector2.length; l++) { - if (selector2[l][i] != token) { - matching = false; + if (value == '\\') { + const sequence = peek(6); + let escapeSequence = ''; + let codepoint; + let i; + for (i = 1; i < sequence.length; i++) { + codepoint = sequence.charCodeAt(i); + if (codepoint == 0x20 || + (codepoint >= 0x61 && codepoint <= 0x66) || + (codepoint >= 0x41 && codepoint <= 0x46) || + (codepoint >= 0x30 && codepoint <= 0x39)) { + escapeSequence += sequence[i]; + if (codepoint == 0x20) { + break; + } + continue; + } break; } + // not hex or new line + // @ts-ignore + if (i == 1 && !isNewLine(codepoint)) { + buffer += sequence[i]; + next(2); + continue; + } + if (escapeSequence.trimEnd().length > 0) { + const codepoint = Number(`0x${escapeSequence.trimEnd()}`); + if (codepoint == 0 || + // leading surrogate + (0xD800 <= codepoint && codepoint <= 0xDBFF) || + // trailing surrogate + (0xDC00 <= codepoint && codepoint <= 0xDFFF)) { + buffer += String.fromCodePoint(0xFFFD); + } + else { + buffer += String.fromCodePoint(codepoint); + } + next(escapeSequence.length + 1); + continue; + } + // buffer += value; + if (ind >= iterator.length) { + // drop '\\' at the end + yield pushToken(buffer); + break; + } + buffer += next(2); + continue; } - } - if (!matching) { - break; - } - if (token == ',') { - match.push([]); - } - else { - if (token.endsWith('(')) { - matchFunction++; - } - if (token.endsWith('[')) { - inAttr++; + if (value == quote) { + buffer += value; + yield pushToken(buffer, hasNewLine ? 'Bad-string' : 'String'); + next(); + // i += value.length; + buffer = ''; + break; } - else if (token == ')') { - matchFunction--; + if (isNewLine(value.charCodeAt(0))) { + hasNewLine = true; } - else if (token == ']') { - inAttr--; + if (hasNewLine && value == ';') { + yield pushToken(buffer, 'Bad-string'); + buffer = ''; + break; } - match.at(-1).push(token); + buffer += value; + // i += value.length; + next(); } } - // invalid function - if (matchFunction != 0 || inAttr != 0) { - return null; - } - if (parentType != 'Rule') { - for (const part of match) { - if (part.length > 0 && combinators.includes(part[0].charAt(0))) { - return null; - } + function peek(count = 1) { + if (count == 1) { + return iterator.charAt(ind + 1); } + return iterator.slice(ind + 1, ind + count + 1); } - if (match.length > 1) { - console.error(`unsupported multilevel matching`); - console.error({ match, selector1, selector2 }); - return null; + function prev(count = 1) { + if (count == 1) { + return ind == 0 ? '' : iterator.charAt(ind - 1); + } + return iterator.slice(ind - 1 - count, ind - 1); } - for (const part of match) { - while (part.length > 0) { - const token = part.at(-1); - if (token == ' ' || combinators.includes(token) || notEndingWith.includes(token.at(-1))) { - part.pop(); - continue; + function next(count = 1) { + let char = ''; + while (count-- > 0 && ind < iterator.length) { + const codepoint = iterator.charCodeAt(++ind); + if (isNaN(codepoint)) { + return char; + } + char += iterator.charAt(ind); + if (isNewLine(codepoint)) { + lin++; + col = 0; + } + else { + col++; } - break; } + return char; } - if (match.every(t => t.length == 0)) { - return null; - } - if (eq([['&']], match)) { - return null; - } - function reduce(acc, curr) { - if (acc === null) { - return null; - } - let hasCompoundSelector = true; - curr = curr.slice(match[0].length); - while (curr.length > 0) { - if (curr[0] == ' ') { - hasCompoundSelector = false; - curr.unshift('&'); - continue; + while (value = next()) { + if (ind >= iterator.length) { + if (buffer.length > 0) { + yield pushToken(buffer); + buffer = ''; } break; } - // invalid function match - if (curr.length > 0 && curr[0].endsWith('(') && curr.at(-1) != ')') { - return null; - } - if (curr.length == 1 && combinators.includes(curr[0].charAt(0))) { - return null; - } - if (hasCompoundSelector && curr.length > 0) { - hasCompoundSelector = !['&'].concat(combinators).includes(curr[0].charAt(0)); + if (isWhiteSpace(value.charCodeAt(0))) { + if (buffer.length > 0) { + yield pushToken(buffer); + buffer = ''; + } + while (value = next()) { + if (ind >= iterator.length) { + break; + } + if (!isWhiteSpace(value.charCodeAt(0))) { + break; + } + } + yield pushToken('', 'Whitespace'); + buffer = ''; + if (ind >= iterator.length) { + break; + } } - if (curr[0] == ':is(') { - let inFunction = 0; - let canReduce = true; - const isCompound = curr.reduce((acc, token, index) => { - if (index == 0) { - inFunction++; - canReduce = curr[1] == '&'; + switch (value) { + case '/': + if (buffer.length > 0) { + yield pushToken(buffer); + buffer = ''; + if (peek() != '*') { + yield pushToken(value); + break; + } } - else if (token.endsWith('(')) { - if (inFunction == 0) { - canReduce = false; + buffer += value; + if (peek() == '*') { + buffer += '*'; + // i++; + next(); + while (value = next()) { + if (ind >= iterator.length) { + yield pushToken(buffer, 'Bad-comment'); + break; + } + if (value == '\\') { + buffer += value; + value = next(); + if (ind >= iterator.length) { + yield pushToken(buffer, 'Bad-comment'); + break; + } + buffer += value; + continue; + } + if (value == '*') { + buffer += value; + value = next(); + if (ind >= iterator.length) { + yield pushToken(buffer, 'Bad-comment'); + break; + } + buffer += value; + if (value == '/') { + yield pushToken(buffer, 'Comment'); + buffer = ''; + break; + } + } + else { + buffer += value; + } } - inFunction++; } - else if (token == ')') { - inFunction--; + break; + case '<': + if (buffer.length > 0) { + yield pushToken(buffer); + buffer = ''; } - else if (token == ',') { - if (!canReduce) { - canReduce = curr[index + 1] == '&'; - } - acc.push([]); + buffer += value; + value = next(); + if (ind >= iterator.length) { + break; } - else - acc.at(-1)?.push(token); - return acc; - }, [[]]); - if (inFunction > 0) { - canReduce = false; - } - if (canReduce) { - curr = isCompound.reduce((acc, curr) => { - if (acc.length > 0) { - acc.push(','); + if (peek(3) == '!--') { + while (value = next()) { + if (ind >= iterator.length) { + break; + } + buffer += value; + if (value == '>' && prev(2) == '--') { + yield pushToken(buffer, 'CDOCOMM'); + buffer = ''; + break; + } } - acc.push(...curr); - return acc; - }, []); - } - } - // @todo: check hasCompoundSelector && curr[0] == '&' && curr[1] == ' ' - acc.push(match.length == 0 ? ['&'] : (hasCompoundSelector && curr[0] != '&' && (curr.length == 0 || !combinators.includes(curr[0].charAt(0))) ? ['&'].concat(curr) : curr)); - return acc; - } - // @ts-ignore - selector1 = selector1.reduce(reduce, []); - // @ts-ignore - selector2 = selector2.reduce(reduce, []); - return selector1 == null || selector2 == null ? null : { - eq: eq(selector1, selector2), - match, - selector1, - selector2 - }; - } - function reduceSelector(selector) { - if (selector.length == 0) { - return null; - } - const optimized = []; - const k = selector.reduce((acc, curr) => acc == 0 ? curr.length : (curr.length == 0 ? acc : Math.min(acc, curr.length)), 0); - let i = 0; - let j; - let match; - for (; i < k; i++) { - const item = selector[0][i]; - match = true; - for (j = 1; j < selector.length; j++) { - if (item != selector[j][i]) { - match = false; + } + if (ind >= iterator.length) { + yield pushToken(buffer, 'BADCDO'); + buffer = ''; + } break; - } - } - if (!match) { - break; - } - optimized.push(item); - } - while (optimized.length > 0) { - const last = optimized.at(-1); - if ((last == ' ' || combinators.includes(last))) { - optimized.pop(); - continue; - } - break; - } - selector.forEach((selector) => selector.splice(0, optimized.length)); - // combinator - if (combinators.includes(optimized.at(-1))) { - const combinator = optimized.pop(); - selector.forEach(selector => selector.unshift(combinator)); - } - let reducible = optimized.length == 1; - if (optimized[0] == '&' && optimized[1] == ' ') { - optimized.splice(0, 2); - } - if (optimized.length == 0 || - (optimized[0].charAt(0) == '&' || - selector.length == 1)) { - return { - match: false, - optimized, - selector: selector.map(selector => selector[0] == '&' && selector[1] == ' ' ? selector.slice(2) : selector), - reducible: selector.length > 1 && selector.every((selector) => !combinators.includes(selector[0])) - }; - } - return { - match: true, - optimized, - selector: selector.reduce((acc, curr) => { - let hasCompound = true; - if (hasCompound && curr.length > 0) { - hasCompound = !['&'].concat(combinators).includes(curr[0].charAt(0)); - } - // @ts-ignore - if (hasCompound && curr[0] == ' ') { - hasCompound = false; - curr.unshift('&'); - } - if (curr.length == 0) { - curr.push('&'); - hasCompound = false; - } - if (reducible) { - const chr = curr[0].charAt(0); + case '\\': + value = next(); + // EOF + if (ind + 1 >= iterator.length) { + // end of stream ignore \\ + yield pushToken(buffer); + buffer = ''; + break; + } + buffer += value; + break; + case '"': + case "'": + yield* consumeString(value); + break; + case '~': + case '|': + if (buffer.length > 0) { + yield pushToken(buffer); + buffer = ''; + } + buffer += value; + value = next(); + if (ind >= iterator.length) { + yield pushToken(buffer); + buffer = ''; + break; + } + if (value == '=') { + buffer += value; + yield pushToken(buffer, buffer[0] == '~' ? 'Includes' : 'Dash-matches'); + buffer = ''; + break; + } + yield pushToken(buffer); + while (isWhiteSpace(value.charCodeAt(0))) { + value = next(); + } + buffer = value; + break; + case '>': + if (buffer !== '') { + yield pushToken(buffer); + buffer = ''; + } + yield pushToken('', 'Gt'); + consumeWhiteSpace(); + break; + case '.': + const codepoint = peek().charCodeAt(0); + if (!isDigit(codepoint) && buffer !== '') { + yield pushToken(buffer); + buffer = value; + break; + } + buffer += value; + break; + case '+': + case ':': + case ',': + case '=': + if (buffer.length > 0) { + yield pushToken(buffer); + buffer = ''; + } + if (value == ':' && ':' == peek()) { + buffer += value + next(); + break; + } + yield pushToken(value); + buffer = ''; + if (value == '+' && isWhiteSpace(peek().charCodeAt(0))) { + yield pushToken(next()); + } + while (isWhiteSpace(peek().charCodeAt(0))) { + next(); + } + break; + case ')': + if (buffer.length > 0) { + yield pushToken(buffer); + buffer = ''; + } + yield pushToken('', 'End-parens'); + break; + case '(': + if (buffer.length == 0) { + yield pushToken('', 'Start-parens'); + break; + } + buffer += value; // @ts-ignore - reducible = chr == '.' || chr == ':' || isIdentStart(chr.codePointAt(0)); - } - acc.push(hasCompound ? ['&'].concat(curr) : curr); - return acc; - }, []), - reducible: selector.every((selector) => !['>', '+', '~', '&'].includes(selector[0])) - }; - } - function reducer(acc, curr, index, array) { - // trim :is() - if (array.length == 1 && array[0][0] == ':is(' && array[0].at(-1) == ')') { - curr = curr.slice(1, -1); - } - if (curr[0] == '&') { - if (curr[1] == ' ') { - curr.splice(0, 2); - } - else if (combinators.includes(curr[1])) { - curr.splice(0, 1); + if (buffer == 'url(') { + yield pushToken(buffer); + buffer = ''; + // consume either string or url token + let whitespace = ''; + value = peek(); + while (isWhiteSpace(value.charCodeAt(0))) { + whitespace += value; + } + if (whitespace.length > 0) { + next(whitespace.length); + } + value = peek(); + if (value == '"' || value == "'") { + yield* consumeString(next()); + break; + } + else { + buffer = ''; + do { + let cp = value.charCodeAt(0); + // EOF - + if (cp == null) { + yield pushToken('', 'Bad-url-token'); + break; + } + // ')' + if (cp == 0x29 || cp == null) { + if (buffer.length == 0) { + yield pushToken(buffer, 'Bad-url-token'); + } + else { + yield pushToken(buffer, 'Url-token'); + } + if (cp != null) { + yield pushToken(next()); + } + break; + } + if (isWhiteSpace(cp)) { + whitespace = next(); + while (true) { + value = peek(); + cp = value.charCodeAt(0); + if (isWhiteSpace(cp)) { + whitespace += value; + continue; + } + break; + } + if (cp == null || cp == 0x29) { + continue; + } + // bad url token + buffer += next(whitespace.length); + do { + value = peek(); + cp = value.charCodeAt(0); + if (cp == null || cp == 0x29) { + break; + } + buffer += next(); + } while (true); + yield pushToken(buffer, 'Bad-url-token'); + continue; + } + buffer += next(); + value = peek(); + } while (true); + buffer = ''; + } + break; + } + yield pushToken(buffer); + buffer = ''; + break; + case '[': + case ']': + case '{': + case '}': + case ';': + if (buffer.length > 0) { + yield pushToken(buffer); + buffer = ''; + } + yield pushToken(value); + break; + case '!': + if (buffer.length > 0) { + yield pushToken(buffer); + buffer = ''; + } + const important = peek(9); + if (important == 'important') { + yield pushToken('', 'Important'); + next(9); + buffer = ''; + break; + } + buffer = '!'; + break; + default: + buffer += value; + break; } } - acc.push(curr.join('')); - return acc; + if (buffer.length > 0) { + yield pushToken(buffer); + } } const urlTokenMatcher = /^(["']?)[a-zA-Z0-9_/.-][a-zA-Z0-9_/:.#?-]+(\1)$/; const funcLike = ['Start-parens', 'Func', 'UrlFunc', 'Pseudo-class-func']; + /** + * + * @param iterator + * @param opt + */ async function parse$1(iterator, opt = {}) { const errors = []; const options = { src: '', sourcemap: false, - compress: false, + minify: true, nestingRules: false, resolveImport: false, resolveUrls: false, @@ -3066,208 +3723,64 @@ if (options.resolveImport) { options.resolveUrls = true; } - let ind = -1; - let lin = 1; - let col = 0; - const tokens = []; const src = options.src; const stack = []; const ast = { typ: "StyleSheet", chi: [] }; - const position = { - ind: Math.max(ind, 0), - lin: lin, - col: Math.max(col, 1) - }; - let value; - let buffer = ''; - let total = iterator.length; - let bytesIn = total; + let tokens = []; let map = new Map; + let bytesIn = 0; let context = ast; if (options.sourcemap) { ast.loc = { sta: { - ind: ind, - lin: lin, - col: col + ind: 0, + lin: 1, + col: 1 }, src: '' }; } - function getType(val) { - if (val === '') { - throw new Error('empty string?'); - } - if (val == ':') { - return { typ: 'Colon' }; - } - if (val == ')') { - return { typ: 'End-parens' }; - } - if (val == '(') { - return { typ: 'Start-parens' }; - } - if (val == '=') { - return { typ: 'Delim', val }; - } - if (val == ';') { - return { typ: 'Semi-colon' }; + async function parseNode(results) { + let tokens = results.map(mapToken); + let i; + let loc; + for (i = 0; i < tokens.length; i++) { + if (tokens[i].typ == 'Comment') { + // @ts-ignore + context.chi.push(tokens[i]); + const position = map.get(tokens[i]); + loc = { + sta: position, + src + }; + if (options.sourcemap) { + tokens[i].loc = loc; + } + } + else if (tokens[i].typ != 'Whitespace') { + break; + } } - if (val == ',') { - return { typ: 'Comma' }; + tokens = tokens.slice(i); + if (tokens.length == 0) { + return null; } - if (val == '<') { - return { typ: 'Lt' }; + let delim = tokens.at(-1); + if (delim.typ == 'Semi-colon' || delim.typ == 'Block-start' || delim.typ == 'Block-end') { + tokens.pop(); } - if (val == '>') { - return { typ: 'Gt' }; + else { + delim = { typ: 'Semi-colon' }; } - if (isPseudo(val)) { - return val.endsWith('(') ? { - typ: 'Pseudo-class-func', - val: val.slice(0, -1), - chi: [] - } - : { - typ: 'Pseudo-class', - val - }; + // @ts-ignore + while (['Whitespace', 'Bad-string', 'Bad-comment'].includes(tokens.at(-1)?.typ)) { + tokens.pop(); } - if (isAtKeyword(val)) { - return { - typ: 'At-rule', - val: val.slice(1) - }; - } - if (isFunction(val)) { - val = val.slice(0, -1); - return { - typ: val == 'url' ? 'UrlFunc' : 'Func', - val, - chi: [] - }; - } - if (isNumber(val)) { - return { - typ: 'Number', - val - }; - } - if (isDimension(val)) { - return parseDimension(val); - } - if (isPercentage(val)) { - return { - typ: 'Perc', - val: val.slice(0, -1) - }; - } - if (val == 'currentColor') { - return { - typ: 'Color', - val, - kin: 'lit' - }; - } - if (isIdent(val)) { - return { - typ: 'Iden', - val - }; - } - if (val.charAt(0) == '#' && isHash(val)) { - return { - typ: 'Hash', - val - }; - } - if ('"\''.includes(val.charAt(0))) { - return { - typ: 'Unclosed-string', - val - }; - } - return { - typ: 'Literal', - val - }; - } - // consume and throw away - function consume(open, close) { - let count = 1; - let chr; - while (true) { - chr = next(); - if (chr == '\\') { - if (peek() === '') { - break; - } - continue; - } - else if (chr == '/' && peek() == '*') { - next(); - while (true) { - chr = next(); - if (chr === '') { - break; - } - if (chr == '*' && peek() == '/') { - next(); - break; - } - } - } - else if (chr == close) { - count--; - } - else if (chr == open) { - count++; - } - if (chr === '' || count == 0) { - break; - } - } - } - async function parseNode(tokens) { - let i; - let loc; - for (i = 0; i < tokens.length; i++) { - if (tokens[i].typ == 'Comment') { - // @ts-ignore - context.chi.push(tokens[i]); - const position = map.get(tokens[i]); - loc = { - sta: position, - src - }; - if (options.sourcemap) { - tokens[i].loc = loc; - } - } - else if (tokens[i].typ != 'Whitespace') { - break; - } - } - tokens = tokens.slice(i); - if (tokens.length == 0) { - return null; - } - let delim = tokens.at(-1); - if (delim.typ == 'Semi-colon' || delim.typ == 'Block-start' || delim.typ == 'Block-end') { - tokens.pop(); - } - else { - delim = { typ: 'Semi-colon' }; - } - // @ts-ignore - while (['Whitespace', 'Bad-string', 'Bad-comment'].includes(tokens.at(-1)?.typ)) { - tokens.pop(); - } - if (tokens.length == 0) { - return null; + if (tokens.length == 0) { + return null; } if (tokens[0]?.typ == 'At-rule') { const atRule = tokens.shift(); @@ -3329,7 +3842,7 @@ // @ts-ignore const root = await options.load(url, options.src).then((src) => { return parse$1(src, Object.assign({}, options, { - compress: false, + minify: false, // @ts-ignore src: options.resolve(url, options.src).absolute })); @@ -3380,626 +3893,336 @@ // 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; - } - } + // 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, 'Rule', { compress: options.compress }).reduce((acc, curr, index, array) => { + parseTokens(tokens, { minify: options.minify }).reduce((acc, curr, index, array) => { if (curr.typ == 'Whitespace') { - if (array[index - 1]?.val == '+' || array[index + 1]?.val == '+') { + if (array[index - 1]?.typ == 'Gt' || + array[index + 1]?.typ == 'Gt' || + combinators.includes(array[index - 1]?.val) || + combinators.includes(array[index + 1]?.val)) { return acc; } } - let t = renderToken(curr, { compress: true }); + let t = renderToken(curr, { minify: true }); if (t == ',') { acc.push([]); } - else { - acc[acc.length - 1].push(t); - } - return acc; - }, [[]]).reduce((acc, curr) => { - acc.set(curr.join(''), curr); - return acc; - }, uniq); - const node = { - typ: 'Rule', - // @ts-ignore - sel: [...uniq.keys()].join(','), - chi: [] - }; - let raw = [...uniq.values()]; - Object.defineProperty(node, 'raw', { enumerable: false, writable: true, value: raw }); - loc = { - sta: position, - src - }; - if (options.sourcemap) { - node.loc = loc; - } - // @ts-ignore - context.chi.push(node); - return node; - } - else { - // declaration - // @ts-ignore - let name = null; - // @ts-ignore - let value = null; - for (let i = 0; i < tokens.length; i++) { - if (tokens[i].typ == 'Comment') { - continue; - } - if (tokens[i].typ == 'Colon') { - name = tokens.slice(0, i); - value = parseTokens(tokens.slice(i + 1), 'Declaration', { - parseColor: true, - src: options.src, - resolveUrls: options.resolveUrls, - resolve: options.resolve, - cwd: options.cwd - }); - } - } - if (name == null) { - name = tokens; - } - const position = map.get(name[0]); - if (name.length > 0) { - for (let i = 1; i < name.length; i++) { - if (name[i].typ != 'Whitespace' && name[i].typ != 'Comment') { - errors.push({ - action: 'drop', - message: 'invalid declaration', - location: { src, ...position } - }); - return null; - } - } - } - if (value == null) { - errors.push({ action: 'drop', message: 'invalid declaration', location: { src, ...position } }); - return null; - } - if (value.length == 0) { - errors.push({ action: 'drop', message: 'invalid declaration', location: { src, ...position } }); - return null; - } - const node = { - typ: 'Declaration', - // @ts-ignore - nam: renderToken(name.shift(), { removeComments: true }), - // @ts-ignore - val: value - }; - while (node.val[0]?.typ == 'Whitespace') { - node.val.shift(); - } - if (node.val.length == 0) { - errors.push({ action: 'drop', message: 'invalid declaration', location: { src, ...position } }); - return null; - } - // @ts-ignore - context.chi.push(node); - return null; - } - } - } - function peek(count = 1) { - if (count == 1) { - return iterator.charAt(ind + 1); - } - return iterator.slice(ind + 1, ind + count + 1); - } - function prev(count = 1) { - if (count == 1) { - return ind == 0 ? '' : iterator.charAt(ind - 1); - } - return iterator.slice(ind - 1 - count, ind - 1); - } - function next(count = 1) { - let char = ''; - while (count-- > 0 && ind < total) { - const codepoint = iterator.charCodeAt(++ind); - if (isNaN(codepoint)) { - return char; - } - char += iterator.charAt(ind); - if (isNewLine(codepoint)) { - lin++; - col = 0; - } - else { - col++; - } - } - return char; - } - function pushToken(token) { - tokens.push(token); - map.set(token, { ...position }); - position.ind = ind; - position.lin = lin; - position.col = col == 0 ? 1 : col; - } - function consumeWhiteSpace() { - let count = 0; - while (isWhiteSpace(iterator.charAt(count + ind + 1).charCodeAt(0))) { - count++; - } - next(count); - return count; - } - function consumeString(quoteStr) { - const quote = quoteStr; - let value; - let hasNewLine = false; - if (buffer.length > 0) { - pushToken(getType(buffer)); - buffer = ''; - } - buffer += quoteStr; - while (ind < total) { - value = peek(); - if (ind >= total) { - pushToken({ typ: hasNewLine ? 'Bad-string' : 'Unclosed-string', val: buffer }); - break; - } - if (value == '\\') { - const sequence = peek(6); - let escapeSequence = ''; - let codepoint; - let i; - for (i = 1; i < sequence.length; i++) { - codepoint = sequence.charCodeAt(i); - if (codepoint == 0x20 || - (codepoint >= 0x61 && codepoint <= 0x66) || - (codepoint >= 0x41 && codepoint <= 0x46) || - (codepoint >= 0x30 && codepoint <= 0x39)) { - escapeSequence += sequence[i]; - if (codepoint == 0x20) { - break; - } - continue; - } - break; - } - // not hex or new line - // @ts-ignore - if (i == 1 && !isNewLine(codepoint)) { - buffer += sequence[i]; - next(2); - continue; - } - if (escapeSequence.trimEnd().length > 0) { - const codepoint = Number(`0x${escapeSequence.trimEnd()}`); - if (codepoint == 0 || - // leading surrogate - (0xD800 <= codepoint && codepoint <= 0xDBFF) || - // trailing surrogate - (0xDC00 <= codepoint && codepoint <= 0xDFFF)) { - buffer += String.fromCodePoint(0xFFFD); - } - else { - buffer += String.fromCodePoint(codepoint); - } - next(escapeSequence.length + 1); - continue; - } - // buffer += value; - if (ind >= total) { - // drop '\\' at the end - pushToken(getType(buffer)); - break; - } - buffer += next(2); - continue; - } - if (value == quote) { - buffer += value; - pushToken({ typ: hasNewLine ? 'Bad-string' : 'String', val: buffer }); - next(); - // i += value.length; - buffer = ''; - break; - } - if (isNewLine(value.charCodeAt(0))) { - hasNewLine = true; - } - if (hasNewLine && value == ';') { - pushToken({ typ: 'Bad-string', val: buffer }); - buffer = ''; - break; - } - buffer += value; - // i += value.length; - next(); - } - } - while (ind < total) { - value = next(); - if (ind >= total) { - if (buffer.length > 0) { - pushToken(getType(buffer)); - buffer = ''; - } - break; - } - if (isWhiteSpace(value.charCodeAt(0))) { - if (buffer.length > 0) { - pushToken(getType(buffer)); - buffer = ''; - } - while (ind < total) { - value = next(); - if (ind >= total) { - break; - } - if (!isWhiteSpace(value.charCodeAt(0))) { - break; - } - } - pushToken({ typ: 'Whitespace' }); - buffer = ''; - if (ind >= total) { - break; - } - } - switch (value) { - case '/': - if (buffer.length > 0 && tokens.at(-1)?.typ == 'Whitespace') { - pushToken(getType(buffer)); - buffer = ''; - if (peek() != '*') { - pushToken(getType(value)); - break; - } - } - buffer += value; - if (peek() == '*') { - buffer += '*'; - // i++; - next(); - while (ind < total) { - value = next(); - if (ind >= total) { - pushToken({ - typ: 'Bad-comment', val: buffer - }); - break; - } - if (value == '\\') { - buffer += value; - value = next(); - if (ind >= total) { - pushToken({ - typ: 'Bad-comment', - val: buffer - }); - break; - } - buffer += value; - continue; - } - if (value == '*') { - buffer += value; - value = next(); - if (ind >= total) { - pushToken({ - typ: 'Bad-comment', val: buffer - }); - break; - } - buffer += value; - if (value == '/') { - pushToken({ typ: 'Comment', val: buffer }); - buffer = ''; - break; - } - } - else { - buffer += value; - } - } - } - break; - case '<': - if (buffer.length > 0) { - pushToken(getType(buffer)); - buffer = ''; - } - buffer += value; - value = next(); - if (ind >= total) { - break; - } - if (peek(3) == '!--') { - while (ind < total) { - value = next(); - if (ind >= total) { - break; - } - buffer += value; - if (value == '>' && prev(2) == '--') { - pushToken({ - typ: 'CDOCOMM', - val: buffer - }); - buffer = ''; - break; - } - } - } - if (ind >= total) { - pushToken({ typ: 'BADCDO', val: buffer }); - buffer = ''; - } - break; - case '\\': - value = next(); - // EOF - if (ind + 1 >= total) { - // end of stream ignore \\ - pushToken(getType(buffer)); - buffer = ''; - break; - } - buffer += value; - break; - case '"': - case "'": - consumeString(value); - break; - case '~': - case '|': - if (tokens.at(-1)?.typ == 'Whitespace') { - tokens.pop(); - } - if (buffer.length > 0) { - pushToken(getType(buffer)); - buffer = ''; - } - buffer += value; - value = next(); - if (ind >= total) { - pushToken(getType(buffer)); - buffer = ''; - break; - } - if (value == '=') { - buffer += value; - pushToken({ - typ: buffer[0] == '~' ? 'Includes' : 'Dash-matches', - val: buffer - }); - buffer = ''; - break; - } - pushToken(getType(buffer)); - while (isWhiteSpace(value.charCodeAt(0))) { - value = next(); - } - buffer = value; - break; - case '>': - if (buffer !== '') { - pushToken(getType(buffer)); - buffer = ''; - } - if (tokens[tokens.length - 1]?.typ == 'Whitespace') { - tokens.pop(); - } - pushToken({ typ: 'Gt' }); - consumeWhiteSpace(); - break; - case '.': - const codepoint = peek().charCodeAt(0); - if (!isDigit(codepoint) && buffer !== '') { - pushToken(getType(buffer)); - buffer = value; - break; - } - buffer += value; - break; - case '+': - case ':': - case ',': - case '=': - if (buffer.length > 0) { - pushToken(getType(buffer)); - buffer = ''; - } - if (value == ':' && ':' == peek()) { - buffer += value + next(); - break; - } - pushToken(getType(value)); - buffer = ''; - if (value == '+' && isWhiteSpace(peek().charCodeAt(0))) { - pushToken(getType(next())); - } - while (isWhiteSpace(peek().charCodeAt(0))) { - next(); + else { + acc[acc.length - 1].push(t); + } + return acc; + }, [[]]).reduce((acc, curr) => { + acc.set(curr.join(''), curr); + return acc; + }, uniq); + const node = { + typ: 'Rule', + // @ts-ignore + sel: [...uniq.keys()].join(','), + chi: [] + }; + let raw = [...uniq.values()]; + Object.defineProperty(node, 'raw', { enumerable: false, writable: true, value: raw }); + loc = { + sta: position, + src + }; + if (options.sourcemap) { + node.loc = loc; } - break; - case ')': - if (buffer.length > 0) { - pushToken(getType(buffer)); - buffer = ''; + // @ts-ignore + context.chi.push(node); + return node; + } + else { + // declaration + // @ts-ignore + let name = null; + // @ts-ignore + let value = null; + for (let i = 0; i < tokens.length; i++) { + if (tokens[i].typ == 'Comment') { + continue; + } + if (tokens[i].typ == 'Colon') { + name = tokens.slice(0, i); + value = parseTokens(tokens.slice(i + 1), { + parseColor: true, + src: options.src, + resolveUrls: options.resolveUrls, + resolve: options.resolve, + cwd: options.cwd + }); + } } - pushToken({ typ: 'End-parens' }); - break; - case '(': - if (buffer.length == 0) { - pushToken({ typ: 'Start-parens' }); + if (name == null) { + name = tokens; } - else { - buffer += value; - pushToken(getType(buffer)); - buffer = ''; - const token = tokens[tokens.length - 1]; - if (token.typ == 'UrlFunc') { - // consume either string or url token - let whitespace = ''; - value = peek(); - while (isWhiteSpace(value.charCodeAt(0))) { - whitespace += value; - } - if (whitespace.length > 0) { - next(whitespace.length); - } - value = peek(); - if (value == '"' || value == "'") { - consumeString(next()); - let token = tokens[tokens.length - 1]; - if (['String', 'Literal'].includes(token.typ) && urlTokenMatcher.test(token.val)) { - if (token.val.slice(1, 6) != 'data:') { - if (token.typ == 'String') { - token.val = token.val.slice(1, -1); - } - // @ts-ignore - token.typ = 'Url-token'; - } - } - break; - } - else { - buffer = ''; - do { - let cp = value.charCodeAt(0); - // EOF - - if (cp == null) { - pushToken({ typ: 'Bad-url-token', val: buffer }); - break; - } - // ')' - if (cp == 0x29 || cp == null) { - if (buffer.length == 0) { - pushToken({ typ: 'Bad-url-token', val: '' }); - } - else { - pushToken({ typ: 'Url-token', val: buffer }); - } - if (cp != null) { - pushToken(getType(next())); - } - break; - } - if (isWhiteSpace(cp)) { - whitespace = next(); - while (true) { - value = peek(); - cp = value.charCodeAt(0); - if (isWhiteSpace(cp)) { - whitespace += value; - continue; - } - break; - } - if (cp == null || cp == 0x29) { - continue; - } - // bad url token - buffer += next(whitespace.length); - do { - value = peek(); - cp = value.charCodeAt(0); - if (cp == null || cp == 0x29) { - break; - } - buffer += next(); - } while (true); - pushToken({ typ: 'Bad-url-token', val: buffer }); - continue; - } - buffer += next(); - value = peek(); - } while (true); - buffer = ''; + const position = map.get(name[0]); + if (name.length > 0) { + for (let i = 1; i < name.length; i++) { + if (name[i].typ != 'Whitespace' && name[i].typ != 'Comment') { + errors.push({ + action: 'drop', + message: 'invalid declaration', + location: { src, ...position } + }); + return null; } } } - break; - case '[': - case ']': - case '{': - case '}': - case ';': - if (buffer.length > 0) { - pushToken(getType(buffer)); - buffer = ''; + if (value == null) { + errors.push({ + action: 'drop', + message: 'invalid declaration', + location: { src, ...position } + }); + return null; } - pushToken(getBlockType(value)); - let node = null; - if (value == '{' || value == ';') { - node = await parseNode(tokens); - if (node != null) { - stack.push(node); - // @ts-ignore - context = node; - } - else if (value == '{') { - // node == null - // consume and throw away until the closing '}' or EOF - consume('{', '}'); - } - tokens.length = 0; - map.clear(); + if (value.length == 0) { + errors.push({ + action: 'drop', + message: 'invalid declaration', + location: { src, ...position } + }); + return null; } - else if (value == '}') { - await parseNode(tokens); - const previousNode = stack.pop(); + const node = { + typ: 'Declaration', // @ts-ignore - context = stack[stack.length - 1] || ast; + nam: renderToken(name.shift(), { removeComments: true }), // @ts-ignore - if (options.removeEmpty && previousNode != null && previousNode.chi.length == 0 && context.chi[context.chi.length - 1] == previousNode) { - context.chi.pop(); - } - tokens.length = 0; - map.clear(); - buffer = ''; - } - break; - case '!': - if (buffer.length > 0) { - pushToken(getType(buffer)); - buffer = ''; + val: value + }; + while (node.val[0]?.typ == 'Whitespace') { + node.val.shift(); } - const important = peek(9); - if (important == 'important') { - if (tokens[tokens.length - 1]?.typ == 'Whitespace') { - tokens.pop(); - } - pushToken({ typ: 'Important' }); - next(9); - buffer = ''; - break; + if (node.val.length == 0) { + errors.push({ + action: 'drop', + message: 'invalid declaration', + location: { src, ...position } + }); + return null; } - buffer = '!'; - break; - default: - buffer += value; - break; + // @ts-ignore + context.chi.push(node); + return null; + } } } - if (buffer.length > 0) { - pushToken(getType(buffer)); + function mapToken(token) { + const node = getTokenType(token.token, token.hint); + map.set(node, token.position); + return node; + } + const iter = tokenize(iterator); + let item; + while (true) { + item = iter.next().value; + if (item == null) { + break; + } + tokens.push(item); + bytesIn = item.bytesIn; + if (item.token == ';' || item.token == '{') { + let node = await parseNode(tokens); + if (node != null) { + stack.push(node); + // @ts-ignore + context = node; + } + else if (item.token == '{') { + // node == null + // consume and throw away until the closing '}' or EOF + let inBlock = 1; + do { + item = iter.next().value; + if (item == null) { + break; + } + if (item.token == '{') { + inBlock++; + } + else if (item.token == '}') { + inBlock--; + } + } while (inBlock != 0); + } + tokens = []; + map = new Map; + } + else if (item.token == '}') { + await parseNode(tokens); + const previousNode = stack.pop(); + // @ts-ignore + context = stack[stack.length - 1] || ast; + // @ts-ignore + if (options.removeEmpty && previousNode != null && previousNode.chi.length == 0 && context.chi[context.chi.length - 1] == previousNode) { + context.chi.pop(); + } + tokens = []; + map = new Map; + } } if (tokens.length > 0) { await parseNode(tokens); } - if (options.compress) { + if (options.minify) { if (ast.chi.length > 0) { - deduplicate(ast, options, true); + minify(ast, options, true); } } return { ast, errors, bytesIn }; } - function parseTokens(tokens, nodeType, options = {}) { + function parseString(src, options = { location: false }) { + return [...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) { + throw new Error('empty string?'); + } + if (hint != null) { + return ([ + 'Whitespace', 'Semi-colon', 'Colon', 'Block-start', + 'Block-start', 'Attr-start', 'Attr-end', 'Start-parens', 'End-parens', + 'Comma', 'Gt', 'Lt' + ].includes(hint) ? { typ: hint } : { typ: hint, val }); + } + if (val == ' ') { + return { typ: 'Whitespace' }; + } + if (val == ';') { + return { typ: 'Semi-colon' }; + } + if (val == '{') { + return { typ: 'Block-start' }; + } + if (val == '}') { + return { typ: 'Block-end' }; + } + if (val == '[') { + return { typ: 'Attr-start' }; + } + if (val == ']') { + return { typ: 'Attr-end' }; + } + if (val == ':') { + return { typ: 'Colon' }; + } + if (val == ')') { + return { typ: 'End-parens' }; + } + if (val == '(') { + return { typ: 'Start-parens' }; + } + if (val == '=') { + return { typ: 'Delim', val }; + } + if (val == ';') { + return { typ: 'Semi-colon' }; + } + if (val == ',') { + return { typ: 'Comma' }; + } + if (val == '<') { + return { typ: 'Lt' }; + } + if (val == '>') { + return { typ: 'Gt' }; + } + if (isPseudo(val)) { + return val.endsWith('(') ? { + typ: 'Pseudo-class-func', + val: val.slice(0, -1), + chi: [] + } + : { + typ: 'Pseudo-class', + val + }; + } + if (isAtKeyword(val)) { + return { + typ: 'At-rule', + val: val.slice(1) + }; + } + if (isFunction(val)) { + val = val.slice(0, -1); + return { + typ: val == 'url' ? 'UrlFunc' : 'Func', + val, + chi: [] + }; + } + if (isNumber(val)) { + return { + typ: 'Number', + val + }; + } + if (isDimension(val)) { + return parseDimension(val); + } + if (isPercentage(val)) { + return { + typ: 'Perc', + val: val.slice(0, -1) + }; + } + const v = val.toLowerCase(); + if (v == 'currentcolor' || val == 'transparent' || v in COLORS_NAMES) { + return { + typ: 'Color', + val, + kin: 'lit' + }; + } + if (isIdent(val)) { + return { + typ: 'Iden', + val + }; + } + if (val.charAt(0) == '#' && isHexColor(val)) { + return { + typ: 'Color', + val, + kin: 'hex' + }; + } + if (val.charAt(0) == '#' && isHash(val)) { + return { + typ: 'Hash', + val + }; + } + if ('"\''.includes(val.charAt(0))) { + return { + typ: 'Unclosed-string', + val + }; + } + return { + typ: 'Literal', + val + }; + } + function parseTokens(tokens, options = {}) { for (let i = 0; i < tokens.length; i++) { const t = tokens[i]; if (t.typ == 'Whitespace' && ((i == 0 || @@ -4053,7 +4276,7 @@ if (t.chi.length > 1) { /*(t).chi =*/ // @ts-ignore - parseTokens(t.chi, t.typ, options); + parseTokens(t.chi, t.typ); } // @ts-ignore t.chi.forEach(val => { @@ -4165,8 +4388,8 @@ // @ts-ignore if (t.chi.length > 0) { // @ts-ignore - parseTokens(t.chi, t.typ, options); - if (t.typ == 'Pseudo-class-func' && t.val == ':is' && options.compress) { + parseTokens(t.chi, t.typ); + if (t.typ == 'Pseudo-class-func' && t.val == ':is' && options.minify) { // const count = t.chi.filter(t => t.typ != 'Comment').length; if (count == 1 || @@ -4204,40 +4427,9 @@ } return tokens; } - function getBlockType(chr) { - if (chr == ';') { - return { typ: 'Semi-colon' }; - } - if (chr == '{') { - return { typ: 'Block-start' }; - } - if (chr == '}') { - return { typ: 'Block-end' }; - } - if (chr == '[') { - return { typ: 'Attr-start' }; - } - if (chr == ']') { - return { typ: 'Attr-end' }; - } - throw new Error(`unhandled token: '${chr}'`); - } - - function* walk(node) { - // @ts-ignore - yield* doWalk(node, null, null); - } - function* doWalk(node, parent, root) { - yield { node, parent, root }; - if ('chi' in node) { - for (const child of node.chi) { - yield* doWalk(child, node, (root ?? node)); - } - } - } async function transform$1(css, options = {}) { - options = { compress: true, removeEmpty: true, ...options }; + options = { minify: true, removeEmpty: true, ...options }; const startTime = performance.now(); const parseResult = await parse$1(css, options); const renderTime = performance.now(); @@ -4385,18 +4577,44 @@ })); } - exports.deduplicate = deduplicate; - exports.deduplicateRule = deduplicateRule; + exports.combinators = combinators; exports.dirname = dirname; + exports.getConfig = getConfig; exports.hasDeclaration = hasDeclaration; + exports.isAngle = isAngle; + exports.isAtKeyword = isAtKeyword; + exports.isDigit = isDigit; + exports.isDimension = isDimension; + exports.isFrequency = isFrequency; + exports.isFunction = isFunction; + exports.isHash = isHash; + exports.isHexColor = isHexColor; + exports.isHexDigit = isHexDigit; + exports.isIdent = isIdent; + exports.isIdentCodepoint = isIdentCodepoint; + exports.isIdentStart = isIdentStart; + exports.isLength = isLength; + exports.isNewLine = isNewLine; + exports.isNumber = isNumber; + exports.isPercentage = isPercentage; + exports.isPseudo = isPseudo; + exports.isResolution = isResolution; + exports.isTime = isTime; + exports.isWhiteSpace = isWhiteSpace; exports.load = load; exports.matchUrl = matchUrl; + exports.minify = minify; + exports.minifyRule = minifyRule; exports.parse = parse; + exports.parseDimension = parseDimension; + exports.parseString = parseString; exports.reduceSelector = reduceSelector; exports.render = render; exports.renderToken = renderToken; exports.resolve = resolve; + exports.tokenize = tokenize; exports.transform = transform; + exports.urlTokenMatcher = urlTokenMatcher; exports.walk = walk; })); diff --git a/dist/index.cjs b/dist/index.cjs index 034515e..8827622 100644 --- a/dist/index.cjs +++ b/dist/index.cjs @@ -6,13 +6,14 @@ var promises = require('fs/promises'); // https://www.w3.org/TR/2021/CRD-css-syntax-3-20211224/#typedef-ident-token // '\\' const REVERSE_SOLIDUS = 0x5c; +const dimensionUnits = [ + 'q', 'cap', 'ch', 'cm', 'cqb', 'cqh', 'cqi', 'cqmax', 'cqmin', 'cqw', 'dvb', + 'dvh', 'dvi', 'dvmax', 'dvmin', 'dvw', 'em', 'ex', 'ic', 'in', 'lh', 'lvb', + 'lvh', 'lvi', 'lvmax', 'lvw', 'mm', 'pc', 'pt', 'px', 'rem', 'rlh', 'svb', + 'svh', 'svi', 'svmin', 'svw', 'vb', 'vh', 'vi', 'vmax', 'vmin', 'vw' +]; function isLength(dimension) { - return 'unit' in dimension && [ - 'q', 'cap', 'ch', 'cm', 'cqb', 'cqh', 'cqi', 'cqmax', 'cqmin', 'cqw', 'dvb', - 'dvh', 'dvi', 'dvmax', 'dvmin', 'dvw', 'em', 'ex', 'ic', 'in', 'lh', 'lvb', - 'lvh', 'lvi', 'lvmax', 'lvw', 'mm', 'pc', 'pt', 'px', 'rem', 'rlh', 'svb', - 'svh', 'svi', 'svmin', 'svw', 'vb', 'vh', 'vi', 'vmax', 'vmin', 'vw' - ].includes(dimension.unit.toLowerCase()); + return 'unit' in dimension && dimensionUnits.includes(dimension.unit.toLowerCase()); } function isResolution(dimension) { return 'unit' in dimension && ['dpi', 'dpcm', 'dppx', 'x'].includes(dimension.unit.toLowerCase()); @@ -237,6 +238,22 @@ function isHexColor(name) { } return true; } +function isHexDigit(name) { + if (name.length || name.length > 6) { + return false; + } + for (let chr of name) { + let codepoint = chr.charCodeAt(0); + if (!isDigit(codepoint) && + // A F + !(codepoint >= 0x41 && codepoint <= 0x46) && + // a f + !(codepoint >= 0x61 && codepoint <= 0x66)) { + return false; + } + } + return true; +} function isFunction(name) { return name.endsWith('(') && isIdent(name.slice(0, -1)); } @@ -372,6 +389,7 @@ var properties = { }, "border-width": { shorthand: "border-width", + map: "border", properties: [ "border-top-width", "border-right-width", @@ -382,6 +400,9 @@ var properties = { "Length", "Perc" ], + "default": [ + "medium" + ], keywords: [ "thin", "medium", @@ -389,19 +410,24 @@ var properties = { ] }, "border-top-width": { + map: "border", shorthand: "border-width" }, "border-right-width": { + map: "border", shorthand: "border-width" }, "border-bottom-width": { + map: "border", shorthand: "border-width" }, "border-left-width": { + map: "border", shorthand: "border-width" }, "border-style": { shorthand: "border-style", + map: "border", properties: [ "border-top-style", "border-right-style", @@ -410,6 +436,9 @@ var properties = { ], types: [ ], + "default": [ + "none" + ], keywords: [ "none", "hidden", @@ -424,19 +453,24 @@ var properties = { ] }, "border-top-style": { + map: "border", shorthand: "border-style" }, "border-right-style": { + map: "border", shorthand: "border-style" }, "border-bottom-style": { + map: "border", shorthand: "border-style" }, "border-left-style": { + map: "border", shorthand: "border-style" }, "border-color": { shorthand: "border-color", + map: "border", properties: [ "border-top-color", "border-right-color", @@ -446,23 +480,95 @@ var properties = { types: [ "Color" ], + "default": [ + "currentcolor" + ], keywords: [ ] }, "border-top-color": { + map: "border", shorthand: "border-color" }, "border-right-color": { + map: "border", shorthand: "border-color" }, "border-bottom-color": { + map: "border", shorthand: "border-color" }, "border-left-color": { + map: "border", shorthand: "border-color" } }; var map = { + border: { + shorthand: "border", + pattern: "border-color border-style border-width", + keywords: [ + "none" + ], + "default": [ + "0", + "none" + ], + properties: { + "border-color": { + types: [ + "Color" + ], + "default": [ + "currentcolor" + ], + keywords: [ + ] + }, + "border-style": { + types: [ + ], + "default": [ + "none" + ], + keywords: [ + "none", + "hidden", + "dotted", + "dashed", + "solid", + "double", + "groove", + "ridge", + "inset", + "outset" + ] + }, + "border-width": { + types: [ + "Length", + "Perc" + ], + "default": [ + "medium" + ], + keywords: [ + "thin", + "medium", + "thick" + ] + } + } + }, + "border-color": { + shorthand: "border" + }, + "border-style": { + shorthand: "border" + }, + "border-width": { + shorthand: "border" + }, outline: { shorthand: "outline", pattern: "outline-color outline-style outline-width", @@ -479,12 +585,10 @@ var map = { "Color" ], "default": [ - "currentColor", - "invert" + "currentColor" ], keywords: [ - "currentColor", - "invert" + "currentColor" ] }, "outline-style": { @@ -822,6 +926,7 @@ var map = { "default": [ "transparent" ], + multiple: true, keywords: [ ] }, @@ -842,6 +947,7 @@ var map = { "default": [ "scroll" ], + multiple: true, keywords: [ "scroll", "fixed", @@ -854,6 +960,7 @@ var map = { "default": [ "border-box" ], + multiple: true, keywords: [ "border-box", "padding-box", @@ -867,6 +974,7 @@ var map = { "default": [ "padding-box" ], + multiple: true, keywords: [ "border-box", "padding-box", @@ -1456,7 +1564,7 @@ function hsl2rgb(h, s, l, a = null) { } function render(data, opt = {}) { - const options = Object.assign(opt.compress ? { + const options = Object.assign(opt.minify ?? true ? { indent: '', newLine: '', removeComments: true @@ -1541,8 +1649,11 @@ function doRender(data, options, reducer, level = 0, indents = []) { function renderToken(token, options = {}) { switch (token.typ) { case 'Color': - if (options.compress || options.colorConvert) { - let value = token.kin == 'hex' ? token.val.toLowerCase() : ''; + if (options.minify || options.colorConvert) { + if (token.kin == 'lit' && token.val.toLowerCase() == 'currentcolor') { + return 'currentcolor'; + } + let value = token.kin == 'hex' ? token.val.toLowerCase() : (token.kin == 'lit' ? COLORS_NAMES[token.val.toLowerCase()] : ''); if (token.val == 'rgb' || token.val == 'rgba') { value = rgb2Hex(token); } @@ -1586,7 +1697,7 @@ function renderToken(token, options = {}) { case 'UrlFunc': case 'Pseudo-class-func': // @ts-ignore - return ( /* options.compress && 'Pseudo-class-func' == token.typ && token.val.slice(0, 2) == '::' ? token.val.slice(1) :*/token.val ?? '') + '(' + token.chi.reduce((acc, curr) => { + return ( /* options.minify && 'Pseudo-class-func' == token.typ && token.val.slice(0, 2) == '::' ? token.val.slice(1) :*/token.val ?? '') + '(' + token.chi.reduce((acc, curr) => { if (options.removeComments && curr.typ == 'Comment') { if (!options.preserveLicense || !curr.val.startsWith('/*!')) { return acc; @@ -1677,21 +1788,45 @@ function renderToken(token, options = {}) { case 'String': case 'Iden': case 'Delim': - return /* options.compress && 'Pseudo-class' == token.typ && '::' == token.val.slice(0, 2) ? token.val.slice(1) : */ token.val; + 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)}`); } function eq(a, b) { - if ((typeof a != 'object') || typeof b != 'object') { + if (a == null || b == null) { + return a == b; + } + if (typeof a != 'object' || typeof b != 'object') { return a === b; } + if (Object.getPrototypeOf(a) !== Object.getPrototypeOf(b)) { + return false; + } + if (Array.isArray(a)) { + if (a.length != b.length) { + return false; + } + let i = 0; + for (; i < a.length; i++) { + if (!eq(a[i], b[i])) { + return false; + } + } + return true; + } const k1 = Object.keys(a); const k2 = Object.keys(b); - return k1.length == k2.length && - k1.every((key) => { - return eq(a[key], b[key]); - }); + if (k1.length != k2.length) { + return false; + } + let key; + for (key of k1) { + if (!eq(a[key], b[key])) { + return false; + } + } + return true; } class PropertySet { @@ -1703,8 +1838,7 @@ class PropertySet { } add(declaration) { if (declaration.nam == this.config.shorthand) { - this.declarations.clear(); - this.declarations.set(declaration.nam, declaration); + this.declarations = new Map; } else { // expand shorthand @@ -1727,6 +1861,10 @@ class PropertySet { } if (token.typ != 'Whitespace' && token.typ != 'Comment') { if (token.typ == 'Iden' && this.config.keywords.includes(token.val)) { + if (tokens.length == 0) { + tokens.push([]); + current++; + } tokens[current].push(token); } if (token.typ == 'Literal' && token.val == this.config.separator) { @@ -1742,10 +1880,6 @@ class PropertySet { this.declarations.delete(this.config.shorthand); for (const values of tokens) { this.config.properties.forEach((property, index) => { - // if (property == declaration.nam) { - // - // return; - // } if (!this.declarations.has(property)) { this.declarations.set(property, { typ: 'Declaration', @@ -1774,30 +1908,20 @@ class PropertySet { this.declarations.set(declaration.nam, declaration); return this; } - // declaration.chi = declaration.chi.reduce((acc: Token[], token: Token) => { - // - // if (this.config.types.includes(token.typ) || ('0' == (token).chi && ( - // this.config.types.includes('Length') || - // this.config.types.includes('Angle') || - // this.config.types.includes('Dimension'))) || (token.typ == 'Iden' && this.config.keywords.includes(token.chi))) { - // - // acc.push(token); - // } - // - // return acc; - // }, []); - this.declarations.set(declaration.nam, declaration); } + this.declarations.set(declaration.nam, declaration); return this; } + isShortHand() { + if (this.declarations.has(this.config.shorthand)) { + return this.declarations.size == 1; + } + return this.config.properties.length == this.declarations.size; + } [Symbol.iterator]() { let iterator; const declarations = this.declarations; - if (declarations.size < this.config.properties.length || this.config.properties.some((property, index) => { - return !declarations.has(property) || (index > 0 && - // @ts-ignore - declarations.get(property).val.length != declarations.get(this.config.properties[Math.floor(index / 2)]).val.length); - })) { + if (declarations.size < this.config.properties.length) { iterator = declarations.values(); } else { @@ -1855,17 +1979,20 @@ class PropertySet { return acc; }, []) }][Symbol.iterator](); - return { - next() { - return iterator.next(); - } - }; + // return { + // next() { + // + // return iterator.next(); + // } + // } } - return { - next() { - return iterator.next(); - } - }; + return iterator; + // return { + // next() { + // + // return iterator.next(); + // } + // } } } @@ -1880,34 +2007,7 @@ function matchType(val, properties) { return false; } -function getTokenType(val) { - if (val == 'transparent' || val == 'currentcolor') { - return { - typ: 'Color', - val, - kin: 'lit' - }; - } - if (val.endsWith('%')) { - return { - typ: 'Perc', - val: val.slice(0, -1) - }; - } - return { - typ: isNumber(val) ? 'Number' : 'Iden', - val - }; -} -function parseString(val) { - return val.split(/\s/).map(getTokenType).reduce((acc, curr) => { - if (acc.length > 0) { - acc.push({ typ: 'Whitespace' }); - } - acc.push(curr); - return acc; - }, []); -} +const propertiesConfig = getConfig(); class PropertyMap { config; declarations; @@ -1922,7 +2022,7 @@ class PropertyMap { } add(declaration) { if (declaration.nam == this.config.shorthand) { - this.declarations.clear(); + this.declarations = new Map; this.declarations.set(declaration.nam, declaration); } else { @@ -1999,8 +2099,7 @@ class PropertyMap { const defaults = parseString(props.default[0]); if (!(property in tokens)) { tokens[property] = [ - [...defaults - ] + [...defaults] ]; } else { @@ -2034,145 +2133,229 @@ class PropertyMap { }, new Map); } } - this.declarations.set(declaration.nam, declaration); + // @ts-ignore + const config = propertiesConfig.properties[declaration.nam]; + let property = declaration.nam; + if (config != null) { + property = config.shorthand; + let value = this.declarations.get(property); + if (!(value instanceof PropertySet)) { + // @ts-ignore + this.declarations.set(property, new PropertySet(propertiesConfig.properties[config.shorthand])); + // Token[] + if (value != null) { + // @ts-ignore + this.declarations.get(property).add(value); + } + } + this.declarations.get(property).add(declaration); + } + else { + this.declarations.set(declaration.nam, declaration); + } } return this; } [Symbol.iterator]() { - let requiredCount = Object.keys(this.config.properties).reduce((acc, curr) => this.declarations.has(curr) && this.config.properties[curr].required ? ++acc : acc, 0); - if (requiredCount == 0) { - requiredCount = this.declarations.size; - } - if (requiredCount < this.requiredCount) { - // if (this.declarations.size == 1 && this.declarations.has(this.config.shorthand)) { - // - // this.declarations - // } - return this.declarations.values(); - } - let count = 0; - const separator = this.config.separator; - const tokens = {}; - // @ts-ignore - const valid = Object.entries(this.config.properties).reduce((acc, curr) => { - if (!this.declarations.has(curr[0])) { - if (curr[1].required) { - acc.push(curr[0]); + let iterable; + let requiredCount = 0; + let property; + let isShorthand = true; + for (property of Object.keys(this.config.properties)) { + if (this.config.properties[property].required) { + if (!this.declarations.has(property)) { + isShorthand = false; + break; } - return acc; - } - let current = 0; - const props = this.config.properties[curr[0]]; - // @ts-ignore - for (const val of this.declarations.get(curr[0]).val) { - if (separator != null && separator.typ == val.typ && eq(separator, val)) { - current++; - if (tokens[curr[0]].length == current) { - tokens[curr[0]].push([]); + else { + const val = this.declarations.get(property); + if (val instanceof PropertySet && !val.isShortHand()) { + isShorthand = false; + break; } - continue; - } - if (val.typ == 'Whitespace' || val.typ == 'Comment') { - continue; - } - if (props.multiple && props.separator != null && props.separator.typ == val.typ && eq(val, props.separator)) { - continue; - } - if (matchType(val, curr[1])) { - if (!(curr[0] in tokens)) { - tokens[curr[0]] = [[]]; + else { + requiredCount++; } - // is default value - tokens[curr[0]][current].push(val); - continue; } - acc.push(curr[0]); - break; } - if (count == 0) { - count = current; - } - return acc; - }, []); - if (valid.length > 0 || Object.values(tokens).every(v => v.every(v => v.length == count))) { - return this.declarations.values(); } - const values = Object.entries(tokens).reduce((acc, curr) => { - const props = this.config.properties[curr[0]]; - for (let i = 0; i < curr[1].length; i++) { - if (acc.length == i) { - acc.push([]); - } - let values = curr[1][i].reduce((acc, curr) => { - if (acc.length > 0) { - acc.push({ typ: 'Whitespace' }); + if (requiredCount == 0) { + requiredCount = this.declarations.size; + } + if (!isShorthand || requiredCount < this.requiredCount) { + // @ts-ignore + iterable = this.declarations.values(); + } + else { + let count = 0; + const separator = this.config.separator; + const tokens = {}; + // @ts-ignore + /* 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]); } - acc.push(curr); return acc; - }, []); - if (props.default.includes(curr[1][i].reduce((acc, curr) => acc + renderToken(curr) + ' ', '').trimEnd())) { - continue; } - values = values.filter((val) => { + 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([]); + } + continue; + } if (val.typ == 'Whitespace' || val.typ == 'Comment') { - return false; + continue; } - return !(val.typ == 'Iden' && props.default.includes(val.val)); - }); - if (values.length > 0) { - if ('mapping' in props) { - // @ts-ignore - if (!('constraints' in props) || !('max' in props.constraints) || values.length <= props.constraints.mapping.max) { - let i = values.length; - while (i--) { + 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]] = [[]]; + } + // is default value + tokens[curr[0]][current].push(val); + // continue; + } + else { + acc.push(curr[0]); + break; + } + } + if (count == 0) { + count = current; + } + return acc; + }, []); + count++; + if (!Object.values(tokens).every(v => v.length == count)) { + // @ts-ignore + iterable = this.declarations.values(); + } + else { + const values = Object.entries(tokens).reduce((acc, curr) => { + const props = this.config.properties[curr[0]]; + for (let i = 0; i < curr[1].length; i++) { + if (acc.length == i) { + acc.push([]); + } + let values = curr[1][i].reduce((acc, curr) => { + if (acc.length > 0) { + acc.push({ typ: 'Whitespace' }); + } + acc.push(curr); + return acc; + }, []); + // @todo remove renderToken call + if (props.default.includes(curr[1][i].reduce((acc, curr) => acc + renderToken(curr) + ' ', '').trimEnd())) { + continue; + } + let doFilterDefault = true; + if (curr[0] in propertiesConfig.properties) { + for (let v of values) { + if (!['Whitespace', 'Comment', 'Iden'].includes(v.typ) + || (v.typ == 'Iden' && !this.config.properties[curr[0]].default.includes(v.val))) { + doFilterDefault = false; + break; + } + } + } + // remove default values + values = values.filter((val) => { + if (val.typ == 'Whitespace' || val.typ == 'Comment') { + return false; + } + return !doFilterDefault || !(val.typ == 'Iden' && props.default.includes(val.val)); + }); + if (values.length > 0) { + if ('mapping' in props) { // @ts-ignore - if (values[i].typ == 'Iden' && values[i].val in props.mapping) { - // @ts-ignore - values.splice(i, 1, ...parseString(props.mapping[values[i].val])); + if (!('constraints' in props) || !('max' in props.constraints) || values.length <= props.constraints.mapping.max) { + let i = values.length; + while (i--) { + // @ts-ignore + if (values[i].typ == 'Iden' && values[i].val in props.mapping) { + // @ts-ignore + values.splice(i, 1, ...parseString(props.mapping[values[i].val])); + } + } } } + if ('prefix' in props) { + // @ts-ignore + acc[i].push({ ...props.prefix }); + } + else if (acc[i].length > 0) { + acc[i].push({ typ: 'Whitespace' }); + } + acc[i].push(...values.reduce((acc, curr) => { + if (acc.length > 0) { + // @ts-ignore + acc.push({ ...(props.separator ?? { typ: 'Whitespace' }) }); + } + // @ts-ignore + acc.push(curr); + return acc; + }, [])); } } - if ('prefix' in props) { - // @ts-ignore - acc[i].push({ ...props.prefix }); + return acc; + }, []).reduce((acc, curr) => { + if (acc.length > 0) { + acc.push({ ...separator }); } - else if (acc[i].length > 0) { - acc[i].push({ typ: 'Whitespace' }); + if (curr.length == 0 && this.config.default.length > 0) { + curr.push(...parseString(this.config.default[0]).reduce((acc, curr) => { + if (acc.length > 0) { + acc.push({ typ: 'Whitespace' }); + } + acc.push(curr); + return acc; + }, [])); } - acc[i].push(...values.reduce((acc, curr) => { - if (acc.length > 0) { + acc.push(...curr); + return acc; + }, []); + iterable = [{ + typ: 'Declaration', + nam: this.config.shorthand, + val: values + }][Symbol.iterator](); + } + } + const iterators = []; + return { + // @ts-ignore + next() { + let v = iterable.next(); + while (v.done || v.value instanceof PropertySet) { + if (v.value instanceof PropertySet) { + // @ts-ignore + iterators.push(iterable); + iterable = v.value[Symbol.iterator](); + v = iterable.next(); + } + if (v.done) { + if (iterators.length > 0) { // @ts-ignore - acc.push({ ...(props.separator ?? { typ: 'Whitespace' }) }); + iterable = iterators.pop(); + v = iterable.next(); + } + if (v.done && iterators.length == 0) { + break; } - // @ts-ignore - acc.push(curr); - return acc; - }, [])); - } - } - return acc; - }, []).reduce((acc, curr) => { - if (acc.length > 0) { - acc.push({ ...separator }); - } - if (curr.length == 0) { - curr.push(...this.config.default[0].split(/\s/).map(getTokenType).reduce((acc, curr) => { - if (acc.length > 0) { - acc.push({ typ: 'Whitespace' }); } - acc.push(curr); - return acc; - }, [])); + } + return v; } - acc.push(...curr); - return acc; - }, []); - return [{ - typ: 'Declaration', - nam: this.config.shorthand, - val: values - }][Symbol.iterator](); + }; } } @@ -2182,33 +2365,61 @@ class PropertyList { constructor() { this.declarations = new Map; } + set(nam, value) { + return this.add({ typ: 'Declaration', nam, val: Array.isArray(value) ? value : parseString(String(value)) }); + } add(declaration) { if (declaration.typ != 'Declaration') { this.declarations.set(Number(Math.random().toString().slice(2)).toString(36), declaration); return this; } - const propertyName = declaration.nam; + let propertyName = declaration.nam; + let shortHandType; + let shorthand; if (propertyName in config.properties) { // @ts-ignore - const shorthand = config.properties[propertyName].shorthand; - if (!this.declarations.has(shorthand)) { + if ('map' in config.properties[propertyName]) { + shortHandType = 'map'; // @ts-ignore - this.declarations.set(shorthand, new PropertySet(config.properties[shorthand])); + shorthand = config.properties[propertyName].map; } - this.declarations.get(shorthand).add(declaration); - return this; + else { + shortHandType = 'set'; + // @ts-ignore + shorthand = config.properties[propertyName].shorthand; + } + } + else if (propertyName in config.map) { + shortHandType = 'map'; + // @ts-ignore + shorthand = config.map[propertyName].shorthand; } - if (propertyName in config.map) { + // @ts-ignore + if (shortHandType == 'map') { // @ts-ignore - const shorthand = config.map[propertyName].shorthand; if (!this.declarations.has(shorthand)) { // @ts-ignore this.declarations.set(shorthand, new PropertyMap(config.map[shorthand])); } + // @ts-ignore this.declarations.get(shorthand).add(declaration); - return this; + // return this; + } + // @ts-ignore + else if (shortHandType == 'set') { + // @ts-ignore + // const shorthand: string = config.properties[propertyName].shorthand; + if (!this.declarations.has(shorthand)) { + // @ts-ignore + this.declarations.set(shorthand, new PropertySet(config.properties[shorthand])); + } + // @ts-ignore + this.declarations.get(shorthand).add(declaration); + // return this; + } + else { + this.declarations.set(propertyName, declaration); } - this.declarations.set(propertyName, declaration); return this; } [Symbol.iterator]() { @@ -2237,56 +2448,300 @@ class PropertyList { } } -const configuration = getConfig(); const combinators = ['+', '>', '~']; const notEndingWith = ['(', '['].concat(combinators); -function wrapNodes(previous, node, match, ast, i, nodeIndex) { - // @ts-ignore - let pSel = match.selector1.reduce(reducer, []).join(','); - // @ts-ignore - let nSel = match.selector2.reduce(reducer, []).join(','); - // @ts-ignore - const wrapper = { ...previous, chi: [], sel: match.match.reduce(reducer, []).join(',') }; - // @ts-ignore - Object.defineProperty(wrapper, 'raw', { - enumerable: false, - writable: true, +function minify(ast, options = {}, recursive = false) { + function wrapNodes(previous, node, match, ast, i, nodeIndex) { // @ts-ignore - value: match.match.map(t => t.slice()) - }); - if (pSel == '&' || pSel === '') { + let pSel = match.selector1.reduce(reducer, []).join(','); + // @ts-ignore + let nSel = match.selector2.reduce(reducer, []).join(','); // @ts-ignore - wrapper.chi.push(...previous.chi); + const wrapper = { ...previous, chi: [], sel: match.match.reduce(reducer, []).join(',') }; // @ts-ignore - if ((nSel == '&' || nSel === '') && hasOnlyDeclarations(previous)) { + Object.defineProperty(wrapper, 'raw', { + enumerable: false, + writable: true, // @ts-ignore - wrapper.chi.push(...node.chi); + value: match.match.map(t => t.slice()) + }); + if (pSel == '&' || pSel === '') { + // @ts-ignore + wrapper.chi.push(...previous.chi); + // @ts-ignore + if ((nSel == '&' || nSel === '') && hasOnlyDeclarations(previous)) { + // @ts-ignore + wrapper.chi.push(...node.chi); + } + else { + // @ts-ignore + wrapper.chi.push(node); + } } else { // @ts-ignore - wrapper.chi.push(node); + wrapper.chi.push(previous, node); } + // @ts-ignore + ast.chi.splice(i, 1, wrapper); + // @ts-ignore + ast.chi.splice(nodeIndex, 1); + // @ts-ignore + previous.sel = pSel; + // @ts-ignore + previous.raw = match.selector1; + // @ts-ignore + node.sel = nSel; + // @ts-ignore + node.raw = match.selector2; + reduceRuleSelector(wrapper); + return wrapper; } - else { + function reducer(acc, curr, index, array) { + // trim :is() + if (array.length == 1 && array[0][0] == ':is(' && array[0].at(-1) == ')') { + curr = curr.slice(1, -1); + } + if (curr[0] == '&') { + if (curr[1] == ' ' && !isIdent(curr[2]) && !isFunction(curr[2])) { + curr.splice(0, 2); + } + else if (combinators.includes(curr[1])) { + curr.splice(0, 1); + } + } + else if (ast.typ == 'Rule' && (isIdent(curr[0]) || isFunction(curr[0]))) { + curr.unshift('&', ' '); + } + acc.push(curr.join('')); + return acc; + } + function diff(n1, n2, options = {}) { + let node1 = n1; + let node2 = n2; + let exchanged = false; + if (node1.chi.length > node2.chi.length) { + const t = node1; + node1 = node2; + node2 = t; + exchanged = true; + } + let i = node1.chi.length; + let j = node2.chi.length; + if (i == 0 || j == 0) { + // @ts-ignore + return null; + } + // @ts-ignore + const raw1 = node1.raw; + // @ts-ignore + const raw2 = node2.raw; + // @ts-ignore + node1 = { ...node1, chi: node1.chi.slice() }; + node2 = { ...node2, chi: node2.chi.slice() }; + if (raw1 != null) { + Object.defineProperty(node1, 'raw', { enumerable: false, writable: true, value: raw1 }); + } + if (raw2 != null) { + Object.defineProperty(node2, 'raw', { enumerable: false, writable: true, value: raw2 }); + } + const intersect = []; + while (i--) { + if (node1.chi[i].typ == 'Comment') { + continue; + } + j = node2.chi.length; + if (j == 0) { + break; + } + while (j--) { + if (node2.chi[j].typ == 'Comment') { + continue; + } + if (node1.chi[i].nam == node2.chi[j].nam) { + if (eq(node1.chi[i], node2.chi[j])) { + intersect.push(node1.chi[i]); + node1.chi.splice(i, 1); + node2.chi.splice(j, 1); + break; + } + } + } + } // @ts-ignore - wrapper.chi.push(previous, node); + const result = (intersect.length == 0 ? null : { + ...node1, + // @ts-ignore + sel: [...new Set([...(n1?.raw?.reduce(reducer, []) || splitRule(n1.sel)).concat(n2?.raw?.reduce(reducer, []) || splitRule(n2.sel))])].join(','), + chi: intersect.reverse() + }); + if (result == null || [n1, n2].reduce((acc, curr) => curr.chi.length == 0 ? acc : acc + render(curr, options).code.length, 0) <= [node1, node2, result].reduce((acc, curr) => curr.chi.length == 0 ? acc : acc + render(curr, options).code.length, 0)) { + // @ts-ignore + return null; + } + return { result, node1: exchanged ? node2 : node1, node2: exchanged ? node2 : node2 }; + } + function matchSelectors(selector1, selector2, parentType) { + let match = [[]]; + const j = Math.min(selector1.reduce((acc, curr) => Math.min(acc, curr.length), selector1.length > 0 ? selector1[0].length : 0), selector2.reduce((acc, curr) => Math.min(acc, curr.length), selector2.length > 0 ? selector2[0].length : 0)); + let i = 0; + let k; + let l; + let token; + let matching = true; + let matchFunction = 0; + let inAttr = 0; + for (; i < j; i++) { + k = 0; + token = selector1[0][i]; + for (; k < selector1.length; k++) { + if (selector1[k][i] != token) { + matching = false; + break; + } + } + if (matching) { + l = 0; + for (; l < selector2.length; l++) { + if (selector2[l][i] != token) { + matching = false; + break; + } + } + } + if (!matching) { + break; + } + if (token == ',') { + match.push([]); + } + else { + if (token.endsWith('(')) { + matchFunction++; + } + if (token.endsWith('[')) { + inAttr++; + } + else if (token == ')') { + matchFunction--; + } + else if (token == ']') { + inAttr--; + } + match.at(-1).push(token); + } + } + // invalid function + if (matchFunction != 0 || inAttr != 0) { + return null; + } + if (parentType != 'Rule') { + for (const part of match) { + if (part.length > 0 && combinators.includes(part[0].charAt(0))) { + return null; + } + } + } + if (match.length > 1) { + console.error(`unsupported multilevel matching`); + console.error({ match, selector1, selector2 }); + return null; + } + for (const part of match) { + while (part.length > 0) { + const token = part.at(-1); + if (token == ' ' || combinators.includes(token) || notEndingWith.includes(token.at(-1))) { + part.pop(); + continue; + } + break; + } + } + if (match.every(t => t.length == 0)) { + return null; + } + if (eq([['&']], match)) { + return null; + } + function reduce(acc, curr) { + if (acc === null) { + return null; + } + let hasCompoundSelector = true; + curr = curr.slice(match[0].length); + while (curr.length > 0) { + if (curr[0] == ' ') { + hasCompoundSelector = false; + curr.unshift('&'); + continue; + } + break; + } + // invalid function match + if (curr.length > 0 && curr[0].endsWith('(') && curr.at(-1) != ')') { + return null; + } + if (curr.length == 1 && combinators.includes(curr[0].charAt(0))) { + return null; + } + if (hasCompoundSelector && curr.length > 0) { + hasCompoundSelector = !['&'].concat(combinators).includes(curr[0].charAt(0)); + } + if (curr[0] == ':is(') { + let inFunction = 0; + let canReduce = true; + const isCompound = curr.reduce((acc, token, index) => { + if (index == 0) { + inFunction++; + canReduce = curr[1] == '&'; + } + else if (token.endsWith('(')) { + if (inFunction == 0) { + canReduce = false; + } + inFunction++; + } + else if (token == ')') { + inFunction--; + } + else if (token == ',') { + if (!canReduce) { + canReduce = curr[index + 1] == '&'; + } + acc.push([]); + } + else + acc.at(-1)?.push(token); + return acc; + }, [[]]); + if (inFunction > 0) { + canReduce = false; + } + if (canReduce) { + curr = isCompound.reduce((acc, curr) => { + if (acc.length > 0) { + acc.push(','); + } + acc.push(...curr); + return acc; + }, []); + } + } + // @todo: check hasCompoundSelector && curr[0] == '&' && curr[1] == ' ' + acc.push(match.length == 0 ? ['&'] : (hasCompoundSelector && curr[0] != '&' && (curr.length == 0 || !combinators.includes(curr[0].charAt(0))) ? ['&'].concat(curr) : curr)); + return acc; + } + // @ts-ignore + selector1 = selector1.reduce(reduce, []); + // @ts-ignore + selector2 = selector2.reduce(reduce, []); + return selector1 == null || selector2 == null ? null : { + eq: eq(selector1, selector2), + match, + selector1, + selector2 + }; } - // @ts-ignore - ast.chi.splice(i, 1, wrapper); - // @ts-ignore - ast.chi.splice(nodeIndex, 1); - // @ts-ignore - previous.sel = pSel; - // @ts-ignore - previous.raw = match.selector1; - // @ts-ignore - node.sel = nSel; - // @ts-ignore - node.raw = match.selector2; - reduceRuleSelector(wrapper); - return wrapper; -} -function deduplicate(ast, options = {}, recursive = false) { // @ts-ignore if (('chi' in ast) && ast.chi?.length > 0) { let i = 0; @@ -2326,7 +2781,8 @@ function deduplicate(ast, options = {}, recursive = false) { // @ts-ignore if (options.nestingRules) { // @ts-ignore - if (previous != null && previous.typ == 'Rule') { + if (previous?.typ == 'Rule') { + // @ts-ignore reduceRuleSelector(previous); // @ts-ignore match = matchSelectors(previous.raw, node.raw, ast.typ); @@ -2365,7 +2821,7 @@ function deduplicate(ast, options = {}, recursive = false) { nodeIndex = --i; // @ts-ignore previous = ast.chi[nodeIndex]; - deduplicate(wrapper, options, recursive); + minify(wrapper, options, recursive); continue; } // @ts-ignore @@ -2414,13 +2870,24 @@ function deduplicate(ast, options = {}, recursive = false) { } else if (combinators.includes(curr[0])) { curr.unshift('&'); + wrap = false; } // @ts-ignore - acc.push(curr.map(t => t.replaceAll('&', node.optimized.optimized[0])).join('')); + acc.push(curr); return acc; }, []); + if (!wrap) { + wrap = selector.some(s => s[0] != '&'); + } + const rule = selector.map(s => { + if (s[0] == '&') { + // @ts-ignore + s[0] = node.optimized.optimized[0]; + } + return s.join(''); + }).join(','); // @ts-ignore - node.sel = (wrap ? node.optimized.optimized[0] : '') + `:is(${selector.join(',')})`; + node.sel = wrap ? node.optimized.optimized[0] + `:is(${rule})` : rule; } } // @ts-ignore @@ -2451,10 +2918,10 @@ function deduplicate(ast, options = {}, recursive = false) { // @ts-ignore if (hasDeclaration(node)) { // @ts-ignore - deduplicateRule(node); + minifyRule(node); } else { - deduplicate(node, options, recursive); + minify(node, options, recursive); } i--; previous = node; @@ -2496,10 +2963,10 @@ function deduplicate(ast, options = {}, recursive = false) { // @ts-ignore if (hasDeclaration(previous)) { // @ts-ignore - deduplicateRule(previous); + minifyRule(previous); } else { - deduplicate(previous, options, recursive); + minify(previous, options, recursive); } } } @@ -2510,18 +2977,97 @@ function deduplicate(ast, options = {}, recursive = false) { if (recursive && node != null && ('chi' in node)) { // @ts-ignore if (node.chi.some(n => n.typ == 'Declaration')) { - deduplicateRule(node); + minifyRule(node); } else { // @ts-ignore if (!(node.typ == 'AtRule' && node.nam != 'font-face')) { - deduplicate(node, options, recursive); + minify(node, options, recursive); } } } } return ast; } +function reduceSelector(selector) { + if (selector.length == 0) { + return null; + } + const optimized = []; + const k = selector.reduce((acc, curr) => acc == 0 ? curr.length : (curr.length == 0 ? acc : Math.min(acc, curr.length)), 0); + let i = 0; + let j; + let match; + for (; i < k; i++) { + const item = selector[0][i]; + match = true; + for (j = 1; j < selector.length; j++) { + if (item != selector[j][i]) { + match = false; + break; + } + } + if (!match) { + break; + } + optimized.push(item); + } + while (optimized.length > 0) { + const last = optimized.at(-1); + if ((last == ' ' || combinators.includes(last))) { + optimized.pop(); + continue; + } + break; + } + selector.forEach((selector) => selector.splice(0, optimized.length)); + // combinator + if (combinators.includes(optimized.at(-1))) { + const combinator = optimized.pop(); + selector.forEach(selector => selector.unshift(combinator)); + } + let reducible = optimized.length == 1; + if (optimized[0] == '&' && optimized[1] == ' ') { + optimized.splice(0, 2); + } + if (optimized.length == 0 || + (optimized[0].charAt(0) == '&' || + selector.length == 1)) { + return { + match: false, + optimized, + selector: selector.map(selector => selector[0] == '&' && selector[1] == ' ' ? selector.slice(2) : selector), + reducible: selector.length > 1 && selector.every((selector) => !combinators.includes(selector[0])) + }; + } + return { + match: true, + optimized, + selector: selector.reduce((acc, curr) => { + let hasCompound = true; + if (hasCompound && curr.length > 0) { + hasCompound = !['&'].concat(combinators).includes(curr[0].charAt(0)); + } + // @ts-ignore + if (hasCompound && curr[0] == ' ') { + hasCompound = false; + curr.unshift('&'); + } + if (curr.length == 0) { + curr.push('&'); + hasCompound = false; + } + if (reducible) { + const chr = curr[0].charAt(0); + // @ts-ignore + reducible = chr == '.' || chr == ':' || isIdentStart(chr.codePointAt(0)); + } + acc.push(hasCompound ? ['&'].concat(curr) : curr); + return acc; + }, []), + reducible: selector.every((selector) => !['>', '+', '~', '&'].includes(selector[0])) + }; +} function hasOnlyDeclarations(node) { let k = node.chi.length; while (k--) { @@ -2544,7 +3090,7 @@ function hasDeclaration(node) { } return true; } -function deduplicateRule(ast) { +function minifyRule(ast) { // @ts-ignore if (!('chi' in ast) || ast.chi?.length <= 1) { return ast; @@ -2552,45 +3098,19 @@ function deduplicateRule(ast) { // @ts-ignore const j = ast.chi.length; let k = 0; - let map = new Map; + let properties = new PropertyList(); // @ts-ignore for (; k < j; k++) { // @ts-ignore const node = ast.chi[k]; - if (node.typ == 'Comment') { - // @ts-ignore - map.set(node, node); + if (node.typ == 'Comment' || node.typ == 'Declaration') { + properties.add(node); continue; } - else if (node.typ != 'Declaration') { - break; - } - if (node.nam in configuration.map || - node.nam in configuration.properties) { - // @ts-ignore - const shorthand = node.nam in configuration.map ? configuration.map[node.nam].shorthand : configuration.properties[node.nam].shorthand; - if (!map.has(shorthand)) { - map.set(shorthand, new PropertyList()); - } - map.get(shorthand).add(node); - } - else { - map.set(node.nam, node); - } - } - const children = []; - for (let child of map.values()) { - if (child instanceof PropertyList) { - // @ts-ignore - children.push(...child); - } - else { - // @ts-ignore - children.push(child); - } + break; } // @ts-ignore - ast.chi = children.concat(ast.chi?.slice(k)); + ast.chi = [...properties].concat(ast.chi.slice(k)); return ast; } function splitRule(buffer) { @@ -2716,345 +3236,482 @@ function reduceRuleSelector(node) { } // } } -function diff(n1, n2, options = {}) { - let node1 = n1; - let node2 = n2; - let exchanged = false; - if (node1.chi.length > node2.chi.length) { - const t = node1; - node1 = node2; - node2 = t; - exchanged = true; - } - let i = node1.chi.length; - let j = node2.chi.length; - if (i == 0 || j == 0) { - // @ts-ignore - return null; - } - // @ts-ignore - const raw1 = node1.raw; - // @ts-ignore - // const optimized1 = node1.optimized; - // @ts-ignore - const raw2 = node2.raw; + +function* walk(node) { // @ts-ignore - // const optimized2 = node2.optimized; - node1 = { ...node1, chi: node1.chi.slice() }; - node2 = { ...node2, chi: node2.chi.slice() }; - if (raw1 != null) { - Object.defineProperty(node1, 'raw', { enumerable: false, writable: true, value: raw1 }); - } - // if (optimized1 != null) { - // Object.defineProperty(node1, 'optimized', {enumerable: false, writable: true, value: optimized1}); - // } - if (raw2 != null) { - Object.defineProperty(node2, 'raw', { enumerable: false, writable: true, value: raw2 }); - } - // if (optimized2 != null) { - // Object.defineProperty(node2, 'optimized', {enumerable: false, writable: true, value: optimized2}); - // } - const intersect = []; - while (i--) { - if (node1.chi[i].typ == 'Comment') { - continue; - } - j = node2.chi.length; - if (j == 0) { - break; + yield* doWalk(node, null, null); +} +function* doWalk(node, parent, root) { + yield { node, parent, root }; + if ('chi' in node) { + for (const child of node.chi) { + yield* doWalk(child, node, (root ?? node)); } - while (j--) { - if (node2.chi[j].typ == 'Comment') { - continue; - } - if (node1.chi[i].nam == node2.chi[j].nam) { - if (eq(node1.chi[i], node2.chi[j])) { - intersect.push(node1.chi[i]); - node1.chi.splice(i, 1); - node2.chi.splice(j, 1); - break; - } - } + } +} + +function* tokenize(iterator) { + let ind = -1; + let lin = 1; + let col = 0; + const position = { + ind: Math.max(ind, 0), + lin: lin, + col: Math.max(col, 1) + }; + let value; + let buffer = ''; + function consumeWhiteSpace() { + let count = 0; + while (isWhiteSpace(iterator.charAt(count + ind + 1).charCodeAt(0))) { + count++; } + next(count); + return count; } - // @ts-ignore - const result = (intersect.length == 0 ? null : { - ...node1, - // @ts-ignore - sel: [...new Set([...(n1?.raw?.reduce(reducer, []) || splitRule(n1.sel)).concat(n2?.raw?.reduce(reducer, []) || splitRule(n2.sel))])].join(','), - chi: intersect.reverse() - }); - if (result == null || [n1, n2].reduce((acc, curr) => curr.chi.length == 0 ? acc : acc + render(curr, options).code.length, 0) <= [node1, node2, result].reduce((acc, curr) => curr.chi.length == 0 ? acc : acc + render(curr, options).code.length, 0)) { - // @ts-ignore - return null; + function pushToken(token, hint) { + const result = { token, hint, position: { ...position }, bytesIn: ind }; + position.ind = ind; + position.lin = lin; + position.col = col == 0 ? 1 : col; + return result; } - return { result, node1: exchanged ? node2 : node1, node2: exchanged ? node2 : node2 }; -} -function matchSelectors(selector1, selector2, parentType) { - let match = [[]]; - const j = Math.min(selector1.reduce((acc, curr) => Math.min(acc, curr.length), selector1.length > 0 ? selector1[0].length : 0), selector2.reduce((acc, curr) => Math.min(acc, curr.length), selector2.length > 0 ? selector2[0].length : 0)); - let i = 0; - let k; - let l; - let token; - let matching = true; - let matchFunction = 0; - let inAttr = 0; - for (; i < j; i++) { - k = 0; - token = selector1[0][i]; - for (; k < selector1.length; k++) { - if (selector1[k][i] != token) { - matching = false; + function* consumeString(quoteStr) { + const quote = quoteStr; + let value; + let hasNewLine = false; + if (buffer.length > 0) { + yield pushToken(buffer); + buffer = ''; + } + buffer += quoteStr; + while (value = peek()) { + if (ind >= iterator.length) { + yield pushToken(buffer, hasNewLine ? 'Bad-string' : 'Unclosed-string'); break; } - } - if (matching) { - l = 0; - for (; l < selector2.length; l++) { - if (selector2[l][i] != token) { - matching = false; + if (value == '\\') { + const sequence = peek(6); + let escapeSequence = ''; + let codepoint; + let i; + for (i = 1; i < sequence.length; i++) { + codepoint = sequence.charCodeAt(i); + if (codepoint == 0x20 || + (codepoint >= 0x61 && codepoint <= 0x66) || + (codepoint >= 0x41 && codepoint <= 0x46) || + (codepoint >= 0x30 && codepoint <= 0x39)) { + escapeSequence += sequence[i]; + if (codepoint == 0x20) { + break; + } + continue; + } break; } + // not hex or new line + // @ts-ignore + if (i == 1 && !isNewLine(codepoint)) { + buffer += sequence[i]; + next(2); + continue; + } + if (escapeSequence.trimEnd().length > 0) { + const codepoint = Number(`0x${escapeSequence.trimEnd()}`); + if (codepoint == 0 || + // leading surrogate + (0xD800 <= codepoint && codepoint <= 0xDBFF) || + // trailing surrogate + (0xDC00 <= codepoint && codepoint <= 0xDFFF)) { + buffer += String.fromCodePoint(0xFFFD); + } + else { + buffer += String.fromCodePoint(codepoint); + } + next(escapeSequence.length + 1); + continue; + } + // buffer += value; + if (ind >= iterator.length) { + // drop '\\' at the end + yield pushToken(buffer); + break; + } + buffer += next(2); + continue; } - } - if (!matching) { - break; - } - if (token == ',') { - match.push([]); - } - else { - if (token.endsWith('(')) { - matchFunction++; - } - if (token.endsWith('[')) { - inAttr++; + if (value == quote) { + buffer += value; + yield pushToken(buffer, hasNewLine ? 'Bad-string' : 'String'); + next(); + // i += value.length; + buffer = ''; + break; } - else if (token == ')') { - matchFunction--; + if (isNewLine(value.charCodeAt(0))) { + hasNewLine = true; } - else if (token == ']') { - inAttr--; + if (hasNewLine && value == ';') { + yield pushToken(buffer, 'Bad-string'); + buffer = ''; + break; } - match.at(-1).push(token); + buffer += value; + // i += value.length; + next(); } } - // invalid function - if (matchFunction != 0 || inAttr != 0) { - return null; - } - if (parentType != 'Rule') { - for (const part of match) { - if (part.length > 0 && combinators.includes(part[0].charAt(0))) { - return null; - } + function peek(count = 1) { + if (count == 1) { + return iterator.charAt(ind + 1); } + return iterator.slice(ind + 1, ind + count + 1); } - if (match.length > 1) { - console.error(`unsupported multilevel matching`); - console.error({ match, selector1, selector2 }); - return null; + function prev(count = 1) { + if (count == 1) { + return ind == 0 ? '' : iterator.charAt(ind - 1); + } + return iterator.slice(ind - 1 - count, ind - 1); } - for (const part of match) { - while (part.length > 0) { - const token = part.at(-1); - if (token == ' ' || combinators.includes(token) || notEndingWith.includes(token.at(-1))) { - part.pop(); - continue; + function next(count = 1) { + let char = ''; + while (count-- > 0 && ind < iterator.length) { + const codepoint = iterator.charCodeAt(++ind); + if (isNaN(codepoint)) { + return char; + } + char += iterator.charAt(ind); + if (isNewLine(codepoint)) { + lin++; + col = 0; + } + else { + col++; } - break; } + return char; } - if (match.every(t => t.length == 0)) { - return null; - } - if (eq([['&']], match)) { - return null; - } - function reduce(acc, curr) { - if (acc === null) { - return null; - } - let hasCompoundSelector = true; - curr = curr.slice(match[0].length); - while (curr.length > 0) { - if (curr[0] == ' ') { - hasCompoundSelector = false; - curr.unshift('&'); - continue; + while (value = next()) { + if (ind >= iterator.length) { + if (buffer.length > 0) { + yield pushToken(buffer); + buffer = ''; } break; } - // invalid function match - if (curr.length > 0 && curr[0].endsWith('(') && curr.at(-1) != ')') { - return null; - } - if (curr.length == 1 && combinators.includes(curr[0].charAt(0))) { - return null; - } - if (hasCompoundSelector && curr.length > 0) { - hasCompoundSelector = !['&'].concat(combinators).includes(curr[0].charAt(0)); + if (isWhiteSpace(value.charCodeAt(0))) { + if (buffer.length > 0) { + yield pushToken(buffer); + buffer = ''; + } + while (value = next()) { + if (ind >= iterator.length) { + break; + } + if (!isWhiteSpace(value.charCodeAt(0))) { + break; + } + } + yield pushToken('', 'Whitespace'); + buffer = ''; + if (ind >= iterator.length) { + break; + } } - if (curr[0] == ':is(') { - let inFunction = 0; - let canReduce = true; - const isCompound = curr.reduce((acc, token, index) => { - if (index == 0) { - inFunction++; - canReduce = curr[1] == '&'; + switch (value) { + case '/': + if (buffer.length > 0) { + yield pushToken(buffer); + buffer = ''; + if (peek() != '*') { + yield pushToken(value); + break; + } } - else if (token.endsWith('(')) { - if (inFunction == 0) { - canReduce = false; + buffer += value; + if (peek() == '*') { + buffer += '*'; + // i++; + next(); + while (value = next()) { + if (ind >= iterator.length) { + yield pushToken(buffer, 'Bad-comment'); + break; + } + if (value == '\\') { + buffer += value; + value = next(); + if (ind >= iterator.length) { + yield pushToken(buffer, 'Bad-comment'); + break; + } + buffer += value; + continue; + } + if (value == '*') { + buffer += value; + value = next(); + if (ind >= iterator.length) { + yield pushToken(buffer, 'Bad-comment'); + break; + } + buffer += value; + if (value == '/') { + yield pushToken(buffer, 'Comment'); + buffer = ''; + break; + } + } + else { + buffer += value; + } } - inFunction++; } - else if (token == ')') { - inFunction--; + break; + case '<': + if (buffer.length > 0) { + yield pushToken(buffer); + buffer = ''; } - else if (token == ',') { - if (!canReduce) { - canReduce = curr[index + 1] == '&'; - } - acc.push([]); + buffer += value; + value = next(); + if (ind >= iterator.length) { + break; } - else - acc.at(-1)?.push(token); - return acc; - }, [[]]); - if (inFunction > 0) { - canReduce = false; - } - if (canReduce) { - curr = isCompound.reduce((acc, curr) => { - if (acc.length > 0) { - acc.push(','); + if (peek(3) == '!--') { + while (value = next()) { + if (ind >= iterator.length) { + break; + } + buffer += value; + if (value == '>' && prev(2) == '--') { + yield pushToken(buffer, 'CDOCOMM'); + buffer = ''; + break; + } } - acc.push(...curr); - return acc; - }, []); - } - } - // @todo: check hasCompoundSelector && curr[0] == '&' && curr[1] == ' ' - acc.push(match.length == 0 ? ['&'] : (hasCompoundSelector && curr[0] != '&' && (curr.length == 0 || !combinators.includes(curr[0].charAt(0))) ? ['&'].concat(curr) : curr)); - return acc; - } - // @ts-ignore - selector1 = selector1.reduce(reduce, []); - // @ts-ignore - selector2 = selector2.reduce(reduce, []); - return selector1 == null || selector2 == null ? null : { - eq: eq(selector1, selector2), - match, - selector1, - selector2 - }; -} -function reduceSelector(selector) { - if (selector.length == 0) { - return null; - } - const optimized = []; - const k = selector.reduce((acc, curr) => acc == 0 ? curr.length : (curr.length == 0 ? acc : Math.min(acc, curr.length)), 0); - let i = 0; - let j; - let match; - for (; i < k; i++) { - const item = selector[0][i]; - match = true; - for (j = 1; j < selector.length; j++) { - if (item != selector[j][i]) { - match = false; + } + if (ind >= iterator.length) { + yield pushToken(buffer, 'BADCDO'); + buffer = ''; + } break; - } - } - if (!match) { - break; - } - optimized.push(item); - } - while (optimized.length > 0) { - const last = optimized.at(-1); - if ((last == ' ' || combinators.includes(last))) { - optimized.pop(); - continue; - } - break; - } - selector.forEach((selector) => selector.splice(0, optimized.length)); - // combinator - if (combinators.includes(optimized.at(-1))) { - const combinator = optimized.pop(); - selector.forEach(selector => selector.unshift(combinator)); - } - let reducible = optimized.length == 1; - if (optimized[0] == '&' && optimized[1] == ' ') { - optimized.splice(0, 2); - } - if (optimized.length == 0 || - (optimized[0].charAt(0) == '&' || - selector.length == 1)) { - return { - match: false, - optimized, - selector: selector.map(selector => selector[0] == '&' && selector[1] == ' ' ? selector.slice(2) : selector), - reducible: selector.length > 1 && selector.every((selector) => !combinators.includes(selector[0])) - }; - } - return { - match: true, - optimized, - selector: selector.reduce((acc, curr) => { - let hasCompound = true; - if (hasCompound && curr.length > 0) { - hasCompound = !['&'].concat(combinators).includes(curr[0].charAt(0)); - } - // @ts-ignore - if (hasCompound && curr[0] == ' ') { - hasCompound = false; - curr.unshift('&'); - } - if (curr.length == 0) { - curr.push('&'); - hasCompound = false; - } - if (reducible) { - const chr = curr[0].charAt(0); + case '\\': + value = next(); + // EOF + if (ind + 1 >= iterator.length) { + // end of stream ignore \\ + yield pushToken(buffer); + buffer = ''; + break; + } + buffer += value; + break; + case '"': + case "'": + yield* consumeString(value); + break; + case '~': + case '|': + if (buffer.length > 0) { + yield pushToken(buffer); + buffer = ''; + } + buffer += value; + value = next(); + if (ind >= iterator.length) { + yield pushToken(buffer); + buffer = ''; + break; + } + if (value == '=') { + buffer += value; + yield pushToken(buffer, buffer[0] == '~' ? 'Includes' : 'Dash-matches'); + buffer = ''; + break; + } + yield pushToken(buffer); + while (isWhiteSpace(value.charCodeAt(0))) { + value = next(); + } + buffer = value; + break; + case '>': + if (buffer !== '') { + yield pushToken(buffer); + buffer = ''; + } + yield pushToken('', 'Gt'); + consumeWhiteSpace(); + break; + case '.': + const codepoint = peek().charCodeAt(0); + if (!isDigit(codepoint) && buffer !== '') { + yield pushToken(buffer); + buffer = value; + break; + } + buffer += value; + break; + case '+': + case ':': + case ',': + case '=': + if (buffer.length > 0) { + yield pushToken(buffer); + buffer = ''; + } + if (value == ':' && ':' == peek()) { + buffer += value + next(); + break; + } + yield pushToken(value); + buffer = ''; + if (value == '+' && isWhiteSpace(peek().charCodeAt(0))) { + yield pushToken(next()); + } + while (isWhiteSpace(peek().charCodeAt(0))) { + next(); + } + break; + case ')': + if (buffer.length > 0) { + yield pushToken(buffer); + buffer = ''; + } + yield pushToken('', 'End-parens'); + break; + case '(': + if (buffer.length == 0) { + yield pushToken('', 'Start-parens'); + break; + } + buffer += value; // @ts-ignore - reducible = chr == '.' || chr == ':' || isIdentStart(chr.codePointAt(0)); - } - acc.push(hasCompound ? ['&'].concat(curr) : curr); - return acc; - }, []), - reducible: selector.every((selector) => !['>', '+', '~', '&'].includes(selector[0])) - }; -} -function reducer(acc, curr, index, array) { - // trim :is() - if (array.length == 1 && array[0][0] == ':is(' && array[0].at(-1) == ')') { - curr = curr.slice(1, -1); - } - if (curr[0] == '&') { - if (curr[1] == ' ') { - curr.splice(0, 2); - } - else if (combinators.includes(curr[1])) { - curr.splice(0, 1); + if (buffer == 'url(') { + yield pushToken(buffer); + buffer = ''; + // consume either string or url token + let whitespace = ''; + value = peek(); + while (isWhiteSpace(value.charCodeAt(0))) { + whitespace += value; + } + if (whitespace.length > 0) { + next(whitespace.length); + } + value = peek(); + if (value == '"' || value == "'") { + yield* consumeString(next()); + break; + } + else { + buffer = ''; + do { + let cp = value.charCodeAt(0); + // EOF - + if (cp == null) { + yield pushToken('', 'Bad-url-token'); + break; + } + // ')' + if (cp == 0x29 || cp == null) { + if (buffer.length == 0) { + yield pushToken(buffer, 'Bad-url-token'); + } + else { + yield pushToken(buffer, 'Url-token'); + } + if (cp != null) { + yield pushToken(next()); + } + break; + } + if (isWhiteSpace(cp)) { + whitespace = next(); + while (true) { + value = peek(); + cp = value.charCodeAt(0); + if (isWhiteSpace(cp)) { + whitespace += value; + continue; + } + break; + } + if (cp == null || cp == 0x29) { + continue; + } + // bad url token + buffer += next(whitespace.length); + do { + value = peek(); + cp = value.charCodeAt(0); + if (cp == null || cp == 0x29) { + break; + } + buffer += next(); + } while (true); + yield pushToken(buffer, 'Bad-url-token'); + continue; + } + buffer += next(); + value = peek(); + } while (true); + buffer = ''; + } + break; + } + yield pushToken(buffer); + buffer = ''; + break; + case '[': + case ']': + case '{': + case '}': + case ';': + if (buffer.length > 0) { + yield pushToken(buffer); + buffer = ''; + } + yield pushToken(value); + break; + case '!': + if (buffer.length > 0) { + yield pushToken(buffer); + buffer = ''; + } + const important = peek(9); + if (important == 'important') { + yield pushToken('', 'Important'); + next(9); + buffer = ''; + break; + } + buffer = '!'; + break; + default: + buffer += value; + break; } } - acc.push(curr.join('')); - return acc; + if (buffer.length > 0) { + yield pushToken(buffer); + } } const urlTokenMatcher = /^(["']?)[a-zA-Z0-9_/.-][a-zA-Z0-9_/:.#?-]+(\1)$/; const funcLike = ['Start-parens', 'Func', 'UrlFunc', 'Pseudo-class-func']; +/** + * + * @param iterator + * @param opt + */ async function parse$1(iterator, opt = {}) { const errors = []; const options = { src: '', sourcemap: false, - compress: false, + minify: true, nestingRules: false, resolveImport: false, resolveUrls: false, @@ -3064,208 +3721,64 @@ async function parse$1(iterator, opt = {}) { if (options.resolveImport) { options.resolveUrls = true; } - let ind = -1; - let lin = 1; - let col = 0; - const tokens = []; const src = options.src; const stack = []; const ast = { typ: "StyleSheet", chi: [] }; - const position = { - ind: Math.max(ind, 0), - lin: lin, - col: Math.max(col, 1) - }; - let value; - let buffer = ''; - let total = iterator.length; - let bytesIn = total; + let tokens = []; let map = new Map; + let bytesIn = 0; let context = ast; if (options.sourcemap) { ast.loc = { sta: { - ind: ind, - lin: lin, - col: col + ind: 0, + lin: 1, + col: 1 }, src: '' }; } - function getType(val) { - if (val === '') { - throw new Error('empty string?'); - } - if (val == ':') { - return { typ: 'Colon' }; - } - if (val == ')') { - return { typ: 'End-parens' }; - } - if (val == '(') { - return { typ: 'Start-parens' }; - } - if (val == '=') { - return { typ: 'Delim', val }; - } - if (val == ';') { - return { typ: 'Semi-colon' }; + async function parseNode(results) { + let tokens = results.map(mapToken); + let i; + let loc; + for (i = 0; i < tokens.length; i++) { + if (tokens[i].typ == 'Comment') { + // @ts-ignore + context.chi.push(tokens[i]); + const position = map.get(tokens[i]); + loc = { + sta: position, + src + }; + if (options.sourcemap) { + tokens[i].loc = loc; + } + } + else if (tokens[i].typ != 'Whitespace') { + break; + } } - if (val == ',') { - return { typ: 'Comma' }; + tokens = tokens.slice(i); + if (tokens.length == 0) { + return null; } - if (val == '<') { - return { typ: 'Lt' }; + let delim = tokens.at(-1); + if (delim.typ == 'Semi-colon' || delim.typ == 'Block-start' || delim.typ == 'Block-end') { + tokens.pop(); } - if (val == '>') { - return { typ: 'Gt' }; + else { + delim = { typ: 'Semi-colon' }; } - if (isPseudo(val)) { - return val.endsWith('(') ? { - typ: 'Pseudo-class-func', - val: val.slice(0, -1), - chi: [] - } - : { - typ: 'Pseudo-class', - val - }; + // @ts-ignore + while (['Whitespace', 'Bad-string', 'Bad-comment'].includes(tokens.at(-1)?.typ)) { + tokens.pop(); } - if (isAtKeyword(val)) { - return { - typ: 'At-rule', - val: val.slice(1) - }; - } - if (isFunction(val)) { - val = val.slice(0, -1); - return { - typ: val == 'url' ? 'UrlFunc' : 'Func', - val, - chi: [] - }; - } - if (isNumber(val)) { - return { - typ: 'Number', - val - }; - } - if (isDimension(val)) { - return parseDimension(val); - } - if (isPercentage(val)) { - return { - typ: 'Perc', - val: val.slice(0, -1) - }; - } - if (val == 'currentColor') { - return { - typ: 'Color', - val, - kin: 'lit' - }; - } - if (isIdent(val)) { - return { - typ: 'Iden', - val - }; - } - if (val.charAt(0) == '#' && isHash(val)) { - return { - typ: 'Hash', - val - }; - } - if ('"\''.includes(val.charAt(0))) { - return { - typ: 'Unclosed-string', - val - }; - } - return { - typ: 'Literal', - val - }; - } - // consume and throw away - function consume(open, close) { - let count = 1; - let chr; - while (true) { - chr = next(); - if (chr == '\\') { - if (peek() === '') { - break; - } - continue; - } - else if (chr == '/' && peek() == '*') { - next(); - while (true) { - chr = next(); - if (chr === '') { - break; - } - if (chr == '*' && peek() == '/') { - next(); - break; - } - } - } - else if (chr == close) { - count--; - } - else if (chr == open) { - count++; - } - if (chr === '' || count == 0) { - break; - } - } - } - async function parseNode(tokens) { - let i; - let loc; - for (i = 0; i < tokens.length; i++) { - if (tokens[i].typ == 'Comment') { - // @ts-ignore - context.chi.push(tokens[i]); - const position = map.get(tokens[i]); - loc = { - sta: position, - src - }; - if (options.sourcemap) { - tokens[i].loc = loc; - } - } - else if (tokens[i].typ != 'Whitespace') { - break; - } - } - tokens = tokens.slice(i); - if (tokens.length == 0) { - return null; - } - let delim = tokens.at(-1); - if (delim.typ == 'Semi-colon' || delim.typ == 'Block-start' || delim.typ == 'Block-end') { - tokens.pop(); - } - else { - delim = { typ: 'Semi-colon' }; - } - // @ts-ignore - while (['Whitespace', 'Bad-string', 'Bad-comment'].includes(tokens.at(-1)?.typ)) { - tokens.pop(); - } - if (tokens.length == 0) { - return null; + if (tokens.length == 0) { + return null; } if (tokens[0]?.typ == 'At-rule') { const atRule = tokens.shift(); @@ -3327,7 +3840,7 @@ async function parse$1(iterator, opt = {}) { // @ts-ignore const root = await options.load(url, options.src).then((src) => { return parse$1(src, Object.assign({}, options, { - compress: false, + minify: false, // @ts-ignore src: options.resolve(url, options.src).absolute })); @@ -3378,626 +3891,336 @@ 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; - } - } + // 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, 'Rule', { compress: options.compress }).reduce((acc, curr, index, array) => { + parseTokens(tokens, { minify: options.minify }).reduce((acc, curr, index, array) => { if (curr.typ == 'Whitespace') { - if (array[index - 1]?.val == '+' || array[index + 1]?.val == '+') { + if (array[index - 1]?.typ == 'Gt' || + array[index + 1]?.typ == 'Gt' || + combinators.includes(array[index - 1]?.val) || + combinators.includes(array[index + 1]?.val)) { return acc; } } - let t = renderToken(curr, { compress: true }); + let t = renderToken(curr, { minify: true }); if (t == ',') { acc.push([]); } - else { - acc[acc.length - 1].push(t); - } - return acc; - }, [[]]).reduce((acc, curr) => { - acc.set(curr.join(''), curr); - return acc; - }, uniq); - const node = { - typ: 'Rule', - // @ts-ignore - sel: [...uniq.keys()].join(','), - chi: [] - }; - let raw = [...uniq.values()]; - Object.defineProperty(node, 'raw', { enumerable: false, writable: true, value: raw }); - loc = { - sta: position, - src - }; - if (options.sourcemap) { - node.loc = loc; - } - // @ts-ignore - context.chi.push(node); - return node; - } - else { - // declaration - // @ts-ignore - let name = null; - // @ts-ignore - let value = null; - for (let i = 0; i < tokens.length; i++) { - if (tokens[i].typ == 'Comment') { - continue; - } - if (tokens[i].typ == 'Colon') { - name = tokens.slice(0, i); - value = parseTokens(tokens.slice(i + 1), 'Declaration', { - parseColor: true, - src: options.src, - resolveUrls: options.resolveUrls, - resolve: options.resolve, - cwd: options.cwd - }); - } - } - if (name == null) { - name = tokens; - } - const position = map.get(name[0]); - if (name.length > 0) { - for (let i = 1; i < name.length; i++) { - if (name[i].typ != 'Whitespace' && name[i].typ != 'Comment') { - errors.push({ - action: 'drop', - message: 'invalid declaration', - location: { src, ...position } - }); - return null; - } - } - } - if (value == null) { - errors.push({ action: 'drop', message: 'invalid declaration', location: { src, ...position } }); - return null; - } - if (value.length == 0) { - errors.push({ action: 'drop', message: 'invalid declaration', location: { src, ...position } }); - return null; - } - const node = { - typ: 'Declaration', - // @ts-ignore - nam: renderToken(name.shift(), { removeComments: true }), - // @ts-ignore - val: value - }; - while (node.val[0]?.typ == 'Whitespace') { - node.val.shift(); - } - if (node.val.length == 0) { - errors.push({ action: 'drop', message: 'invalid declaration', location: { src, ...position } }); - return null; - } - // @ts-ignore - context.chi.push(node); - return null; - } - } - } - function peek(count = 1) { - if (count == 1) { - return iterator.charAt(ind + 1); - } - return iterator.slice(ind + 1, ind + count + 1); - } - function prev(count = 1) { - if (count == 1) { - return ind == 0 ? '' : iterator.charAt(ind - 1); - } - return iterator.slice(ind - 1 - count, ind - 1); - } - function next(count = 1) { - let char = ''; - while (count-- > 0 && ind < total) { - const codepoint = iterator.charCodeAt(++ind); - if (isNaN(codepoint)) { - return char; - } - char += iterator.charAt(ind); - if (isNewLine(codepoint)) { - lin++; - col = 0; - } - else { - col++; - } - } - return char; - } - function pushToken(token) { - tokens.push(token); - map.set(token, { ...position }); - position.ind = ind; - position.lin = lin; - position.col = col == 0 ? 1 : col; - } - function consumeWhiteSpace() { - let count = 0; - while (isWhiteSpace(iterator.charAt(count + ind + 1).charCodeAt(0))) { - count++; - } - next(count); - return count; - } - function consumeString(quoteStr) { - const quote = quoteStr; - let value; - let hasNewLine = false; - if (buffer.length > 0) { - pushToken(getType(buffer)); - buffer = ''; - } - buffer += quoteStr; - while (ind < total) { - value = peek(); - if (ind >= total) { - pushToken({ typ: hasNewLine ? 'Bad-string' : 'Unclosed-string', val: buffer }); - break; - } - if (value == '\\') { - const sequence = peek(6); - let escapeSequence = ''; - let codepoint; - let i; - for (i = 1; i < sequence.length; i++) { - codepoint = sequence.charCodeAt(i); - if (codepoint == 0x20 || - (codepoint >= 0x61 && codepoint <= 0x66) || - (codepoint >= 0x41 && codepoint <= 0x46) || - (codepoint >= 0x30 && codepoint <= 0x39)) { - escapeSequence += sequence[i]; - if (codepoint == 0x20) { - break; - } - continue; - } - break; - } - // not hex or new line - // @ts-ignore - if (i == 1 && !isNewLine(codepoint)) { - buffer += sequence[i]; - next(2); - continue; - } - if (escapeSequence.trimEnd().length > 0) { - const codepoint = Number(`0x${escapeSequence.trimEnd()}`); - if (codepoint == 0 || - // leading surrogate - (0xD800 <= codepoint && codepoint <= 0xDBFF) || - // trailing surrogate - (0xDC00 <= codepoint && codepoint <= 0xDFFF)) { - buffer += String.fromCodePoint(0xFFFD); - } - else { - buffer += String.fromCodePoint(codepoint); - } - next(escapeSequence.length + 1); - continue; - } - // buffer += value; - if (ind >= total) { - // drop '\\' at the end - pushToken(getType(buffer)); - break; - } - buffer += next(2); - continue; - } - if (value == quote) { - buffer += value; - pushToken({ typ: hasNewLine ? 'Bad-string' : 'String', val: buffer }); - next(); - // i += value.length; - buffer = ''; - break; - } - if (isNewLine(value.charCodeAt(0))) { - hasNewLine = true; - } - if (hasNewLine && value == ';') { - pushToken({ typ: 'Bad-string', val: buffer }); - buffer = ''; - break; - } - buffer += value; - // i += value.length; - next(); - } - } - while (ind < total) { - value = next(); - if (ind >= total) { - if (buffer.length > 0) { - pushToken(getType(buffer)); - buffer = ''; - } - break; - } - if (isWhiteSpace(value.charCodeAt(0))) { - if (buffer.length > 0) { - pushToken(getType(buffer)); - buffer = ''; - } - while (ind < total) { - value = next(); - if (ind >= total) { - break; - } - if (!isWhiteSpace(value.charCodeAt(0))) { - break; - } - } - pushToken({ typ: 'Whitespace' }); - buffer = ''; - if (ind >= total) { - break; - } - } - switch (value) { - case '/': - if (buffer.length > 0 && tokens.at(-1)?.typ == 'Whitespace') { - pushToken(getType(buffer)); - buffer = ''; - if (peek() != '*') { - pushToken(getType(value)); - break; - } - } - buffer += value; - if (peek() == '*') { - buffer += '*'; - // i++; - next(); - while (ind < total) { - value = next(); - if (ind >= total) { - pushToken({ - typ: 'Bad-comment', val: buffer - }); - break; - } - if (value == '\\') { - buffer += value; - value = next(); - if (ind >= total) { - pushToken({ - typ: 'Bad-comment', - val: buffer - }); - break; - } - buffer += value; - continue; - } - if (value == '*') { - buffer += value; - value = next(); - if (ind >= total) { - pushToken({ - typ: 'Bad-comment', val: buffer - }); - break; - } - buffer += value; - if (value == '/') { - pushToken({ typ: 'Comment', val: buffer }); - buffer = ''; - break; - } - } - else { - buffer += value; - } - } - } - break; - case '<': - if (buffer.length > 0) { - pushToken(getType(buffer)); - buffer = ''; - } - buffer += value; - value = next(); - if (ind >= total) { - break; - } - if (peek(3) == '!--') { - while (ind < total) { - value = next(); - if (ind >= total) { - break; - } - buffer += value; - if (value == '>' && prev(2) == '--') { - pushToken({ - typ: 'CDOCOMM', - val: buffer - }); - buffer = ''; - break; - } - } - } - if (ind >= total) { - pushToken({ typ: 'BADCDO', val: buffer }); - buffer = ''; - } - break; - case '\\': - value = next(); - // EOF - if (ind + 1 >= total) { - // end of stream ignore \\ - pushToken(getType(buffer)); - buffer = ''; - break; - } - buffer += value; - break; - case '"': - case "'": - consumeString(value); - break; - case '~': - case '|': - if (tokens.at(-1)?.typ == 'Whitespace') { - tokens.pop(); - } - if (buffer.length > 0) { - pushToken(getType(buffer)); - buffer = ''; - } - buffer += value; - value = next(); - if (ind >= total) { - pushToken(getType(buffer)); - buffer = ''; - break; - } - if (value == '=') { - buffer += value; - pushToken({ - typ: buffer[0] == '~' ? 'Includes' : 'Dash-matches', - val: buffer - }); - buffer = ''; - break; - } - pushToken(getType(buffer)); - while (isWhiteSpace(value.charCodeAt(0))) { - value = next(); - } - buffer = value; - break; - case '>': - if (buffer !== '') { - pushToken(getType(buffer)); - buffer = ''; - } - if (tokens[tokens.length - 1]?.typ == 'Whitespace') { - tokens.pop(); - } - pushToken({ typ: 'Gt' }); - consumeWhiteSpace(); - break; - case '.': - const codepoint = peek().charCodeAt(0); - if (!isDigit(codepoint) && buffer !== '') { - pushToken(getType(buffer)); - buffer = value; - break; - } - buffer += value; - break; - case '+': - case ':': - case ',': - case '=': - if (buffer.length > 0) { - pushToken(getType(buffer)); - buffer = ''; - } - if (value == ':' && ':' == peek()) { - buffer += value + next(); - break; - } - pushToken(getType(value)); - buffer = ''; - if (value == '+' && isWhiteSpace(peek().charCodeAt(0))) { - pushToken(getType(next())); - } - while (isWhiteSpace(peek().charCodeAt(0))) { - next(); + else { + acc[acc.length - 1].push(t); + } + return acc; + }, [[]]).reduce((acc, curr) => { + acc.set(curr.join(''), curr); + return acc; + }, uniq); + const node = { + typ: 'Rule', + // @ts-ignore + sel: [...uniq.keys()].join(','), + chi: [] + }; + let raw = [...uniq.values()]; + Object.defineProperty(node, 'raw', { enumerable: false, writable: true, value: raw }); + loc = { + sta: position, + src + }; + if (options.sourcemap) { + node.loc = loc; } - break; - case ')': - if (buffer.length > 0) { - pushToken(getType(buffer)); - buffer = ''; + // @ts-ignore + context.chi.push(node); + return node; + } + else { + // declaration + // @ts-ignore + let name = null; + // @ts-ignore + let value = null; + for (let i = 0; i < tokens.length; i++) { + if (tokens[i].typ == 'Comment') { + continue; + } + if (tokens[i].typ == 'Colon') { + name = tokens.slice(0, i); + value = parseTokens(tokens.slice(i + 1), { + parseColor: true, + src: options.src, + resolveUrls: options.resolveUrls, + resolve: options.resolve, + cwd: options.cwd + }); + } } - pushToken({ typ: 'End-parens' }); - break; - case '(': - if (buffer.length == 0) { - pushToken({ typ: 'Start-parens' }); + if (name == null) { + name = tokens; } - else { - buffer += value; - pushToken(getType(buffer)); - buffer = ''; - const token = tokens[tokens.length - 1]; - if (token.typ == 'UrlFunc') { - // consume either string or url token - let whitespace = ''; - value = peek(); - while (isWhiteSpace(value.charCodeAt(0))) { - whitespace += value; - } - if (whitespace.length > 0) { - next(whitespace.length); - } - value = peek(); - if (value == '"' || value == "'") { - consumeString(next()); - let token = tokens[tokens.length - 1]; - if (['String', 'Literal'].includes(token.typ) && urlTokenMatcher.test(token.val)) { - if (token.val.slice(1, 6) != 'data:') { - if (token.typ == 'String') { - token.val = token.val.slice(1, -1); - } - // @ts-ignore - token.typ = 'Url-token'; - } - } - break; - } - else { - buffer = ''; - do { - let cp = value.charCodeAt(0); - // EOF - - if (cp == null) { - pushToken({ typ: 'Bad-url-token', val: buffer }); - break; - } - // ')' - if (cp == 0x29 || cp == null) { - if (buffer.length == 0) { - pushToken({ typ: 'Bad-url-token', val: '' }); - } - else { - pushToken({ typ: 'Url-token', val: buffer }); - } - if (cp != null) { - pushToken(getType(next())); - } - break; - } - if (isWhiteSpace(cp)) { - whitespace = next(); - while (true) { - value = peek(); - cp = value.charCodeAt(0); - if (isWhiteSpace(cp)) { - whitespace += value; - continue; - } - break; - } - if (cp == null || cp == 0x29) { - continue; - } - // bad url token - buffer += next(whitespace.length); - do { - value = peek(); - cp = value.charCodeAt(0); - if (cp == null || cp == 0x29) { - break; - } - buffer += next(); - } while (true); - pushToken({ typ: 'Bad-url-token', val: buffer }); - continue; - } - buffer += next(); - value = peek(); - } while (true); - buffer = ''; + const position = map.get(name[0]); + if (name.length > 0) { + for (let i = 1; i < name.length; i++) { + if (name[i].typ != 'Whitespace' && name[i].typ != 'Comment') { + errors.push({ + action: 'drop', + message: 'invalid declaration', + location: { src, ...position } + }); + return null; } } } - break; - case '[': - case ']': - case '{': - case '}': - case ';': - if (buffer.length > 0) { - pushToken(getType(buffer)); - buffer = ''; + if (value == null) { + errors.push({ + action: 'drop', + message: 'invalid declaration', + location: { src, ...position } + }); + return null; } - pushToken(getBlockType(value)); - let node = null; - if (value == '{' || value == ';') { - node = await parseNode(tokens); - if (node != null) { - stack.push(node); - // @ts-ignore - context = node; - } - else if (value == '{') { - // node == null - // consume and throw away until the closing '}' or EOF - consume('{', '}'); - } - tokens.length = 0; - map.clear(); + if (value.length == 0) { + errors.push({ + action: 'drop', + message: 'invalid declaration', + location: { src, ...position } + }); + return null; } - else if (value == '}') { - await parseNode(tokens); - const previousNode = stack.pop(); + const node = { + typ: 'Declaration', // @ts-ignore - context = stack[stack.length - 1] || ast; + nam: renderToken(name.shift(), { removeComments: true }), // @ts-ignore - if (options.removeEmpty && previousNode != null && previousNode.chi.length == 0 && context.chi[context.chi.length - 1] == previousNode) { - context.chi.pop(); - } - tokens.length = 0; - map.clear(); - buffer = ''; - } - break; - case '!': - if (buffer.length > 0) { - pushToken(getType(buffer)); - buffer = ''; + val: value + }; + while (node.val[0]?.typ == 'Whitespace') { + node.val.shift(); } - const important = peek(9); - if (important == 'important') { - if (tokens[tokens.length - 1]?.typ == 'Whitespace') { - tokens.pop(); - } - pushToken({ typ: 'Important' }); - next(9); - buffer = ''; - break; + if (node.val.length == 0) { + errors.push({ + action: 'drop', + message: 'invalid declaration', + location: { src, ...position } + }); + return null; } - buffer = '!'; - break; - default: - buffer += value; - break; + // @ts-ignore + context.chi.push(node); + return null; + } } } - if (buffer.length > 0) { - pushToken(getType(buffer)); + function mapToken(token) { + const node = getTokenType(token.token, token.hint); + map.set(node, token.position); + return node; + } + const iter = tokenize(iterator); + let item; + while (true) { + item = iter.next().value; + if (item == null) { + break; + } + tokens.push(item); + bytesIn = item.bytesIn; + if (item.token == ';' || item.token == '{') { + let node = await parseNode(tokens); + if (node != null) { + stack.push(node); + // @ts-ignore + context = node; + } + else if (item.token == '{') { + // node == null + // consume and throw away until the closing '}' or EOF + let inBlock = 1; + do { + item = iter.next().value; + if (item == null) { + break; + } + if (item.token == '{') { + inBlock++; + } + else if (item.token == '}') { + inBlock--; + } + } while (inBlock != 0); + } + tokens = []; + map = new Map; + } + else if (item.token == '}') { + await parseNode(tokens); + const previousNode = stack.pop(); + // @ts-ignore + context = stack[stack.length - 1] || ast; + // @ts-ignore + if (options.removeEmpty && previousNode != null && previousNode.chi.length == 0 && context.chi[context.chi.length - 1] == previousNode) { + context.chi.pop(); + } + tokens = []; + map = new Map; + } } if (tokens.length > 0) { await parseNode(tokens); } - if (options.compress) { + if (options.minify) { if (ast.chi.length > 0) { - deduplicate(ast, options, true); + minify(ast, options, true); } } return { ast, errors, bytesIn }; } -function parseTokens(tokens, nodeType, options = {}) { +function parseString(src, options = { location: false }) { + return [...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) { + throw new Error('empty string?'); + } + if (hint != null) { + return ([ + 'Whitespace', 'Semi-colon', 'Colon', 'Block-start', + 'Block-start', 'Attr-start', 'Attr-end', 'Start-parens', 'End-parens', + 'Comma', 'Gt', 'Lt' + ].includes(hint) ? { typ: hint } : { typ: hint, val }); + } + if (val == ' ') { + return { typ: 'Whitespace' }; + } + if (val == ';') { + return { typ: 'Semi-colon' }; + } + if (val == '{') { + return { typ: 'Block-start' }; + } + if (val == '}') { + return { typ: 'Block-end' }; + } + if (val == '[') { + return { typ: 'Attr-start' }; + } + if (val == ']') { + return { typ: 'Attr-end' }; + } + if (val == ':') { + return { typ: 'Colon' }; + } + if (val == ')') { + return { typ: 'End-parens' }; + } + if (val == '(') { + return { typ: 'Start-parens' }; + } + if (val == '=') { + return { typ: 'Delim', val }; + } + if (val == ';') { + return { typ: 'Semi-colon' }; + } + if (val == ',') { + return { typ: 'Comma' }; + } + if (val == '<') { + return { typ: 'Lt' }; + } + if (val == '>') { + return { typ: 'Gt' }; + } + if (isPseudo(val)) { + return val.endsWith('(') ? { + typ: 'Pseudo-class-func', + val: val.slice(0, -1), + chi: [] + } + : { + typ: 'Pseudo-class', + val + }; + } + if (isAtKeyword(val)) { + return { + typ: 'At-rule', + val: val.slice(1) + }; + } + if (isFunction(val)) { + val = val.slice(0, -1); + return { + typ: val == 'url' ? 'UrlFunc' : 'Func', + val, + chi: [] + }; + } + if (isNumber(val)) { + return { + typ: 'Number', + val + }; + } + if (isDimension(val)) { + return parseDimension(val); + } + if (isPercentage(val)) { + return { + typ: 'Perc', + val: val.slice(0, -1) + }; + } + const v = val.toLowerCase(); + if (v == 'currentcolor' || val == 'transparent' || v in COLORS_NAMES) { + return { + typ: 'Color', + val, + kin: 'lit' + }; + } + if (isIdent(val)) { + return { + typ: 'Iden', + val + }; + } + if (val.charAt(0) == '#' && isHexColor(val)) { + return { + typ: 'Color', + val, + kin: 'hex' + }; + } + if (val.charAt(0) == '#' && isHash(val)) { + return { + typ: 'Hash', + val + }; + } + if ('"\''.includes(val.charAt(0))) { + return { + typ: 'Unclosed-string', + val + }; + } + return { + typ: 'Literal', + val + }; +} +function parseTokens(tokens, options = {}) { for (let i = 0; i < tokens.length; i++) { const t = tokens[i]; if (t.typ == 'Whitespace' && ((i == 0 || @@ -4051,7 +4274,7 @@ function parseTokens(tokens, nodeType, options = {}) { if (t.chi.length > 1) { /*(t).chi =*/ // @ts-ignore - parseTokens(t.chi, t.typ, options); + parseTokens(t.chi, t.typ); } // @ts-ignore t.chi.forEach(val => { @@ -4163,8 +4386,8 @@ function parseTokens(tokens, nodeType, options = {}) { // @ts-ignore if (t.chi.length > 0) { // @ts-ignore - parseTokens(t.chi, t.typ, options); - if (t.typ == 'Pseudo-class-func' && t.val == ':is' && options.compress) { + parseTokens(t.chi, t.typ); + if (t.typ == 'Pseudo-class-func' && t.val == ':is' && options.minify) { // const count = t.chi.filter(t => t.typ != 'Comment').length; if (count == 1 || @@ -4202,40 +4425,9 @@ function parseTokens(tokens, nodeType, options = {}) { } return tokens; } -function getBlockType(chr) { - if (chr == ';') { - return { typ: 'Semi-colon' }; - } - if (chr == '{') { - return { typ: 'Block-start' }; - } - if (chr == '}') { - return { typ: 'Block-end' }; - } - if (chr == '[') { - return { typ: 'Attr-start' }; - } - if (chr == ']') { - return { typ: 'Attr-end' }; - } - throw new Error(`unhandled token: '${chr}'`); -} - -function* walk(node) { - // @ts-ignore - yield* doWalk(node, null, null); -} -function* doWalk(node, parent, root) { - yield { node, parent, root }; - if ('chi' in node) { - for (const child of node.chi) { - yield* doWalk(child, node, (root ?? node)); - } - } -} async function transform$1(css, options = {}) { - options = { compress: true, removeEmpty: true, ...options }; + options = { minify: true, removeEmpty: true, ...options }; const startTime = performance.now(); const parseResult = await parse$1(css, options); const renderTime = performance.now(); @@ -4369,16 +4561,42 @@ function transform(css, options = {}) { return transform$1(css, Object.assign(options, { load, resolve, cwd: options.cwd ?? process.cwd() })); } -exports.deduplicate = deduplicate; -exports.deduplicateRule = deduplicateRule; +exports.combinators = combinators; exports.dirname = dirname; +exports.getConfig = getConfig; exports.hasDeclaration = hasDeclaration; +exports.isAngle = isAngle; +exports.isAtKeyword = isAtKeyword; +exports.isDigit = isDigit; +exports.isDimension = isDimension; +exports.isFrequency = isFrequency; +exports.isFunction = isFunction; +exports.isHash = isHash; +exports.isHexColor = isHexColor; +exports.isHexDigit = isHexDigit; +exports.isIdent = isIdent; +exports.isIdentCodepoint = isIdentCodepoint; +exports.isIdentStart = isIdentStart; +exports.isLength = isLength; +exports.isNewLine = isNewLine; +exports.isNumber = isNumber; +exports.isPercentage = isPercentage; +exports.isPseudo = isPseudo; +exports.isResolution = isResolution; +exports.isTime = isTime; +exports.isWhiteSpace = isWhiteSpace; exports.load = load; exports.matchUrl = matchUrl; +exports.minify = minify; +exports.minifyRule = minifyRule; exports.parse = parse; +exports.parseDimension = parseDimension; +exports.parseString = parseString; exports.reduceSelector = reduceSelector; exports.render = render; exports.renderToken = renderToken; exports.resolve = resolve; +exports.tokenize = tokenize; exports.transform = transform; +exports.urlTokenMatcher = urlTokenMatcher; exports.walk = walk; diff --git a/dist/index.d.ts b/dist/index.d.ts index 385ebfe..749146e 100644 --- a/dist/index.d.ts +++ b/dist/index.d.ts @@ -173,6 +173,275 @@ 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 PropertiesConfig { + properties: PropertiesConfigProperties; + map: Map; +} +interface Map { + border: Border; + "border-color": BackgroundPositionClass; + "border-style": BackgroundPositionClass; + "border-width": BackgroundPositionClass; + outline: Outline; + "outline-color": BackgroundPositionClass; + "outline-style": BackgroundPositionClass; + "outline-width": BackgroundPositionClass; + font: Font; + "font-weight": BackgroundPositionClass; + "font-style": BackgroundPositionClass; + "font-size": BackgroundPositionClass; + "line-height": BackgroundPositionClass; + "font-stretch": BackgroundPositionClass; + "font-variant": BackgroundPositionClass; + "font-family": BackgroundPositionClass; + background: Background; + "background-repeat": BackgroundPositionClass; + "background-color": BackgroundPositionClass; + "background-image": BackgroundPositionClass; + "background-attachment": BackgroundPositionClass; + "background-clip": BackgroundPositionClass; + "background-origin": BackgroundPositionClass; + "background-position": BackgroundPositionClass; + "background-size": BackgroundPositionClass; +} +interface Background { + shorthand: string; + pattern: string; + keywords: string[]; + default: any[]; + multiple: boolean; + separator: Separator; + properties: BackgroundProperties; +} +interface BackgroundProperties { + "background-repeat": BackgroundRepeat; + "background-color": PurpleBackgroundAttachment; + "background-image": PurpleBackgroundAttachment; + "background-attachment": PurpleBackgroundAttachment; + "background-clip": PurpleBackgroundAttachment; + "background-origin": PurpleBackgroundAttachment; + "background-position": BackgroundPosition; + "background-size": BackgroundSize; +} +interface PurpleBackgroundAttachment { + types: string[]; + default: string[]; + keywords: string[]; + required?: boolean; + mapping?: BackgroundAttachmentMapping; +} +interface BackgroundAttachmentMapping { + "ultra-condensed": string; + "extra-condensed": string; + condensed: string; + "semi-condensed": string; + normal: string; + "semi-expanded": string; + expanded: string; + "extra-expanded": string; + "ultra-expanded": string; +} +interface BackgroundPosition { + multiple: boolean; + types: string[]; + default: string[]; + keywords: string[]; + mapping: BackgroundPositionMapping; + constraints: BackgroundPositionConstraints; +} +interface BackgroundPositionConstraints { + mapping: ConstraintsMapping; +} +interface ConstraintsMapping { + max: number; +} +interface BackgroundPositionMapping { + left: string; + top: string; + center: string; + bottom: string; + right: string; +} +interface BackgroundRepeat { + types: any[]; + default: string[]; + multiple: boolean; + keywords: string[]; + mapping: BackgroundRepeatMapping; +} +interface BackgroundRepeatMapping { + "repeat no-repeat": string; + "no-repeat repeat": string; + "repeat repeat": string; + "space space": string; + "round round": string; + "no-repeat no-repeat": string; +} +interface BackgroundSize { + multiple: boolean; + previous: string; + prefix: Prefix; + types: string[]; + default: string[]; + keywords: string[]; + mapping: BackgroundSizeMapping; +} +interface BackgroundSizeMapping { + "auto auto": string; +} +interface Prefix { + typ: string; + val: string; +} +interface Separator { + typ: string; +} +interface BackgroundPositionClass { + shorthand: string; +} +interface Border { + shorthand: string; + pattern: string; + keywords: string[]; + default: string[]; + properties: BorderProperties; +} +interface BorderProperties { + "border-color": BorderColorClass; + "border-style": BorderColorClass; + "border-width": BorderColorClass; +} +interface BorderColorClass { +} +interface Font { + shorthand: string; + pattern: string; + keywords: string[]; + default: any[]; + properties: FontProperties; +} +interface FontProperties { + "font-weight": FontWeight; + "font-style": PurpleBackgroundAttachment; + "font-size": PurpleBackgroundAttachment; + "line-height": LineHeight; + "font-stretch": PurpleBackgroundAttachment; + "font-variant": PurpleBackgroundAttachment; + "font-family": FontFamily; +} +interface FontFamily { + types: string[]; + default: any[]; + keywords: string[]; + required: boolean; + multiple: boolean; + separator: Separator; +} +interface FontWeight { + types: string[]; + default: string[]; + keywords: string[]; + constraints: FontWeightConstraints; + mapping: FontWeightMapping; +} +interface FontWeightConstraints { + value: Value; +} +interface Value { + min: string; + max: string; +} +interface FontWeightMapping { + thin: string; + hairline: string; + "extra light": string; + "ultra light": string; + light: string; + normal: string; + regular: string; + medium: string; + "semi bold": string; + "demi bold": string; + bold: string; + "extra bold": string; + "ultra bold": string; + black: string; + heavy: string; + "extra black": string; + "ultra black": string; +} +interface LineHeight { + types: string[]; + default: string[]; + keywords: string[]; + previous: string; + prefix: Prefix; +} +interface Outline { + shorthand: string; + pattern: string; + keywords: string[]; + default: string[]; + properties: OutlineProperties; +} +interface OutlineProperties { + "outline-color": PurpleBackgroundAttachment; + "outline-style": PurpleBackgroundAttachment; + "outline-width": PurpleBackgroundAttachment; +} +interface PropertiesConfigProperties { + inset: BorderRadius; + top: BackgroundPositionClass; + right: BackgroundPositionClass; + bottom: BackgroundPositionClass; + left: BackgroundPositionClass; + margin: BorderRadius; + "margin-top": BackgroundPositionClass; + "margin-right": BackgroundPositionClass; + "margin-bottom": BackgroundPositionClass; + "margin-left": BackgroundPositionClass; + padding: BorderColor; + "padding-top": BackgroundPositionClass; + "padding-right": BackgroundPositionClass; + "padding-bottom": BackgroundPositionClass; + "padding-left": BackgroundPositionClass; + "border-radius": BorderRadius; + "border-top-left-radius": BackgroundPositionClass; + "border-top-right-radius": BackgroundPositionClass; + "border-bottom-right-radius": BackgroundPositionClass; + "border-bottom-left-radius": BackgroundPositionClass; + "border-width": BorderColor; + "border-top-width": BackgroundPositionClass; + "border-right-width": BackgroundPositionClass; + "border-bottom-width": BackgroundPositionClass; + "border-left-width": BackgroundPositionClass; + "border-style": BorderColor; + "border-top-style": BackgroundPositionClass; + "border-right-style": BackgroundPositionClass; + "border-bottom-style": BackgroundPositionClass; + "border-left-style": BackgroundPositionClass; + "border-color": BorderColor; + "border-top-color": BackgroundPositionClass; + "border-right-color": BackgroundPositionClass; + "border-bottom-color": BackgroundPositionClass; + "border-left-color": BackgroundPositionClass; +} +interface BorderColor { + shorthand: string; + map?: string; + properties: string[]; + types: string[]; + keywords: string[]; +} +interface BorderRadius { + shorthand: string; + properties: string[]; + types: string[]; + multiple: boolean; + separator: null | string; + keywords: string[]; +} + interface ErrorDescription { // drop rule or declaration | fix rule or declaration @@ -190,7 +459,7 @@ interface ParserOptions { src?: string; sourcemap?: boolean; - compress?: boolean; + minify?: boolean; nestingRules?: boolean; removeEmpty?: boolean; resolveUrls?: boolean; @@ -203,7 +472,7 @@ interface ParserOptions { interface RenderOptions { - compress?: boolean; + minify?: boolean; preserveLicense?: boolean; indent?: string; newLine?: string; @@ -236,6 +505,13 @@ interface TransformResult extends ParseResult, RenderResult { } } +interface TokenizeResult { + token: string; + hint?: string; + position: Position; + bytesIn: number; +} + interface Position { ind: number; @@ -313,18 +589,50 @@ type AstNode = | AstRule | AstDeclaration; -declare function deduplicate(ast: AstNode, options?: ParserOptions, recursive?: boolean): AstNode; -declare function hasDeclaration(node: AstRule): boolean; -declare function deduplicateRule(ast: AstRule | AstAtRule): AstRule | AstAtRule; +declare const urlTokenMatcher: RegExp; +declare function parseString(src: string, options?: { + location: boolean; +}): Token[]; + +declare function tokenize(iterator: string): Generator; + +declare function isLength(dimension: DimensionToken): boolean; +declare function isResolution(dimension: DimensionToken): boolean; +declare function isAngle(dimension: DimensionToken): boolean; +declare function isTime(dimension: DimensionToken): boolean; +declare function isFrequency(dimension: DimensionToken): boolean; +declare function isIdentStart(codepoint: number): boolean; +declare function isDigit(codepoint: number): boolean; +declare function isIdentCodepoint(codepoint: number): boolean; +declare function isIdent(name: string): boolean; +declare function isPseudo(name: string): boolean; +declare function isHash(name: string): boolean; +declare function isNumber(name: string): boolean; +declare function isDimension(name: string): boolean; +declare function isPercentage(name: string): boolean; +declare function parseDimension(name: string): DimensionToken | LengthToken | AngleToken; +declare function isHexColor(name: string): boolean; +declare function isHexDigit(name: string): boolean; +declare function isFunction(name: string): boolean; +declare function isAtKeyword(name: string): boolean; +declare function isNewLine(codepoint: number): boolean; +declare function isWhiteSpace(codepoint: number): boolean; + +declare const getConfig: () => PropertiesConfig; + +declare function render(data: AstNode, opt?: RenderOptions): RenderResult; +declare function renderToken(token: Token, options?: RenderOptions): string; + +declare const combinators: string[]; +declare function minify(ast: AstNode, options?: ParserOptions, recursive?: boolean): AstNode; declare function reduceSelector(selector: string[][]): { match: boolean; optimized: string[]; selector: string[][]; reducible: boolean; } | null; - -declare function render(data: AstNode, opt?: RenderOptions): RenderResult; -declare function renderToken(token: Token, options?: RenderOptions): string; +declare function hasDeclaration(node: AstRule): boolean; +declare function minifyRule(ast: AstRule | AstAtRule): AstRule | AstAtRule; declare function walk(node: AstNode): Generator<{ node: AstNode; @@ -344,4 +652,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 { deduplicate, deduplicateRule, dirname, hasDeclaration, load, matchUrl, parse, reduceSelector, render, renderToken, resolve, transform, 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, 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 8a6f020..7c48004 100644 --- a/dist/index.js +++ b/dist/index.js @@ -1,6 +1,10 @@ export { parse, transform } from './node/index.js'; -export { deduplicate, deduplicateRule, hasDeclaration, reduceSelector } from './lib/parser/deduplicate.js'; +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 { render, renderToken } from './lib/renderer/render.js'; -export { walk } from './lib/walker/walk.js'; +export { combinators, hasDeclaration, minify, minifyRule, reduceSelector } from './lib/ast/minify.js'; +export { walk } from './lib/ast/walk.js'; export { load } from './node/load.js'; export { dirname, matchUrl, resolve } from './lib/fs/resolve.js'; diff --git a/dist/lib/parser/deduplicate.js b/dist/lib/ast/minify.js similarity index 62% rename from dist/lib/parser/deduplicate.js rename to dist/lib/ast/minify.js index 6e3ad93..746c7ee 100644 --- a/dist/lib/parser/deduplicate.js +++ b/dist/lib/ast/minify.js @@ -1,59 +1,303 @@ -import { isIdentStart, isWhiteSpace } from './utils/syntax.js'; -import { getConfig } from './utils/config.js'; -import { PropertyList } from './declaration/list.js'; -import { eq } from './utils/eq.js'; +import { isIdentStart, isIdent, isFunction, isWhiteSpace } from '../parser/utils/syntax.js'; +import { PropertyList } from '../parser/declaration/list.js'; +import { eq } from '../parser/utils/eq.js'; import { render } from '../renderer/render.js'; +import '../renderer/utils/color.js'; -const configuration = getConfig(); const combinators = ['+', '>', '~']; const notEndingWith = ['(', '['].concat(combinators); -function wrapNodes(previous, node, match, ast, i, nodeIndex) { - // @ts-ignore - let pSel = match.selector1.reduce(reducer, []).join(','); - // @ts-ignore - let nSel = match.selector2.reduce(reducer, []).join(','); - // @ts-ignore - const wrapper = { ...previous, chi: [], sel: match.match.reduce(reducer, []).join(',') }; - // @ts-ignore - Object.defineProperty(wrapper, 'raw', { - enumerable: false, - writable: true, +function minify(ast, options = {}, recursive = false) { + function wrapNodes(previous, node, match, ast, i, nodeIndex) { // @ts-ignore - value: match.match.map(t => t.slice()) - }); - if (pSel == '&' || pSel === '') { + let pSel = match.selector1.reduce(reducer, []).join(','); // @ts-ignore - wrapper.chi.push(...previous.chi); + let nSel = match.selector2.reduce(reducer, []).join(','); // @ts-ignore - if ((nSel == '&' || nSel === '') && hasOnlyDeclarations(previous)) { + const wrapper = { ...previous, chi: [], sel: match.match.reduce(reducer, []).join(',') }; + // @ts-ignore + Object.defineProperty(wrapper, 'raw', { + enumerable: false, + writable: true, + // @ts-ignore + value: match.match.map(t => t.slice()) + }); + if (pSel == '&' || pSel === '') { + // @ts-ignore + wrapper.chi.push(...previous.chi); // @ts-ignore - wrapper.chi.push(...node.chi); + if ((nSel == '&' || nSel === '') && hasOnlyDeclarations(previous)) { + // @ts-ignore + wrapper.chi.push(...node.chi); + } + else { + // @ts-ignore + wrapper.chi.push(node); + } } else { // @ts-ignore - wrapper.chi.push(node); + wrapper.chi.push(previous, node); } + // @ts-ignore + ast.chi.splice(i, 1, wrapper); + // @ts-ignore + ast.chi.splice(nodeIndex, 1); + // @ts-ignore + previous.sel = pSel; + // @ts-ignore + previous.raw = match.selector1; + // @ts-ignore + node.sel = nSel; + // @ts-ignore + node.raw = match.selector2; + reduceRuleSelector(wrapper); + return wrapper; + } + function reducer(acc, curr, index, array) { + // trim :is() + if (array.length == 1 && array[0][0] == ':is(' && array[0].at(-1) == ')') { + curr = curr.slice(1, -1); + } + if (curr[0] == '&') { + if (curr[1] == ' ' && !isIdent(curr[2]) && !isFunction(curr[2])) { + curr.splice(0, 2); + } + else if (combinators.includes(curr[1])) { + curr.splice(0, 1); + } + } + else if (ast.typ == 'Rule' && (isIdent(curr[0]) || isFunction(curr[0]))) { + curr.unshift('&', ' '); + } + acc.push(curr.join('')); + return acc; } - else { + function diff(n1, n2, options = {}) { + let node1 = n1; + let node2 = n2; + let exchanged = false; + if (node1.chi.length > node2.chi.length) { + const t = node1; + node1 = node2; + node2 = t; + exchanged = true; + } + let i = node1.chi.length; + let j = node2.chi.length; + if (i == 0 || j == 0) { + // @ts-ignore + return null; + } + // @ts-ignore + const raw1 = node1.raw; // @ts-ignore - wrapper.chi.push(previous, node); + const raw2 = node2.raw; + // @ts-ignore + node1 = { ...node1, chi: node1.chi.slice() }; + node2 = { ...node2, chi: node2.chi.slice() }; + if (raw1 != null) { + Object.defineProperty(node1, 'raw', { enumerable: false, writable: true, value: raw1 }); + } + if (raw2 != null) { + Object.defineProperty(node2, 'raw', { enumerable: false, writable: true, value: raw2 }); + } + const intersect = []; + while (i--) { + if (node1.chi[i].typ == 'Comment') { + continue; + } + j = node2.chi.length; + if (j == 0) { + break; + } + while (j--) { + if (node2.chi[j].typ == 'Comment') { + continue; + } + if (node1.chi[i].nam == node2.chi[j].nam) { + if (eq(node1.chi[i], node2.chi[j])) { + intersect.push(node1.chi[i]); + node1.chi.splice(i, 1); + node2.chi.splice(j, 1); + break; + } + } + } + } + // @ts-ignore + const result = (intersect.length == 0 ? null : { + ...node1, + // @ts-ignore + sel: [...new Set([...(n1?.raw?.reduce(reducer, []) || splitRule(n1.sel)).concat(n2?.raw?.reduce(reducer, []) || splitRule(n2.sel))])].join(','), + chi: intersect.reverse() + }); + if (result == null || [n1, n2].reduce((acc, curr) => curr.chi.length == 0 ? acc : acc + render(curr, options).code.length, 0) <= [node1, node2, result].reduce((acc, curr) => curr.chi.length == 0 ? acc : acc + render(curr, options).code.length, 0)) { + // @ts-ignore + return null; + } + return { result, node1: exchanged ? node2 : node1, node2: exchanged ? node2 : node2 }; + } + function matchSelectors(selector1, selector2, parentType) { + let match = [[]]; + const j = Math.min(selector1.reduce((acc, curr) => Math.min(acc, curr.length), selector1.length > 0 ? selector1[0].length : 0), selector2.reduce((acc, curr) => Math.min(acc, curr.length), selector2.length > 0 ? selector2[0].length : 0)); + let i = 0; + let k; + let l; + let token; + let matching = true; + let matchFunction = 0; + let inAttr = 0; + for (; i < j; i++) { + k = 0; + token = selector1[0][i]; + for (; k < selector1.length; k++) { + if (selector1[k][i] != token) { + matching = false; + break; + } + } + if (matching) { + l = 0; + for (; l < selector2.length; l++) { + if (selector2[l][i] != token) { + matching = false; + break; + } + } + } + if (!matching) { + break; + } + if (token == ',') { + match.push([]); + } + else { + if (token.endsWith('(')) { + matchFunction++; + } + if (token.endsWith('[')) { + inAttr++; + } + else if (token == ')') { + matchFunction--; + } + else if (token == ']') { + inAttr--; + } + match.at(-1).push(token); + } + } + // invalid function + if (matchFunction != 0 || inAttr != 0) { + return null; + } + if (parentType != 'Rule') { + for (const part of match) { + if (part.length > 0 && combinators.includes(part[0].charAt(0))) { + return null; + } + } + } + if (match.length > 1) { + console.error(`unsupported multilevel matching`); + console.error({ match, selector1, selector2 }); + return null; + } + for (const part of match) { + while (part.length > 0) { + const token = part.at(-1); + if (token == ' ' || combinators.includes(token) || notEndingWith.includes(token.at(-1))) { + part.pop(); + continue; + } + break; + } + } + if (match.every(t => t.length == 0)) { + return null; + } + if (eq([['&']], match)) { + return null; + } + function reduce(acc, curr) { + if (acc === null) { + return null; + } + let hasCompoundSelector = true; + curr = curr.slice(match[0].length); + while (curr.length > 0) { + if (curr[0] == ' ') { + hasCompoundSelector = false; + curr.unshift('&'); + continue; + } + break; + } + // invalid function match + if (curr.length > 0 && curr[0].endsWith('(') && curr.at(-1) != ')') { + return null; + } + if (curr.length == 1 && combinators.includes(curr[0].charAt(0))) { + return null; + } + if (hasCompoundSelector && curr.length > 0) { + hasCompoundSelector = !['&'].concat(combinators).includes(curr[0].charAt(0)); + } + if (curr[0] == ':is(') { + let inFunction = 0; + let canReduce = true; + const isCompound = curr.reduce((acc, token, index) => { + if (index == 0) { + inFunction++; + canReduce = curr[1] == '&'; + } + else if (token.endsWith('(')) { + if (inFunction == 0) { + canReduce = false; + } + inFunction++; + } + else if (token == ')') { + inFunction--; + } + else if (token == ',') { + if (!canReduce) { + canReduce = curr[index + 1] == '&'; + } + acc.push([]); + } + else + acc.at(-1)?.push(token); + return acc; + }, [[]]); + if (inFunction > 0) { + canReduce = false; + } + if (canReduce) { + curr = isCompound.reduce((acc, curr) => { + if (acc.length > 0) { + acc.push(','); + } + acc.push(...curr); + return acc; + }, []); + } + } + // @todo: check hasCompoundSelector && curr[0] == '&' && curr[1] == ' ' + acc.push(match.length == 0 ? ['&'] : (hasCompoundSelector && curr[0] != '&' && (curr.length == 0 || !combinators.includes(curr[0].charAt(0))) ? ['&'].concat(curr) : curr)); + return acc; + } + // @ts-ignore + selector1 = selector1.reduce(reduce, []); + // @ts-ignore + selector2 = selector2.reduce(reduce, []); + return selector1 == null || selector2 == null ? null : { + eq: eq(selector1, selector2), + match, + selector1, + selector2 + }; } - // @ts-ignore - ast.chi.splice(i, 1, wrapper); - // @ts-ignore - ast.chi.splice(nodeIndex, 1); - // @ts-ignore - previous.sel = pSel; - // @ts-ignore - previous.raw = match.selector1; - // @ts-ignore - node.sel = nSel; - // @ts-ignore - node.raw = match.selector2; - reduceRuleSelector(wrapper); - return wrapper; -} -function deduplicate(ast, options = {}, recursive = false) { // @ts-ignore if (('chi' in ast) && ast.chi?.length > 0) { let i = 0; @@ -93,7 +337,8 @@ function deduplicate(ast, options = {}, recursive = false) { // @ts-ignore if (options.nestingRules) { // @ts-ignore - if (previous != null && previous.typ == 'Rule') { + if (previous?.typ == 'Rule') { + // @ts-ignore reduceRuleSelector(previous); // @ts-ignore match = matchSelectors(previous.raw, node.raw, ast.typ); @@ -132,7 +377,7 @@ function deduplicate(ast, options = {}, recursive = false) { nodeIndex = --i; // @ts-ignore previous = ast.chi[nodeIndex]; - deduplicate(wrapper, options, recursive); + minify(wrapper, options, recursive); continue; } // @ts-ignore @@ -181,13 +426,24 @@ function deduplicate(ast, options = {}, recursive = false) { } else if (combinators.includes(curr[0])) { curr.unshift('&'); + wrap = false; } // @ts-ignore - acc.push(curr.map(t => t.replaceAll('&', node.optimized.optimized[0])).join('')); + acc.push(curr); return acc; }, []); + if (!wrap) { + wrap = selector.some(s => s[0] != '&'); + } + const rule = selector.map(s => { + if (s[0] == '&') { + // @ts-ignore + s[0] = node.optimized.optimized[0]; + } + return s.join(''); + }).join(','); // @ts-ignore - node.sel = (wrap ? node.optimized.optimized[0] : '') + `:is(${selector.join(',')})`; + node.sel = wrap ? node.optimized.optimized[0] + `:is(${rule})` : rule; } } // @ts-ignore @@ -218,10 +474,10 @@ function deduplicate(ast, options = {}, recursive = false) { // @ts-ignore if (hasDeclaration(node)) { // @ts-ignore - deduplicateRule(node); + minifyRule(node); } else { - deduplicate(node, options, recursive); + minify(node, options, recursive); } i--; previous = node; @@ -263,10 +519,10 @@ function deduplicate(ast, options = {}, recursive = false) { // @ts-ignore if (hasDeclaration(previous)) { // @ts-ignore - deduplicateRule(previous); + minifyRule(previous); } else { - deduplicate(previous, options, recursive); + minify(previous, options, recursive); } } } @@ -277,18 +533,97 @@ function deduplicate(ast, options = {}, recursive = false) { if (recursive && node != null && ('chi' in node)) { // @ts-ignore if (node.chi.some(n => n.typ == 'Declaration')) { - deduplicateRule(node); + minifyRule(node); } else { // @ts-ignore if (!(node.typ == 'AtRule' && node.nam != 'font-face')) { - deduplicate(node, options, recursive); + minify(node, options, recursive); } } } } return ast; } +function reduceSelector(selector) { + if (selector.length == 0) { + return null; + } + const optimized = []; + const k = selector.reduce((acc, curr) => acc == 0 ? curr.length : (curr.length == 0 ? acc : Math.min(acc, curr.length)), 0); + let i = 0; + let j; + let match; + for (; i < k; i++) { + const item = selector[0][i]; + match = true; + for (j = 1; j < selector.length; j++) { + if (item != selector[j][i]) { + match = false; + break; + } + } + if (!match) { + break; + } + optimized.push(item); + } + while (optimized.length > 0) { + const last = optimized.at(-1); + if ((last == ' ' || combinators.includes(last))) { + optimized.pop(); + continue; + } + break; + } + selector.forEach((selector) => selector.splice(0, optimized.length)); + // combinator + if (combinators.includes(optimized.at(-1))) { + const combinator = optimized.pop(); + selector.forEach(selector => selector.unshift(combinator)); + } + let reducible = optimized.length == 1; + if (optimized[0] == '&' && optimized[1] == ' ') { + optimized.splice(0, 2); + } + if (optimized.length == 0 || + (optimized[0].charAt(0) == '&' || + selector.length == 1)) { + return { + match: false, + optimized, + selector: selector.map(selector => selector[0] == '&' && selector[1] == ' ' ? selector.slice(2) : selector), + reducible: selector.length > 1 && selector.every((selector) => !combinators.includes(selector[0])) + }; + } + return { + match: true, + optimized, + selector: selector.reduce((acc, curr) => { + let hasCompound = true; + if (hasCompound && curr.length > 0) { + hasCompound = !['&'].concat(combinators).includes(curr[0].charAt(0)); + } + // @ts-ignore + if (hasCompound && curr[0] == ' ') { + hasCompound = false; + curr.unshift('&'); + } + if (curr.length == 0) { + curr.push('&'); + hasCompound = false; + } + if (reducible) { + const chr = curr[0].charAt(0); + // @ts-ignore + reducible = chr == '.' || chr == ':' || isIdentStart(chr.codePointAt(0)); + } + acc.push(hasCompound ? ['&'].concat(curr) : curr); + return acc; + }, []), + reducible: selector.every((selector) => !['>', '+', '~', '&'].includes(selector[0])) + }; +} function hasOnlyDeclarations(node) { let k = node.chi.length; while (k--) { @@ -311,7 +646,7 @@ function hasDeclaration(node) { } return true; } -function deduplicateRule(ast) { +function minifyRule(ast) { // @ts-ignore if (!('chi' in ast) || ast.chi?.length <= 1) { return ast; @@ -319,45 +654,19 @@ function deduplicateRule(ast) { // @ts-ignore const j = ast.chi.length; let k = 0; - let map = new Map; + let properties = new PropertyList(); // @ts-ignore for (; k < j; k++) { // @ts-ignore const node = ast.chi[k]; - if (node.typ == 'Comment') { - // @ts-ignore - map.set(node, node); + if (node.typ == 'Comment' || node.typ == 'Declaration') { + properties.add(node); continue; } - else if (node.typ != 'Declaration') { - break; - } - if (node.nam in configuration.map || - node.nam in configuration.properties) { - // @ts-ignore - const shorthand = node.nam in configuration.map ? configuration.map[node.nam].shorthand : configuration.properties[node.nam].shorthand; - if (!map.has(shorthand)) { - map.set(shorthand, new PropertyList()); - } - map.get(shorthand).add(node); - } - else { - map.set(node.nam, node); - } - } - const children = []; - for (let child of map.values()) { - if (child instanceof PropertyList) { - // @ts-ignore - children.push(...child); - } - else { - // @ts-ignore - children.push(child); - } + break; } // @ts-ignore - ast.chi = children.concat(ast.chi?.slice(k)); + ast.chi = [...properties].concat(ast.chi.slice(k)); return ast; } function splitRule(buffer) { @@ -483,335 +792,5 @@ function reduceRuleSelector(node) { } // } } -function diff(n1, n2, options = {}) { - let node1 = n1; - let node2 = n2; - let exchanged = false; - if (node1.chi.length > node2.chi.length) { - const t = node1; - node1 = node2; - node2 = t; - exchanged = true; - } - let i = node1.chi.length; - let j = node2.chi.length; - if (i == 0 || j == 0) { - // @ts-ignore - return null; - } - // @ts-ignore - const raw1 = node1.raw; - // @ts-ignore - // const optimized1 = node1.optimized; - // @ts-ignore - const raw2 = node2.raw; - // @ts-ignore - // const optimized2 = node2.optimized; - node1 = { ...node1, chi: node1.chi.slice() }; - node2 = { ...node2, chi: node2.chi.slice() }; - if (raw1 != null) { - Object.defineProperty(node1, 'raw', { enumerable: false, writable: true, value: raw1 }); - } - // if (optimized1 != null) { - // Object.defineProperty(node1, 'optimized', {enumerable: false, writable: true, value: optimized1}); - // } - if (raw2 != null) { - Object.defineProperty(node2, 'raw', { enumerable: false, writable: true, value: raw2 }); - } - // if (optimized2 != null) { - // Object.defineProperty(node2, 'optimized', {enumerable: false, writable: true, value: optimized2}); - // } - const intersect = []; - while (i--) { - if (node1.chi[i].typ == 'Comment') { - continue; - } - j = node2.chi.length; - if (j == 0) { - break; - } - while (j--) { - if (node2.chi[j].typ == 'Comment') { - continue; - } - if (node1.chi[i].nam == node2.chi[j].nam) { - if (eq(node1.chi[i], node2.chi[j])) { - intersect.push(node1.chi[i]); - node1.chi.splice(i, 1); - node2.chi.splice(j, 1); - break; - } - } - } - } - // @ts-ignore - const result = (intersect.length == 0 ? null : { - ...node1, - // @ts-ignore - sel: [...new Set([...(n1?.raw?.reduce(reducer, []) || splitRule(n1.sel)).concat(n2?.raw?.reduce(reducer, []) || splitRule(n2.sel))])].join(','), - chi: intersect.reverse() - }); - if (result == null || [n1, n2].reduce((acc, curr) => curr.chi.length == 0 ? acc : acc + render(curr, options).code.length, 0) <= [node1, node2, result].reduce((acc, curr) => curr.chi.length == 0 ? acc : acc + render(curr, options).code.length, 0)) { - // @ts-ignore - return null; - } - return { result, node1: exchanged ? node2 : node1, node2: exchanged ? node2 : node2 }; -} -function matchSelectors(selector1, selector2, parentType) { - let match = [[]]; - const j = Math.min(selector1.reduce((acc, curr) => Math.min(acc, curr.length), selector1.length > 0 ? selector1[0].length : 0), selector2.reduce((acc, curr) => Math.min(acc, curr.length), selector2.length > 0 ? selector2[0].length : 0)); - let i = 0; - let k; - let l; - let token; - let matching = true; - let matchFunction = 0; - let inAttr = 0; - for (; i < j; i++) { - k = 0; - token = selector1[0][i]; - for (; k < selector1.length; k++) { - if (selector1[k][i] != token) { - matching = false; - break; - } - } - if (matching) { - l = 0; - for (; l < selector2.length; l++) { - if (selector2[l][i] != token) { - matching = false; - break; - } - } - } - if (!matching) { - break; - } - if (token == ',') { - match.push([]); - } - else { - if (token.endsWith('(')) { - matchFunction++; - } - if (token.endsWith('[')) { - inAttr++; - } - else if (token == ')') { - matchFunction--; - } - else if (token == ']') { - inAttr--; - } - match.at(-1).push(token); - } - } - // invalid function - if (matchFunction != 0 || inAttr != 0) { - return null; - } - if (parentType != 'Rule') { - for (const part of match) { - if (part.length > 0 && combinators.includes(part[0].charAt(0))) { - return null; - } - } - } - if (match.length > 1) { - console.error(`unsupported multilevel matching`); - console.error({ match, selector1, selector2 }); - return null; - } - for (const part of match) { - while (part.length > 0) { - const token = part.at(-1); - if (token == ' ' || combinators.includes(token) || notEndingWith.includes(token.at(-1))) { - part.pop(); - continue; - } - break; - } - } - if (match.every(t => t.length == 0)) { - return null; - } - if (eq([['&']], match)) { - return null; - } - function reduce(acc, curr) { - if (acc === null) { - return null; - } - let hasCompoundSelector = true; - curr = curr.slice(match[0].length); - while (curr.length > 0) { - if (curr[0] == ' ') { - hasCompoundSelector = false; - curr.unshift('&'); - continue; - } - break; - } - // invalid function match - if (curr.length > 0 && curr[0].endsWith('(') && curr.at(-1) != ')') { - return null; - } - if (curr.length == 1 && combinators.includes(curr[0].charAt(0))) { - return null; - } - if (hasCompoundSelector && curr.length > 0) { - hasCompoundSelector = !['&'].concat(combinators).includes(curr[0].charAt(0)); - } - if (curr[0] == ':is(') { - let inFunction = 0; - let canReduce = true; - const isCompound = curr.reduce((acc, token, index) => { - if (index == 0) { - inFunction++; - canReduce = curr[1] == '&'; - } - else if (token.endsWith('(')) { - if (inFunction == 0) { - canReduce = false; - } - inFunction++; - } - else if (token == ')') { - inFunction--; - } - else if (token == ',') { - if (!canReduce) { - canReduce = curr[index + 1] == '&'; - } - acc.push([]); - } - else - acc.at(-1)?.push(token); - return acc; - }, [[]]); - if (inFunction > 0) { - canReduce = false; - } - if (canReduce) { - curr = isCompound.reduce((acc, curr) => { - if (acc.length > 0) { - acc.push(','); - } - acc.push(...curr); - return acc; - }, []); - } - } - // @todo: check hasCompoundSelector && curr[0] == '&' && curr[1] == ' ' - acc.push(match.length == 0 ? ['&'] : (hasCompoundSelector && curr[0] != '&' && (curr.length == 0 || !combinators.includes(curr[0].charAt(0))) ? ['&'].concat(curr) : curr)); - return acc; - } - // @ts-ignore - selector1 = selector1.reduce(reduce, []); - // @ts-ignore - selector2 = selector2.reduce(reduce, []); - return selector1 == null || selector2 == null ? null : { - eq: eq(selector1, selector2), - match, - selector1, - selector2 - }; -} -function reduceSelector(selector) { - if (selector.length == 0) { - return null; - } - const optimized = []; - const k = selector.reduce((acc, curr) => acc == 0 ? curr.length : (curr.length == 0 ? acc : Math.min(acc, curr.length)), 0); - let i = 0; - let j; - let match; - for (; i < k; i++) { - const item = selector[0][i]; - match = true; - for (j = 1; j < selector.length; j++) { - if (item != selector[j][i]) { - match = false; - break; - } - } - if (!match) { - break; - } - optimized.push(item); - } - while (optimized.length > 0) { - const last = optimized.at(-1); - if ((last == ' ' || combinators.includes(last))) { - optimized.pop(); - continue; - } - break; - } - selector.forEach((selector) => selector.splice(0, optimized.length)); - // combinator - if (combinators.includes(optimized.at(-1))) { - const combinator = optimized.pop(); - selector.forEach(selector => selector.unshift(combinator)); - } - let reducible = optimized.length == 1; - if (optimized[0] == '&' && optimized[1] == ' ') { - optimized.splice(0, 2); - } - if (optimized.length == 0 || - (optimized[0].charAt(0) == '&' || - selector.length == 1)) { - return { - match: false, - optimized, - selector: selector.map(selector => selector[0] == '&' && selector[1] == ' ' ? selector.slice(2) : selector), - reducible: selector.length > 1 && selector.every((selector) => !combinators.includes(selector[0])) - }; - } - return { - match: true, - optimized, - selector: selector.reduce((acc, curr) => { - let hasCompound = true; - if (hasCompound && curr.length > 0) { - hasCompound = !['&'].concat(combinators).includes(curr[0].charAt(0)); - } - // @ts-ignore - if (hasCompound && curr[0] == ' ') { - hasCompound = false; - curr.unshift('&'); - } - if (curr.length == 0) { - curr.push('&'); - hasCompound = false; - } - if (reducible) { - const chr = curr[0].charAt(0); - // @ts-ignore - reducible = chr == '.' || chr == ':' || isIdentStart(chr.codePointAt(0)); - } - acc.push(hasCompound ? ['&'].concat(curr) : curr); - return acc; - }, []), - reducible: selector.every((selector) => !['>', '+', '~', '&'].includes(selector[0])) - }; -} -function reducer(acc, curr, index, array) { - // trim :is() - if (array.length == 1 && array[0][0] == ':is(' && array[0].at(-1) == ')') { - curr = curr.slice(1, -1); - } - if (curr[0] == '&') { - if (curr[1] == ' ') { - curr.splice(0, 2); - } - else if (combinators.includes(curr[1])) { - curr.splice(0, 1); - } - } - acc.push(curr.join('')); - return acc; -} -export { deduplicate, deduplicateRule, hasDeclaration, reduceSelector }; +export { combinators, hasDeclaration, minify, minifyRule, reduceSelector }; diff --git a/dist/lib/walker/walk.js b/dist/lib/ast/walk.js similarity index 100% rename from dist/lib/walker/walk.js rename to dist/lib/ast/walk.js diff --git a/dist/lib/parser/declaration/list.js b/dist/lib/parser/declaration/list.js index ddaa76a..ecfb830 100644 --- a/dist/lib/parser/declaration/list.js +++ b/dist/lib/parser/declaration/list.js @@ -1,6 +1,7 @@ import { PropertySet } from './set.js'; import { getConfig } from '../utils/config.js'; import { PropertyMap } from './map.js'; +import { parseString } from '../parse.js'; const config = getConfig(); class PropertyList { @@ -8,33 +9,61 @@ class PropertyList { constructor() { this.declarations = new Map; } + set(nam, value) { + return this.add({ typ: 'Declaration', nam, val: Array.isArray(value) ? value : parseString(String(value)) }); + } add(declaration) { if (declaration.typ != 'Declaration') { this.declarations.set(Number(Math.random().toString().slice(2)).toString(36), declaration); return this; } - const propertyName = declaration.nam; + let propertyName = declaration.nam; + let shortHandType; + let shorthand; if (propertyName in config.properties) { // @ts-ignore - const shorthand = config.properties[propertyName].shorthand; + if ('map' in config.properties[propertyName]) { + shortHandType = 'map'; + // @ts-ignore + shorthand = config.properties[propertyName].map; + } + else { + shortHandType = 'set'; + // @ts-ignore + shorthand = config.properties[propertyName].shorthand; + } + } + else if (propertyName in config.map) { + shortHandType = 'map'; + // @ts-ignore + shorthand = config.map[propertyName].shorthand; + } + // @ts-ignore + if (shortHandType == 'map') { + // @ts-ignore if (!this.declarations.has(shorthand)) { // @ts-ignore - this.declarations.set(shorthand, new PropertySet(config.properties[shorthand])); + this.declarations.set(shorthand, new PropertyMap(config.map[shorthand])); } + // @ts-ignore this.declarations.get(shorthand).add(declaration); - return this; + // return this; } - if (propertyName in config.map) { + // @ts-ignore + else if (shortHandType == 'set') { // @ts-ignore - const shorthand = config.map[propertyName].shorthand; + // const shorthand: string = config.properties[propertyName].shorthand; if (!this.declarations.has(shorthand)) { // @ts-ignore - this.declarations.set(shorthand, new PropertyMap(config.map[shorthand])); + this.declarations.set(shorthand, new PropertySet(config.properties[shorthand])); } + // @ts-ignore this.declarations.get(shorthand).add(declaration); - return this; + // return this; + } + else { + this.declarations.set(propertyName, declaration); } - this.declarations.set(propertyName, declaration); return this; } [Symbol.iterator]() { diff --git a/dist/lib/parser/declaration/map.js b/dist/lib/parser/declaration/map.js index 4419710..f1272d3 100644 --- a/dist/lib/parser/declaration/map.js +++ b/dist/lib/parser/declaration/map.js @@ -1,36 +1,11 @@ import { eq } from '../utils/eq.js'; -import { isNumber } from '../utils/syntax.js'; +import { getConfig } from '../utils/config.js'; import { renderToken } from '../../renderer/render.js'; import { matchType } from '../utils/type.js'; +import { parseString } from '../parse.js'; +import { PropertySet } from './set.js'; -function getTokenType(val) { - if (val == 'transparent' || val == 'currentcolor') { - return { - typ: 'Color', - val, - kin: 'lit' - }; - } - if (val.endsWith('%')) { - return { - typ: 'Perc', - val: val.slice(0, -1) - }; - } - return { - typ: isNumber(val) ? 'Number' : 'Iden', - val - }; -} -function parseString(val) { - return val.split(/\s/).map(getTokenType).reduce((acc, curr) => { - if (acc.length > 0) { - acc.push({ typ: 'Whitespace' }); - } - acc.push(curr); - return acc; - }, []); -} +const propertiesConfig = getConfig(); class PropertyMap { config; declarations; @@ -45,7 +20,7 @@ class PropertyMap { } add(declaration) { if (declaration.nam == this.config.shorthand) { - this.declarations.clear(); + this.declarations = new Map; this.declarations.set(declaration.nam, declaration); } else { @@ -122,8 +97,7 @@ class PropertyMap { const defaults = parseString(props.default[0]); if (!(property in tokens)) { tokens[property] = [ - [...defaults - ] + [...defaults] ]; } else { @@ -157,145 +131,229 @@ class PropertyMap { }, new Map); } } - this.declarations.set(declaration.nam, declaration); + // @ts-ignore + const config = propertiesConfig.properties[declaration.nam]; + let property = declaration.nam; + if (config != null) { + property = config.shorthand; + let value = this.declarations.get(property); + if (!(value instanceof PropertySet)) { + // @ts-ignore + this.declarations.set(property, new PropertySet(propertiesConfig.properties[config.shorthand])); + // Token[] + if (value != null) { + // @ts-ignore + this.declarations.get(property).add(value); + } + } + this.declarations.get(property).add(declaration); + } + else { + this.declarations.set(declaration.nam, declaration); + } } return this; } [Symbol.iterator]() { - let requiredCount = Object.keys(this.config.properties).reduce((acc, curr) => this.declarations.has(curr) && this.config.properties[curr].required ? ++acc : acc, 0); - if (requiredCount == 0) { - requiredCount = this.declarations.size; - } - if (requiredCount < this.requiredCount) { - // if (this.declarations.size == 1 && this.declarations.has(this.config.shorthand)) { - // - // this.declarations - // } - return this.declarations.values(); - } - let count = 0; - const separator = this.config.separator; - const tokens = {}; - // @ts-ignore - const valid = Object.entries(this.config.properties).reduce((acc, curr) => { - if (!this.declarations.has(curr[0])) { - if (curr[1].required) { - acc.push(curr[0]); + let iterable; + let requiredCount = 0; + let property; + let isShorthand = true; + for (property of Object.keys(this.config.properties)) { + if (this.config.properties[property].required) { + if (!this.declarations.has(property)) { + isShorthand = false; + break; } - return acc; - } - let current = 0; - const props = this.config.properties[curr[0]]; - // @ts-ignore - for (const val of this.declarations.get(curr[0]).val) { - if (separator != null && separator.typ == val.typ && eq(separator, val)) { - current++; - if (tokens[curr[0]].length == current) { - tokens[curr[0]].push([]); + else { + const val = this.declarations.get(property); + if (val instanceof PropertySet && !val.isShortHand()) { + isShorthand = false; + break; } - continue; - } - if (val.typ == 'Whitespace' || val.typ == 'Comment') { - continue; - } - if (props.multiple && props.separator != null && props.separator.typ == val.typ && eq(val, props.separator)) { - continue; - } - if (matchType(val, curr[1])) { - if (!(curr[0] in tokens)) { - tokens[curr[0]] = [[]]; + else { + requiredCount++; } - // is default value - tokens[curr[0]][current].push(val); - continue; } - acc.push(curr[0]); - break; } - if (count == 0) { - count = current; - } - return acc; - }, []); - if (valid.length > 0 || Object.values(tokens).every(v => v.every(v => v.length == count))) { - return this.declarations.values(); } - const values = Object.entries(tokens).reduce((acc, curr) => { - const props = this.config.properties[curr[0]]; - for (let i = 0; i < curr[1].length; i++) { - if (acc.length == i) { - acc.push([]); - } - let values = curr[1][i].reduce((acc, curr) => { - if (acc.length > 0) { - acc.push({ typ: 'Whitespace' }); + if (requiredCount == 0) { + requiredCount = this.declarations.size; + } + if (!isShorthand || requiredCount < this.requiredCount) { + // @ts-ignore + iterable = this.declarations.values(); + } + else { + let count = 0; + const separator = this.config.separator; + const tokens = {}; + // @ts-ignore + /* 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]); } - acc.push(curr); return acc; - }, []); - if (props.default.includes(curr[1][i].reduce((acc, curr) => acc + renderToken(curr) + ' ', '').trimEnd())) { - continue; } - values = values.filter((val) => { + 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([]); + } + continue; + } if (val.typ == 'Whitespace' || val.typ == 'Comment') { - return false; + continue; } - return !(val.typ == 'Iden' && props.default.includes(val.val)); - }); - if (values.length > 0) { - if ('mapping' in props) { - // @ts-ignore - if (!('constraints' in props) || !('max' in props.constraints) || values.length <= props.constraints.mapping.max) { - let i = values.length; - while (i--) { + 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]] = [[]]; + } + // is default value + tokens[curr[0]][current].push(val); + // continue; + } + else { + acc.push(curr[0]); + break; + } + } + if (count == 0) { + count = current; + } + return acc; + }, []); + count++; + if (!Object.values(tokens).every(v => v.length == count)) { + // @ts-ignore + iterable = this.declarations.values(); + } + else { + const values = Object.entries(tokens).reduce((acc, curr) => { + const props = this.config.properties[curr[0]]; + for (let i = 0; i < curr[1].length; i++) { + if (acc.length == i) { + acc.push([]); + } + let values = curr[1][i].reduce((acc, curr) => { + if (acc.length > 0) { + acc.push({ typ: 'Whitespace' }); + } + acc.push(curr); + return acc; + }, []); + // @todo remove renderToken call + if (props.default.includes(curr[1][i].reduce((acc, curr) => acc + renderToken(curr) + ' ', '').trimEnd())) { + continue; + } + let doFilterDefault = true; + if (curr[0] in propertiesConfig.properties) { + for (let v of values) { + if (!['Whitespace', 'Comment', 'Iden'].includes(v.typ) + || (v.typ == 'Iden' && !this.config.properties[curr[0]].default.includes(v.val))) { + doFilterDefault = false; + break; + } + } + } + // remove default values + values = values.filter((val) => { + if (val.typ == 'Whitespace' || val.typ == 'Comment') { + return false; + } + return !doFilterDefault || !(val.typ == 'Iden' && props.default.includes(val.val)); + }); + if (values.length > 0) { + if ('mapping' in props) { // @ts-ignore - if (values[i].typ == 'Iden' && values[i].val in props.mapping) { - // @ts-ignore - values.splice(i, 1, ...parseString(props.mapping[values[i].val])); + if (!('constraints' in props) || !('max' in props.constraints) || values.length <= props.constraints.mapping.max) { + let i = values.length; + while (i--) { + // @ts-ignore + if (values[i].typ == 'Iden' && values[i].val in props.mapping) { + // @ts-ignore + values.splice(i, 1, ...parseString(props.mapping[values[i].val])); + } + } } } + if ('prefix' in props) { + // @ts-ignore + acc[i].push({ ...props.prefix }); + } + else if (acc[i].length > 0) { + acc[i].push({ typ: 'Whitespace' }); + } + acc[i].push(...values.reduce((acc, curr) => { + if (acc.length > 0) { + // @ts-ignore + acc.push({ ...(props.separator ?? { typ: 'Whitespace' }) }); + } + // @ts-ignore + acc.push(curr); + return acc; + }, [])); } } - if ('prefix' in props) { - // @ts-ignore - acc[i].push({ ...props.prefix }); + return acc; + }, []).reduce((acc, curr) => { + if (acc.length > 0) { + acc.push({ ...separator }); } - else if (acc[i].length > 0) { - acc[i].push({ typ: 'Whitespace' }); + if (curr.length == 0 && this.config.default.length > 0) { + curr.push(...parseString(this.config.default[0]).reduce((acc, curr) => { + if (acc.length > 0) { + acc.push({ typ: 'Whitespace' }); + } + acc.push(curr); + return acc; + }, [])); } - acc[i].push(...values.reduce((acc, curr) => { - if (acc.length > 0) { + acc.push(...curr); + return acc; + }, []); + iterable = [{ + typ: 'Declaration', + nam: this.config.shorthand, + val: values + }][Symbol.iterator](); + } + } + const iterators = []; + return { + // @ts-ignore + next() { + let v = iterable.next(); + while (v.done || v.value instanceof PropertySet) { + if (v.value instanceof PropertySet) { + // @ts-ignore + iterators.push(iterable); + iterable = v.value[Symbol.iterator](); + v = iterable.next(); + } + if (v.done) { + if (iterators.length > 0) { // @ts-ignore - acc.push({ ...(props.separator ?? { typ: 'Whitespace' }) }); + iterable = iterators.pop(); + v = iterable.next(); + } + if (v.done && iterators.length == 0) { + break; } - // @ts-ignore - acc.push(curr); - return acc; - }, [])); - } - } - return acc; - }, []).reduce((acc, curr) => { - if (acc.length > 0) { - acc.push({ ...separator }); - } - if (curr.length == 0) { - curr.push(...this.config.default[0].split(/\s/).map(getTokenType).reduce((acc, curr) => { - if (acc.length > 0) { - acc.push({ typ: 'Whitespace' }); } - acc.push(curr); - return acc; - }, [])); + } + return v; } - acc.push(...curr); - return acc; - }, []); - return [{ - typ: 'Declaration', - nam: this.config.shorthand, - val: values - }][Symbol.iterator](); + }; } } diff --git a/dist/lib/parser/declaration/set.js b/dist/lib/parser/declaration/set.js index 9d96d8d..ff8a0ce 100644 --- a/dist/lib/parser/declaration/set.js +++ b/dist/lib/parser/declaration/set.js @@ -10,8 +10,7 @@ class PropertySet { } add(declaration) { if (declaration.nam == this.config.shorthand) { - this.declarations.clear(); - this.declarations.set(declaration.nam, declaration); + this.declarations = new Map; } else { // expand shorthand @@ -34,6 +33,10 @@ class PropertySet { } if (token.typ != 'Whitespace' && token.typ != 'Comment') { if (token.typ == 'Iden' && this.config.keywords.includes(token.val)) { + if (tokens.length == 0) { + tokens.push([]); + current++; + } tokens[current].push(token); } if (token.typ == 'Literal' && token.val == this.config.separator) { @@ -49,10 +52,6 @@ class PropertySet { this.declarations.delete(this.config.shorthand); for (const values of tokens) { this.config.properties.forEach((property, index) => { - // if (property == declaration.nam) { - // - // return; - // } if (!this.declarations.has(property)) { this.declarations.set(property, { typ: 'Declaration', @@ -81,30 +80,20 @@ class PropertySet { this.declarations.set(declaration.nam, declaration); return this; } - // declaration.chi = declaration.chi.reduce((acc: Token[], token: Token) => { - // - // if (this.config.types.includes(token.typ) || ('0' == (token).chi && ( - // this.config.types.includes('Length') || - // this.config.types.includes('Angle') || - // this.config.types.includes('Dimension'))) || (token.typ == 'Iden' && this.config.keywords.includes(token.chi))) { - // - // acc.push(token); - // } - // - // return acc; - // }, []); - this.declarations.set(declaration.nam, declaration); } + this.declarations.set(declaration.nam, declaration); return this; } + isShortHand() { + if (this.declarations.has(this.config.shorthand)) { + return this.declarations.size == 1; + } + return this.config.properties.length == this.declarations.size; + } [Symbol.iterator]() { let iterator; const declarations = this.declarations; - if (declarations.size < this.config.properties.length || this.config.properties.some((property, index) => { - return !declarations.has(property) || (index > 0 && - // @ts-ignore - declarations.get(property).val.length != declarations.get(this.config.properties[Math.floor(index / 2)]).val.length); - })) { + if (declarations.size < this.config.properties.length) { iterator = declarations.values(); } else { @@ -162,17 +151,20 @@ class PropertySet { return acc; }, []) }][Symbol.iterator](); - return { - next() { - return iterator.next(); - } - }; + // return { + // next() { + // + // return iterator.next(); + // } + // } } - return { - next() { - return iterator.next(); - } - }; + return iterator; + // return { + // next() { + // + // return iterator.next(); + // } + // } } } diff --git a/dist/lib/parser/parse.js b/dist/lib/parser/parse.js index 3a4f960..b786b1f 100644 --- a/dist/lib/parser/parse.js +++ b/dist/lib/parser/parse.js @@ -1,16 +1,22 @@ -import { isWhiteSpace, isDigit, isPseudo, isAtKeyword, isFunction, isNumber, isDimension, parseDimension, isPercentage, isIdent, isHash, isNewLine, isIdentStart, isHexColor } from './utils/syntax.js'; +import { isPseudo, isAtKeyword, isFunction, isNumber, isDimension, parseDimension, isPercentage, isIdent, isHexColor, isHash, isIdentStart } from './utils/syntax.js'; import { renderToken } from '../renderer/render.js'; import { COLORS_NAMES } from '../renderer/utils/color.js'; -import { deduplicate } from './deduplicate.js'; +import { minify, combinators } from '../ast/minify.js'; +import { tokenize } from './tokenize.js'; const urlTokenMatcher = /^(["']?)[a-zA-Z0-9_/.-][a-zA-Z0-9_/:.#?-]+(\1)$/; const funcLike = ['Start-parens', 'Func', 'UrlFunc', 'Pseudo-class-func']; +/** + * + * @param iterator + * @param opt + */ async function parse(iterator, opt = {}) { const errors = []; const options = { src: '', sourcemap: false, - compress: false, + minify: true, nestingRules: false, resolveImport: false, resolveUrls: false, @@ -20,172 +26,28 @@ async function parse(iterator, opt = {}) { if (options.resolveImport) { options.resolveUrls = true; } - let ind = -1; - let lin = 1; - let col = 0; - const tokens = []; const src = options.src; const stack = []; const ast = { typ: "StyleSheet", chi: [] }; - const position = { - ind: Math.max(ind, 0), - lin: lin, - col: Math.max(col, 1) - }; - let value; - let buffer = ''; - let total = iterator.length; - let bytesIn = total; + let tokens = []; let map = new Map; + let bytesIn = 0; let context = ast; if (options.sourcemap) { ast.loc = { sta: { - ind: ind, - lin: lin, - col: col + ind: 0, + lin: 1, + col: 1 }, src: '' }; } - function getType(val) { - if (val === '') { - throw new Error('empty string?'); - } - if (val == ':') { - return { typ: 'Colon' }; - } - if (val == ')') { - return { typ: 'End-parens' }; - } - if (val == '(') { - return { typ: 'Start-parens' }; - } - if (val == '=') { - return { typ: 'Delim', val }; - } - if (val == ';') { - return { typ: 'Semi-colon' }; - } - if (val == ',') { - return { typ: 'Comma' }; - } - if (val == '<') { - return { typ: 'Lt' }; - } - if (val == '>') { - return { typ: 'Gt' }; - } - if (isPseudo(val)) { - return val.endsWith('(') ? { - typ: 'Pseudo-class-func', - val: val.slice(0, -1), - chi: [] - } - : { - typ: 'Pseudo-class', - val - }; - } - if (isAtKeyword(val)) { - return { - typ: 'At-rule', - val: val.slice(1) - }; - } - if (isFunction(val)) { - val = val.slice(0, -1); - return { - typ: val == 'url' ? 'UrlFunc' : 'Func', - val, - chi: [] - }; - } - if (isNumber(val)) { - return { - typ: 'Number', - val - }; - } - if (isDimension(val)) { - return parseDimension(val); - } - if (isPercentage(val)) { - return { - typ: 'Perc', - val: val.slice(0, -1) - }; - } - if (val == 'currentColor') { - return { - typ: 'Color', - val, - kin: 'lit' - }; - } - if (isIdent(val)) { - return { - typ: 'Iden', - val - }; - } - if (val.charAt(0) == '#' && isHash(val)) { - return { - typ: 'Hash', - val - }; - } - if ('"\''.includes(val.charAt(0))) { - return { - typ: 'Unclosed-string', - val - }; - } - return { - typ: 'Literal', - val - }; - } - // consume and throw away - function consume(open, close) { - let count = 1; - let chr; - while (true) { - chr = next(); - if (chr == '\\') { - if (peek() === '') { - break; - } - continue; - } - else if (chr == '/' && peek() == '*') { - next(); - while (true) { - chr = next(); - if (chr === '') { - break; - } - if (chr == '*' && peek() == '/') { - next(); - break; - } - } - } - else if (chr == close) { - count--; - } - else if (chr == open) { - count++; - } - if (chr === '' || count == 0) { - break; - } - } - } - async function parseNode(tokens) { + async function parseNode(results) { + let tokens = results.map(mapToken); let i; let loc; for (i = 0; i < tokens.length; i++) { @@ -283,7 +145,7 @@ async function parse(iterator, opt = {}) { // @ts-ignore const root = await options.load(url, options.src).then((src) => { return parse(src, Object.assign({}, options, { - compress: false, + minify: false, // @ts-ignore src: options.resolve(url, options.src).absolute })); @@ -334,20 +196,24 @@ 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; - } - } + // 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, 'Rule', { compress: options.compress }).reduce((acc, curr, index, array) => { + parseTokens(tokens, { minify: options.minify }).reduce((acc, curr, index, array) => { if (curr.typ == 'Whitespace') { - if (array[index - 1]?.val == '+' || array[index + 1]?.val == '+') { + if (array[index - 1]?.typ == 'Gt' || + array[index + 1]?.typ == 'Gt' || + combinators.includes(array[index - 1]?.val) || + combinators.includes(array[index + 1]?.val)) { return acc; } } - let t = renderToken(curr, { compress: true }); + let t = renderToken(curr, { minify: true }); if (t == ',') { acc.push([]); } @@ -390,7 +256,7 @@ async function parse(iterator, opt = {}) { } if (tokens[i].typ == 'Colon') { name = tokens.slice(0, i); - value = parseTokens(tokens.slice(i + 1), 'Declaration', { + value = parseTokens(tokens.slice(i + 1), { parseColor: true, src: options.src, resolveUrls: options.resolveUrls, @@ -416,11 +282,19 @@ async function parse(iterator, opt = {}) { } } if (value == null) { - errors.push({ action: 'drop', message: 'invalid declaration', location: { src, ...position } }); + errors.push({ + action: 'drop', + message: 'invalid declaration', + location: { src, ...position } + }); return null; } if (value.length == 0) { - errors.push({ action: 'drop', message: 'invalid declaration', location: { src, ...position } }); + errors.push({ + action: 'drop', + message: 'invalid declaration', + location: { src, ...position } + }); return null; } const node = { @@ -434,7 +308,11 @@ async function parse(iterator, opt = {}) { node.val.shift(); } if (node.val.length == 0) { - errors.push({ action: 'drop', message: 'invalid declaration', location: { src, ...position } }); + errors.push({ + action: 'drop', + message: 'invalid declaration', + location: { src, ...position } + }); return null; } // @ts-ignore @@ -443,517 +321,211 @@ async function parse(iterator, opt = {}) { } } } - function peek(count = 1) { - if (count == 1) { - return iterator.charAt(ind + 1); - } - return iterator.slice(ind + 1, ind + count + 1); - } - function prev(count = 1) { - if (count == 1) { - return ind == 0 ? '' : iterator.charAt(ind - 1); - } - return iterator.slice(ind - 1 - count, ind - 1); + function mapToken(token) { + const node = getTokenType(token.token, token.hint); + map.set(node, token.position); + return node; } - function next(count = 1) { - let char = ''; - while (count-- > 0 && ind < total) { - const codepoint = iterator.charCodeAt(++ind); - if (isNaN(codepoint)) { - return char; - } - char += iterator.charAt(ind); - if (isNewLine(codepoint)) { - lin++; - col = 0; - } - else { - col++; - } - } - return char; - } - function pushToken(token) { - tokens.push(token); - map.set(token, { ...position }); - position.ind = ind; - position.lin = lin; - position.col = col == 0 ? 1 : col; - } - function consumeWhiteSpace() { - let count = 0; - while (isWhiteSpace(iterator.charAt(count + ind + 1).charCodeAt(0))) { - count++; - } - next(count); - return count; - } - function consumeString(quoteStr) { - const quote = quoteStr; - let value; - let hasNewLine = false; - if (buffer.length > 0) { - pushToken(getType(buffer)); - buffer = ''; + const iter = tokenize(iterator); + let item; + while (true) { + item = iter.next().value; + if (item == null) { + break; } - buffer += quoteStr; - while (ind < total) { - value = peek(); - if (ind >= total) { - pushToken({ typ: hasNewLine ? 'Bad-string' : 'Unclosed-string', val: buffer }); - break; - } - if (value == '\\') { - const sequence = peek(6); - let escapeSequence = ''; - let codepoint; - let i; - for (i = 1; i < sequence.length; i++) { - codepoint = sequence.charCodeAt(i); - if (codepoint == 0x20 || - (codepoint >= 0x61 && codepoint <= 0x66) || - (codepoint >= 0x41 && codepoint <= 0x46) || - (codepoint >= 0x30 && codepoint <= 0x39)) { - escapeSequence += sequence[i]; - if (codepoint == 0x20) { - break; - } - continue; - } - break; - } - // not hex or new line + tokens.push(item); + bytesIn = item.bytesIn; + if (item.token == ';' || item.token == '{') { + let node = await parseNode(tokens); + if (node != null) { + stack.push(node); // @ts-ignore - if (i == 1 && !isNewLine(codepoint)) { - buffer += sequence[i]; - next(2); - continue; - } - if (escapeSequence.trimEnd().length > 0) { - const codepoint = Number(`0x${escapeSequence.trimEnd()}`); - if (codepoint == 0 || - // leading surrogate - (0xD800 <= codepoint && codepoint <= 0xDBFF) || - // trailing surrogate - (0xDC00 <= codepoint && codepoint <= 0xDFFF)) { - buffer += String.fromCodePoint(0xFFFD); + context = node; + } + else if (item.token == '{') { + // node == null + // consume and throw away until the closing '}' or EOF + let inBlock = 1; + do { + item = iter.next().value; + if (item == null) { + break; } - else { - buffer += String.fromCodePoint(codepoint); + if (item.token == '{') { + inBlock++; } - next(escapeSequence.length + 1); - continue; - } - // buffer += value; - if (ind >= total) { - // drop '\\' at the end - pushToken(getType(buffer)); - break; - } - buffer += next(2); - continue; - } - if (value == quote) { - buffer += value; - pushToken({ typ: hasNewLine ? 'Bad-string' : 'String', val: buffer }); - next(); - // i += value.length; - buffer = ''; - break; - } - if (isNewLine(value.charCodeAt(0))) { - hasNewLine = true; - } - if (hasNewLine && value == ';') { - pushToken({ typ: 'Bad-string', val: buffer }); - buffer = ''; - break; - } - buffer += value; - // i += value.length; - next(); - } - } - while (ind < total) { - value = next(); - if (ind >= total) { - if (buffer.length > 0) { - pushToken(getType(buffer)); - buffer = ''; + else if (item.token == '}') { + inBlock--; + } + } while (inBlock != 0); } - break; + tokens = []; + map = new Map; } - if (isWhiteSpace(value.charCodeAt(0))) { - if (buffer.length > 0) { - pushToken(getType(buffer)); - buffer = ''; - } - while (ind < total) { - value = next(); - if (ind >= total) { - break; - } - if (!isWhiteSpace(value.charCodeAt(0))) { - break; - } - } - pushToken({ typ: 'Whitespace' }); - buffer = ''; - if (ind >= total) { - break; + else if (item.token == '}') { + await parseNode(tokens); + const previousNode = stack.pop(); + // @ts-ignore + context = stack[stack.length - 1] || ast; + // @ts-ignore + if (options.removeEmpty && previousNode != null && previousNode.chi.length == 0 && context.chi[context.chi.length - 1] == previousNode) { + context.chi.pop(); } - } - switch (value) { - case '/': - if (buffer.length > 0 && tokens.at(-1)?.typ == 'Whitespace') { - pushToken(getType(buffer)); - buffer = ''; - if (peek() != '*') { - pushToken(getType(value)); - break; - } - } - buffer += value; - if (peek() == '*') { - buffer += '*'; - // i++; - next(); - while (ind < total) { - value = next(); - if (ind >= total) { - pushToken({ - typ: 'Bad-comment', val: buffer - }); - break; - } - if (value == '\\') { - buffer += value; - value = next(); - if (ind >= total) { - pushToken({ - typ: 'Bad-comment', - val: buffer - }); - break; - } - buffer += value; - continue; - } - if (value == '*') { - buffer += value; - value = next(); - if (ind >= total) { - pushToken({ - typ: 'Bad-comment', val: buffer - }); - break; - } - buffer += value; - if (value == '/') { - pushToken({ typ: 'Comment', val: buffer }); - buffer = ''; - break; - } - } - else { - buffer += value; - } - } - } - break; - case '<': - if (buffer.length > 0) { - pushToken(getType(buffer)); - buffer = ''; - } - buffer += value; - value = next(); - if (ind >= total) { - break; - } - if (peek(3) == '!--') { - while (ind < total) { - value = next(); - if (ind >= total) { - break; - } - buffer += value; - if (value == '>' && prev(2) == '--') { - pushToken({ - typ: 'CDOCOMM', - val: buffer - }); - buffer = ''; - break; - } - } - } - if (ind >= total) { - pushToken({ typ: 'BADCDO', val: buffer }); - buffer = ''; - } - break; - case '\\': - value = next(); - // EOF - if (ind + 1 >= total) { - // end of stream ignore \\ - pushToken(getType(buffer)); - buffer = ''; - break; - } - buffer += value; - break; - case '"': - case "'": - consumeString(value); - break; - case '~': - case '|': - if (tokens.at(-1)?.typ == 'Whitespace') { - tokens.pop(); - } - if (buffer.length > 0) { - pushToken(getType(buffer)); - buffer = ''; - } - buffer += value; - value = next(); - if (ind >= total) { - pushToken(getType(buffer)); - buffer = ''; - break; - } - if (value == '=') { - buffer += value; - pushToken({ - typ: buffer[0] == '~' ? 'Includes' : 'Dash-matches', - val: buffer - }); - buffer = ''; - break; - } - pushToken(getType(buffer)); - while (isWhiteSpace(value.charCodeAt(0))) { - value = next(); - } - buffer = value; - break; - case '>': - if (buffer !== '') { - pushToken(getType(buffer)); - buffer = ''; - } - if (tokens[tokens.length - 1]?.typ == 'Whitespace') { - tokens.pop(); - } - pushToken({ typ: 'Gt' }); - consumeWhiteSpace(); - break; - case '.': - const codepoint = peek().charCodeAt(0); - if (!isDigit(codepoint) && buffer !== '') { - pushToken(getType(buffer)); - buffer = value; - break; - } - buffer += value; - break; - case '+': - case ':': - case ',': - case '=': - if (buffer.length > 0) { - pushToken(getType(buffer)); - buffer = ''; - } - if (value == ':' && ':' == peek()) { - buffer += value + next(); - break; - } - pushToken(getType(value)); - buffer = ''; - if (value == '+' && isWhiteSpace(peek().charCodeAt(0))) { - pushToken(getType(next())); - } - while (isWhiteSpace(peek().charCodeAt(0))) { - next(); - } - break; - case ')': - if (buffer.length > 0) { - pushToken(getType(buffer)); - buffer = ''; - } - pushToken({ typ: 'End-parens' }); - break; - case '(': - if (buffer.length == 0) { - pushToken({ typ: 'Start-parens' }); - } - else { - buffer += value; - pushToken(getType(buffer)); - buffer = ''; - const token = tokens[tokens.length - 1]; - if (token.typ == 'UrlFunc') { - // consume either string or url token - let whitespace = ''; - value = peek(); - while (isWhiteSpace(value.charCodeAt(0))) { - whitespace += value; - } - if (whitespace.length > 0) { - next(whitespace.length); - } - value = peek(); - if (value == '"' || value == "'") { - consumeString(next()); - let token = tokens[tokens.length - 1]; - if (['String', 'Literal'].includes(token.typ) && urlTokenMatcher.test(token.val)) { - if (token.val.slice(1, 6) != 'data:') { - if (token.typ == 'String') { - token.val = token.val.slice(1, -1); - } - // @ts-ignore - token.typ = 'Url-token'; - } - } - break; - } - else { - buffer = ''; - do { - let cp = value.charCodeAt(0); - // EOF - - if (cp == null) { - pushToken({ typ: 'Bad-url-token', val: buffer }); - break; - } - // ')' - if (cp == 0x29 || cp == null) { - if (buffer.length == 0) { - pushToken({ typ: 'Bad-url-token', val: '' }); - } - else { - pushToken({ typ: 'Url-token', val: buffer }); - } - if (cp != null) { - pushToken(getType(next())); - } - break; - } - if (isWhiteSpace(cp)) { - whitespace = next(); - while (true) { - value = peek(); - cp = value.charCodeAt(0); - if (isWhiteSpace(cp)) { - whitespace += value; - continue; - } - break; - } - if (cp == null || cp == 0x29) { - continue; - } - // bad url token - buffer += next(whitespace.length); - do { - value = peek(); - cp = value.charCodeAt(0); - if (cp == null || cp == 0x29) { - break; - } - buffer += next(); - } while (true); - pushToken({ typ: 'Bad-url-token', val: buffer }); - continue; - } - buffer += next(); - value = peek(); - } while (true); - buffer = ''; - } - } - } - break; - case '[': - case ']': - case '{': - case '}': - case ';': - if (buffer.length > 0) { - pushToken(getType(buffer)); - buffer = ''; - } - pushToken(getBlockType(value)); - let node = null; - if (value == '{' || value == ';') { - node = await parseNode(tokens); - if (node != null) { - stack.push(node); - // @ts-ignore - context = node; - } - else if (value == '{') { - // node == null - // consume and throw away until the closing '}' or EOF - consume('{', '}'); - } - tokens.length = 0; - map.clear(); - } - else if (value == '}') { - await parseNode(tokens); - const previousNode = stack.pop(); - // @ts-ignore - context = stack[stack.length - 1] || ast; - // @ts-ignore - if (options.removeEmpty && previousNode != null && previousNode.chi.length == 0 && context.chi[context.chi.length - 1] == previousNode) { - context.chi.pop(); - } - tokens.length = 0; - map.clear(); - buffer = ''; - } - break; - case '!': - if (buffer.length > 0) { - pushToken(getType(buffer)); - buffer = ''; - } - const important = peek(9); - if (important == 'important') { - if (tokens[tokens.length - 1]?.typ == 'Whitespace') { - tokens.pop(); - } - pushToken({ typ: 'Important' }); - next(9); - buffer = ''; - break; - } - buffer = '!'; - break; - default: - buffer += value; - break; + tokens = []; + map = new Map; } } - if (buffer.length > 0) { - pushToken(getType(buffer)); - } if (tokens.length > 0) { await parseNode(tokens); } - if (options.compress) { + if (options.minify) { if (ast.chi.length > 0) { - deduplicate(ast, options, true); + minify(ast, options, true); } } return { ast, errors, bytesIn }; } -function parseTokens(tokens, nodeType, options = {}) { +function parseString(src, options = { location: false }) { + return [...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) { + throw new Error('empty string?'); + } + if (hint != null) { + return ([ + 'Whitespace', 'Semi-colon', 'Colon', 'Block-start', + 'Block-start', 'Attr-start', 'Attr-end', 'Start-parens', 'End-parens', + 'Comma', 'Gt', 'Lt' + ].includes(hint) ? { typ: hint } : { typ: hint, val }); + } + if (val == ' ') { + return { typ: 'Whitespace' }; + } + if (val == ';') { + return { typ: 'Semi-colon' }; + } + if (val == '{') { + return { typ: 'Block-start' }; + } + if (val == '}') { + return { typ: 'Block-end' }; + } + if (val == '[') { + return { typ: 'Attr-start' }; + } + if (val == ']') { + return { typ: 'Attr-end' }; + } + if (val == ':') { + return { typ: 'Colon' }; + } + if (val == ')') { + return { typ: 'End-parens' }; + } + if (val == '(') { + return { typ: 'Start-parens' }; + } + if (val == '=') { + return { typ: 'Delim', val }; + } + if (val == ';') { + return { typ: 'Semi-colon' }; + } + if (val == ',') { + return { typ: 'Comma' }; + } + if (val == '<') { + return { typ: 'Lt' }; + } + if (val == '>') { + return { typ: 'Gt' }; + } + if (isPseudo(val)) { + return val.endsWith('(') ? { + typ: 'Pseudo-class-func', + val: val.slice(0, -1), + chi: [] + } + : { + typ: 'Pseudo-class', + val + }; + } + if (isAtKeyword(val)) { + return { + typ: 'At-rule', + val: val.slice(1) + }; + } + if (isFunction(val)) { + val = val.slice(0, -1); + return { + typ: val == 'url' ? 'UrlFunc' : 'Func', + val, + chi: [] + }; + } + if (isNumber(val)) { + return { + typ: 'Number', + val + }; + } + if (isDimension(val)) { + return parseDimension(val); + } + if (isPercentage(val)) { + return { + typ: 'Perc', + val: val.slice(0, -1) + }; + } + const v = val.toLowerCase(); + if (v == 'currentcolor' || val == 'transparent' || v in COLORS_NAMES) { + return { + typ: 'Color', + val, + kin: 'lit' + }; + } + if (isIdent(val)) { + return { + typ: 'Iden', + val + }; + } + if (val.charAt(0) == '#' && isHexColor(val)) { + return { + typ: 'Color', + val, + kin: 'hex' + }; + } + if (val.charAt(0) == '#' && isHash(val)) { + return { + typ: 'Hash', + val + }; + } + if ('"\''.includes(val.charAt(0))) { + return { + typ: 'Unclosed-string', + val + }; + } + return { + typ: 'Literal', + val + }; +} +function parseTokens(tokens, options = {}) { for (let i = 0; i < tokens.length; i++) { const t = tokens[i]; if (t.typ == 'Whitespace' && ((i == 0 || @@ -1007,7 +579,7 @@ function parseTokens(tokens, nodeType, options = {}) { if (t.chi.length > 1) { /*(t).chi =*/ // @ts-ignore - parseTokens(t.chi, t.typ, options); + parseTokens(t.chi, t.typ); } // @ts-ignore t.chi.forEach(val => { @@ -1119,8 +691,8 @@ function parseTokens(tokens, nodeType, options = {}) { // @ts-ignore if (t.chi.length > 0) { // @ts-ignore - parseTokens(t.chi, t.typ, options); - if (t.typ == 'Pseudo-class-func' && t.val == ':is' && options.compress) { + parseTokens(t.chi, t.typ); + if (t.typ == 'Pseudo-class-func' && t.val == ':is' && options.minify) { // const count = t.chi.filter(t => t.typ != 'Comment').length; if (count == 1 || @@ -1158,23 +730,5 @@ function parseTokens(tokens, nodeType, options = {}) { } return tokens; } -function getBlockType(chr) { - if (chr == ';') { - return { typ: 'Semi-colon' }; - } - if (chr == '{') { - return { typ: 'Block-start' }; - } - if (chr == '}') { - return { typ: 'Block-end' }; - } - if (chr == '[') { - return { typ: 'Attr-start' }; - } - if (chr == ']') { - return { typ: 'Attr-end' }; - } - throw new Error(`unhandled token: '${chr}'`); -} -export { parse }; +export { parse, parseString, urlTokenMatcher }; diff --git a/dist/lib/parser/tokenize.js b/dist/lib/parser/tokenize.js new file mode 100644 index 0000000..5d832e8 --- /dev/null +++ b/dist/lib/parser/tokenize.js @@ -0,0 +1,452 @@ +import { isWhiteSpace, isDigit, isNewLine } from './utils/syntax.js'; + +function* tokenize(iterator) { + let ind = -1; + let lin = 1; + let col = 0; + const position = { + ind: Math.max(ind, 0), + lin: lin, + col: Math.max(col, 1) + }; + let value; + let buffer = ''; + function consumeWhiteSpace() { + let count = 0; + while (isWhiteSpace(iterator.charAt(count + ind + 1).charCodeAt(0))) { + count++; + } + next(count); + return count; + } + function pushToken(token, hint) { + const result = { token, hint, position: { ...position }, bytesIn: ind }; + position.ind = ind; + position.lin = lin; + position.col = col == 0 ? 1 : col; + return result; + } + function* consumeString(quoteStr) { + const quote = quoteStr; + let value; + let hasNewLine = false; + if (buffer.length > 0) { + yield pushToken(buffer); + buffer = ''; + } + buffer += quoteStr; + while (value = peek()) { + if (ind >= iterator.length) { + yield pushToken(buffer, hasNewLine ? 'Bad-string' : 'Unclosed-string'); + break; + } + if (value == '\\') { + const sequence = peek(6); + let escapeSequence = ''; + let codepoint; + let i; + for (i = 1; i < sequence.length; i++) { + codepoint = sequence.charCodeAt(i); + if (codepoint == 0x20 || + (codepoint >= 0x61 && codepoint <= 0x66) || + (codepoint >= 0x41 && codepoint <= 0x46) || + (codepoint >= 0x30 && codepoint <= 0x39)) { + escapeSequence += sequence[i]; + if (codepoint == 0x20) { + break; + } + continue; + } + break; + } + // not hex or new line + // @ts-ignore + if (i == 1 && !isNewLine(codepoint)) { + buffer += sequence[i]; + next(2); + continue; + } + if (escapeSequence.trimEnd().length > 0) { + const codepoint = Number(`0x${escapeSequence.trimEnd()}`); + if (codepoint == 0 || + // leading surrogate + (0xD800 <= codepoint && codepoint <= 0xDBFF) || + // trailing surrogate + (0xDC00 <= codepoint && codepoint <= 0xDFFF)) { + buffer += String.fromCodePoint(0xFFFD); + } + else { + buffer += String.fromCodePoint(codepoint); + } + next(escapeSequence.length + 1); + continue; + } + // buffer += value; + if (ind >= iterator.length) { + // drop '\\' at the end + yield pushToken(buffer); + break; + } + buffer += next(2); + continue; + } + if (value == quote) { + buffer += value; + yield pushToken(buffer, hasNewLine ? 'Bad-string' : 'String'); + next(); + // i += value.length; + buffer = ''; + break; + } + if (isNewLine(value.charCodeAt(0))) { + hasNewLine = true; + } + if (hasNewLine && value == ';') { + yield pushToken(buffer, 'Bad-string'); + buffer = ''; + break; + } + buffer += value; + // i += value.length; + next(); + } + } + function peek(count = 1) { + if (count == 1) { + return iterator.charAt(ind + 1); + } + return iterator.slice(ind + 1, ind + count + 1); + } + function prev(count = 1) { + if (count == 1) { + return ind == 0 ? '' : iterator.charAt(ind - 1); + } + return iterator.slice(ind - 1 - count, ind - 1); + } + function next(count = 1) { + let char = ''; + while (count-- > 0 && ind < iterator.length) { + const codepoint = iterator.charCodeAt(++ind); + if (isNaN(codepoint)) { + return char; + } + char += iterator.charAt(ind); + if (isNewLine(codepoint)) { + lin++; + col = 0; + } + else { + col++; + } + } + return char; + } + while (value = next()) { + if (ind >= iterator.length) { + if (buffer.length > 0) { + yield pushToken(buffer); + buffer = ''; + } + break; + } + if (isWhiteSpace(value.charCodeAt(0))) { + if (buffer.length > 0) { + yield pushToken(buffer); + buffer = ''; + } + while (value = next()) { + if (ind >= iterator.length) { + break; + } + if (!isWhiteSpace(value.charCodeAt(0))) { + break; + } + } + yield pushToken('', 'Whitespace'); + buffer = ''; + if (ind >= iterator.length) { + break; + } + } + switch (value) { + case '/': + if (buffer.length > 0) { + yield pushToken(buffer); + buffer = ''; + if (peek() != '*') { + yield pushToken(value); + break; + } + } + buffer += value; + if (peek() == '*') { + buffer += '*'; + // i++; + next(); + while (value = next()) { + if (ind >= iterator.length) { + yield pushToken(buffer, 'Bad-comment'); + break; + } + if (value == '\\') { + buffer += value; + value = next(); + if (ind >= iterator.length) { + yield pushToken(buffer, 'Bad-comment'); + break; + } + buffer += value; + continue; + } + if (value == '*') { + buffer += value; + value = next(); + if (ind >= iterator.length) { + yield pushToken(buffer, 'Bad-comment'); + break; + } + buffer += value; + if (value == '/') { + yield pushToken(buffer, 'Comment'); + buffer = ''; + break; + } + } + else { + buffer += value; + } + } + } + break; + case '<': + if (buffer.length > 0) { + yield pushToken(buffer); + buffer = ''; + } + buffer += value; + value = next(); + if (ind >= iterator.length) { + break; + } + if (peek(3) == '!--') { + while (value = next()) { + if (ind >= iterator.length) { + break; + } + buffer += value; + if (value == '>' && prev(2) == '--') { + yield pushToken(buffer, 'CDOCOMM'); + buffer = ''; + break; + } + } + } + if (ind >= iterator.length) { + yield pushToken(buffer, 'BADCDO'); + buffer = ''; + } + break; + case '\\': + value = next(); + // EOF + if (ind + 1 >= iterator.length) { + // end of stream ignore \\ + yield pushToken(buffer); + buffer = ''; + break; + } + buffer += value; + break; + case '"': + case "'": + yield* consumeString(value); + break; + case '~': + case '|': + if (buffer.length > 0) { + yield pushToken(buffer); + buffer = ''; + } + buffer += value; + value = next(); + if (ind >= iterator.length) { + yield pushToken(buffer); + buffer = ''; + break; + } + if (value == '=') { + buffer += value; + yield pushToken(buffer, buffer[0] == '~' ? 'Includes' : 'Dash-matches'); + buffer = ''; + break; + } + yield pushToken(buffer); + while (isWhiteSpace(value.charCodeAt(0))) { + value = next(); + } + buffer = value; + break; + case '>': + if (buffer !== '') { + yield pushToken(buffer); + buffer = ''; + } + yield pushToken('', 'Gt'); + consumeWhiteSpace(); + break; + case '.': + const codepoint = peek().charCodeAt(0); + if (!isDigit(codepoint) && buffer !== '') { + yield pushToken(buffer); + buffer = value; + break; + } + buffer += value; + break; + case '+': + case ':': + case ',': + case '=': + if (buffer.length > 0) { + yield pushToken(buffer); + buffer = ''; + } + if (value == ':' && ':' == peek()) { + buffer += value + next(); + break; + } + yield pushToken(value); + buffer = ''; + if (value == '+' && isWhiteSpace(peek().charCodeAt(0))) { + yield pushToken(next()); + } + while (isWhiteSpace(peek().charCodeAt(0))) { + next(); + } + break; + case ')': + if (buffer.length > 0) { + yield pushToken(buffer); + buffer = ''; + } + yield pushToken('', 'End-parens'); + break; + case '(': + if (buffer.length == 0) { + yield pushToken('', 'Start-parens'); + break; + } + buffer += value; + // @ts-ignore + if (buffer == 'url(') { + yield pushToken(buffer); + buffer = ''; + // consume either string or url token + let whitespace = ''; + value = peek(); + while (isWhiteSpace(value.charCodeAt(0))) { + whitespace += value; + } + if (whitespace.length > 0) { + next(whitespace.length); + } + value = peek(); + if (value == '"' || value == "'") { + yield* consumeString(next()); + break; + } + else { + buffer = ''; + do { + let cp = value.charCodeAt(0); + // EOF - + if (cp == null) { + yield pushToken('', 'Bad-url-token'); + break; + } + // ')' + if (cp == 0x29 || cp == null) { + if (buffer.length == 0) { + yield pushToken(buffer, 'Bad-url-token'); + } + else { + yield pushToken(buffer, 'Url-token'); + } + if (cp != null) { + yield pushToken(next()); + } + break; + } + if (isWhiteSpace(cp)) { + whitespace = next(); + while (true) { + value = peek(); + cp = value.charCodeAt(0); + if (isWhiteSpace(cp)) { + whitespace += value; + continue; + } + break; + } + if (cp == null || cp == 0x29) { + continue; + } + // bad url token + buffer += next(whitespace.length); + do { + value = peek(); + cp = value.charCodeAt(0); + if (cp == null || cp == 0x29) { + break; + } + buffer += next(); + } while (true); + yield pushToken(buffer, 'Bad-url-token'); + continue; + } + buffer += next(); + value = peek(); + } while (true); + buffer = ''; + } + break; + } + yield pushToken(buffer); + buffer = ''; + break; + case '[': + case ']': + case '{': + case '}': + case ';': + if (buffer.length > 0) { + yield pushToken(buffer); + buffer = ''; + } + yield pushToken(value); + break; + case '!': + if (buffer.length > 0) { + yield pushToken(buffer); + buffer = ''; + } + const important = peek(9); + if (important == 'important') { + yield pushToken('', 'Important'); + next(9); + buffer = ''; + break; + } + buffer = '!'; + break; + default: + buffer += value; + break; + } + } + if (buffer.length > 0) { + yield pushToken(buffer); + } +} + +export { tokenize }; diff --git a/dist/lib/parser/utils/eq.js b/dist/lib/parser/utils/eq.js index a99e89b..4fa5874 100644 --- a/dist/lib/parser/utils/eq.js +++ b/dist/lib/parser/utils/eq.js @@ -1,13 +1,37 @@ function eq(a, b) { - if ((typeof a != 'object') || typeof b != 'object') { + if (a == null || b == null) { + return a == b; + } + if (typeof a != 'object' || typeof b != 'object') { return a === b; } + if (Object.getPrototypeOf(a) !== Object.getPrototypeOf(b)) { + return false; + } + if (Array.isArray(a)) { + if (a.length != b.length) { + return false; + } + let i = 0; + for (; i < a.length; i++) { + if (!eq(a[i], b[i])) { + return false; + } + } + return true; + } const k1 = Object.keys(a); const k2 = Object.keys(b); - return k1.length == k2.length && - k1.every((key) => { - return eq(a[key], b[key]); - }); + if (k1.length != k2.length) { + return false; + } + let key; + for (key of k1) { + if (!eq(a[key], b[key])) { + return false; + } + } + return true; } export { eq }; diff --git a/dist/lib/parser/utils/syntax.js b/dist/lib/parser/utils/syntax.js index 176647f..585b708 100644 --- a/dist/lib/parser/utils/syntax.js +++ b/dist/lib/parser/utils/syntax.js @@ -2,13 +2,14 @@ // https://www.w3.org/TR/2021/CRD-css-syntax-3-20211224/#typedef-ident-token // '\\' const REVERSE_SOLIDUS = 0x5c; +const dimensionUnits = [ + 'q', 'cap', 'ch', 'cm', 'cqb', 'cqh', 'cqi', 'cqmax', 'cqmin', 'cqw', 'dvb', + 'dvh', 'dvi', 'dvmax', 'dvmin', 'dvw', 'em', 'ex', 'ic', 'in', 'lh', 'lvb', + 'lvh', 'lvi', 'lvmax', 'lvw', 'mm', 'pc', 'pt', 'px', 'rem', 'rlh', 'svb', + 'svh', 'svi', 'svmin', 'svw', 'vb', 'vh', 'vi', 'vmax', 'vmin', 'vw' +]; function isLength(dimension) { - return 'unit' in dimension && [ - 'q', 'cap', 'ch', 'cm', 'cqb', 'cqh', 'cqi', 'cqmax', 'cqmin', 'cqw', 'dvb', - 'dvh', 'dvi', 'dvmax', 'dvmin', 'dvw', 'em', 'ex', 'ic', 'in', 'lh', 'lvb', - 'lvh', 'lvi', 'lvmax', 'lvw', 'mm', 'pc', 'pt', 'px', 'rem', 'rlh', 'svb', - 'svh', 'svi', 'svmin', 'svw', 'vb', 'vh', 'vi', 'vmax', 'vmin', 'vw' - ].includes(dimension.unit.toLowerCase()); + return 'unit' in dimension && dimensionUnits.includes(dimension.unit.toLowerCase()); } function isResolution(dimension) { return 'unit' in dimension && ['dpi', 'dpcm', 'dppx', 'x'].includes(dimension.unit.toLowerCase()); @@ -233,6 +234,22 @@ function isHexColor(name) { } return true; } +function isHexDigit(name) { + if (name.length || name.length > 6) { + return false; + } + for (let chr of name) { + let codepoint = chr.charCodeAt(0); + if (!isDigit(codepoint) && + // A F + !(codepoint >= 0x41 && codepoint <= 0x46) && + // a f + !(codepoint >= 0x61 && codepoint <= 0x66)) { + return false; + } + } + return true; +} function isFunction(name) { return name.endsWith('(') && isIdent(name.slice(0, -1)); } @@ -249,4 +266,4 @@ function isWhiteSpace(codepoint) { codepoint == 0xa || codepoint == 0xc || codepoint == 0xd; } -export { isAngle, isAtKeyword, isDigit, isDimension, isFrequency, isFunction, isHash, isHexColor, isIdent, isIdentCodepoint, isIdentStart, isLength, isNewLine, isNumber, isPercentage, isPseudo, isResolution, isTime, isWhiteSpace, parseDimension }; +export { isAngle, isAtKeyword, isDigit, isDimension, isFrequency, isFunction, isHash, isHexColor, isHexDigit, isIdent, isIdentCodepoint, isIdentStart, isLength, isNewLine, isNumber, isPercentage, isPseudo, isResolution, isTime, isWhiteSpace, parseDimension }; diff --git a/dist/lib/renderer/render.js b/dist/lib/renderer/render.js index 984cda2..886116f 100644 --- a/dist/lib/renderer/render.js +++ b/dist/lib/renderer/render.js @@ -1,7 +1,7 @@ -import { rgb2Hex, hsl2Hex, hwb2hex, cmyk2hex, NAMES_COLORS } from './utils/color.js'; +import { COLORS_NAMES, rgb2Hex, hsl2Hex, hwb2hex, cmyk2hex, NAMES_COLORS } from './utils/color.js'; function render(data, opt = {}) { - const options = Object.assign(opt.compress ? { + const options = Object.assign(opt.minify ?? true ? { indent: '', newLine: '', removeComments: true @@ -86,8 +86,11 @@ function doRender(data, options, reducer, level = 0, indents = []) { function renderToken(token, options = {}) { switch (token.typ) { case 'Color': - if (options.compress || options.colorConvert) { - let value = token.kin == 'hex' ? token.val.toLowerCase() : ''; + if (options.minify || options.colorConvert) { + if (token.kin == 'lit' && token.val.toLowerCase() == 'currentcolor') { + return 'currentcolor'; + } + let value = token.kin == 'hex' ? token.val.toLowerCase() : (token.kin == 'lit' ? COLORS_NAMES[token.val.toLowerCase()] : ''); if (token.val == 'rgb' || token.val == 'rgba') { value = rgb2Hex(token); } @@ -131,7 +134,7 @@ function renderToken(token, options = {}) { case 'UrlFunc': case 'Pseudo-class-func': // @ts-ignore - return ( /* options.compress && 'Pseudo-class-func' == token.typ && token.val.slice(0, 2) == '::' ? token.val.slice(1) :*/token.val ?? '') + '(' + token.chi.reduce((acc, curr) => { + return ( /* options.minify && 'Pseudo-class-func' == token.typ && token.val.slice(0, 2) == '::' ? token.val.slice(1) :*/token.val ?? '') + '(' + token.chi.reduce((acc, curr) => { if (options.removeComments && curr.typ == 'Comment') { if (!options.preserveLicense || !curr.val.startsWith('/*!')) { return acc; @@ -222,7 +225,7 @@ function renderToken(token, options = {}) { case 'String': case 'Iden': case 'Delim': - return /* options.compress && 'Pseudo-class' == token.typ && '::' == token.val.slice(0, 2) ? token.val.slice(1) : */ token.val; + 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)}`); } diff --git a/dist/lib/transform.js b/dist/lib/transform.js index 6a9d73f..31ea354 100644 --- a/dist/lib/transform.js +++ b/dist/lib/transform.js @@ -2,7 +2,7 @@ import { parse } from './parser/parse.js'; import { render } from './renderer/render.js'; async function transform(css, options = {}) { - options = { compress: true, removeEmpty: true, ...options }; + options = { minify: true, removeEmpty: true, ...options }; const startTime = performance.now(); const parseResult = await parse(css, options); const renderTime = performance.now(); diff --git a/dist/node/index.js b/dist/node/index.js index 714cb6d..763e775 100644 --- a/dist/node/index.js +++ b/dist/node/index.js @@ -1,4 +1,5 @@ import { parse as parse$1 } from '../lib/parser/parse.js'; +export { parseString, urlTokenMatcher } from '../lib/parser/parse.js'; import '../lib/renderer/utils/color.js'; import { transform as transform$1 } from '../lib/transform.js'; import { load } from './load.js'; diff --git a/dist/web/index.js b/dist/web/index.js index cacb141..ff4c2f8 100644 --- a/dist/web/index.js +++ b/dist/web/index.js @@ -1,8 +1,12 @@ import { parse as parse$1 } from '../lib/parser/parse.js'; -export { deduplicate, deduplicateRule, hasDeclaration, reduceSelector } from '../lib/parser/deduplicate.js'; +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 { render, renderToken } from '../lib/renderer/render.js'; -export { walk } from '../lib/walker/walk.js'; import { transform as transform$1 } from '../lib/transform.js'; +export { combinators, hasDeclaration, minify, minifyRule, reduceSelector } from '../lib/ast/minify.js'; +export { walk } from '../lib/ast/walk.js'; import { load } from './load.js'; import { resolve, dirname } from '../lib/fs/resolve.js'; export { matchUrl } from '../lib/fs/resolve.js'; diff --git a/package.json b/package.json index fea45d9..666cfae 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-alpha5", + "version": "0.0.1-rc1", "exports": { ".": "./dist/index.js", "./web": "./dist/web/index.js", @@ -11,9 +11,10 @@ "typings": "dist/index.d.ts", "scripts": { "build": "rollup -c", - "debug": "web-test-runner \"test/**/*.web.js\" --manual --open --node-resolve --root-dir=.", - "test": "web-test-runner \"test/**/*.web-spec.js\" --node-resolve --root-dir=.; mocha --reporter-options='maxDiffSize=181920' \"test/**/*.spec.js\"", - "test:cov": "web-test-runner \"test/**/*.web-spec.js\" --node-resolve --root-dir=. --coverage; c8 --reporter=html --reporter=text --reporter=json-summary mocha --reporter-options='maxDiffSize=181920' \"test/**/*.spec.js\"" + "test": "web-test-runner \"test/**/*.web-spec.js\" --node-resolve --root-dir=.; mocha --reporter-options='maxDiffSize=1801920' \"test/**/*.spec.js\"", + "test:cov": "web-test-runner \"test/**/*.web-spec.js\" --node-resolve --root-dir=. --coverage; c8 --reporter=html --reporter=text --reporter=json-summary mocha --reporter-options='maxDiffSize=1801920' \"test/**/*.spec.js\"", + "profile": "node --inspect-brk test/inspect.mjs", + "debug": "web-test-runner \"test/**/*.web.js\" --manual --open --node-resolve --root-dir=." }, "repository": { "type": "git", @@ -22,9 +23,14 @@ "keywords": [ "parser", "css", + "css parser", "css-parser", "node", - "browser" + "ast", + "browser", + "css nesting", + "css compiler", + "nested css" ], "author": "Thierry Bela", "license": "MIT OR LGPL-3.0", diff --git a/src/@types/config.ts b/src/@types/config.ts new file mode 100644 index 0000000..bd6a203 --- /dev/null +++ b/src/@types/config.ts @@ -0,0 +1,303 @@ + +// generated from config.json at https://app.quicktype.io/?l=ts + +export interface PropertiesConfig { + properties: PropertiesConfigProperties; + map: Map; +} + +export interface Map { + border: Border; + "border-color": BackgroundPositionClass; + "border-style": BackgroundPositionClass; + "border-width": BackgroundPositionClass; + outline: Outline; + "outline-color": BackgroundPositionClass; + "outline-style": BackgroundPositionClass; + "outline-width": BackgroundPositionClass; + font: Font; + "font-weight": BackgroundPositionClass; + "font-style": BackgroundPositionClass; + "font-size": BackgroundPositionClass; + "line-height": BackgroundPositionClass; + "font-stretch": BackgroundPositionClass; + "font-variant": BackgroundPositionClass; + "font-family": BackgroundPositionClass; + background: Background; + "background-repeat": BackgroundPositionClass; + "background-color": BackgroundPositionClass; + "background-image": BackgroundPositionClass; + "background-attachment": BackgroundPositionClass; + "background-clip": BackgroundPositionClass; + "background-origin": BackgroundPositionClass; + "background-position": BackgroundPositionClass; + "background-size": BackgroundPositionClass; +} + +export interface Background { + shorthand: string; + pattern: string; + keywords: string[]; + default: any[]; + multiple: boolean; + separator: Separator; + properties: BackgroundProperties; +} + +export interface BackgroundProperties { + "background-repeat": BackgroundRepeat; + "background-color": PurpleBackgroundAttachment; + "background-image": PurpleBackgroundAttachment; + "background-attachment": PurpleBackgroundAttachment; + "background-clip": PurpleBackgroundAttachment; + "background-origin": PurpleBackgroundAttachment; + "background-position": BackgroundPosition; + "background-size": BackgroundSize; +} + +export interface PurpleBackgroundAttachment { + types: string[]; + default: string[]; + keywords: string[]; + required?: boolean; + mapping?: BackgroundAttachmentMapping; +} + +export interface BackgroundAttachmentMapping { + "ultra-condensed": string; + "extra-condensed": string; + condensed: string; + "semi-condensed": string; + normal: string; + "semi-expanded": string; + expanded: string; + "extra-expanded": string; + "ultra-expanded": string; +} + +export interface BackgroundPosition { + multiple: boolean; + types: string[]; + default: string[]; + keywords: string[]; + mapping: BackgroundPositionMapping; + constraints: BackgroundPositionConstraints; +} + +export interface BackgroundPositionConstraints { + mapping: ConstraintsMapping; +} + +export interface ConstraintsMapping { + max: number; +} + +export interface BackgroundPositionMapping { + left: string; + top: string; + center: string; + bottom: string; + right: string; +} + +export interface BackgroundRepeat { + types: any[]; + default: string[]; + multiple: boolean; + keywords: string[]; + mapping: BackgroundRepeatMapping; +} + +export interface BackgroundRepeatMapping { + "repeat no-repeat": string; + "no-repeat repeat": string; + "repeat repeat": string; + "space space": string; + "round round": string; + "no-repeat no-repeat": string; +} + +export interface BackgroundSize { + multiple: boolean; + previous: string; + prefix: Prefix; + types: string[]; + default: string[]; + keywords: string[]; + mapping: BackgroundSizeMapping; +} + +export interface BackgroundSizeMapping { + "auto auto": string; +} + +export interface Prefix { + typ: string; + val: string; +} + +export interface Separator { + typ: string; +} + +export interface BackgroundPositionClass { + shorthand: string; +} + +export interface Border { + shorthand: string; + pattern: string; + keywords: string[]; + default: string[]; + properties: BorderProperties; +} + +export interface BorderProperties { + "border-color": BorderColorClass; + "border-style": BorderColorClass; + "border-width": BorderColorClass; +} + +export interface BorderColorClass { +} + +export interface Font { + shorthand: string; + pattern: string; + keywords: string[]; + default: any[]; + properties: FontProperties; +} + +export interface FontProperties { + "font-weight": FontWeight; + "font-style": PurpleBackgroundAttachment; + "font-size": PurpleBackgroundAttachment; + "line-height": LineHeight; + "font-stretch": PurpleBackgroundAttachment; + "font-variant": PurpleBackgroundAttachment; + "font-family": FontFamily; +} + +export interface FontFamily { + types: string[]; + default: any[]; + keywords: string[]; + required: boolean; + multiple: boolean; + separator: Separator; +} + +export interface FontWeight { + types: string[]; + default: string[]; + keywords: string[]; + constraints: FontWeightConstraints; + mapping: FontWeightMapping; +} + +export interface FontWeightConstraints { + value: Value; +} + +export interface Value { + min: string; + max: string; +} + +export interface FontWeightMapping { + thin: string; + hairline: string; + "extra light": string; + "ultra light": string; + light: string; + normal: string; + regular: string; + medium: string; + "semi bold": string; + "demi bold": string; + bold: string; + "extra bold": string; + "ultra bold": string; + black: string; + heavy: string; + "extra black": string; + "ultra black": string; +} + +export interface LineHeight { + types: string[]; + default: string[]; + keywords: string[]; + previous: string; + prefix: Prefix; +} + +export interface Outline { + shorthand: string; + pattern: string; + keywords: string[]; + default: string[]; + properties: OutlineProperties; +} + +export interface OutlineProperties { + "outline-color": PurpleBackgroundAttachment; + "outline-style": PurpleBackgroundAttachment; + "outline-width": PurpleBackgroundAttachment; +} + +export interface PropertiesConfigProperties { + inset: BorderRadius; + top: BackgroundPositionClass; + right: BackgroundPositionClass; + bottom: BackgroundPositionClass; + left: BackgroundPositionClass; + margin: BorderRadius; + "margin-top": BackgroundPositionClass; + "margin-right": BackgroundPositionClass; + "margin-bottom": BackgroundPositionClass; + "margin-left": BackgroundPositionClass; + padding: BorderColor; + "padding-top": BackgroundPositionClass; + "padding-right": BackgroundPositionClass; + "padding-bottom": BackgroundPositionClass; + "padding-left": BackgroundPositionClass; + "border-radius": BorderRadius; + "border-top-left-radius": BackgroundPositionClass; + "border-top-right-radius": BackgroundPositionClass; + "border-bottom-right-radius": BackgroundPositionClass; + "border-bottom-left-radius": BackgroundPositionClass; + "border-width": BorderColor; + "border-top-width": BackgroundPositionClass; + "border-right-width": BackgroundPositionClass; + "border-bottom-width": BackgroundPositionClass; + "border-left-width": BackgroundPositionClass; + "border-style": BorderColor; + "border-top-style": BackgroundPositionClass; + "border-right-style": BackgroundPositionClass; + "border-bottom-style": BackgroundPositionClass; + "border-left-style": BackgroundPositionClass; + "border-color": BorderColor; + "border-top-color": BackgroundPositionClass; + "border-right-color": BackgroundPositionClass; + "border-bottom-color": BackgroundPositionClass; + "border-left-color": BackgroundPositionClass; +} + +export interface BorderColor { + shorthand: string; + map?: string; + properties: string[]; + types: string[]; + keywords: string[]; +} + +export interface BorderRadius { + shorthand: string; + properties: string[]; + types: string[]; + multiple: boolean; + separator: null | string; + keywords: string[]; +} diff --git a/src/@types/index.d.ts b/src/@types/index.d.ts index 0a0c0ea..48377c2 100644 --- a/src/@types/index.d.ts +++ b/src/@types/index.d.ts @@ -4,6 +4,7 @@ export * from './validation'; export * from './tokenize'; export * from './stringiterator'; export * from './shorthand'; +export * from './config'; export declare type NodeTraverseCallback = (node: AstNode, location: Location, parent: AstRuleList, root: AstRuleStyleSheet) => void; @@ -29,7 +30,7 @@ export interface ParserOptions { src?: string; sourcemap?: boolean; - compress?: boolean; + minify?: boolean; nestingRules?: boolean; removeEmpty?: boolean; resolveUrls?: boolean; @@ -42,7 +43,7 @@ export interface ParserOptions { export interface RenderOptions { - compress?: boolean; + minify?: boolean; preserveLicense?: boolean; indent?: string; newLine?: string; @@ -79,6 +80,12 @@ export interface ParseTokenOptions extends ParserOptions { parseColor?: boolean; } +export interface TokenizeResult { + token: string; + hint?: string; + position: Position; + bytesIn: number; +} export interface MatchedSelector { match: string[][]; diff --git a/src/@types/shorthand.js b/src/@types/shorthand.js deleted file mode 100644 index cb0ff5c..0000000 --- a/src/@types/shorthand.js +++ /dev/null @@ -1 +0,0 @@ -export {}; diff --git a/src/@types/shorthand.ts b/src/@types/shorthand.ts index 4683600..918c507 100644 --- a/src/@types/shorthand.ts +++ b/src/@types/shorthand.ts @@ -8,6 +8,7 @@ export interface PropertyType { export interface ShorthandPropertyType { shorthand: string; + map?: string; properties: string[]; types: string[]; multiple: boolean; diff --git a/src/@types/stringiterator.js b/src/@types/stringiterator.js deleted file mode 100644 index cb0ff5c..0000000 --- a/src/@types/stringiterator.js +++ /dev/null @@ -1 +0,0 @@ -export {}; diff --git a/src/@types/tokenize.js b/src/@types/tokenize.js deleted file mode 100644 index cb0ff5c..0000000 --- a/src/@types/tokenize.js +++ /dev/null @@ -1 +0,0 @@ -export {}; diff --git a/src/config.json b/src/config.json index a471450..6d632c8 100644 --- a/src/config.json +++ b/src/config.json @@ -116,6 +116,7 @@ }, "border-width": { "shorthand": "border-width", + "map": "border", "properties": [ "border-top-width", "border-right-width", @@ -126,6 +127,9 @@ "Length", "Perc" ], + "default": [ + "medium" + ], "keywords": [ "thin", "medium", @@ -133,19 +137,24 @@ ] }, "border-top-width": { + "map": "border", "shorthand": "border-width" }, "border-right-width": { + "map": "border", "shorthand": "border-width" }, "border-bottom-width": { + "map": "border", "shorthand": "border-width" }, "border-left-width": { + "map": "border", "shorthand": "border-width" }, "border-style": { "shorthand": "border-style", + "map": "border", "properties": [ "border-top-style", "border-right-style", @@ -153,6 +162,9 @@ "border-left-style" ], "types": [], + "default": [ + "none" + ], "keywords": [ "none", "hidden", @@ -167,19 +179,24 @@ ] }, "border-top-style": { + "map": "border", "shorthand": "border-style" }, "border-right-style": { + "map": "border", "shorthand": "border-style" }, "border-bottom-style": { + "map": "border", "shorthand": "border-style" }, "border-left-style": { + "map": "border", "shorthand": "border-style" }, "border-color": { "shorthand": "border-color", + "map": "border", "properties": [ "border-top-color", "border-right-color", @@ -189,22 +206,92 @@ "types": [ "Color" ], + "default": [ + "currentcolor" + ], "keywords": [] }, "border-top-color": { + "map": "border", "shorthand": "border-color" }, "border-right-color": { + "map": "border", "shorthand": "border-color" }, "border-bottom-color": { + "map": "border", "shorthand": "border-color" }, "border-left-color": { + "map": "border", "shorthand": "border-color" } }, "map": { + "border": { + "shorthand": "border", + "pattern": "border-color border-style border-width", + "keywords": [ + "none" + ], + "default": [ + "0", + "none" + ], + "properties": { + "border-color": { + "types": [ + "Color" + ], + "default": [ + "currentcolor" + ], + "keywords": [] + }, + "border-style": { + "types": [], + "default": [ + "none" + ], + "keywords": [ + "none", + "hidden", + "dotted", + "dashed", + "solid", + "double", + "groove", + "ridge", + "inset", + "outset" + ] + }, + "border-width": { + "types": [ + "Length", + "Perc" + ], + "default": [ + "medium" + ], + "keywords": [ + "thin", + "medium", + "thick" + ] + } + } + }, + "border-color": { + "shorthand": "border" + }, + "border-style": { + "shorthand": "border" + }, + "border-width": { + "shorthand": "border" + }, "outline": { "shorthand": "outline", "pattern": "outline-color outline-style outline-width", @@ -221,12 +308,10 @@ "Color" ], "default": [ - "currentColor", - "invert" + "currentColor" ], "keywords": [ - "currentColor", - "invert" + "currentColor" ] }, "outline-style": { @@ -557,6 +642,7 @@ "default": [ "transparent" ], + "multiple": true, "keywords": [] }, "background-image": { @@ -575,6 +661,7 @@ "default": [ "scroll" ], + "multiple": true, "keywords": [ "scroll", "fixed", @@ -586,6 +673,7 @@ "default": [ "border-box" ], + "multiple": true, "keywords": [ "border-box", "padding-box", @@ -598,6 +686,7 @@ "default": [ "padding-box" ], + "multiple": true, "keywords": [ "border-box", "padding-box", diff --git a/src/index.js b/src/index.js deleted file mode 100644 index d0dddb6..0000000 --- a/src/index.js +++ /dev/null @@ -1 +0,0 @@ -export * from './node'; diff --git a/src/lib/ast/index.ts b/src/lib/ast/index.ts new file mode 100644 index 0000000..490f983 --- /dev/null +++ b/src/lib/ast/index.ts @@ -0,0 +1,3 @@ + +export * from './minify'; +export * from './walk'; \ No newline at end of file diff --git a/src/lib/parser/deduplicate.ts b/src/lib/ast/minify.ts similarity index 61% rename from src/lib/parser/deduplicate.ts rename to src/lib/ast/minify.ts index 12aca7c..1a33e5c 100644 --- a/src/lib/parser/deduplicate.ts +++ b/src/lib/ast/minify.ts @@ -1,82 +1,432 @@ -import {getConfig, isIdentStart, isWhiteSpace} from "./utils"; +import {getConfig, isFunction, isIdent, isIdentStart, isWhiteSpace} from "../parser/utils"; import { AstAtRule, AstDeclaration, AstNode, - AstRule, AstRuleStyleSheet, - MatchedSelector, + AstRule, MatchedSelector, NodeType, OptimizedSelector, - ParserOptions + ParserOptions, ShorthandPropertyType } from "../../@types"; -import {PropertyList} from "./declaration"; -import {eq} from "./utils/eq"; +import {PropertyList} from "../parser/declaration"; +import {eq} from "../parser/utils/eq"; import {render} from "../renderer"; const configuration = getConfig(); -const combinators = ['+', '>', '~']; +export const combinators = ['+', '>', '~']; const notEndingWith = ['(', '['].concat(combinators); -function wrapNodes(previous: AstRule, node: AstRule, match: MatchedSelector, ast: AstNode, i: number, nodeIndex: number): AstRule { +export function minify(ast: AstNode, options: ParserOptions = {}, recursive: boolean = false): AstNode { - // @ts-ignore - let pSel = match.selector1.reduce(reducer, []).join(','); - - // @ts-ignore - let nSel = match.selector2.reduce(reducer, []).join(','); + function wrapNodes(previous: AstRule, node: AstRule, match: MatchedSelector, ast: AstNode, i: number, nodeIndex: number): AstRule { -// @ts-ignore - const wrapper = {...previous, chi: [], sel: match.match.reduce(reducer, []).join(',')}; + // @ts-ignore + let pSel = match.selector1.reduce(reducer, []).join(','); - // @ts-ignore - Object.defineProperty(wrapper, 'raw', { - enumerable: false, - writable: true, // @ts-ignore - value: match.match.map(t => t.slice()) - }); + let nSel = match.selector2.reduce(reducer, []).join(','); - if (pSel == '&' || pSel === '') { +// @ts-ignore + const wrapper = {...previous, chi: [], sel: match.match.reduce(reducer, []).join(',')}; // @ts-ignore - wrapper.chi.push(...previous.chi); + Object.defineProperty(wrapper, 'raw', { + enumerable: false, + writable: true, + // @ts-ignore + value: match.match.map(t => t.slice()) + }); - // @ts-ignore - if ((nSel == '&' || nSel === '') && hasOnlyDeclarations(previous)) { + if (pSel == '&' || pSel === '') { + + // @ts-ignore + wrapper.chi.push(...previous.chi); // @ts-ignore - wrapper.chi.push(...node.chi); + if ((nSel == '&' || nSel === '') && hasOnlyDeclarations(previous)) { + + // @ts-ignore + wrapper.chi.push(...node.chi); + } else { + + // @ts-ignore + wrapper.chi.push(node); + } } else { // @ts-ignore - wrapper.chi.push(node); + wrapper.chi.push(previous, node); } - } else { // @ts-ignore - wrapper.chi.push(previous, node); + ast.chi.splice(i, 1, wrapper); + // @ts-ignore + ast.chi.splice(nodeIndex, 1); + // @ts-ignore + previous.sel = pSel; + // @ts-ignore + previous.raw = match.selector1; + // @ts-ignore + node.sel = nSel; + // @ts-ignore + node.raw = match.selector2; + + + reduceRuleSelector(wrapper); + return wrapper; } - // @ts-ignore - ast.chi.splice(i, 1, wrapper); - // @ts-ignore - ast.chi.splice(nodeIndex, 1); - // @ts-ignore - previous.sel = pSel; - // @ts-ignore - previous.raw = match.selector1; - // @ts-ignore - node.sel = nSel; - // @ts-ignore - node.raw = match.selector2; + function reducer(acc: string[], curr: string[], index: number, array: string[][]) { + // trim :is() + if (array.length == 1 && array[0][0] == ':is(' && array[0].at(-1) == ')') { - reduceRuleSelector(wrapper); - return wrapper; -} + curr = curr.slice(1, -1); + } + + if (curr[0] == '&') { + + if (curr[1] == ' ' && !isIdent(curr[2]) && !isFunction(curr[2])) { + + curr.splice(0, 2); + } + + else if (combinators.includes(curr[1])) { + + curr.splice(0, 1); + } + } + + else if (ast.typ == 'Rule' && (isIdent(curr[0]) || isFunction(curr[0]))) { + + curr.unshift('&', ' '); + } + + acc.push(curr.join('')); + return acc; + } + + function diff(n1: AstRule, n2: AstRule, options: ParserOptions = {}) { + let node1 = n1; + let node2 = n2; + let exchanged = false; + if (node1.chi.length > node2.chi.length) { + const t = node1; + node1 = node2; + node2 = t; + exchanged = true; + } + let i = node1.chi.length; + let j = node2.chi.length; + if (i == 0 || j == 0) { + // @ts-ignore + return null; + } + // @ts-ignore + const raw1 = node1.raw; + + // @ts-ignore + const raw2 = node2.raw; + // @ts-ignore + node1 = {...node1, chi: node1.chi.slice()}; + node2 = {...node2, chi: node2.chi.slice()}; + if (raw1 != null) { + Object.defineProperty(node1, 'raw', {enumerable: false, writable: true, value: raw1}); + } + + if (raw2 != null) { + Object.defineProperty(node2, 'raw', {enumerable: false, writable: true, value: raw2}); + } + + const intersect = []; + + while (i--) { + + if (node1.chi[i].typ == 'Comment') { + + continue; + } + + j = node2.chi.length; + + if (j == 0) { + + break; + } + + while (j--) { + + if (node2.chi[j].typ == 'Comment') { + + continue; + } + + if ((node1.chi[i]).nam == (node2.chi[j]).nam) { + + if (eq(node1.chi[i], node2.chi[j])) { + + intersect.push(node1.chi[i]); + node1.chi.splice(i, 1); + node2.chi.splice(j, 1); + break; + } + } + } + } + + // @ts-ignore + const result = (intersect.length == 0 ? null : { + ...node1, + // @ts-ignore + sel: [...new Set([...(n1?.raw?.reduce(reducer, []) || splitRule(n1.sel)).concat(n2?.raw?.reduce(reducer, []) || splitRule(n2.sel))])].join(','), + chi: intersect.reverse() + }); + if (result == null || [n1, n2].reduce((acc, curr) => curr.chi.length == 0 ? acc : acc + render(curr, options).code.length, 0) <= [node1, node2, result].reduce((acc, curr) => curr.chi.length == 0 ? acc : acc + render(curr, options).code.length, 0)) { + // @ts-ignore + return null; + } + return {result, node1: exchanged ? node2 : node1, node2: exchanged ? node2 : node2}; + } + + function matchSelectors(selector1: string[][], selector2: string[][], parentType: NodeType): null | MatchedSelector { + + let match: string[][] = [[]]; + const j = Math.min( + selector1.reduce((acc, curr) => Math.min(acc, curr.length), selector1.length > 0 ? selector1[0].length : 0), + selector2.reduce((acc, curr) => Math.min(acc, curr.length), selector2.length > 0 ? selector2[0].length : 0) + ); + + let i: number = 0; + let k: number; + let l: number; + let token: string; + let matching: boolean = true; + let matchFunction: number = 0; + let inAttr: number = 0; + + for (; i < j; i++) { + + k = 0; + token = selector1[0][i]; + + for (; k < selector1.length; k++) { + + if (selector1[k][i] != token) { + + matching = false; + break; + } + } + + if (matching) { + + l = 0; + for (; l < selector2.length; l++) { + + if (selector2[l][i] != token) { + + matching = false; + break; + } + } + } + + if (!matching) { + + break; + } + + if (token == ',') { + + match.push([]); + } else { + + if (token.endsWith('(')) { + + matchFunction++; + } + + if (token.endsWith('[')) { + + inAttr++; + } else if (token == ')') { + + matchFunction--; + } else if (token == ']') { + + inAttr--; + } + + (match.at(-1)).push(token); + } + } + + // invalid function + if (matchFunction != 0 || inAttr != 0) { + + return null; + } + + if (parentType != 'Rule') { + + for (const part of match) { + + if (part.length > 0 && combinators.includes(part[0].charAt(0))) { + + return null; + } + } + } + if (match.length > 1) { + + console.error(`unsupported multilevel matching`); + console.error({match, selector1, selector2}); + return null; + } + + for (const part of match) { + + while (part.length > 0) { + + const token = part.at(-1); + + if (token == ' ' || combinators.includes(token) || notEndingWith.includes(token.at(-1))) { + + part.pop(); + continue; + } + + break; + } + } + + if (match.every(t => t.length == 0)) { + + return null; + } + + if (eq([['&']], match)) { + + return null; + } + + function reduce(acc: string[][], curr: string[]) { + + if (acc === null) { + + return null; + } + + let hasCompoundSelector = true; + + curr = curr.slice(match[0].length); + + while (curr.length > 0) { + + if (curr[0] == ' ') { + + hasCompoundSelector = false; + curr.unshift('&'); + continue; + } + + break; + } + + // invalid function match + if (curr.length > 0 && curr[0].endsWith('(') && curr.at(-1) != ')') { + + return null; + } + + if (curr.length == 1 && combinators.includes(curr[0].charAt(0))) { + + return null; + } + + if (hasCompoundSelector && curr.length > 0) { + + hasCompoundSelector = !['&'].concat(combinators).includes(curr[0].charAt(0)); + } + + if (curr[0] == ':is(') { + + let inFunction = 0; + let canReduce = true; + const isCompound = curr.reduce((acc, token, index: number) => { + + if (index == 0) { + + inFunction++; + canReduce = curr[1] == '&'; + } else if (token.endsWith('(')) { + + if (inFunction == 0) { + + canReduce = false; + } + + inFunction++; + } else if (token == ')') { + + inFunction--; + } else if (token == ',') { + + if (!canReduce) { + + canReduce = curr[index + 1] == '&'; + } + + acc.push([]); + } else acc.at(-1)?.push(token); + + return acc; + + }, [[]]); + + if (inFunction > 0) { + + canReduce = false; + } + + if (canReduce) { + + curr = isCompound.reduce((acc, curr) => { + + if (acc.length > 0) { + + acc.push(','); + } + + acc.push(...curr); + + return acc + }, []); + } + } + + // @todo: check hasCompoundSelector && curr[0] == '&' && curr[1] == ' ' + + acc.push(match.length == 0 ? ['&'] : (hasCompoundSelector && curr[0] != '&' && (curr.length == 0 || !combinators.includes(curr[0].charAt(0))) ? ['&'].concat(curr) : curr)) + + return acc; + } + + + // @ts-ignore + selector1 = selector1.reduce(reduce, []); + // @ts-ignore + selector2 = selector2.reduce(reduce, []); + + return selector1 == null || selector2 == null ? null : { + eq: eq(selector1, selector2), + match, + selector1, + selector2 + } + } -export function deduplicate(ast: AstNode, options: ParserOptions = {}, recursive: boolean = false): AstNode { // @ts-ignore if (('chi' in ast) && ast.chi?.length > 0) { let i: number = 0; @@ -121,8 +471,9 @@ export function deduplicate(ast: AstNode, options: ParserOptions = {}, recursive if (options.nestingRules) { // @ts-ignore - if (previous != null && previous.typ == 'Rule') { + if (previous?.typ == 'Rule') { + // @ts-ignore reduceRuleSelector(previous); // @ts-ignore @@ -174,7 +525,7 @@ export function deduplicate(ast: AstNode, options: ParserOptions = {}, recursive nodeIndex = --i; // @ts-ignore previous = ast.chi[nodeIndex]; - deduplicate(wrapper, options, recursive); + minify(wrapper, options, recursive); continue; } // @ts-ignore @@ -210,7 +561,7 @@ export function deduplicate(ast: AstNode, options: ParserOptions = {}, recursive let wrap: boolean = true; // @ts-ignore - const selector: string[] = node.optimized.selector.reduce((acc, curr) => { + const selector: string[][] = node.optimized.selector.reduce((acc, curr) => { if (curr[0] == '&') { @@ -236,17 +587,34 @@ export function deduplicate(ast: AstNode, options: ParserOptions = {}, recursive else if (combinators.includes(curr[0])) { curr.unshift('&'); + wrap = false; } // @ts-ignore - acc.push(curr.map(t => t.replaceAll('&', node.optimized.optimized[0])).join('')); + acc.push(curr); return acc }, []); + if (!wrap) { + + wrap = selector.some(s => s[0] != '&'); + } + + const rule = selector.map(s => { + + if (s[0] == '&') { + + // @ts-ignore + s[0] = node.optimized.optimized[0]; + } + + return s.join('') + }).join(','); + // @ts-ignore - node.sel = (wrap ? node.optimized.optimized[0] : '') + `:is(${selector.join(',')})`; + node.sel = wrap ? node.optimized.optimized[0] + `:is(${rule})` : rule; } } // @ts-ignore @@ -277,9 +645,9 @@ export function deduplicate(ast: AstNode, options: ParserOptions = {}, recursive // @ts-ignore if (hasDeclaration(node)) { // @ts-ignore - deduplicateRule(node); + minifyRule(node); } else { - deduplicate(node, options, recursive); + minify(node, options, recursive); } i--; previous = node; @@ -313,14 +681,15 @@ export function deduplicate(ast: AstNode, options: ParserOptions = {}, recursive } } } + // @ts-ignore if (recursive && previous != node) { // @ts-ignore if (hasDeclaration(previous)) { // @ts-ignore - deduplicateRule(previous); + minifyRule(previous); } else { - deduplicate(previous, options, recursive); + minify(previous, options, recursive); } } } @@ -331,11 +700,11 @@ export function deduplicate(ast: AstNode, options: ParserOptions = {}, recursive if (recursive && node != null && ('chi' in node)) { // @ts-ignore if (node.chi.some(n => n.typ == 'Declaration')) { - deduplicateRule(node); + minifyRule(node); } else { // @ts-ignore if (!(node.typ == 'AtRule' && (node).nam != 'font-face')) { - deduplicate(node, options, recursive); + minify(node, options, recursive); } } } @@ -343,20 +712,122 @@ export function deduplicate(ast: AstNode, options: ParserOptions = {}, recursive return ast; } -function hasOnlyDeclarations(node: AstRule): boolean { - let k: number = node.chi.length; - - while (k--) { - - if (node.chi[k].typ == 'Comment') { - - continue; - } +export function reduceSelector(selector: string[][]) { - return node.chi[k].typ == 'Declaration'; + if (selector.length == 0) { + return null; } - return true; + const optimized: string[] = []; + const k = selector.reduce((acc, curr) => acc == 0 ? curr.length : (curr.length == 0 ? acc : Math.min(acc, curr.length)), 0); + let i = 0; + let j; + let match; + for (; i < k; i++) { + const item = selector[0][i]; + match = true; + for (j = 1; j < selector.length; j++) { + if (item != selector[j][i]) { + match = false; + break; + } + } + if (!match) { + break; + } + + optimized.push(item); + } + + while (optimized.length > 0) { + + const last = optimized.at(-1); + + if ((last == ' ' || combinators.includes(last))) { + + optimized.pop(); + continue; + } + + break; + } + + selector.forEach((selector) => selector.splice(0, optimized.length)); + + // combinator + if (combinators.includes(optimized.at(-1))) { + const combinator = optimized.pop(); + selector.forEach(selector => selector.unshift(combinator)); + } + + let reducible = optimized.length == 1; + + if (optimized[0] == '&' && optimized[1] == ' ') { + + optimized.splice(0, 2); + } + + if (optimized.length == 0 || + (optimized[0].charAt(0) == '&' || + selector.length == 1)) { + return { + match: false, + optimized, + selector: selector.map(selector => selector[0] == '&' && selector[1] == ' ' ? selector.slice(2) : selector), + reducible: selector.length > 1 && selector.every((selector) => !combinators.includes(selector[0])) + }; + } + + return { + match: true, + optimized, + selector: selector.reduce((acc, curr) => { + + let hasCompound = true; + + if (hasCompound && curr.length > 0) { + + hasCompound = !['&'].concat(combinators).includes(curr[0].charAt(0)); + } + + // @ts-ignore + if (hasCompound && curr[0] == ' ') { + + hasCompound = false; + curr.unshift('&'); + } + + if (curr.length == 0) { + curr.push('&'); + hasCompound = false; + } + + if (reducible) { + const chr = curr[0].charAt(0); + // @ts-ignore + reducible = chr == '.' || chr == ':' || isIdentStart(chr.codePointAt(0)); + } + + acc.push(hasCompound ? ['&'].concat(curr) : curr); + return acc; + }, []), + reducible: selector.every((selector) => !['>', '+', '~', '&'].includes(selector[0])) + }; +} +function hasOnlyDeclarations(node: AstRule): boolean { + let k: number = node.chi.length; + + while (k--) { + + if (node.chi[k].typ == 'Comment') { + + continue; + } + + return node.chi[k].typ == 'Declaration'; + } + + return true; } export function hasDeclaration(node: AstRule): boolean { @@ -376,71 +847,37 @@ export function hasDeclaration(node: AstRule): boolean { return true; } -export function deduplicateRule(ast: AstRule | AstAtRule): AstRule | AstAtRule { +export function minifyRule(ast: AstRule | AstAtRule): AstRule | AstAtRule { + // @ts-ignore if (!('chi' in ast) || ast.chi?.length <= 1) { + return ast; } + // @ts-ignore - const j = ast.chi.length; - let k = 0; - let map = new Map; + const j: number = ast.chi.length; + let k: number = 0; + let properties: PropertyList = new PropertyList(); + // @ts-ignore for (; k < j; k++) { + // @ts-ignore const node = ast.chi[k]; - if (node.typ == 'Comment') { - // @ts-ignore - map.set(node, node); - continue; - } else if (node.typ != 'Declaration') { - break; - } - if ((node).nam in configuration.map || - (node).nam in configuration.properties) { - // @ts-ignore - const shorthand = node.nam in configuration.map ? configuration.map[node.nam].shorthand : configuration.properties[node.nam].shorthand; - if (!map.has(shorthand)) { - map.set(shorthand, new PropertyList()); - } - map.get(shorthand).add(node); - } else { - map.set((node).nam, node); - } - } - - const children = []; - - for (let child of map.values()) { - if (child instanceof PropertyList) { - // @ts-ignore - children.push(...child); - } else { - // @ts-ignore - children.push(child); - } - } - // @ts-ignore - ast.chi = children.concat(ast.chi?.slice(k)); - return ast; -} - -function reduceRawTokens(raw: string[][]) { - return raw.reduce((acc, curr) => { - acc.push(curr.join('')); - return acc; - }, []).join(','); -} + if (node.typ == 'Comment' || node.typ == 'Declaration') { -function trimRawToken(raw: string[]) { - while (raw.length > 0) { - if (raw[0] == ' ') { - raw.shift(); + properties.add(node); continue; } + break; } + + // @ts-ignore + ast.chi = [...properties].concat(ast.chi.slice(k)); + return ast; } function splitRule(buffer: string): string[][] { @@ -589,445 +1026,4 @@ function reduceRuleSelector(node: AstRule) { } } // } -} - -function diff(n1: AstRule, n2: AstRule, options: ParserOptions = {}) { - let node1 = n1; - let node2 = n2; - let exchanged = false; - if (node1.chi.length > node2.chi.length) { - const t = node1; - node1 = node2; - node2 = t; - exchanged = true; - } - let i = node1.chi.length; - let j = node2.chi.length; - if (i == 0 || j == 0) { - // @ts-ignore - return null; - } - // @ts-ignore - const raw1 = node1.raw; - // @ts-ignore - // const optimized1 = node1.optimized; - // @ts-ignore - const raw2 = node2.raw; - // @ts-ignore - // const optimized2 = node2.optimized; - node1 = {...node1, chi: node1.chi.slice()}; - node2 = {...node2, chi: node2.chi.slice()}; - if (raw1 != null) { - Object.defineProperty(node1, 'raw', {enumerable: false, writable: true, value: raw1}); - } - // if (optimized1 != null) { - // Object.defineProperty(node1, 'optimized', {enumerable: false, writable: true, value: optimized1}); - // } - if (raw2 != null) { - Object.defineProperty(node2, 'raw', {enumerable: false, writable: true, value: raw2}); - } - // if (optimized2 != null) { - // Object.defineProperty(node2, 'optimized', {enumerable: false, writable: true, value: optimized2}); - // } - const intersect = []; - while (i--) { - if (node1.chi[i].typ == 'Comment') { - continue; - } - j = node2.chi.length; - if (j == 0) { - break; - } - while (j--) { - if (node2.chi[j].typ == 'Comment') { - continue; - } - if ((node1.chi[i]).nam == (node2.chi[j]).nam) { - if (eq(node1.chi[i], node2.chi[j])) { - intersect.push(node1.chi[i]); - node1.chi.splice(i, 1); - node2.chi.splice(j, 1); - break; - } - } - } - } - // @ts-ignore - const result = (intersect.length == 0 ? null : { - ...node1, - // @ts-ignore - sel: [...new Set([...(n1?.raw?.reduce(reducer, []) || splitRule(n1.sel)).concat(n2?.raw?.reduce(reducer, []) || splitRule(n2.sel))])].join(','), - chi: intersect.reverse() - }); - if (result == null || [n1, n2].reduce((acc, curr) => curr.chi.length == 0 ? acc : acc + render(curr, options).code.length, 0) <= [node1, node2, result].reduce((acc, curr) => curr.chi.length == 0 ? acc : acc + render(curr, options).code.length, 0)) { - // @ts-ignore - return null; - } - return {result, node1: exchanged ? node2 : node1, node2: exchanged ? node2 : node2}; -} - -function matchSelectors(selector1: string[][], selector2: string[][], parentType: NodeType): null | MatchedSelector { - - let match: string[][] = [[]]; - const j = Math.min( - selector1.reduce((acc, curr) => Math.min(acc, curr.length), selector1.length > 0 ? selector1[0].length : 0), - selector2.reduce((acc, curr) => Math.min(acc, curr.length), selector2.length > 0 ? selector2[0].length : 0) - ); - - let i: number = 0; - let k: number; - let l: number; - let token: string; - let matching: boolean = true; - let matchFunction: number = 0; - let inAttr: number = 0; - - for (; i < j; i++) { - - k = 0; - token = selector1[0][i]; - - for (; k < selector1.length; k++) { - - if (selector1[k][i] != token) { - - matching = false; - break; - } - } - - if (matching) { - - l = 0; - for (; l < selector2.length; l++) { - - if (selector2[l][i] != token) { - - matching = false; - break; - } - } - } - - if (!matching) { - - break; - } - - if (token == ',') { - - match.push([]); - } else { - - if (token.endsWith('(')) { - - matchFunction++; - } - - if (token.endsWith('[')) { - - inAttr++; - } else if (token == ')') { - - matchFunction--; - } else if (token == ']') { - - inAttr--; - } - - (match.at(-1)).push(token); - } - } - - // invalid function - if (matchFunction != 0 || inAttr != 0) { - - return null; - } - - if (parentType != 'Rule') { - - for (const part of match) { - - if (part.length > 0 && combinators.includes(part[0].charAt(0))) { - - return null; - } - } - } - if (match.length > 1) { - - console.error(`unsupported multilevel matching`); - console.error({match, selector1, selector2}); - return null; - } - - for (const part of match) { - - while (part.length > 0) { - - const token = part.at(-1); - - if (token == ' ' || combinators.includes(token) || notEndingWith.includes(token.at(-1))) { - - part.pop(); - continue; - } - - break; - } - } - - if (match.every(t => t.length == 0)) { - - return null; - } - - if (eq([['&']], match)) { - - return null; - } - - function reduce(acc: string[][], curr: string[]) { - - if (acc === null) { - - return null; - } - - let hasCompoundSelector = true; - - curr = curr.slice(match[0].length); - - while (curr.length > 0) { - - if (curr[0] == ' ') { - - hasCompoundSelector = false; - curr.unshift('&'); - continue; - } - - break; - } - - // invalid function match - if (curr.length > 0 && curr[0].endsWith('(') && curr.at(-1) != ')') { - - return null; - } - - if (curr.length == 1 && combinators.includes(curr[0].charAt(0))) { - - return null; - } - - if (hasCompoundSelector && curr.length > 0) { - - hasCompoundSelector = !['&'].concat(combinators).includes(curr[0].charAt(0)); - } - - if (curr[0] == ':is(') { - - let inFunction = 0; - let canReduce = true; - const isCompound = curr.reduce((acc, token, index: number) => { - - if (index == 0) { - - inFunction++; - canReduce = curr[1] == '&'; - } else if (token.endsWith('(')) { - - if (inFunction == 0) { - - canReduce = false; - } - - inFunction++; - } else if (token == ')') { - - inFunction--; - } else if (token == ',') { - - if (!canReduce) { - - canReduce = curr[index + 1] == '&'; - } - - acc.push([]); - } else acc.at(-1)?.push(token); - - return acc; - - }, [[]]); - - if (inFunction > 0) { - - canReduce = false; - } - - if (canReduce) { - - curr = isCompound.reduce((acc, curr) => { - - if (acc.length > 0) { - - acc.push(','); - } - - acc.push(...curr); - - return acc - }, []); - } - } - - // @todo: check hasCompoundSelector && curr[0] == '&' && curr[1] == ' ' - - acc.push(match.length == 0 ? ['&'] : (hasCompoundSelector && curr[0] != '&' && (curr.length == 0 || !combinators.includes(curr[0].charAt(0))) ? ['&'].concat(curr) : curr)) - - return acc; - } - - - // @ts-ignore - selector1 = selector1.reduce(reduce, []); - // @ts-ignore - selector2 = selector2.reduce(reduce, []); - - return selector1 == null || selector2 == null ? null : { - eq: eq(selector1, selector2), - match, - selector1, - selector2 - } -} - -export function reduceSelector(selector: string[][]) { - - if (selector.length == 0) { - return null; - } - - const optimized: string[] = []; - const k = selector.reduce((acc, curr) => acc == 0 ? curr.length : (curr.length == 0 ? acc : Math.min(acc, curr.length)), 0); - let i = 0; - let j; - let match; - for (; i < k; i++) { - const item = selector[0][i]; - match = true; - for (j = 1; j < selector.length; j++) { - if (item != selector[j][i]) { - match = false; - break; - } - } - if (!match) { - break; - } - - optimized.push(item); - } - - while (optimized.length > 0) { - - const last = optimized.at(-1); - - if ((last == ' ' || combinators.includes(last))) { - - optimized.pop(); - continue; - } - - break; - } - - selector.forEach((selector) => selector.splice(0, optimized.length)); - - // combinator - if (combinators.includes(optimized.at(-1))) { - const combinator = optimized.pop(); - selector.forEach(selector => selector.unshift(combinator)); - } - - let reducible = optimized.length == 1; - - if (optimized[0] == '&' && optimized[1] == ' ') { - - optimized.splice(0, 2); - } - - if (optimized.length == 0 || - (optimized[0].charAt(0) == '&' || - selector.length == 1)) { - return { - match: false, - optimized, - selector: selector.map(selector => selector[0] == '&' && selector[1] == ' ' ? selector.slice(2) : selector), - reducible: selector.length > 1 && selector.every((selector) => !combinators.includes(selector[0])) - }; - } - - return { - match: true, - optimized, - selector: selector.reduce((acc, curr) => { - - let hasCompound = true; - - if (hasCompound && curr.length > 0) { - - hasCompound = !['&'].concat(combinators).includes(curr[0].charAt(0)); - } - - // @ts-ignore - if (hasCompound && curr[0] == ' ') { - - hasCompound = false; - curr.unshift('&'); - } - - if (curr.length == 0) { - curr.push('&'); - hasCompound = false; - } - - if (reducible) { - const chr = curr[0].charAt(0); - // @ts-ignore - reducible = chr == '.' || chr == ':' || isIdentStart(chr.codePointAt(0)); - } - - acc.push(hasCompound ? ['&'].concat(curr) : curr); - return acc; - }, []), - reducible: selector.every((selector) => !['>', '+', '~', '&'].includes(selector[0])) - }; -} - -function reducer(acc: string[], curr: string[], index: number, array: string[][]) { - - // trim :is() - if (array.length == 1 && array[0][0] == ':is(' && array[0].at(-1) == ')') { - - curr = curr.slice(1, -1); - } - - if (curr[0] == '&') { - - if (curr[1] == ' ') { - - curr.splice(0, 2); - } - - else if (combinators.includes(curr[1])) { - - curr.splice(0, 1); - } - } - - acc.push(curr.join('')); - return acc; -} +} \ No newline at end of file diff --git a/src/lib/walker/walk.ts b/src/lib/ast/walk.ts similarity index 100% rename from src/lib/walker/walk.ts rename to src/lib/ast/walk.ts diff --git a/src/lib/index.js b/src/lib/index.js deleted file mode 100644 index 999b85f..0000000 --- a/src/lib/index.js +++ /dev/null @@ -1,4 +0,0 @@ -export * from './parser'; -export * from './renderer'; -export * from './walker'; -export * from './transform'; diff --git a/src/lib/index.ts b/src/lib/index.ts index cac2e96..fb7d60b 100644 --- a/src/lib/index.ts +++ b/src/lib/index.ts @@ -1,5 +1,5 @@ export * from './parser'; export * from './renderer'; -export * from './walker'; -export * from './transform'; \ No newline at end of file +export * from './transform'; +export * from './ast'; \ No newline at end of file diff --git a/src/lib/parser/declaration/index.js b/src/lib/parser/declaration/index.js deleted file mode 100644 index 4e2c6d2..0000000 --- a/src/lib/parser/declaration/index.js +++ /dev/null @@ -1,3 +0,0 @@ -export * from './list'; -export * from './set'; -export * from './map'; diff --git a/src/lib/parser/declaration/list.js b/src/lib/parser/declaration/list.js deleted file mode 100644 index c689ac5..0000000 --- a/src/lib/parser/declaration/list.js +++ /dev/null @@ -1,63 +0,0 @@ -import { PropertySet } from "./set"; -import { getConfig } from "../utils"; -import { PropertyMap } from "./map"; -const config = getConfig(); -export class PropertyList { - declarations; - constructor() { - this.declarations = new Map; - } - add(declaration) { - if (declaration.typ != 'Declaration') { - this.declarations.set(Number(Math.random().toString().slice(2)).toString(36), declaration); - return this; - } - const propertyName = declaration.nam; - if (propertyName in config.properties) { - // @ts-ignore - const shorthand = config.properties[propertyName].shorthand; - if (!this.declarations.has(shorthand)) { - // @ts-ignore - this.declarations.set(shorthand, new PropertySet(config.properties[shorthand])); - } - this.declarations.get(shorthand).add(declaration); - return this; - } - if (propertyName in config.map) { - // @ts-ignore - const shorthand = config.map[propertyName].shorthand; - if (!this.declarations.has(shorthand)) { - // @ts-ignore - this.declarations.set(shorthand, new PropertyMap(config.map[shorthand])); - } - this.declarations.get(shorthand).add(declaration); - return this; - } - this.declarations.set(propertyName, declaration); - return this; - } - [Symbol.iterator]() { - let iterator = this.declarations.values(); - const iterators = []; - return { - next() { - let value = iterator.next(); - while ((value.done && iterators.length > 0) || - value.value instanceof PropertySet || - value.value instanceof PropertyMap) { - if (value.value instanceof PropertySet || value.value instanceof PropertyMap) { - iterators.unshift(iterator); - // @ts-ignore - iterator = value.value[Symbol.iterator](); - value = iterator.next(); - } - if (value.done && iterators.length > 0) { - iterator = iterators.shift(); - value = iterator.next(); - } - } - return value; - } - }; - } -} diff --git a/src/lib/parser/declaration/list.ts b/src/lib/parser/declaration/list.ts index 158ecae..c3fb317 100644 --- a/src/lib/parser/declaration/list.ts +++ b/src/lib/parser/declaration/list.ts @@ -1,7 +1,8 @@ -import {AstDeclaration, AstNode, ShorthandMapType, ShorthandPropertyType} from "../../../@types"; +import {AstDeclaration, AstNode, ShorthandMapType, ShorthandPropertyType, Token} from "../../../@types"; import {PropertySet} from "./set"; import {getConfig} from "../utils"; import {PropertyMap} from "./map"; +import {parseString} from "../parse"; const config = getConfig(); @@ -14,6 +15,11 @@ export class PropertyList { this.declarations = new Map; } + set(nam: string, value: string | Token[]) { + + return this.add({typ: 'Declaration', nam, val: Array.isArray(value) ? value : parseString(String(value))}); + } + add(declaration: AstNode) { if (declaration.typ != 'Declaration') { @@ -22,39 +28,70 @@ export class PropertyList { return this; } - const propertyName: string = (declaration).nam; + let propertyName: string = (declaration).nam; + let shortHandType: 'map' | 'set'; + let shorthand: string; if (propertyName in config.properties) { // @ts-ignore - const shorthand: string = config.properties[propertyName].shorthand; - - if (!this.declarations.has(shorthand)) { + if ('map' in (config.properties[propertyName])) { + shortHandType = 'map'; // @ts-ignore - this.declarations.set(shorthand, new PropertySet(config.properties[shorthand])); + shorthand = config.properties[propertyName].map; } - (this.declarations.get(shorthand)).add(declaration); - return this; + else { + + shortHandType = 'set'; + // @ts-ignore + shorthand = config.properties[propertyName].shorthand; + } } - if (propertyName in config.map) { + else if (propertyName in config.map) { + shortHandType = 'map'; // @ts-ignore - const shorthand: string = config.map[propertyName].shorthand; + shorthand = config.map[propertyName].shorthand; + } + // @ts-ignore + if (shortHandType == 'map') { + + // @ts-ignore if (!this.declarations.has(shorthand)) { // @ts-ignore this.declarations.set(shorthand, new PropertyMap(config.map[shorthand])); } + // @ts-ignore (this.declarations.get(shorthand)).add(declaration); - return this; + // return this; + } + + // @ts-ignore + else if (shortHandType == 'set') { + + // @ts-ignore + // const shorthand: string = config.properties[propertyName].shorthand; + + if (!this.declarations.has(shorthand)) { + + // @ts-ignore + this.declarations.set(shorthand, new PropertySet(config.properties[shorthand])); + } + + // @ts-ignore + (this.declarations.get(shorthand)).add(declaration); + // return this; + } else { + + this.declarations.set(propertyName, declaration); } - this.declarations.set(propertyName, declaration); return this; } diff --git a/src/lib/parser/declaration/map.js b/src/lib/parser/declaration/map.js deleted file mode 100644 index 358ebd5..0000000 --- a/src/lib/parser/declaration/map.js +++ /dev/null @@ -1,299 +0,0 @@ -import { eq } from "../utils/eq"; -import { isNumber } from "../utils"; -import { renderToken } from "../../renderer"; -import { matchType } from "../utils/type"; -function getTokenType(val) { - if (val == 'transparent' || val == 'currentcolor') { - return { - typ: 'Color', - val, - kin: 'lit' - }; - } - if (val.endsWith('%')) { - return { - typ: 'Perc', - val: val.slice(0, -1) - }; - } - return { - typ: isNumber(val) ? 'Number' : 'Iden', - val - }; -} -function parseString(val) { - return val.split(/\s/).map(getTokenType).reduce((acc, curr) => { - if (acc.length > 0) { - acc.push({ typ: 'Whitespace' }); - } - acc.push(curr); - return acc; - }, []); -} -export class PropertyMap { - config; - declarations; - requiredCount; - pattern; - constructor(config) { - const values = Object.values(config.properties); - this.requiredCount = values.reduce((acc, curr) => curr.required ? ++acc : acc, 0) || values.length; - this.config = config; - this.declarations = new Map; - this.pattern = config.pattern.split(/\s/); - } - add(declaration) { - if (declaration.nam == this.config.shorthand) { - this.declarations.clear(); - this.declarations.set(declaration.nam, declaration); - } - else { - const separator = this.config.separator; - // expand shorthand - if (declaration.nam != this.config.shorthand && this.declarations.has(this.config.shorthand)) { - const tokens = {}; - const values = []; - // @ts-ignore - this.declarations.get(this.config.shorthand).val.slice().reduce((acc, curr) => { - if (separator != null && separator.typ == curr.typ && eq(separator, curr)) { - acc.push([]); - return acc; - } - // else { - // @ts-ignore - acc.at(-1).push(curr); - // } - return acc; - }, [[]]). - // @ts-ignore - reduce((acc, list, current) => { - values.push(...this.pattern.reduce((acc, property) => { - // let current: number = 0; - const props = this.config.properties[property]; - for (let i = 0; i < acc.length; i++) { - if (acc[i].typ == 'Comment' || acc[i].typ == 'Whitespace') { - acc.splice(i, 1); - i--; - continue; - } - if (matchType(acc[i], props)) { - if ('prefix' in props && props.previous != null && !(props.previous in tokens)) { - return acc; - } - if (!(property in tokens)) { - tokens[property] = [[acc[i]]]; - } - else { - if (current == tokens[property].length) { - tokens[property].push([acc[i]]); - // tokens[property][current].push(); - } - else { - tokens[property][current].push({ typ: 'Whitespace' }, acc[i]); - } - } - acc.splice(i, 1); - i--; - // @ts-ignore - if ('prefix' in props && acc[i]?.typ == props.prefix.typ) { - // @ts-ignore - if (eq(acc[i], this.config.properties[property].prefix)) { - acc.splice(i, 1); - i--; - } - } - if (props.multiple) { - continue; - } - return acc; - } - else { - if (property in tokens && tokens[property].length > current) { - return acc; - } - } - } - if (property in tokens && tokens[property].length > current) { - return acc; - } - // default - if (props.default.length > 0) { - const defaults = parseString(props.default[0]); - if (!(property in tokens)) { - tokens[property] = [ - [...defaults - ] - ]; - } - else { - if (current == tokens[property].length) { - tokens[property].push([]); - tokens[property][current].push(...defaults); - } - else { - tokens[property][current].push({ typ: 'Whitespace' }, ...defaults); - } - } - } - return acc; - }, list)); - return values; - }, []); - if (values.length == 0) { - this.declarations = Object.entries(tokens).reduce((acc, curr) => { - acc.set(curr[0], { - typ: 'Declaration', - nam: curr[0], - val: curr[1].reduce((acc, curr) => { - if (acc.length > 0) { - acc.push({ ...separator }); - } - acc.push(...curr); - return acc; - }, []) - }); - return acc; - }, new Map); - } - } - this.declarations.set(declaration.nam, declaration); - } - return this; - } - [Symbol.iterator]() { - let requiredCount = Object.keys(this.config.properties).reduce((acc, curr) => this.declarations.has(curr) && this.config.properties[curr].required ? ++acc : acc, 0); - if (requiredCount == 0) { - requiredCount = this.declarations.size; - } - if (requiredCount < this.requiredCount) { - // if (this.declarations.size == 1 && this.declarations.has(this.config.shorthand)) { - // - // this.declarations - // } - return this.declarations.values(); - } - let count = 0; - const separator = this.config.separator; - const tokens = {}; - // @ts-ignore - const valid = Object.entries(this.config.properties).reduce((acc, curr) => { - if (!this.declarations.has(curr[0])) { - if (curr[1].required) { - acc.push(curr[0]); - } - return acc; - } - let current = 0; - const props = this.config.properties[curr[0]]; - // @ts-ignore - for (const val of this.declarations.get(curr[0]).val) { - if (separator != null && separator.typ == val.typ && eq(separator, val)) { - current++; - if (tokens[curr[0]].length == current) { - tokens[curr[0]].push([]); - } - continue; - } - if (val.typ == 'Whitespace' || val.typ == 'Comment') { - continue; - } - if (props.multiple && props.separator != null && props.separator.typ == val.typ && eq(val, props.separator)) { - continue; - } - if (matchType(val, curr[1])) { - if (!(curr[0] in tokens)) { - tokens[curr[0]] = [[]]; - } - // is default value - tokens[curr[0]][current].push(val); - continue; - } - acc.push(curr[0]); - break; - } - if (count == 0) { - count = current; - } - return acc; - }, []); - if (valid.length > 0 || Object.values(tokens).every(v => v.every(v => v.length == count))) { - return this.declarations.values(); - } - const values = Object.entries(tokens).reduce((acc, curr) => { - const props = this.config.properties[curr[0]]; - for (let i = 0; i < curr[1].length; i++) { - if (acc.length == i) { - acc.push([]); - } - let values = curr[1][i].reduce((acc, curr) => { - if (acc.length > 0) { - acc.push({ typ: 'Whitespace' }); - } - acc.push(curr); - return acc; - }, []); - if (props.default.includes(curr[1][i].reduce((acc, curr) => acc + renderToken(curr) + ' ', '').trimEnd())) { - continue; - } - values = values.filter((val) => { - if (val.typ == 'Whitespace' || val.typ == 'Comment') { - return false; - } - return !(val.typ == 'Iden' && props.default.includes(val.val)); - }); - if (values.length > 0) { - if ('mapping' in props) { - // @ts-ignore - if (!('constraints' in props) || !('max' in props.constraints) || values.length <= props.constraints.mapping.max) { - let i = values.length; - while (i--) { - // @ts-ignore - if (values[i].typ == 'Iden' && values[i].val in props.mapping) { - // @ts-ignore - values.splice(i, 1, ...parseString(props.mapping[values[i].val])); - } - } - } - } - if ('prefix' in props) { - // @ts-ignore - acc[i].push({ ...props.prefix }); - } - else if (acc[i].length > 0) { - acc[i].push({ typ: 'Whitespace' }); - } - acc[i].push(...values.reduce((acc, curr) => { - if (acc.length > 0) { - // @ts-ignore - acc.push({ ...(props.separator ?? { typ: 'Whitespace' }) }); - } - // @ts-ignore - acc.push(curr); - return acc; - }, [])); - } - } - return acc; - }, []).reduce((acc, curr) => { - if (acc.length > 0) { - acc.push({ ...separator }); - } - if (curr.length == 0) { - curr.push(...this.config.default[0].split(/\s/).map(getTokenType).reduce((acc, curr) => { - if (acc.length > 0) { - acc.push({ typ: 'Whitespace' }); - } - acc.push(curr); - return acc; - }, [])); - } - acc.push(...curr); - return acc; - }, []); - return [{ - typ: 'Declaration', - nam: this.config.shorthand, - val: values - }][Symbol.iterator](); - } -} diff --git a/src/lib/parser/declaration/map.ts b/src/lib/parser/declaration/map.ts index eed7ff3..a5da198 100644 --- a/src/lib/parser/declaration/map.ts +++ b/src/lib/parser/declaration/map.ts @@ -1,61 +1,22 @@ import { AstDeclaration, - ColorToken, - IdentToken, - NumberToken, - PercentageToken, - PropertyMapType, + PropertyMapType, ShorthandPropertyType, Token } from "../../../@types"; import {ShorthandMapType} from "../../../@types"; import {eq} from "../utils/eq"; -import {isNumber} from "../utils"; +import {getConfig} from "../utils"; import {renderToken} from "../../renderer"; import {matchType} from "../utils/type"; +import {parseString} from "../parse"; +import {PropertySet} from "./set"; -function getTokenType(val: string): Token { - - if (val == 'transparent' || val == 'currentcolor') { - - return { - - typ: 'Color', - val, - kin: 'lit' - } - } - - if (val.endsWith('%')) { - return { - typ: 'Perc', - val: val.slice(0, -1) - } - } - - return { - typ: isNumber(val) ? 'Number' : 'Iden', - val - }; -} - -function parseString(val: string) { - return val.split(/\s/).map(getTokenType).reduce((acc: Token[], curr: Token) => { - - if (acc.length > 0) { - - acc.push({typ: 'Whitespace'}); - } - - acc.push(curr); - return acc; - - }, []); -} +const propertiesConfig = getConfig(); export class PropertyMap { protected config: ShorthandMapType; - protected declarations: Map; + protected declarations: Map; protected requiredCount: any; protected pattern: string[]; @@ -72,7 +33,7 @@ export class PropertyMap { if (declaration.nam == this.config.shorthand) { - this.declarations.clear(); + this.declarations = new Map; this.declarations.set(declaration.nam, declaration); } else { @@ -101,110 +62,109 @@ export class PropertyMap { return acc; }, [[]]). // @ts-ignore - reduce((acc: Token[][], list: Token[], current: number) => { + reduce((acc: Token[][], list: Token[], current: number) => { - values.push(...this.pattern.reduce((acc: Token[], property: string) => { + values.push(...this.pattern.reduce((acc: Token[], property: string) => { - // let current: number = 0; - const props: PropertyMapType = this.config.properties[property]; + // let current: number = 0; + const props: PropertyMapType = this.config.properties[property]; - for (let i = 0; i < acc.length; i++) { + for (let i = 0; i < acc.length; i++) { - if (acc[i].typ == 'Comment' || acc[i].typ == 'Whitespace') { + if (acc[i].typ == 'Comment' || acc[i].typ == 'Whitespace') { - acc.splice(i, 1); - i--; + acc.splice(i, 1); + i--; - continue; - } + continue; + } - if (matchType(acc[i], props)) { + if (matchType(acc[i], props)) { - if ('prefix' in props && props.previous != null && !(props.previous in tokens)) { + if ('prefix' in props && props.previous != null && !(props.previous in tokens)) { - return acc; - } + return acc; + } - if (!(property in tokens)) { + if (!(property in tokens)) { - tokens[property] = [[acc[i]]]; + tokens[property] = [[acc[i]]]; - } else { + } else { - if (current == tokens[property].length) { + if (current == tokens[property].length) { - tokens[property].push([acc[i]]); - // tokens[property][current].push(); - } else { + tokens[property].push([acc[i]]); + // tokens[property][current].push(); + } else { - tokens[property][current].push({typ: 'Whitespace'}, acc[i]); + tokens[property][current].push({typ: 'Whitespace'}, acc[i]); + } } - } - - acc.splice(i, 1); - i--; - // @ts-ignore - if ('prefix' in props && acc[i]?.typ == props.prefix.typ) { + acc.splice(i, 1); + i--; // @ts-ignore - if (eq(acc[i], this.config.properties[property].prefix)) { + if ('prefix' in props && acc[i]?.typ == props.prefix.typ) { - acc.splice(i, 1); - i--; - } - } + // @ts-ignore + if (eq(acc[i], this.config.properties[property].prefix)) { - if ((props).multiple) { + acc.splice(i, 1); + i--; + } + } - continue; - } + if ((props).multiple) { - return acc; - } else { + continue; + } - if (property in tokens && tokens[property].length > current) { return acc; + } else { + + if (property in tokens && tokens[property].length > current) { + return acc; + } } } - } - if (property in tokens && tokens[property].length > current) { + if (property in tokens && tokens[property].length > current) { - return acc; - } + return acc; + } - // default - if (props.default.length > 0) { + // default + if (props.default.length > 0) { - const defaults = parseString(props.default[0]); + const defaults = parseString(props.default[0]); - if (!(property in tokens)) { + if (!(property in tokens)) { - tokens[property] = [ - [...defaults - ] - ]; + tokens[property] = [ + [...defaults] + ]; - } else { + } else { - if (current == tokens[property].length) { + if (current == tokens[property].length) { - tokens[property].push([]); - tokens[property][current].push(...defaults); - } else { + tokens[property].push([]); + tokens[property][current].push(...defaults); + } else { - tokens[property][current].push({typ: 'Whitespace'}, ...defaults); + tokens[property][current].push({typ: 'Whitespace'}, ...defaults); + } } } - } - return acc; + return acc; - }, list)); + }, list)); - return values; - }, []); + return values; + }, []); if (values.length == 0) { @@ -224,232 +184,348 @@ export class PropertyMap { acc.push(...curr); return acc; }, []) - }) + }); return acc; }, new Map); } } - this.declarations.set(declaration.nam, declaration); + // @ts-ignore + const config: ShorthandPropertyType = propertiesConfig.properties[declaration.nam]; + + let property = declaration.nam; + + if (config != null) { + + property = config.shorthand; + + let value = this.declarations.get(property); + + if (!(value instanceof PropertySet)) { + + // @ts-ignore + this.declarations.set(property, new PropertySet(propertiesConfig.properties[config.shorthand])); + + // Token[] + if (value != null) { + + // @ts-ignore + (this.declarations.get(property)).add(value); + } + } + + (this.declarations.get(property)).add(declaration); + } else { + + this.declarations.set(declaration.nam, declaration); + } } return this; } - [Symbol.iterator](): IterableIterator { + [Symbol.iterator]() { + + let iterable: IterableIterator; + let requiredCount: number = 0; + let property: string; + let isShorthand: boolean = true; + + for (property of Object.keys(this.config.properties)) { + + if (this.config.properties[property].required) { + + if (!this.declarations.has(property)) { - let requiredCount = Object.keys(this.config.properties).reduce((acc: number, curr: string) => this.declarations.has(curr) && this.config.properties[curr].required ? ++acc : acc, 0); + isShorthand = false; + break; + } else { + + const val = this.declarations.get(property); + + if (val instanceof PropertySet && !val.isShortHand()) { + + isShorthand = false; + break; + } else { + + requiredCount++; + } + } + } + } if (requiredCount == 0) { requiredCount = this.declarations.size; } - if (requiredCount < this.requiredCount) { + if (!isShorthand || requiredCount < this.requiredCount) { - // if (this.declarations.size == 1 && this.declarations.has(this.config.shorthand)) { - // - // this.declarations - // } + // @ts-ignore + iterable = this.declarations.values(); + } else { - return this.declarations.values(); - } + let count = 0; + const separator = this.config.separator; + const tokens = <{ [key: string]: Token[][] }>{}; - let count = 0; - const separator = this.config.separator; - const tokens = <{ [key: string]: Token[][] }>{}; + // @ts-ignore + /* const valid: string[] =*/ Object.entries(this.config.properties).reduce((acc, curr) => { - // @ts-ignore - const valid: string[] = Object.entries(this.config.properties).reduce((acc, curr) => { + if (!this.declarations.has(curr[0])) { - if (!this.declarations.has(curr[0])) { + if (curr[1].required) { - if (curr[1].required) { + acc.push(curr[0]); + } - acc.push(curr[0]); + return acc; } - return acc; - } + let current = 0; - let current = 0; + const props = this.config.properties[curr[0]]; + const declaration = this.declarations.get(curr[0]); - const props = this.config.properties[curr[0]]; + // @ts-ignore + for (const val of (declaration instanceof PropertySet ? [...declaration][0] : declaration).val) { - // @ts-ignore - for (const val of this.declarations.get(curr[0]).val) { + if (separator != null && separator.typ == val.typ && eq(separator, val)) { - if (separator != null && separator.typ == val.typ && eq(separator, val)) { + current++; - current++; + if (tokens[curr[0]].length == current) { - if (tokens[curr[0]].length == current) { + tokens[curr[0]].push([]); + } - tokens[curr[0]].push([]); + continue; } - continue; - } + if (val.typ == 'Whitespace' || val.typ == 'Comment') { + + 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(val, props.separator)) { + if (matchType(val, curr[1])) { - continue; - } + if (!(curr[0] in tokens)) { + + tokens[curr[0]] = [[]]; + } - if (matchType(val, curr[1])) { + // is default value - if (!(curr[0] in tokens)) { + tokens[curr[0]][current].push(val); + // continue; + } else { - tokens[curr[0]] = [[]]; + acc.push(curr[0]); + break; } + } - // is default value + if (count == 0) { - tokens[curr[0]][current].push(val); - continue; + count = current; } - acc.push(curr[0]); - break; - } + return acc; + }, []); - if (count == 0) { + count++; - count = current; - } + if (!Object.values(tokens).every(v => v.length == count)) { - return acc; - }, []); + // @ts-ignore + iterable = this.declarations.values(); + } else { - if (valid.length > 0 || Object.values(tokens).every(v => v.every(v => v.length == count))) { + const values: Token[] = Object.entries(tokens).reduce((acc, curr) => { - return this.declarations.values(); - } + const props = this.config.properties[curr[0]]; - const values: Token[] = Object.entries(tokens).reduce((acc, curr) => { + for (let i = 0; i < curr[1].length; i++) { - const props = this.config.properties[curr[0]]; + if (acc.length == i) { - for (let i = 0; i < curr[1].length; i++) { + acc.push([]); + } - if (acc.length == i) { + let values: Token[] = curr[1][i].reduce((acc, curr) => { - acc.push([]); - } + if (acc.length > 0) { - let values: Token[] = curr[1][i].reduce((acc, curr) => { + acc.push({typ: 'Whitespace'}) + } - if (acc.length > 0) { + acc.push(curr); - acc.push({typ: 'Whitespace'}) - } + return acc; - acc.push(curr); + }, []); - return acc; + // @todo remove renderToken call + if (props.default.includes(curr[1][i].reduce((acc, curr) => acc + renderToken(curr) + ' ', '').trimEnd())) { - }, []); + continue; + } - if (props.default.includes(curr[1][i].reduce((acc, curr) => acc + renderToken(curr) + ' ', '').trimEnd())) { + let doFilterDefault: boolean = true; - continue; - } + if (curr[0] in propertiesConfig.properties) { - values = values.filter((val: Token) => { + for (let v of values) { - if (val.typ == 'Whitespace' || val.typ == 'Comment') { + if (!['Whitespace', 'Comment', 'Iden'].includes(v.typ) + || (v.typ == 'Iden' && !this.config.properties[curr[0]].default.includes(v.val))) { - return false; - } + doFilterDefault = false; + break; + } + } + } - return !(val.typ == 'Iden' && props.default.includes(val.val)); - }); + // remove default values + values = values.filter((val: Token) => { - if (values.length > 0) { + if (val.typ == 'Whitespace' || val.typ == 'Comment') { - if ('mapping' in props) { + return false; + } - // @ts-ignore - if (!('constraints' in props) || !('max' in props.constraints) || values.length <= props.constraints.mapping.max) { + return !doFilterDefault || !(val.typ == 'Iden' && props.default.includes(val.val)); + }); + + if (values.length > 0) { - let i = values.length; - while (i--) { + if ('mapping' in props) { // @ts-ignore - if (values[i].typ == 'Iden' && values[i].val in props.mapping) { + if (!('constraints' in props) || !('max' in props.constraints) || values.length <= props.constraints.mapping.max) { - // @ts-ignore - values.splice(i, 1, ...parseString(props.mapping[values[i].val])); + let i = values.length; + while (i--) { + + // @ts-ignore + if (values[i].typ == 'Iden' && values[i].val in props.mapping) { + + // @ts-ignore + values.splice(i, 1, ...parseString(props.mapping[values[i].val])); + } + } } + } - } - } + if ('prefix' in props) { - if ('prefix' in props) { + // @ts-ignore + acc[i].push({...props.prefix}); + } else if (acc[i].length > 0) { - // @ts-ignore - acc[i].push({...props.prefix}); + acc[i].push({typ: 'Whitespace'}); + } + + acc[i].push(...values.reduce((acc, curr) => { + + if (acc.length > 0) { + + // @ts-ignore + acc.push({...(props.separator ?? {typ: 'Whitespace'})}); + } + + // @ts-ignore + acc.push(curr); + return acc; + }, [])) + } } - else if (acc[i].length > 0) { + return acc; + }, []).reduce((acc, curr) => { + + if (acc.length > 0) { - acc[i].push({typ: 'Whitespace'}); + acc.push({...separator}); } - acc[i].push(...values.reduce((acc, curr) => { + if (curr.length == 0 && this.config.default.length > 0) { - if (acc.length > 0) { + curr.push(...parseString(this.config.default[0]).reduce((acc: Token[], curr: Token) => { - // @ts-ignore - acc.push({...(props.separator ?? {typ: 'Whitespace'})}); - } + if (acc.length > 0) { - // @ts-ignore - acc.push(curr); - return acc; - }, [])) - } - } + acc.push({typ: 'Whitespace'}); + } - return acc; - }, []).reduce((acc, curr) => { + acc.push(curr); + return acc; - if (acc.length > 0) { + }, [])) + } + + acc.push(...curr); + return acc; - acc.push({...separator}); + }, []); + + iterable = [{ + typ: 'Declaration', + nam: this.config.shorthand, + val: values + }][Symbol.iterator](); } + } - if (curr.length == 0) { + const iterators = []>[]; - curr.push(...this.config.default[0].split(/\s/).map(getTokenType).reduce((acc: Token[], curr: Token) => { + return { - if (acc.length > 0) { + // @ts-ignore + next() { + + let v = iterable.next(); + + while (v.done || v.value instanceof PropertySet) { + + if (v.value instanceof PropertySet) { + + // @ts-ignore + iterators.push(iterable); + + iterable = (v.value)[Symbol.iterator](); - acc.push({typ: 'Whitespace'}); + v = iterable.next(); } - acc.push(curr); - return acc; + if (v.done) { - }, [])) - } + if (iterators.length > 0) { + + // @ts-ignore + iterable = iterators.pop(); - acc.push(...curr); - return acc; + v = iterable.next(); + } - }, []); + if (v.done && iterators.length == 0) { - return [{ - typ: 'Declaration', - nam: this.config.shorthand, - val: values - }][Symbol.iterator](); + break; + } + } + } + + return v; + + } + }; } } \ No newline at end of file diff --git a/src/lib/parser/declaration/set.js b/src/lib/parser/declaration/set.js deleted file mode 100644 index 33726e6..0000000 --- a/src/lib/parser/declaration/set.js +++ /dev/null @@ -1,176 +0,0 @@ -import { eq } from "../utils/eq"; -import { isLength } from "../utils"; -export class PropertySet { - config; - declarations; - constructor(config) { - this.config = config; - this.declarations = new Map; - } - add(declaration) { - if (declaration.nam == this.config.shorthand) { - this.declarations.clear(); - this.declarations.set(declaration.nam, declaration); - } - else { - // expand shorthand - if (declaration.nam != this.config.shorthand && this.declarations.has(this.config.shorthand)) { - let isValid = true; - let current = -1; - const tokens = []; - // @ts-ignore - for (let token of this.declarations.get(this.config.shorthand).val) { - if (this.config.types.includes(token.typ) || (token.typ == 'Number' && token.val == '0' && - (this.config.types.includes('Length') || - this.config.types.includes('Angle') || - this.config.types.includes('Dimension')))) { - if (tokens.length == 0) { - tokens.push([]); - current++; - } - tokens[current].push(token); - continue; - } - if (token.typ != 'Whitespace' && token.typ != 'Comment') { - if (token.typ == 'Iden' && this.config.keywords.includes(token.val)) { - tokens[current].push(token); - } - if (token.typ == 'Literal' && token.val == this.config.separator) { - tokens.push([]); - current++; - continue; - } - isValid = false; - break; - } - } - if (isValid && tokens.length > 0) { - this.declarations.delete(this.config.shorthand); - for (const values of tokens) { - this.config.properties.forEach((property, index) => { - // if (property == declaration.nam) { - // - // return; - // } - if (!this.declarations.has(property)) { - this.declarations.set(property, { - typ: 'Declaration', - nam: property, - val: [] - }); - } - while (index > 0 && index >= values.length) { - if (index > 1) { - index %= 2; - } - else { - index = 0; - break; - } - } - // @ts-ignore - const val = this.declarations.get(property).val; - if (val.length > 0) { - val.push({ typ: 'Whitespace' }); - } - val.push({ ...values[index] }); - }); - } - } - this.declarations.set(declaration.nam, declaration); - return this; - } - // declaration.chi = declaration.chi.reduce((acc: Token[], token: Token) => { - // - // if (this.config.types.includes(token.typ) || ('0' == (token).chi && ( - // this.config.types.includes('Length') || - // this.config.types.includes('Angle') || - // this.config.types.includes('Dimension'))) || (token.typ == 'Iden' && this.config.keywords.includes(token.chi))) { - // - // acc.push(token); - // } - // - // return acc; - // }, []); - this.declarations.set(declaration.nam, declaration); - } - return this; - } - [Symbol.iterator]() { - let iterator; - const declarations = this.declarations; - if (declarations.size < this.config.properties.length || this.config.properties.some((property, index) => { - return !declarations.has(property) || (index > 0 && - // @ts-ignore - declarations.get(property).val.length != declarations.get(this.config.properties[Math.floor(index / 2)]).val.length); - })) { - iterator = declarations.values(); - } - else { - const values = []; - this.config.properties.forEach((property) => { - let index = 0; - // @ts-ignore - for (const token of this.declarations.get(property).val) { - if (token.typ == 'Whitespace') { - continue; - } - if (values.length == index) { - values.push([]); - } - values[index].push(token); - index++; - } - }); - for (const value of values) { - let i = value.length; - while (i-- > 1) { - const t = value[i]; - const k = value[i == 1 ? 0 : i % 2]; - if (t.val == k.val && t.val == '0') { - if ((t.typ == 'Number' && isLength(k)) || - (k.typ == 'Number' && isLength(t)) || - (isLength(k) || isLength(t))) { - value.splice(i, 1); - continue; - } - } - if (eq(t, k)) { - value.splice(i, 1); - continue; - } - break; - } - } - iterator = [{ - typ: 'Declaration', - nam: this.config.shorthand, - val: values.reduce((acc, curr) => { - if (curr.length > 1) { - const k = curr.length * 2 - 1; - let i = 1; - while (i < k) { - curr.splice(i, 0, { typ: 'Whitespace' }); - i += 2; - } - } - if (acc.length > 0) { - acc.push({ typ: 'Literal', val: this.config.separator }); - } - acc.push(...curr); - return acc; - }, []) - }][Symbol.iterator](); - return { - next() { - return iterator.next(); - } - }; - } - return { - next() { - return iterator.next(); - } - }; - } -} diff --git a/src/lib/parser/declaration/set.ts b/src/lib/parser/declaration/set.ts index 0ce19e2..405769d 100644 --- a/src/lib/parser/declaration/set.ts +++ b/src/lib/parser/declaration/set.ts @@ -17,8 +17,7 @@ export class PropertySet { if (declaration.nam == this.config.shorthand) { - this.declarations.clear(); - this.declarations.set(declaration.nam, declaration); + this.declarations = new Map; } else { // expand shorthand @@ -51,6 +50,12 @@ export class PropertySet { if (token.typ == 'Iden' && this.config.keywords.includes(token.val)) { + if (tokens.length == 0) { + + tokens.push([]); + current++; + } + tokens[current].push(token); } @@ -66,7 +71,7 @@ export class PropertySet { } } - if (isValid && tokens.length > 0) { + if (isValid && tokens.length > 0) { this.declarations.delete(this.config.shorthand); @@ -74,11 +79,6 @@ export class PropertySet { this.config.properties.forEach((property: string, index: number) => { - // if (property == declaration.nam) { - // - // return; - // } - if (!this.declarations.has(property)) { this.declarations.set(property, { @@ -116,24 +116,20 @@ export class PropertySet { this.declarations.set(declaration.nam, declaration); return this; } + } - // declaration.chi = declaration.chi.reduce((acc: Token[], token: Token) => { - // - // if (this.config.types.includes(token.typ) || ('0' == (token).chi && ( - // this.config.types.includes('Length') || - // this.config.types.includes('Angle') || - // this.config.types.includes('Dimension'))) || (token.typ == 'Iden' && this.config.keywords.includes(token.chi))) { - // - // acc.push(token); - // } - // - // return acc; - // }, []); + this.declarations.set(declaration.nam, declaration); + return this; + } + + isShortHand() { + + if (this.declarations.has(this.config.shorthand)) { - this.declarations.set(declaration.nam, declaration); + return this.declarations.size == 1; } - return this; + return this.config.properties.length == this.declarations.size; } [Symbol.iterator]() { @@ -141,15 +137,11 @@ export class PropertySet { let iterator: IterableIterator; const declarations: Map = this.declarations; - if (declarations.size < this.config.properties.length || this.config.properties.some((property: string, index: number) => { - - return !declarations.has(property) || (index > 0 && - // @ts-ignore - declarations.get(property).val.length != declarations.get(this.config.properties[Math.floor(index / 2)]).val.length); - })) { + if (declarations.size < this.config.properties.length) { iterator = declarations.values(); - } else { + } + else { const values: Token[][] = []; this.config.properties.forEach((property: string) => { @@ -157,7 +149,7 @@ export class PropertySet { let index: number = 0; // @ts-ignore - for(const token of this.declarations.get(property).val) { + for (const token of this.declarations.get(property).val) { if (token.typ == 'Whitespace') { @@ -232,19 +224,21 @@ export class PropertySet { }, []) }][Symbol.iterator](); - return { - next() { - - return iterator.next(); - } - } + // return { + // next() { + // + // return iterator.next(); + // } + // } } - return { - next() { + return iterator; - return iterator.next(); - } - } + // return { + // next() { + // + // return iterator.next(); + // } + // } } } \ No newline at end of file diff --git a/src/lib/parser/deduplicate.js b/src/lib/parser/deduplicate.js deleted file mode 100644 index 9fc5004..0000000 --- a/src/lib/parser/deduplicate.js +++ /dev/null @@ -1,600 +0,0 @@ -import { getConfig, isIdentStart } from "./utils"; -import { PropertyList } from "./declaration"; -import { eq } from "./utils/eq"; -import { render } from "../renderer"; -const configuration = getConfig(); -const combinators = ['+', '>', '~']; -export function deduplicate(ast, options = {}, recursive = false) { - // @ts-ignore - if (('chi' in ast) && ast.chi?.length > 0) { - let i = 0; - let previous; - let node; - let nodeIndex; - // @ts-ignore - for (; i < ast.chi.length; i++) { - // @ts-ignore - if (ast.chi[i].typ == 'Comment') { - continue; - } - // @ts-ignore - node = ast.chi[i]; - // @ts-ignore - if (previous == node) { - // console.error('idem!'); - // @ts-ignore - ast.chi.splice(i, 1); - i--; - continue; - } - if (node.typ == 'AtRule' && node.nam == 'font-face') { - continue; - } - if (node.typ == 'AtRule' && node.val == 'all') { - // @ts-ignore - ast.chi?.splice(i, 1, ...node.chi); - i--; - continue; - } - // @ts-ignore - if (node.typ == 'Rule') { - reduceRuleSelector(node); - let wrapper; - // @ts-ignore - if (options.nestingRules) { - // if (node.sel == '.card>hr') { - // - // console.error({idem: previous == node, previous, node}); - // } - // @ts-ignore - if (previous != null) { - // @ts-ignore - if (node.optimized != null && - // @ts-ignore - previous.optimized != null && - // @ts-ignore - node.optimized.optimized.length > 0 && - // @ts-ignore - previous.optimized.optimized.length > 0 && - // @ts-ignore - node.optimized.optimized[0] == previous.optimized.optimized[0]) { - // @ts-ignore - if (hasOnlyDeclarations(previous)) { - // @ts-ignore - let pSel = reduceRawTokens(previous.optimized.selector); - if (pSel === '') { - // @ts-ignore - pSel = previous.optimized.optimized.slice(1).join('').trim(); - } - // @ts-ignore - let nSel = reduceRawTokens(node.optimized.selector); - if (nSel === '') { - // @ts-ignore - nSel = node.optimized.optimized.slice(1).join('').trim(); - } - // @ts-ignore - wrapper = { ...node, chi: [], sel: node.optimized.optimized[0] }; - // @ts-ignore - Object.defineProperty(wrapper, 'raw', { - enumerable: false, - writable: true, - // @ts-ignore - value: [[node.optimized.optimized[0].slice()]] - }); - // @ts-ignore - previous.sel = pSel === '' ? '&' : pSel; // reduceRawTokens(previous.optimized.selector); - // @ts-ignore - previous.raw = pSel === '' ? [['&']] : previous.optimized.selector.slice(); - // @ts-ignore - node.sel = nSel === '' ? '&' : nSel; // reduceRawTokens(node.optimized.selector); - // @ts-ignore - node.raw = nSel === '' ? [['&']] : node.optimized.selector.slice(); - // @ts-ignore - wrapper.chi.push(previous, node); - // @ts-ignore - ast.chi.splice(i, 1, wrapper); - // @ts-ignore - ast.chi.splice(nodeIndex, 1); - } - } - } - // @ts-ignore - if (wrapper != null) { - // @ts-ignore - while (i < ast.chi.length) { - // @ts-ignore - const nextNode = ast.chi[i]; - // @ts-ignore - if (nextNode.typ != 'Rule' || nextNode.raw == null) { - break; - } - reduceRuleSelector(nextNode); - // @ts-ignore - if (nextNode.raw.length != 1 || !eq(wrapper.raw[0], nextNode.raw[0].slice(0, wrapper.raw[0].length))) { - break; - } - // @ts-ignore - nextNode.raw[0].splice(0, wrapper.raw[0].length); - // @ts-ignore - trimRawToken(nextNode.raw[0]); - // @ts-ignore - if (nextNode.raw[0].length == 0 || - // @ts-ignore - (nextNode.raw.length == 1 && nextNode.raw[0] == '&')) { - if (hasOnlyDeclarations(wrapper)) { - // @ts-ignore - wrapper.chi.push(...nextNode.chi); - // @ts-ignore - ast.chi.splice(i, 1); - continue; - } - else { - // @ts-ignore - nextNode.raw[0].push('&'); - } - } - // @ts-ignore - nextNode.sel = nextNode.raw.reduce((acc, curr) => { - acc.push(curr.join('')); - return acc; - }, []).join(','); - // @ts-ignore - wrapper.chi.push(nextNode); - // @ts-ignore - ast.chi.splice(i, 1); - node = wrapper; - } - deduplicate(wrapper, options, recursive); - nodeIndex = i; - // @ts-ignore - previous = ast.chi[i]; - continue; - } - // @ts-ignore - else if (node.optimized != null && - // @ts-ignore - node.optimized.match && - // @ts-ignore - node.optimized.selector.length > 1) { - // @ts-ignore - wrapper = { ...node, chi: [], sel: node.optimized.optimized[0] }; - // @ts-ignore - Object.defineProperty(wrapper, 'raw', { - enumerable: false, - writable: true, - // @ts-ignore - value: [[node.optimized.optimized[0]]] - }); - // @ts-ignore - node.sel = reduceRawTokens(node.optimized.selector); - // @ts-ignore - node.raw = node.optimized.selector.slice(); - // @ts-ignore - wrapper.chi.push(node); - // @ts-ignore - ast.chi.splice(i, 1, wrapper); - node = wrapper; - } - } - } - // @ts-ignore - if (previous != null && 'chi' in previous && ('chi' in node)) { - // @ts-ignore - if (previous.typ == node.typ) { - let shouldMerge = true; - // @ts-ignore - let k = previous.chi.length; - while (k-- > 0) { - // @ts-ignore - if (previous.chi[k].typ == 'Comment') { - continue; - } - // @ts-ignore - shouldMerge = previous.chi[k].typ == 'Declaration'; - break; - } - if (shouldMerge) { - // @ts-ignore - if ((node.typ == 'Rule' && node.sel == previous.sel) || - // @ts-ignore - (node.typ == 'AtRule') && node.val != 'font-face' && node.val == previous.val) { - // @ts-ignore - node.chi.unshift(...previous.chi); - // @ts-ignore - ast.chi.splice(nodeIndex, 1); - // @ts-ignore - if (hasDeclaration(node)) { - deduplicateRule(node); - } - else { - deduplicate(node, options, recursive); - } - i--; - previous = node; - nodeIndex = i; - continue; - } - else if (node.typ == 'Rule' && previous?.typ == 'Rule') { - const intersect = diff(previous, node, options); - if (intersect != null) { - if (intersect.node1.chi.length == 0) { - // @ts-ignore - ast.chi.splice(i--, 1); - // @ts-ignore - node = ast.chi[i]; - } - else { - // @ts-ignore - ast.chi.splice(i, 1, intersect.node1); - node = intersect.node1; - } - if (intersect.node2.chi.length == 0) { - // @ts-ignore - ast.chi.splice(nodeIndex, 1, intersect.result); - previous = intersect.result; - } - else { - // @ts-ignore - ast.chi.splice(nodeIndex, 1, intersect.result, intersect.node2); - previous = intersect.result; - // @ts-ignore - i = nodeIndex; - } - } - } - } - } - // @ts-ignore - if (recursive && previous != node) { - // @ts-ignore - if (hasDeclaration(previous)) { - deduplicateRule(previous); - } - else { - deduplicate(previous, options, recursive); - } - } - } - previous = node; - nodeIndex = i; - } - // @ts-ignore - if (recursive && node != null && ('chi' in node)) { - // @ts-ignore - if (node.chi.some(n => n.typ == 'Declaration')) { - deduplicateRule(node); - } - else { - // @ts-ignore - if (!(node.typ == 'AtRule' && node.nam != 'font-face')) { - deduplicate(node, options, recursive); - } - } - } - } - return ast; -} -function hasOnlyDeclarations(node) { - let k = node.chi.length; - while (k--) { - if (node.chi[k].typ == 'Comment') { - continue; - } - return node.chi[k].typ == 'Declaration'; - } - return true; -} -export function hasDeclaration(node) { - // @ts-ignore - for (let i = 0; i < node.chi?.length; i++) { - // @ts-ignore - if (node.chi[i].typ == 'Comment') { - continue; - } - // @ts-ignore - return node.chi[i].typ == 'Declaration'; - } - return true; -} -export function deduplicateRule(ast) { - // @ts-ignore - if (!('chi' in ast) || ast.chi?.length <= 1) { - return ast; - } - // @ts-ignore - const j = ast.chi.length; - let k = 0; - let map = new Map; - // @ts-ignore - for (; k < j; k++) { - // @ts-ignore - const node = ast.chi[k]; - if (node.typ == 'Comment') { - // @ts-ignore - map.set(node, node); - continue; - } - else if (node.typ != 'Declaration') { - break; - } - if (node.nam in configuration.map || - node.nam in configuration.properties) { - // @ts-ignore - const shorthand = node.nam in configuration.map ? configuration.map[node.nam].shorthand : configuration.properties[node.nam].shorthand; - if (!map.has(shorthand)) { - map.set(shorthand, new PropertyList()); - } - map.get(shorthand).add(node); - } - else { - map.set(node.nam, node); - } - } - const children = []; - for (let child of map.values()) { - if (child instanceof PropertyList) { - // @ts-ignore - children.push(...child); - } - else { - // @ts-ignore - children.push(child); - } - } - // @ts-ignore - ast.chi = children.concat(ast.chi?.slice(k)); - return ast; -} -function reduceRawTokens(raw) { - return raw.reduce((acc, curr) => { - acc.push(curr.join('')); - return acc; - }, []).join(','); -} -function trimRawToken(raw) { - while (raw.length > 0) { - if (raw[0] == ' ') { - raw.shift(); - continue; - } - break; - } -} -function splitRule(buffer) { - const result = []; - let str = ''; - for (let i = 0; i < buffer.length; i++) { - let chr = buffer.charAt(i); - if (chr == ',') { - if (str !== '') { - result.push(str); - str = ''; - } - continue; - } - str += chr; - if (chr == '\\') { - str += buffer.charAt(++i); - continue; - } - if (chr == '"' || chr == "'") { - let k = i; - while (++k < buffer.length) { - chr = buffer.charAt(k); - str += chr; - if (chr == '//') { - str += buffer.charAt(++k); - continue; - } - if (chr == buffer.charAt(i)) { - break; - } - } - continue; - } - if (chr == '(' || chr == '[') { - const open = chr; - const close = chr == '(' ? ')' : ']'; - let inParens = 1; - let k = i; - while (++k < buffer.length) { - chr = buffer.charAt(k); - if (chr == '\\') { - str += buffer.slice(k, k + 2); - k++; - continue; - } - str += chr; - if (chr == open) { - inParens++; - } - else if (chr == close) { - inParens--; - } - if (inParens == 0) { - break; - } - } - i = k; - } - } - if (str !== '') { - result.push(str); - } - return result; -} -function reduceRuleSelector(node) { - // @ts-ignore - if (node.raw != null) { - // @ts-ignore - let optimized = reduceSelector(node.raw.reduce((acc, curr) => { - acc.push(curr.slice()); - return acc; - }, [])); - if (optimized != null) { - Object.defineProperty(node, 'optimized', { enumerable: false, writable: true, value: optimized }); - } - if (optimized != null && optimized.match && optimized.reducible && optimized.selector.length > 1) { - const raw = [ - [ - optimized.optimized[0], ':is(' - ].concat(optimized.selector.reduce((acc, curr) => { - if (acc.length > 0) { - acc.push(','); - } - acc.push(...curr); - return acc; - }, [])).concat(')') - ]; - const sel = raw[0].join(''); - if (sel.length < node.sel.length) { - node.sel = sel; - // node.raw = raw; - Object.defineProperty(node, 'raw', { enumerable: false, writable: true, value: raw }); - } - } - } -} -function diff(n1, n2, options = {}) { - let node1 = n1; - let node2 = n2; - let exchanged = false; - if (node1.chi.length > node2.chi.length) { - const t = node1; - node1 = node2; - node2 = t; - exchanged = true; - } - let i = node1.chi.length; - let j = node2.chi.length; - if (i == 0 || j == 0) { - // @ts-ignore - return null; - } - // @ts-ignore - const raw1 = node1.raw; - // @ts-ignore - const optimized1 = node1.optimized; - // @ts-ignore - const raw2 = node2.raw; - // @ts-ignore - const optimized2 = node2.optimized; - node1 = { ...node1, chi: node1.chi.slice() }; - node2 = { ...node2, chi: node2.chi.slice() }; - if (raw1 != null) { - Object.defineProperty(node1, 'raw', { enumerable: false, writable: true, value: raw1 }); - } - if (optimized1 != null) { - Object.defineProperty(node1, 'optimized', { enumerable: false, writable: true, value: optimized1 }); - } - if (raw2 != null) { - Object.defineProperty(node2, 'raw', { enumerable: false, writable: true, value: raw2 }); - } - if (optimized2 != null) { - Object.defineProperty(node2, 'optimized', { enumerable: false, writable: true, value: optimized2 }); - } - const intersect = []; - while (i--) { - if (node1.chi[i].typ == 'Comment') { - continue; - } - j = node2.chi.length; - if (j == 0) { - break; - } - while (j--) { - if (node2.chi[j].typ == 'Comment') { - continue; - } - if (node1.chi[i].nam == node2.chi[j].nam) { - if (eq(node1.chi[i], node2.chi[j])) { - intersect.push(node1.chi[i]); - node1.chi.splice(i, 1); - node2.chi.splice(j, 1); - break; - } - } - } - } - // @ts-ignore - const result = (intersect.length == 0 ? null : { - ...node1, - // @ts-ignore - sel: [...new Set([...(n1?.raw?.reduce(reducer, []) || splitRule(n1.sel)).concat(n2?.raw?.reduce(reducer, []) || splitRule(n2.sel))])].join(), - chi: intersect.reverse() - }); - if (result == null || [n1, n2].reduce((acc, curr) => curr.chi.length == 0 ? acc : acc + render(curr, options).code.length, 0) <= [node1, node2, result].reduce((acc, curr) => curr.chi.length == 0 ? acc : acc + render(curr, options).code.length, 0)) { - // @ts-ignore - return null; - } - return { result, node1: exchanged ? node2 : node1, node2: exchanged ? node2 : node2 }; -} -export function reduceSelector(selector) { - if (selector.length == 0) { - return null; - } - const optimized = []; - const k = selector.reduce((acc, curr) => acc == 0 ? curr.length : (curr.length == 0 ? acc : Math.min(acc, curr.length)), 0); - let i = 0; - let j; - let match; - for (; i < k; i++) { - const item = selector[0][i]; - match = true; - for (j = 1; j < selector.length; j++) { - if (item != selector[j][i]) { - match = false; - break; - } - } - if (!match) { - break; - } - optimized.push(item); - } - selector.forEach((selector) => selector.splice(0, optimized.length)); - // combinator - if (combinators.includes(optimized.at(-1))) { - const combinator = optimized.pop(); - selector.forEach(selector => selector.unshift(combinator)); - } - if (optimized.at(-1) == ' ') { - optimized.pop(); - } - let reducible = optimized.length == 1; - if (optimized.length == 0 || - optimized[0].charAt(0) == '&' || - selector.length == 1) { - return { - match: false, - optimized, - selector, - reducible: selector.length > 1 && selector.every((selector) => !['>', '+', '~'].includes(selector[0])) - }; - } - return { - match: true, - optimized, - selector: selector.reduce((acc, curr) => { - // @ts-ignore - if (curr.length > 0 && curr[0] == ' ') { - curr.shift(); - } - if (curr.length == 0) { - curr.push('&'); - } - if (reducible) { - const chr = curr[0].charAt(0); - // @ts-ignore - reducible = chr == '.' || chr == ':' || isIdentStart(chr.codePointAt(0)); - } - acc.push(curr); - return acc; - }, []), - reducible: selector.every((selector) => !['>', '+', '~', '&'].includes(selector[0])) - }; -} -function reducer(acc, curr) { - acc.push(curr.join('')); - return acc; -} diff --git a/src/lib/parser/index.js b/src/lib/parser/index.js deleted file mode 100644 index c3b1ee5..0000000 --- a/src/lib/parser/index.js +++ /dev/null @@ -1,2 +0,0 @@ -export * from './parse'; -export * from './deduplicate'; diff --git a/src/lib/parser/index.ts b/src/lib/parser/index.ts index 3b8ec1e..8690c42 100644 --- a/src/lib/parser/index.ts +++ b/src/lib/parser/index.ts @@ -1,3 +1,4 @@ export * from './parse'; -export * from './deduplicate'; \ No newline at end of file +export * from './tokenize'; +export * from './utils'; \ No newline at end of file diff --git a/src/lib/parser/parse.js b/src/lib/parser/parse.js deleted file mode 100644 index 0835a17..0000000 --- a/src/lib/parser/parse.js +++ /dev/null @@ -1,1177 +0,0 @@ -import { isAtKeyword, isDigit, isDimension, isFunction, isHash, isHexColor, isIdent, isIdentStart, isNewLine, isNumber, isPercentage, isPseudo, isWhiteSpace, parseDimension } from "./utils"; -import { renderToken } from "../renderer"; -import { COLORS_NAMES } from "../renderer/utils"; -import { deduplicate } from "./deduplicate"; -const urlTokenMatcher = /^(["']?)[a-zA-Z0-9_/.-][a-zA-Z0-9_/:.#?-]+(\1)$/; -const funcLike = ['Start-parens', 'Func', 'UrlFunc', 'Pseudo-class-func']; -export async function parse(iterator, opt = {}) { - const errors = []; - const options = { - src: '', - sourcemap: false, - compress: false, - nestingRules: false, - resolveImport: false, - resolveUrls: false, - removeEmpty: true, - ...opt - }; - if (options.resolveImport) { - options.resolveUrls = true; - } - let ind = -1; - let lin = 1; - let col = 0; - const tokens = []; - const src = options.src; - const stack = []; - const ast = { - typ: "StyleSheet", - chi: [] - }; - const position = { - ind: Math.max(ind, 0), - lin: lin, - col: Math.max(col, 1) - }; - let value; - let buffer = ''; - let total = iterator.length; - let bytesIn = total; - let map = new Map; - let context = ast; - if (options.sourcemap) { - ast.loc = { - sta: { - ind: ind, - lin: lin, - col: col - }, - src: '' - }; - } - function getType(val) { - if (val === '') { - throw new Error('empty string?'); - } - if (val == ':') { - return { typ: 'Colon' }; - } - if (val == ')') { - return { typ: 'End-parens' }; - } - if (val == '(') { - return { typ: 'Start-parens' }; - } - if (val == '=') { - return { typ: 'Delim', val }; - } - if (val == ';') { - return { typ: 'Semi-colon' }; - } - if (val == ',') { - return { typ: 'Comma' }; - } - if (val == '<') { - return { typ: 'Lt' }; - } - if (val == '>') { - return { typ: 'Gt' }; - } - if (isPseudo(val)) { - return val.endsWith('(') ? { - typ: 'Pseudo-class-func', - val: val.slice(0, -1), - chi: [] - } - : { - typ: 'Pseudo-class', - val - }; - } - if (isAtKeyword(val)) { - return { - typ: 'At-rule', - val: val.slice(1) - }; - } - if (isFunction(val)) { - val = val.slice(0, -1); - return { - typ: val == 'url' ? 'UrlFunc' : 'Func', - val, - chi: [] - }; - } - if (isNumber(val)) { - return { - typ: 'Number', - val - }; - } - if (isDimension(val)) { - return parseDimension(val); - } - if (isPercentage(val)) { - return { - typ: 'Perc', - val: val.slice(0, -1) - }; - } - if (val == 'currentColor') { - return { - typ: 'Color', - val, - kin: 'lit' - }; - } - if (isIdent(val)) { - return { - typ: 'Iden', - val - }; - } - if (val.charAt(0) == '#' && isHash(val)) { - return { - typ: 'Hash', - val - }; - } - if ('"\''.includes(val.charAt(0))) { - return { - typ: 'Unclosed-string', - val - }; - } - return { - typ: 'Literal', - val - }; - } - // consume and throw away - function consume(open, close) { - let count = 1; - let chr; - while (true) { - chr = next(); - if (chr == '\\') { - if (peek() === '') { - break; - } - continue; - } - else if (chr == '/' && peek() == '*') { - next(); - while (true) { - chr = next(); - if (chr === '') { - break; - } - if (chr == '*' && peek() == '/') { - next(); - break; - } - } - } - else if (chr == close) { - count--; - } - else if (chr == open) { - count++; - } - if (chr === '' || count == 0) { - break; - } - } - } - async function parseNode(tokens) { - let i; - let loc; - for (i = 0; i < tokens.length; i++) { - if (tokens[i].typ == 'Comment') { - // @ts-ignore - context.chi.push(tokens[i]); - const position = map.get(tokens[i]); - loc = { - sta: position, - src - }; - if (options.sourcemap) { - tokens[i].loc = loc; - } - } - else if (tokens[i].typ != 'Whitespace') { - break; - } - } - tokens = tokens.slice(i); - if (tokens.length == 0) { - return null; - } - let delim = tokens.at(-1); - if (delim.typ == 'Semi-colon' || delim.typ == 'Block-start' || delim.typ == 'Block-end') { - tokens.pop(); - } - else { - delim = { typ: 'Semi-colon' }; - } - // @ts-ignore - while (['Whitespace', 'Bad-string', 'Bad-comment'].includes(tokens.at(-1)?.typ)) { - tokens.pop(); - } - if (tokens.length == 0) { - return null; - } - if (tokens[0]?.typ == 'At-rule') { - const atRule = tokens.shift(); - const position = map.get(atRule); - if (atRule.val == 'charset' && position.ind > 0) { - errors.push({ action: 'drop', message: 'invalid @charset', location: { src, ...position } }); - return null; - } - // @ts-ignore - while (['Whitespace'].includes(tokens[0]?.typ)) { - tokens.shift(); - } - if (atRule.val == 'import') { - // only @charset and @layer are accepted before @import - if (context.chi.length > 0) { - let i = context.chi.length; - while (i--) { - const type = context.chi[i].typ; - if (type == 'Comment') { - continue; - } - if (type != 'AtRule') { - errors.push({ action: 'drop', message: 'invalid @import', location: { src, ...position } }); - return null; - } - const name = context.chi[i].nam; - if (name != 'charset' && name != 'import' && name != 'layer') { - errors.push({ action: 'drop', message: 'invalid @import', location: { src, ...position } }); - return null; - } - break; - } - } - // @ts-ignore - if (tokens[0]?.typ != 'String' && tokens[0]?.typ != 'UrlFunc') { - errors.push({ action: 'drop', message: 'invalid @import', location: { src, ...position } }); - return null; - } - // @ts-ignore - if (tokens[0].typ == 'UrlFunc' && tokens[1]?.typ != 'Url-token' && tokens[1]?.typ != 'String') { - errors.push({ action: 'drop', message: 'invalid @import', location: { src, ...position } }); - return null; - } - } - if (atRule.val == 'import') { - // @ts-ignore - if (tokens[0].typ == 'UrlFunc' && tokens[1].typ == 'Url-token') { - tokens.shift(); - // @ts-ignore - tokens[0].typ = 'String'; - // @ts-ignore - tokens[0].val = `"${tokens[0].val}"`; - } - // @ts-ignore - if (tokens[0].typ == 'String') { - if (options.resolveImport) { - const url = tokens[0].val.slice(1, -1); - try { - // @ts-ignore - const root = await options.load(url, options.src).then((src) => { - return parse(src, Object.assign({}, options, { - compress: false, - // @ts-ignore - src: options.resolve(url, options.src).absolute - })); - }); - bytesIn += root.bytesIn; - if (root.ast.chi.length > 0) { - context.chi.push(...root.ast.chi); - } - if (root.errors.length > 0) { - errors.push(...root.errors); - } - return null; - } - catch (error) { - console.error(error); - } - } - } - } - // https://www.w3.org/TR/css-nesting-1/#conditionals - // allowed nesting at-rules - // there must be a top level rule in the stack - const raw = tokens.reduce((acc, curr) => { - acc.push(renderToken(curr, { removeComments: true })); - return acc; - }, []); - const node = { - typ: 'AtRule', - nam: renderToken(atRule, { removeComments: true }), - val: raw.join('') - }; - Object.defineProperty(node, 'raw', { enumerable: false, writable: false, value: raw }); - if (delim.typ == 'Block-start') { - node.chi = []; - } - loc = { - sta: position, - src - }; - if (options.sourcemap) { - node.loc = loc; - } - // @ts-ignore - context.chi.push(node); - return delim.typ == 'Block-start' ? node : null; - } - else { - // 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, 'Rule', { compress: options.compress }).reduce((acc, curr, index, array) => { - if (curr.typ == 'Whitespace') { - if (array[index - 1]?.val == '+' || array[index + 1]?.val == '+') { - return acc; - } - } - let t = renderToken(curr, { compress: true }); - if (t == ',') { - acc.push([]); - } - else { - acc[acc.length - 1].push(t); - } - return acc; - }, [[]]).reduce((acc, curr) => { - acc.set(curr.join(''), curr); - return acc; - }, uniq); - const node = { - typ: 'Rule', - // @ts-ignore - sel: [...uniq.keys()].join(','), - chi: [] - }; - let raw = [...uniq.values()]; - Object.defineProperty(node, 'raw', { enumerable: false, writable: true, value: raw }); - loc = { - sta: position, - src - }; - if (options.sourcemap) { - node.loc = loc; - } - // @ts-ignore - context.chi.push(node); - return node; - } - else { - // declaration - // @ts-ignore - let name = null; - // @ts-ignore - let value = null; - for (let i = 0; i < tokens.length; i++) { - if (tokens[i].typ == 'Comment') { - continue; - } - if (tokens[i].typ == 'Colon') { - name = tokens.slice(0, i); - value = parseTokens(tokens.slice(i + 1), 'Declaration', { - parseColor: true, - src: options.src, - resolveUrls: options.resolveUrls, - resolve: options.resolve, - cwd: options.cwd - }); - } - } - if (name == null) { - name = tokens; - } - const position = map.get(name[0]); - if (name.length > 0) { - for (let i = 1; i < name.length; i++) { - if (name[i].typ != 'Whitespace' && name[i].typ != 'Comment') { - errors.push({ - action: 'drop', - message: 'invalid declaration', - location: { src, ...position } - }); - return null; - } - } - } - if (value == null) { - errors.push({ action: 'drop', message: 'invalid declaration', location: { src, ...position } }); - return null; - } - if (value.length == 0) { - errors.push({ action: 'drop', message: 'invalid declaration', location: { src, ...position } }); - return null; - } - const node = { - typ: 'Declaration', - // @ts-ignore - nam: renderToken(name.shift(), { removeComments: true }), - // @ts-ignore - val: value - }; - while (node.val[0]?.typ == 'Whitespace') { - node.val.shift(); - } - if (node.val.length == 0) { - errors.push({ action: 'drop', message: 'invalid declaration', location: { src, ...position } }); - return null; - } - // @ts-ignore - context.chi.push(node); - return null; - } - } - } - function peek(count = 1) { - if (count == 1) { - return iterator.charAt(ind + 1); - } - return iterator.slice(ind + 1, ind + count + 1); - } - function prev(count = 1) { - if (count == 1) { - return ind == 0 ? '' : iterator.charAt(ind - 1); - } - return iterator.slice(ind - 1 - count, ind - 1); - } - function next(count = 1) { - let char = ''; - while (count-- > 0 && ind < total) { - const codepoint = iterator.charCodeAt(++ind); - if (isNaN(codepoint)) { - return char; - } - char += iterator.charAt(ind); - if (isNewLine(codepoint)) { - lin++; - col = 0; - } - else { - col++; - } - } - return char; - } - function pushToken(token) { - tokens.push(token); - map.set(token, { ...position }); - position.ind = ind; - position.lin = lin; - position.col = col == 0 ? 1 : col; - } - function consumeWhiteSpace() { - let count = 0; - while (isWhiteSpace(iterator.charAt(count + ind + 1).charCodeAt(0))) { - count++; - } - next(count); - return count; - } - function consumeString(quoteStr) { - const quote = quoteStr; - let value; - let hasNewLine = false; - if (buffer.length > 0) { - pushToken(getType(buffer)); - buffer = ''; - } - buffer += quoteStr; - while (ind < total) { - value = peek(); - if (ind >= total) { - pushToken({ typ: hasNewLine ? 'Bad-string' : 'Unclosed-string', val: buffer }); - break; - } - if (value == '\\') { - const sequence = peek(6); - let escapeSequence = ''; - let codepoint; - let i; - for (i = 1; i < sequence.length; i++) { - codepoint = sequence.charCodeAt(i); - if (codepoint == 0x20 || - (codepoint >= 0x61 && codepoint <= 0x66) || - (codepoint >= 0x41 && codepoint <= 0x46) || - (codepoint >= 0x30 && codepoint <= 0x39)) { - escapeSequence += sequence[i]; - if (codepoint == 0x20) { - break; - } - continue; - } - break; - } - // not hex or new line - // @ts-ignore - if (i == 1 && !isNewLine(codepoint)) { - buffer += sequence[i]; - next(2); - continue; - } - if (escapeSequence.trimEnd().length > 0) { - const codepoint = Number(`0x${escapeSequence.trimEnd()}`); - if (codepoint == 0 || - // leading surrogate - (0xD800 <= codepoint && codepoint <= 0xDBFF) || - // trailing surrogate - (0xDC00 <= codepoint && codepoint <= 0xDFFF)) { - buffer += String.fromCodePoint(0xFFFD); - } - else { - buffer += String.fromCodePoint(codepoint); - } - next(escapeSequence.length + 1); - continue; - } - // buffer += value; - if (ind >= total) { - // drop '\\' at the end - pushToken(getType(buffer)); - break; - } - buffer += next(2); - continue; - } - if (value == quote) { - buffer += value; - pushToken({ typ: hasNewLine ? 'Bad-string' : 'String', val: buffer }); - next(); - // i += value.length; - buffer = ''; - break; - } - if (isNewLine(value.charCodeAt(0))) { - hasNewLine = true; - } - if (hasNewLine && value == ';') { - pushToken({ typ: 'Bad-string', val: buffer }); - buffer = ''; - break; - } - buffer += value; - // i += value.length; - next(); - } - } - while (ind < total) { - value = next(); - if (ind >= total) { - if (buffer.length > 0) { - pushToken(getType(buffer)); - buffer = ''; - } - break; - } - if (isWhiteSpace(value.charCodeAt(0))) { - if (buffer.length > 0) { - pushToken(getType(buffer)); - buffer = ''; - } - while (ind < total) { - value = next(); - if (ind >= total) { - break; - } - if (!isWhiteSpace(value.charCodeAt(0))) { - break; - } - } - pushToken({ typ: 'Whitespace' }); - buffer = ''; - if (ind >= total) { - break; - } - } - switch (value) { - case '/': - if (buffer.length > 0 && tokens.at(-1)?.typ == 'Whitespace') { - pushToken(getType(buffer)); - buffer = ''; - if (peek() != '*') { - pushToken(getType(value)); - break; - } - } - buffer += value; - if (peek() == '*') { - buffer += '*'; - // i++; - next(); - while (ind < total) { - value = next(); - if (ind >= total) { - pushToken({ - typ: 'Bad-comment', val: buffer - }); - break; - } - if (value == '\\') { - buffer += value; - value = next(); - if (ind >= total) { - pushToken({ - typ: 'Bad-comment', - val: buffer - }); - break; - } - buffer += value; - continue; - } - if (value == '*') { - buffer += value; - value = next(); - if (ind >= total) { - pushToken({ - typ: 'Bad-comment', val: buffer - }); - break; - } - buffer += value; - if (value == '/') { - pushToken({ typ: 'Comment', val: buffer }); - buffer = ''; - break; - } - } - else { - buffer += value; - } - } - } - break; - case '<': - if (buffer.length > 0) { - pushToken(getType(buffer)); - buffer = ''; - } - buffer += value; - value = next(); - if (ind >= total) { - break; - } - if (peek(3) == '!--') { - while (ind < total) { - value = next(); - if (ind >= total) { - break; - } - buffer += value; - if (value == '>' && prev(2) == '--') { - pushToken({ - typ: 'CDOCOMM', - val: buffer - }); - buffer = ''; - break; - } - } - } - if (ind >= total) { - pushToken({ typ: 'BADCDO', val: buffer }); - buffer = ''; - } - break; - case '\\': - value = next(); - // EOF - if (ind + 1 >= total) { - // end of stream ignore \\ - pushToken(getType(buffer)); - buffer = ''; - break; - } - buffer += value; - break; - case '"': - case "'": - consumeString(value); - break; - case '~': - case '|': - if (tokens.at(-1)?.typ == 'Whitespace') { - tokens.pop(); - } - if (buffer.length > 0) { - pushToken(getType(buffer)); - buffer = ''; - } - buffer += value; - value = next(); - if (ind >= total) { - pushToken(getType(buffer)); - buffer = ''; - break; - } - if (value == '=') { - buffer += value; - pushToken({ - typ: buffer[0] == '~' ? 'Includes' : 'Dash-matches', - val: buffer - }); - buffer = ''; - break; - } - pushToken(getType(buffer)); - while (isWhiteSpace(value.charCodeAt(0))) { - value = next(); - } - buffer = value; - break; - case '>': - if (tokens[tokens.length - 1]?.typ == 'Whitespace') { - tokens.pop(); - } - if (buffer !== '') { - pushToken(getType(buffer)); - buffer = ''; - } - pushToken({ typ: 'Gt' }); - consumeWhiteSpace(); - break; - case '.': - const codepoint = peek().charCodeAt(0); - if (!isDigit(codepoint) && buffer !== '') { - pushToken(getType(buffer)); - buffer = value; - break; - } - buffer += value; - break; - case '+': - case ':': - case ',': - case '=': - if (buffer.length > 0) { - pushToken(getType(buffer)); - buffer = ''; - } - if (value == ':' && ':' == peek()) { - buffer += value + next(); - break; - } - pushToken(getType(value)); - buffer = ''; - if (value == '+' && isWhiteSpace(peek().charCodeAt(0))) { - pushToken(getType(next())); - } - while (isWhiteSpace(peek().charCodeAt(0))) { - next(); - } - break; - case ')': - if (buffer.length > 0) { - pushToken(getType(buffer)); - buffer = ''; - } - pushToken({ typ: 'End-parens' }); - break; - case '(': - if (buffer.length == 0) { - pushToken({ typ: 'Start-parens' }); - } - else { - buffer += value; - pushToken(getType(buffer)); - buffer = ''; - const token = tokens[tokens.length - 1]; - if (token.typ == 'UrlFunc') { - // consume either string or url token - let whitespace = ''; - value = peek(); - while (isWhiteSpace(value.charCodeAt(0))) { - whitespace += value; - } - if (whitespace.length > 0) { - next(whitespace.length); - } - value = peek(); - if (value == '"' || value == "'") { - consumeString(next()); - let token = tokens[tokens.length - 1]; - if (['String', 'Literal'].includes(token.typ) && urlTokenMatcher.test(token.val)) { - if (token.val.slice(1, 6) != 'data:') { - if (token.typ == 'String') { - token.val = token.val.slice(1, -1); - } - // @ts-ignore - token.typ = 'Url-token'; - } - } - break; - } - else { - buffer = ''; - do { - let cp = value.charCodeAt(0); - // EOF - - if (cp == null) { - pushToken({ typ: 'Bad-url-token', val: buffer }); - break; - } - // ')' - if (cp == 0x29 || cp == null) { - if (buffer.length == 0) { - pushToken({ typ: 'Bad-url-token', val: '' }); - } - else { - pushToken({ typ: 'Url-token', val: buffer }); - } - if (cp != null) { - pushToken(getType(next())); - } - break; - } - if (isWhiteSpace(cp)) { - whitespace = next(); - while (true) { - value = peek(); - cp = value.charCodeAt(0); - if (isWhiteSpace(cp)) { - whitespace += value; - continue; - } - break; - } - if (cp == null || cp == 0x29) { - continue; - } - // bad url token - buffer += next(whitespace.length); - do { - value = peek(); - cp = value.charCodeAt(0); - if (cp == null || cp == 0x29) { - break; - } - buffer += next(); - } while (true); - pushToken({ typ: 'Bad-url-token', val: buffer }); - continue; - } - buffer += next(); - value = peek(); - } while (true); - buffer = ''; - } - } - } - break; - case '[': - case ']': - case '{': - case '}': - case ';': - if (buffer.length > 0) { - pushToken(getType(buffer)); - buffer = ''; - } - pushToken(getBlockType(value)); - let node = null; - if (value == '{' || value == ';') { - node = await parseNode(tokens); - if (node != null) { - stack.push(node); - // @ts-ignore - context = node; - } - else if (value == '{') { - // node == null - // consume and throw away until the closing '}' or EOF - consume('{', '}'); - } - tokens.length = 0; - map.clear(); - } - else if (value == '}') { - await parseNode(tokens); - const previousNode = stack.pop(); - // @ts-ignore - context = stack[stack.length - 1] || ast; - // @ts-ignore - if (options.removeEmpty && previousNode != null && previousNode.chi.length == 0 && context.chi[context.chi.length - 1] == previousNode) { - context.chi.pop(); - } - tokens.length = 0; - map.clear(); - buffer = ''; - } - break; - case '!': - if (buffer.length > 0) { - pushToken(getType(buffer)); - buffer = ''; - } - const important = peek(9); - if (important == 'important') { - if (tokens[tokens.length - 1]?.typ == 'Whitespace') { - tokens.pop(); - } - pushToken({ typ: 'Important' }); - next(9); - buffer = ''; - break; - } - buffer = '!'; - break; - default: - buffer += value; - break; - } - } - if (buffer.length > 0) { - pushToken(getType(buffer)); - } - if (tokens.length > 0) { - await parseNode(tokens); - } - if (options.compress) { - if (ast.chi.length > 0) { - deduplicate(ast, options, true); - } - } - return { ast, errors, bytesIn }; -} -function parseTokens(tokens, nodeType, options = {}) { - for (let i = 0; i < tokens.length; i++) { - const t = tokens[i]; - if (t.typ == 'Whitespace' && ((i == 0 || - i + 1 == tokens.length || - ['Comma'].includes(tokens[i + 1].typ) || - (i > 0 && - tokens[i + 1]?.typ != 'Literal' && - funcLike.includes(tokens[i - 1].typ) && - !['var', 'calc'].includes(tokens[i - 1].val))))) { - tokens.splice(i--, 1); - continue; - } - if (t.typ == 'Colon') { - const typ = tokens[i + 1]?.typ; - if (typ != null) { - if (typ == 'Func') { - tokens[i + 1].val = ':' + tokens[i + 1].val; - tokens[i + 1].typ = 'Pseudo-class-func'; - } - else if (typ == 'Iden') { - tokens[i + 1].val = ':' + tokens[i + 1].val; - tokens[i + 1].typ = 'Pseudo-class'; - } - if (typ == 'Func' || typ == 'Iden') { - tokens.splice(i, 1); - i--; - continue; - } - } - } - if (t.typ == 'Attr-start') { - let k = i; - let inAttr = 1; - while (++k < tokens.length) { - if (tokens[k].typ == 'Attr-end') { - inAttr--; - } - else if (tokens[k].typ == 'Attr-start') { - inAttr++; - } - if (inAttr == 0) { - break; - } - } - Object.assign(t, { typ: 'Attr', chi: tokens.splice(i + 1, k - i) }); - // @ts-ignore - if (t.chi.at(-1).typ == 'Attr-end') { - // @ts-ignore - t.chi.pop(); - // @ts-ignore - if (t.chi.length > 1) { - /*(t).chi =*/ - // @ts-ignore - parseTokens(t.chi, t.typ, options); - } - // @ts-ignore - t.chi.forEach(val => { - if (val.typ == 'String') { - const slice = val.val.slice(1, -1); - if ((slice.charAt(0) != '-' || (slice.charAt(0) == '-' && isIdentStart(slice.charCodeAt(1)))) && isIdent(slice)) { - Object.assign(val, { typ: 'Iden', val: slice }); - } - } - }); - } - continue; - } - if (funcLike.includes(t.typ)) { - let parens = 1; - let k = i; - while (++k < tokens.length) { - if (tokens[k].typ == 'Colon') { - const typ = tokens[k + 1]?.typ; - if (typ != null) { - if (typ == 'Iden') { - tokens[k + 1].typ = 'Pseudo-class'; - tokens[k + 1].val = ':' + tokens[k + 1].val; - } - else if (typ == 'Func') { - tokens[k + 1].typ = 'Pseudo-class-func'; - tokens[k + 1].val = ':' + tokens[k + 1].val; - } - if (typ == 'Func' || typ == 'Iden') { - tokens.splice(k, 1); - k--; - continue; - } - } - } - if (funcLike.includes(tokens[k].typ)) { - parens++; - } - else if (tokens[k].typ == 'End-parens') { - parens--; - } - if (parens == 0) { - break; - } - } - // @ts-ignore - t.chi = tokens.splice(i + 1, k - i); - // @ts-ignore - if (t.chi.at(-1)?.typ == 'End-parens') { - // @ts-ignore - t.chi.pop(); - } - let isColor = true; - // @ts-ignore - if (options.parseColor && ['rgb', 'rgba', 'hsl', 'hsla', 'hwb', 'device-cmyk'].includes(t.val)) { - // @ts-ignore - for (const v of t.chi) { - if (v.typ == 'Func' && v.val == 'var') { - isColor = false; - break; - } - } - if (isColor) { - // @ts-ignore - t.typ = 'Color'; - // @ts-ignore - t.kin = t.val; - // @ts-ignore - let m = t.chi.length; - while (m-- > 0) { - // @ts-ignore - if (t.chi[m].typ == 'Literal') { - // @ts-ignore - if (t.chi[m + 1]?.typ == 'Whitespace') { - // @ts-ignore - t.chi.splice(m + 1, 1); - } - // @ts-ignore - if (t.chi[m - 1]?.typ == 'Whitespace') { - // @ts-ignore - t.chi.splice(m - 1, 1); - m--; - } - } - } - continue; - } - } - if (t.typ == 'UrlFunc') { - // @ts-ignore - if (t.chi[0]?.typ == 'String') { - // @ts-ignore - const value = t.chi[0].val.slice(1, -1); - // @ts-ignore - if (t.chi[0].val.slice(1, 5) != 'data:' && urlTokenMatcher.test(value)) { - // @ts-ignore - t.chi[0].typ = 'Url-token'; - // @ts-ignore - t.chi[0].val = options.src !== '' && options.resolveUrls ? options.resolve(value, options.src).absolute : value; - } - } - if (t.chi[0]?.typ == 'Url-token') { - if (options.src !== '' && options.resolveUrls) { - // @ts-ignore - t.chi[0].val = options.resolve(t.chi[0].val, options.src, options.cwd).relative; - } - } - } - // @ts-ignore - if (t.chi.length > 0) { - // @ts-ignore - parseTokens(t.chi, t.typ, options); - if (t.typ == 'Pseudo-class-func' && t.val == ':is' && options.compress) { - // - const count = t.chi.filter(t => t.typ != 'Comment').length; - if (count == 1 || - (i == 0 && - (tokens[i + 1]?.typ == 'Comma' || tokens.length == i + 1)) || - (tokens[i - 1]?.typ == 'Comma' && (tokens[i + 1]?.typ == 'Comma' || tokens.length == i + 1))) { - tokens.splice(i, 1, ...t.chi); - i = Math.max(0, i - t.chi.length); - } - } - } - continue; - } - if (options.parseColor) { - if (t.typ == 'Iden') { - // named color - const value = t.val.toLowerCase(); - if (COLORS_NAMES[value] != null) { - Object.assign(t, { - typ: 'Color', - val: COLORS_NAMES[value].length < value.length ? COLORS_NAMES[value] : value, - kin: 'hex' - }); - } - continue; - } - if (t.typ == 'Hash' && isHexColor(t.val)) { - // hex color - // @ts-ignore - t.typ = 'Color'; - // @ts-ignore - t.kin = 'hex'; - } - } - } - return tokens; -} -function getBlockType(chr) { - if (chr == ';') { - return { typ: 'Semi-colon' }; - } - if (chr == '{') { - return { typ: 'Block-start' }; - } - if (chr == '}') { - return { typ: 'Block-end' }; - } - if (chr == '[') { - return { typ: 'Attr-start' }; - } - if (chr == ']') { - return { typ: 'Attr-end' }; - } - throw new Error(`unhandled token: '${chr}'`); -} diff --git a/src/lib/parser/parse.ts b/src/lib/parser/parse.ts index 51e8c45..f168d50 100644 --- a/src/lib/parser/parse.ts +++ b/src/lib/parser/parse.ts @@ -1,208 +1,91 @@ import { - AstAtRule, AstComment, AstDeclaration, AstNode, AstRule, AstRuleList, - AstRuleStyleSheet, AtRuleToken, AttrToken, ColorToken, DashMatchToken, - ErrorDescription, FunctionToken, IncludesToken, LiteralToken, Location, NodeType, ParseResult, - ParserOptions, ParseTokenOptions, Position, PseudoClassFunctionToken, PseudoClassToken, StringToken, Token, UrlToken + AstAtRule, + AstComment, + AstDeclaration, + AstNode, + AstRule, + AstRuleList, + AstRuleStyleSheet, + AtRuleToken, + ErrorDescription, + FunctionToken, + LiteralToken, + Location, + ParseResult, + ParserOptions, + ParseTokenOptions, + Position, + PseudoClassFunctionToken, + PseudoClassToken, + Token, + TokenizeResult, + UrlToken } from "../../@types"; import { - isAtKeyword, isDigit, - isDimension, + isAtKeyword, isDimension, isFunction, isHash, isHexColor, - isIdent, isIdentStart, isNewLine, - isNumber, + isIdent, isIdentStart, isNumber, isPercentage, - isPseudo, isWhiteSpace, - parseDimension + isPseudo, parseDimension } from "./utils"; import {renderToken} from "../renderer"; import {COLORS_NAMES} from "../renderer/utils"; -import {deduplicate} from "./deduplicate"; +import {combinators, minify} from "../ast"; +import {tokenize} from "./tokenize"; + +export const urlTokenMatcher = /^(["']?)[a-zA-Z0-9_/.-][a-zA-Z0-9_/:.#?-]+(\1)$/; -const urlTokenMatcher = /^(["']?)[a-zA-Z0-9_/.-][a-zA-Z0-9_/:.#?-]+(\1)$/; const funcLike = ['Start-parens', 'Func', 'UrlFunc', 'Pseudo-class-func']; -export async function parse(iterator: string, opt: ParserOptions = {}): Promise { + +/** + * + * @param iterator + * @param opt + */ +export async function parse(iterator: string, opt: ParserOptions = {}): Promise { const errors: ErrorDescription[] = []; const options: ParserOptions = { src: '', sourcemap: false, - compress: false, + minify: true, nestingRules: false, resolveImport: false, resolveUrls: false, removeEmpty: true, ...opt }; + if (options.resolveImport) { options.resolveUrls = true; } - let ind: number = -1; - let lin: number = 1; - let col: number = 0; - const tokens: Token[] = []; - const src = options.src; + + const src: string = options.src; const stack: Array = []; const ast: AstRuleStyleSheet = { typ: "StyleSheet", chi: [] }; - const position = { - ind: Math.max(ind, 0), - lin: lin, - col: Math.max(col, 1) - }; - let value; - let buffer: string = ''; - let total:number = iterator.length; - let bytesIn: number = total; + + let tokens: TokenizeResult[] = []; let map: Map = new Map; + let bytesIn: number = 0; let context: AstRuleList = ast; if (options.sourcemap) { ast.loc = { sta: { - ind: ind, - lin: lin, - col: col + ind: 0, + lin: 1, + col: 1 }, src: '' }; } - function getType(val: string): Token { - if (val === '') { - throw new Error('empty string?'); - } - if (val == ':') { - return { typ: 'Colon' }; - } - if (val == ')') { - return { typ: 'End-parens' }; - } - if (val == '(') { - return { typ: 'Start-parens' }; - } - if (val == '=') { - return { typ: 'Delim', val }; - } - if (val == ';') { - return { typ: 'Semi-colon' }; - } - if (val == ',') { - return { typ: 'Comma' }; - } - if (val == '<') { - return { typ: 'Lt' }; - } - if (val == '>') { - return { typ: 'Gt' }; - } - if (isPseudo(val)) { - return val.endsWith('(') ? { - typ: 'Pseudo-class-func', - val: val.slice(0, -1), - chi: [] - } - : { - typ: 'Pseudo-class', - val - }; - } - if (isAtKeyword(val)) { - return { - typ: 'At-rule', - val: val.slice(1) - }; - } - if (isFunction(val)) { - val = val.slice(0, -1); - return { - typ: val == 'url' ? 'UrlFunc' : 'Func', - val, - chi: [] - }; - } - if (isNumber(val)) { - return { - typ: 'Number', - val - }; - } - if (isDimension(val)) { - return parseDimension(val); - } - if (isPercentage(val)) { - return { - typ: 'Perc', - val: val.slice(0, -1) - }; - } - if (val == 'currentColor') { - return { - typ: 'Color', - val, - kin: 'lit' - }; - } - if (isIdent(val)) { - return { - typ: 'Iden', - val - }; - } - if (val.charAt(0) == '#' && isHash(val)) { - return { - typ: 'Hash', - val - }; - } - if ('"\''.includes(val.charAt(0))) { - return { - typ: 'Unclosed-string', - val - }; - } - return { - typ: 'Literal', - val - }; - } - // consume and throw away - function consume(open: string, close: string) { - let count = 1; - let chr; - while (true) { - chr = next(); - if (chr == '\\') { - if (peek() === '') { - break; - } - continue; - } - else if (chr == '/' && peek() == '*') { - next(); - while (true) { - chr = next(); - if (chr === '') { - break; - } - if (chr == '*' && peek() == '/') { - next(); - break; - } - } - } - else if (chr == close) { - count--; - } - else if (chr == open) { - count++; - } - if (chr === '' || count == 0) { - break; - } - } - } - async function parseNode(tokens: Token[]) { + + async function parseNode(results: TokenizeResult[]) { + + let tokens: Token[] = results.map(mapToken); let i: number; let loc: Location; @@ -225,8 +108,7 @@ export async function parse(iterator: string, opt: ParserOptions = {}): Promise< (tokens[i]).loc = loc } - } - else if (tokens[i].typ != 'Whitespace') { + } else if (tokens[i].typ != 'Whitespace') { break; } } @@ -240,9 +122,8 @@ export async function parse(iterator: string, opt: ParserOptions = {}): Promise< if (delim.typ == 'Semi-colon' || delim.typ == 'Block-start' || delim.typ == 'Block-end') { tokens.pop(); - } - else { - delim = { typ: 'Semi-colon' }; + } else { + delim = {typ: 'Semi-colon'}; } // @ts-ignore @@ -261,7 +142,7 @@ export async function parse(iterator: string, opt: ParserOptions = {}): Promise< if (atRule.val == 'charset' && position.ind > 0) { - errors.push({ action: 'drop', message: 'invalid @charset', location: { src, ...position } }); + errors.push({action: 'drop', message: 'invalid @charset', location: {src, ...position}}); return null; } @@ -280,14 +161,14 @@ export async function parse(iterator: string, opt: ParserOptions = {}): Promise< continue; } if (type != 'AtRule') { - errors.push({ action: 'drop', message: 'invalid @import', location: { src, ...position } }); + errors.push({action: 'drop', message: 'invalid @import', location: {src, ...position}}); return null; } const name = (context.chi[i]).nam; if (name != 'charset' && name != 'import' && name != 'layer') { - errors.push({ action: 'drop', message: 'invalid @import', location: { src, ...position } }); + errors.push({action: 'drop', message: 'invalid @import', location: {src, ...position}}); return null; } @@ -296,12 +177,12 @@ export async function parse(iterator: string, opt: ParserOptions = {}): Promise< } // @ts-ignore if (tokens[0]?.typ != 'String' && tokens[0]?.typ != 'UrlFunc') { - errors.push({ action: 'drop', message: 'invalid @import', location: { src, ...position } }); + errors.push({action: 'drop', message: 'invalid @import', location: {src, ...position}}); return null; } // @ts-ignore if (tokens[0].typ == 'UrlFunc' && tokens[1]?.typ != 'Url-token' && tokens[1]?.typ != 'String') { - errors.push({ action: 'drop', message: 'invalid @import', location: { src, ...position } }); + errors.push({action: 'drop', message: 'invalid @import', location: {src, ...position}}); return null; } } @@ -320,7 +201,7 @@ export async function parse(iterator: string, opt: ParserOptions = {}): Promise< if (options.resolveImport) { - const url = (tokens[0]).val.slice(1, -1); + const url: string = (tokens[0]).val.slice(1, -1); try { @@ -328,7 +209,7 @@ export async function parse(iterator: string, opt: ParserOptions = {}): Promise< const root: ParseResult = await options.load(url, options.src).then((src: string) => { return parse(src, Object.assign({}, options, { - compress: false, + minify: false, // @ts-ignore src: options.resolve(url, options.src).absolute })) @@ -345,8 +226,7 @@ export async function parse(iterator: string, opt: ParserOptions = {}): Promise< } return null; - } - catch (error) { + } catch (error) { console.error(error); } } @@ -365,11 +245,11 @@ export async function parse(iterator: string, opt: ParserOptions = {}): Promise< const node: AstAtRule = { typ: 'AtRule', - nam: renderToken(atRule, { removeComments: true }), + nam: renderToken(atRule, {removeComments: true}), val: raw.join('') }; - Object.defineProperty(node, 'raw', { enumerable: false, writable: false, value: raw }); + Object.defineProperty(node, 'raw', {enumerable: false, writable: false, value: raw}); if (delim.typ == 'Block-start') { @@ -380,43 +260,47 @@ export async function parse(iterator: string, opt: ParserOptions = {}): Promise< sta: position, src }; + if (options.sourcemap) { node.loc = loc; } + // @ts-ignore context.chi.push(node); return delim.typ == 'Block-start' ? node : null; - } - else { + } else { // rule if (delim.typ == 'Block-start') { 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; - } - } + // 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, 'Rule', {compress: options.compress}).reduce((acc: string[][], curr: Token, index: number, array: Token[]) => { + parseTokens(tokens, {minify: options.minify}).reduce((acc: string[][], curr: Token, index: number, array: Token[]) => { if (curr.typ == 'Whitespace') { - if((array[index - 1])?.val == '+' || (array[index + 1])?.val == '+') { + if ( + array[index - 1]?.typ == 'Gt' || + array[index + 1]?.typ == 'Gt' || + combinators.includes((array[index - 1])?.val) || + combinators.includes((array[index + 1])?.val)) { return acc; } } - let t = renderToken(curr, { compress: true }); + let t = renderToken(curr, {minify: true}); if (t == ',') { acc.push([]); - } - else { + } else { acc[acc.length - 1].push(t); } return acc; @@ -434,7 +318,7 @@ export async function parse(iterator: string, opt: ParserOptions = {}): Promise< }; let raw = [...uniq.values()]; - Object.defineProperty(node, 'raw', { enumerable: false, writable: true, value: raw }); + Object.defineProperty(node, 'raw', {enumerable: false, writable: true, value: raw}); loc = { sta: position, @@ -448,8 +332,7 @@ export async function parse(iterator: string, opt: ParserOptions = {}): Promise< // @ts-ignore context.chi.push(node); return node; - } - else { + } else { // declaration // @ts-ignore let name = null; @@ -462,7 +345,7 @@ export async function parse(iterator: string, opt: ParserOptions = {}): Promise< if (tokens[i].typ == 'Colon') { name = tokens.slice(0, i); - value = parseTokens(tokens.slice(i + 1), 'Declaration', { + value = parseTokens(tokens.slice(i + 1), { parseColor: true, src: options.src, resolveUrls: options.resolveUrls, @@ -482,7 +365,7 @@ export async function parse(iterator: string, opt: ParserOptions = {}): Promise< errors.push({ action: 'drop', message: 'invalid declaration', - location: { src, ...position } + location: {src, ...position} }); return null; @@ -492,19 +375,27 @@ export async function parse(iterator: string, opt: ParserOptions = {}): Promise< if (value == null) { - errors.push({ action: 'drop', message: 'invalid declaration', location: { src, ...position } }); + errors.push({ + action: 'drop', + message: 'invalid declaration', + location: {src, ...position} + }); return null; } if (value.length == 0) { - errors.push({ action: 'drop', message: 'invalid declaration', location: { src, ...position } }); + errors.push({ + action: 'drop', + message: 'invalid declaration', + location: {src, ...position} + }); return null; } const node: AstDeclaration = { typ: 'Declaration', // @ts-ignore - nam: renderToken(name.shift(), { removeComments: true }), + nam: renderToken(name.shift(), {removeComments: true}), // @ts-ignore val: value } @@ -515,7 +406,11 @@ export async function parse(iterator: string, opt: ParserOptions = {}): Promise< if (node.val.length == 0) { - errors.push({ action: 'drop', message: 'invalid declaration', location: { src, ...position } }); + errors.push({ + action: 'drop', + message: 'invalid declaration', + location: {src, ...position} + }); return null; } @@ -525,608 +420,289 @@ export async function parse(iterator: string, opt: ParserOptions = {}): Promise< } } } - function peek(count: number = 1): string { - if (count == 1) { + function mapToken(token: TokenizeResult): Token { - return iterator.charAt(ind + 1); - } + const node = getTokenType(token.token, token.hint); - return iterator.slice(ind + 1, ind + count + 1); + map.set(node, token.position); + return node; } - function prev(count: number = 1): string { + const iter = tokenize(iterator); + let item: TokenizeResult; - if (count == 1) { - return ind == 0 ? '' : iterator.charAt(ind - 1); - } + while (true) { - return iterator.slice(ind - 1 - count, ind - 1); - } + item = iter.next().value; - function next(count: number = 1): string { + if (item == null) { - let char: string = ''; + break; + } - while (count-- > 0 && ind < total) { + tokens.push(item); + bytesIn = item.bytesIn; - const codepoint: number = iterator.charCodeAt(++ind); + if (item.token == ';' || item.token == '{') { - if (isNaN(codepoint)) { + let node = await parseNode(tokens); - return char; - } + if (node != null) { - char += iterator.charAt(ind); + stack.push(node); + // @ts-ignore + context = node; + } else if (item.token == '{') { + // node == null + // consume and throw away until the closing '}' or EOF - if (isNewLine(codepoint)) { + let inBlock = 1; - lin++; - col = 0; - } + do { - else { + item = iter.next().value; - col++; - } - } + if (item == null) { - return char; - } - function pushToken(token: Token) { + break; + } - tokens.push(token); + if (item.token == '{') { - map.set(token, { ...position }); + inBlock++; + } else if (item.token == '}') { - position.ind = ind; - position.lin = lin; - position.col = col == 0 ? 1 : col; - } + inBlock--; + } + } - function consumeWhiteSpace(): number { + while (inBlock != 0); + } - let count: number = 0; + tokens = []; + map = new Map; + } else if (item.token == '}') { - while (isWhiteSpace(iterator.charAt(count + ind + 1).charCodeAt(0))) { + await parseNode(tokens); + const previousNode = stack.pop(); - count++; + // @ts-ignore + context = stack[stack.length - 1] || ast; + // @ts-ignore + if (options.removeEmpty && previousNode != null && previousNode.chi.length == 0 && context.chi[context.chi.length - 1] == previousNode) { + context.chi.pop(); + } + + tokens = []; + map = new Map; } + } - next(count); + if (tokens.length > 0) { - return count; + await parseNode(tokens); } - function consumeString(quoteStr: '"' | "'") { - const quote = quoteStr; - let value; - let hasNewLine: boolean = false; + if (options.minify) { - if (buffer.length > 0) { + if (ast.chi.length > 0) { - pushToken(getType(buffer)); - buffer = ''; + minify(ast, options, true); } + } - buffer += quoteStr; + return {ast, errors, bytesIn}; +} - while (ind < total) { +export function parseString(src: string, options = {location: false}): Token[] { - value = peek(); - if (ind >= total) { + return [...tokenize(src)].map(t => { - pushToken({ typ: hasNewLine ? 'Bad-string' : 'Unclosed-string', val: buffer }); - break; - } + const token = getTokenType(t.token, t.hint); - if (value == '\\') { + if (options.location) { - const sequence: string = peek(6); - let escapeSequence: string = ''; - let codepoint; - let i; - - for (i = 1; i < sequence.length; i++) { - - codepoint = sequence.charCodeAt(i); + Object.assign(token, {loc: t.position}); + } - if (codepoint == 0x20 || - (codepoint >= 0x61 && codepoint <= 0x66) || - (codepoint >= 0x41 && codepoint <= 0x46) || - (codepoint >= 0x30 && codepoint <= 0x39)) { - escapeSequence += sequence[i]; + return token; + }); +} - if (codepoint == 0x20) { +function getTokenType(val: string, hint?: string): Token { - break; - } + if (val === '' && hint == null) { + throw new Error('empty string?'); + } - continue; - } + if (hint != null) { - break; - } + return ([ + 'Whitespace', 'Semi-colon', 'Colon', 'Block-start', + 'Block-start', 'Attr-start', 'Attr-end', 'Start-parens', 'End-parens', + 'Comma', 'Gt', 'Lt' + ].includes(hint) ? {typ: hint} : {typ: hint, val}); + } - // not hex or new line - // @ts-ignore - if (i == 1 && !isNewLine(codepoint)) { + if (val == ' ') { - buffer += sequence[i]; - next(2); - continue; - } + return {typ: 'Whitespace'}; + } - if (escapeSequence.trimEnd().length > 0) { + if (val == ';') { - const codepoint = Number(`0x${escapeSequence.trimEnd()}`); + return {typ: 'Semi-colon'}; + } - if (codepoint == 0 || - // leading surrogate - (0xD800 <= codepoint && codepoint <= 0xDBFF) || - // trailing surrogate - (0xDC00 <= codepoint && codepoint <= 0xDFFF)) { - buffer += String.fromCodePoint(0xFFFD); - } - else { + if (val == '{') { - buffer += String.fromCodePoint(codepoint); - } + return {typ: 'Block-start'}; + } - next(escapeSequence.length + 1); - continue; - } + if (val == '}') { - // buffer += value; - if (ind >= total) { + return {typ: 'Block-end'}; + } - // drop '\\' at the end - pushToken(getType(buffer)); - break; - } + if (val == '[') { - buffer += next(2); - continue; - } + return {typ: 'Attr-start'}; + } - if (value == quote) { + if (val == ']') { + return {typ: 'Attr-end'}; + } - buffer += value; - pushToken({ typ: hasNewLine ? 'Bad-string' : 'String', val: buffer }); - next(); - // i += value.length; - buffer = ''; - break; - } + if (val == ':') { - if (isNewLine(value.charCodeAt(0))) { - hasNewLine = true; - } + return {typ: 'Colon'}; + } + if (val == ')') { - if (hasNewLine && value == ';') { - pushToken({ typ: 'Bad-string', val: buffer }); - buffer = ''; - break; - } + return {typ: 'End-parens'}; + } + if (val == '(') { - buffer += value; - // i += value.length; - next(); - } + return {typ: 'Start-parens'}; } - while (ind < total) { - value = next(); - if (ind >= total) { - if (buffer.length > 0) { - pushToken(getType(buffer)); - buffer = ''; - } - break; - } - if (isWhiteSpace(value.charCodeAt(0))) { - if (buffer.length > 0) { - pushToken(getType(buffer)); - buffer = ''; - } - while (ind < total) { - value = next(); - if (ind >= total) { - break; - } - if (!isWhiteSpace(value.charCodeAt(0))) { - break; - } - } - pushToken({ typ: 'Whitespace' }); - buffer = ''; - if (ind >= total) { - break; - } - } - switch (value) { - case '/': - if (buffer.length > 0 && tokens.at(-1)?.typ == 'Whitespace') { - pushToken(getType(buffer)); - buffer = ''; - if (peek() != '*') { - pushToken(getType(value)); - break; - } - } - buffer += value; - if (peek() == '*') { - buffer += '*'; - // i++; - next(); - while (ind < total) { - value = next(); - if (ind >= total) { - pushToken({ - typ: 'Bad-comment', val: buffer - }); - break; - } - if (value == '\\') { - buffer += value; - value = next(); - if (ind >= total) { - pushToken({ - typ: 'Bad-comment', - val: buffer - }); - break; - } - buffer += value; - continue; - } - if (value == '*') { - buffer += value; - value = next(); - if (ind >= total) { - pushToken({ - typ: 'Bad-comment', val: buffer - }); - break; - } - buffer += value; - if (value == '/') { - pushToken({ typ: 'Comment', val: buffer }); - buffer = ''; - break; - } - } - else { - buffer += value; - } - } - } - break; - case '<': - if (buffer.length > 0) { - pushToken(getType(buffer)); - buffer = ''; - } - buffer += value; - value = next(); - if (ind >= total) { - break; - } - if (peek(3) == '!--') { - while (ind < total) { - value = next(); - if (ind >= total) { - break; - } - buffer += value; - if (value == '>' && prev(2) == '--') { - pushToken({ - typ: 'CDOCOMM', - val: buffer - }); - buffer = ''; - break; - } - } - } - if (ind >= total) { - pushToken({ typ: 'BADCDO', val: buffer }); - buffer = ''; - } - break; - case '\\': - value = next(); - // EOF - if (ind + 1 >= total) { - // end of stream ignore \\ - pushToken(getType(buffer)); - buffer = ''; - break; - } - buffer += value; - break; - case '"': - case "'": - consumeString(value); - break; - case '~': - case '|': - if (tokens.at(-1)?.typ == 'Whitespace') { - tokens.pop(); - } - if (buffer.length > 0) { - pushToken(getType(buffer)); - buffer = ''; - } - buffer += value; - value = next(); - if (ind >= total) { - pushToken(getType(buffer)); - buffer = ''; - break; - } - if (value == '=') { - buffer += value; - pushToken({ - typ: buffer[0] == '~' ? 'Includes' : 'Dash-matches', - val: buffer - }); + if (val == '=') { - buffer = ''; - break; - } + return {typ: 'Delim', val}; + } + if (val == ';') { - pushToken(getType(buffer)); + return {typ: 'Semi-colon'}; + } + if (val == ',') { - while (isWhiteSpace(value.charCodeAt(0))) { + return {typ: 'Comma'}; + } + if (val == '<') { - value = next(); - } + return {typ: 'Lt'}; + } + if (val == '>') { - buffer = value; - break; + return {typ: 'Gt'}; + } - case '>': + if (isPseudo(val)) { - if (buffer !== '') { - pushToken(getType(buffer)); - buffer = ''; - } + return val.endsWith('(') ? { + typ: 'Pseudo-class-func', + val: val.slice(0, -1), + chi: [] + } + : { + typ: 'Pseudo-class', + val + }; + } - if (tokens[tokens.length - 1]?.typ == 'Whitespace') { + if (isAtKeyword(val)) { + return { + typ: 'At-rule', + val: val.slice(1) + }; + } - tokens.pop(); - } + if (isFunction(val)) { + val = val.slice(0, -1); + return { + typ: val == 'url' ? 'UrlFunc' : 'Func', + val, + chi: [] + }; + } - pushToken({ typ: 'Gt' }); - consumeWhiteSpace(); - break; - case '.': + if (isNumber(val)) { + return { + typ: 'Number', + val + }; + } - const codepoint = peek().charCodeAt(0); + if (isDimension(val)) { - if (!isDigit(codepoint) && buffer !== '') { - pushToken(getType(buffer)); - buffer = value; - break; - } - buffer += value; - break; - case '+': - case ':': - case ',': - case '=': - if (buffer.length > 0) { - pushToken(getType(buffer)); - buffer = ''; - } - if (value == ':' && ':' == peek()) { - buffer += value + next(); - break; - } - pushToken(getType(value)); - buffer = ''; - if (value == '+' && isWhiteSpace(peek().charCodeAt(0))) { - pushToken(getType(next())); - } - while (isWhiteSpace(peek().charCodeAt(0))) { - next(); - } - break; - case ')': - if (buffer.length > 0) { - pushToken(getType(buffer)); - buffer = ''; - } - pushToken({ typ: 'End-parens' }); - break; - case '(': - if (buffer.length == 0) { - pushToken({ typ: 'Start-parens' }); - } - else { - buffer += value; - pushToken(getType(buffer)); - buffer = ''; - const token = tokens[tokens.length - 1]; - if (token.typ == 'UrlFunc') { - // consume either string or url token - let whitespace = ''; - value = peek(); - while (isWhiteSpace(value.charCodeAt(0))) { - whitespace += value; - } - if (whitespace.length > 0) { - next(whitespace.length); - } - value = peek(); - if (value == '"' || value == "'") { - consumeString(<'"' | "'"> next()); + return parseDimension(val); + } - let token: Token = tokens[tokens.length - 1]; + if (isPercentage(val)) { + return { + typ: 'Perc', + val: val.slice(0, -1) + }; + } - if (['String', 'Literal'].includes(token.typ) && urlTokenMatcher.test((token).val)) { - if ((token).val.slice(1, 6) != 'data:') { - if (token.typ == 'String') { - token.val = token.val.slice(1, -1); - } + const v = val.toLowerCase(); + if (v == 'currentcolor' || val == 'transparent' || v in COLORS_NAMES) { + return { + typ: 'Color', + val, + kin: 'lit' + }; + } - // @ts-ignore - token.typ = 'Url-token'; - } - } + if (isIdent(val)) { - break; - } + return { + typ: 'Iden', + val + }; + } - else { - - buffer = ''; - - do { - - let cp = value.charCodeAt(0); - - // EOF - - if (cp == null) { - - pushToken({ typ: 'Bad-url-token', val: buffer }); - break; - } - - // ')' - if (cp == 0x29 || cp == null) { - - if (buffer.length == 0) { - - pushToken({ typ: 'Bad-url-token', val: '' }); - } - else { - - pushToken({ typ: 'Url-token', val: buffer }); - } - if (cp != null) { - pushToken(getType(next())); - } - break; - } - if (isWhiteSpace(cp)) { - whitespace = next(); - while (true) { - value = peek(); - cp = value.charCodeAt(0); - if (isWhiteSpace(cp)) { - whitespace += value; - continue; - } - break; - } - if (cp == null || cp == 0x29) { - continue; - } - // bad url token - buffer += next(whitespace.length); - do { - - value = peek(); - cp = value.charCodeAt(0); - if (cp == null || cp == 0x29) { - break; - } - - buffer += next(); - } - while (true); - pushToken({ typ: 'Bad-url-token', val: buffer }); - continue; - } - buffer += next(); - value = peek(); - } - while (true); - buffer = ''; - } - } - } - break; - case '[': - case ']': - case '{': - case '}': - case ';': - if (buffer.length > 0) { - pushToken(getType(buffer)); - buffer = ''; - } - pushToken(getBlockType(value)); - let node = null; - if (value == '{' || value == ';') { + if (val.charAt(0) == '#' && isHexColor(val)) { - node = await parseNode(tokens); - if (node != null) { - stack.push(node); - // @ts-ignore - context = node; - } - else if (value == '{') { - // node == null - // consume and throw away until the closing '}' or EOF - consume('{', '}'); - } - tokens.length = 0; - map.clear(); - } - else if (value == '}') { - await parseNode(tokens); - const previousNode = stack.pop(); - // @ts-ignore - context = stack[stack.length - 1] || ast; - // @ts-ignore - if (options.removeEmpty && previousNode != null && previousNode.chi.length == 0 && context.chi[context.chi.length - 1] == previousNode) { - context.chi.pop(); - } - tokens.length = 0; - map.clear(); - buffer = ''; - } - break; - case '!': - if (buffer.length > 0) { - pushToken(getType(buffer)); - buffer = ''; - } - const important = peek(9); - if (important == 'important') { - if (tokens[tokens.length - 1]?.typ == 'Whitespace') { - tokens.pop(); - } - pushToken({ typ: 'Important' }); - next(9); - buffer = ''; - break; - } - buffer = '!'; - break; - default: - buffer += value; - break; - } - } - if (buffer.length > 0) { - pushToken(getType(buffer)); + return { + typ: 'Color', + val, + kin : 'hex' + }; } - if (tokens.length > 0) { - await parseNode(tokens); + + if (val.charAt(0) == '#' && isHash(val)) { + return { + typ: 'Hash', + val + }; } - if (options.compress) { - if (ast.chi.length > 0) { - deduplicate(ast, options, true); - } + + if ('"\''.includes(val.charAt(0))) { + return { + typ: 'Unclosed-string', + val + }; } - return { ast, errors, bytesIn }; + return { + typ: 'Literal', + val + }; } -function parseTokens(tokens: Token[], nodeType: NodeType, options: ParseTokenOptions = {}) { + +function parseTokens(tokens: Token[], options: ParseTokenOptions = {}) { for (let i = 0; i < tokens.length; i++) { @@ -1150,8 +726,7 @@ function parseTokens(tokens: Token[], nodeType: NodeType, options: ParseTokenOpt (tokens[i + 1]).val = ':' + (tokens[i + 1]).val; tokens[i + 1].typ = 'Pseudo-class-func'; - } - else if (typ == 'Iden') { + } else if (typ == 'Iden') { (tokens[i + 1]).val = ':' + (tokens[i + 1]).val; tokens[i + 1].typ = 'Pseudo-class'; @@ -1172,15 +747,14 @@ function parseTokens(tokens: Token[], nodeType: NodeType, options: ParseTokenOpt while (++k < tokens.length) { if (tokens[k].typ == 'Attr-end') { inAttr--; - } - else if (tokens[k].typ == 'Attr-start') { + } else if (tokens[k].typ == 'Attr-start') { inAttr++; } if (inAttr == 0) { break; } } - Object.assign(t, { typ: 'Attr', chi: tokens.splice(i + 1, k - i) }); + Object.assign(t, {typ: 'Attr', chi: tokens.splice(i + 1, k - i)}); // @ts-ignore if (t.chi.at(-1).typ == 'Attr-end') { // @ts-ignore @@ -1196,7 +770,7 @@ function parseTokens(tokens: Token[], nodeType: NodeType, options: ParseTokenOpt if (val.typ == 'String') { const slice = val.val.slice(1, -1); if ((slice.charAt(0) != '-' || (slice.charAt(0) == '-' && isIdentStart(slice.charCodeAt(1)))) && isIdent(slice)) { - Object.assign(val, { typ: 'Iden', val: slice }); + Object.assign(val, {typ: 'Iden', val: slice}); } } }); @@ -1214,8 +788,7 @@ function parseTokens(tokens: Token[], nodeType: NodeType, options: ParseTokenOpt tokens[k + 1].typ = 'Pseudo-class'; (tokens[k + 1]).val = ':' + (tokens[k + 1]).val; - } - else if (typ == 'Func') { + } else if (typ == 'Func') { (tokens[k + 1]).typ = 'Pseudo-class-func'; (tokens[k + 1]).val = ':' + (tokens[k + 1]).val; @@ -1230,8 +803,7 @@ function parseTokens(tokens: Token[], nodeType: NodeType, options: ParseTokenOpt } if (funcLike.includes(tokens[k].typ)) { parens++; - } - else if (tokens[k].typ == 'End-parens') { + } else if (tokens[k].typ == 'End-parens') { parens--; } if (parens == 0) { @@ -1308,7 +880,7 @@ function parseTokens(tokens: Token[], nodeType: NodeType, options: ParseTokenOpt if (t.chi.length > 0) { // @ts-ignore parseTokens(t.chi, t.typ, options); - if (t.typ == 'Pseudo-class-func' && t.val == ':is' && options.compress) { + if (t.typ == 'Pseudo-class-func' && t.val == ':is' && options.minify) { // const count = t.chi.filter(t => t.typ != 'Comment').length; if (count == 1 || @@ -1323,6 +895,7 @@ function parseTokens(tokens: Token[], nodeType: NodeType, options: ParseTokenOpt } continue; } + if (options.parseColor) { if (t.typ == 'Iden') { // named color @@ -1346,23 +919,4 @@ function parseTokens(tokens: Token[], nodeType: NodeType, options: ParseTokenOpt } } return tokens; -} -function getBlockType(chr: string): Token { - - if (chr == ';') { - return { typ: 'Semi-colon' }; - } - if (chr == '{') { - return { typ: 'Block-start' }; - } - if (chr == '}') { - return { typ: 'Block-end' }; - } - if (chr == '[') { - return { typ: 'Attr-start' }; - } - if (chr == ']') { - return { typ: 'Attr-end' }; - } - throw new Error(`unhandled token: '${chr}'`); -} +} \ No newline at end of file diff --git a/src/lib/parser/tokenize.ts b/src/lib/parser/tokenize.ts new file mode 100644 index 0000000..ffced9a --- /dev/null +++ b/src/lib/parser/tokenize.ts @@ -0,0 +1,623 @@ +import {Token, TokenizeResult} from "../../@types"; +import { + isDigit, + isNewLine, + isWhiteSpace +} from "./utils"; + +export function* tokenize(iterator: string): Generator { + + let ind: number = -1; + let lin: number = 1; + let col: number = 0; + + const position = { + + ind: Math.max(ind, 0), + lin: lin, + col: Math.max(col, 1) + }; + + let value; + let buffer: string = ''; + + function consumeWhiteSpace(): number { + + let count: number = 0; + + while (isWhiteSpace(iterator.charAt(count + ind + 1).charCodeAt(0))) { + + count++; + } + + next(count); + return count; + } + + function pushToken(token: string, hint?: string): TokenizeResult { + + const result = {token, hint, position: {...position}, bytesIn: ind}; + + position.ind = ind; + position.lin = lin; + position.col = col == 0 ? 1 : col; + return result; + } + + function* consumeString(quoteStr: '"' | "'"): Generator { + + const quote = quoteStr; + let value; + let hasNewLine: boolean = false; + + if (buffer.length > 0) { + + yield pushToken(buffer); + buffer = ''; + } + + buffer += quoteStr; + + while (value = peek()) { + + if (ind >= iterator.length) { + + yield pushToken(buffer, hasNewLine ? 'Bad-string' : 'Unclosed-string'); + break; + } + + if (value == '\\') { + + const sequence: string = peek(6); + let escapeSequence: string = ''; + let codepoint; + let i; + + for (i = 1; i < sequence.length; i++) { + + codepoint = sequence.charCodeAt(i); + + if (codepoint == 0x20 || + (codepoint >= 0x61 && codepoint <= 0x66) || + (codepoint >= 0x41 && codepoint <= 0x46) || + (codepoint >= 0x30 && codepoint <= 0x39)) { + escapeSequence += sequence[i]; + + if (codepoint == 0x20) { + + break; + } + + continue; + } + + break; + } + + // not hex or new line + // @ts-ignore + if (i == 1 && !isNewLine(codepoint)) { + + buffer += sequence[i]; + next(2); + continue; + } + + if (escapeSequence.trimEnd().length > 0) { + + const codepoint = Number(`0x${escapeSequence.trimEnd()}`); + + if (codepoint == 0 || + // leading surrogate + (0xD800 <= codepoint && codepoint <= 0xDBFF) || + // trailing surrogate + (0xDC00 <= codepoint && codepoint <= 0xDFFF)) { + buffer += String.fromCodePoint(0xFFFD); + } else { + + buffer += String.fromCodePoint(codepoint); + } + + next(escapeSequence.length + 1); + continue; + } + + // buffer += value; + if (ind >= iterator.length) { + + // drop '\\' at the end + yield pushToken(buffer); + break; + } + + buffer += next(2); + continue; + } + + if (value == quote) { + + buffer += value; + yield pushToken(buffer, hasNewLine ? 'Bad-string' : 'String'); + next(); + // i += value.length; + buffer = ''; + break; + } + + if (isNewLine(value.charCodeAt(0))) { + hasNewLine = true; + } + + if (hasNewLine && value == ';') { + yield pushToken(buffer, 'Bad-string',); + buffer = ''; + break; + } + + buffer += value; + // i += value.length; + next(); + } + } + + function peek(count: number = 1): string { + + if (count == 1) { + + return iterator.charAt(ind + 1); + } + + return iterator.slice(ind + 1, ind + count + 1); + } + + function prev(count: number = 1): string { + + if (count == 1) { + + return ind == 0 ? '' : iterator.charAt(ind - 1); + } + + return iterator.slice(ind - 1 - count, ind - 1); + } + + function next(count: number = 1): string { + + let char: string = ''; + + while (count-- > 0 && ind < iterator.length) { + + const codepoint: number = iterator.charCodeAt(++ind); + + if (isNaN(codepoint)) { + + return char; + } + + char += iterator.charAt(ind); + + if (isNewLine(codepoint)) { + + lin++; + col = 0; + } else { + + col++; + } + } + + return char; + } + + while (value = next()) { + + if (ind >= iterator.length) { + if (buffer.length > 0) { + yield pushToken(buffer); + buffer = ''; + } + break; + } + if (isWhiteSpace(value.charCodeAt(0))) { + + if (buffer.length > 0) { + + yield pushToken(buffer); + buffer = ''; + } + while (value = next()) { + + if (ind >= iterator.length) { + break; + } + if (!isWhiteSpace(value.charCodeAt(0))) { + break; + } + } + + yield pushToken('', 'Whitespace'); + + buffer = ''; + + if (ind >= iterator.length) { + + break; + } + } + switch (value) { + case '/': + + if (buffer.length > 0) { + + yield pushToken(buffer); + + buffer = ''; + + if (peek() != '*') { + + yield pushToken(value); + break; + } + } + buffer += value; + if (peek() == '*') { + + buffer += '*'; + // i++; + next(); + + while (value = next()) { + + if (ind >= iterator.length) { + + yield pushToken(buffer, 'Bad-comment'); + break; + } + if (value == '\\') { + buffer += value; + value = next(); + if (ind >= iterator.length) { + yield pushToken(buffer, 'Bad-comment'); + break; + } + buffer += value; + continue; + } + if (value == '*') { + + buffer += value; + value = next(); + + if (ind >= iterator.length) { + + yield pushToken(buffer, 'Bad-comment'); + break; + } + buffer += value; + if (value == '/') { + yield pushToken(buffer, 'Comment'); + buffer = ''; + break; + } + } else { + buffer += value; + } + } + } + break; + case '<': + if (buffer.length > 0) { + yield pushToken(buffer); + buffer = ''; + } + buffer += value; + value = next(); + if (ind >= iterator.length) { + break; + } + + if (peek(3) == '!--') { + + while (value = next()) { + + if (ind >= iterator.length) { + break; + } + buffer += value; + if (value == '>' && prev(2) == '--') { + + yield pushToken(buffer, 'CDOCOMM'); + buffer = ''; + break; + } + } + } + + if (ind >= iterator.length) { + + yield pushToken(buffer, 'BADCDO'); + buffer = ''; + } + + break; + case '\\': + value = next(); + // EOF + if (ind + 1 >= iterator.length) { + // end of stream ignore \\ + yield pushToken(buffer); + buffer = ''; + break; + } + buffer += value; + break; + case '"': + case "'": + yield* consumeString(value); + break; + case '~': + case '|': + + if (buffer.length > 0) { + yield pushToken(buffer); + buffer = ''; + } + buffer += value; + value = next(); + if (ind >= iterator.length) { + yield pushToken(buffer); + buffer = ''; + break; + } + if (value == '=') { + + buffer += value; + yield pushToken(buffer, buffer[0] == '~' ? 'Includes' : 'Dash-matches'); + + buffer = ''; + break; + } + + yield pushToken(buffer); + + while (isWhiteSpace(value.charCodeAt(0))) { + + value = next(); + } + + buffer = value; + break; + + case '>': + + if (buffer !== '') { + yield pushToken(buffer); + buffer = ''; + } + + yield pushToken('', 'Gt'); + consumeWhiteSpace(); + break; + case '.': + + const codepoint = peek().charCodeAt(0); + + if (!isDigit(codepoint) && buffer !== '') { + yield pushToken(buffer); + buffer = value; + break; + } + + buffer += value; + break; + case '+': + case ':': + case ',': + case '=': + + if (buffer.length > 0) { + + yield pushToken(buffer); + buffer = ''; + } + + if (value == ':' && ':' == peek()) { + + buffer += value + next(); + break; + } + + yield pushToken(value); + buffer = ''; + + if (value == '+' && isWhiteSpace(peek().charCodeAt(0))) { + + yield pushToken(next()); + } + while (isWhiteSpace(peek().charCodeAt(0))) { + + next(); + } + + break; + case ')': + + if (buffer.length > 0) { + yield pushToken(buffer); + buffer = ''; + } + + yield pushToken('', 'End-parens'); + break; + case '(': + + if (buffer.length == 0) { + + yield pushToken('', 'Start-parens'); + break; + } + + buffer += value; + + // @ts-ignore + if (buffer == 'url(') { + + yield pushToken(buffer); + buffer = ''; + + // consume either string or url token + let whitespace = ''; + + value = peek(); + + while (isWhiteSpace(value.charCodeAt(0))) { + + whitespace += value; + } + + if (whitespace.length > 0) { + + next(whitespace.length); + } + + value = peek(); + + if (value == '"' || value == "'") { + + yield* consumeString(<'"' | "'">next()); + + break; + } else { + + buffer = ''; + + do { + + let cp = value.charCodeAt(0); + + // EOF - + if (cp == null) { + + yield pushToken('', 'Bad-url-token'); + break; + } + + // ')' + if (cp == 0x29 || cp == null) { + + if (buffer.length == 0) { + + yield pushToken(buffer, 'Bad-url-token'); + } else { + + yield pushToken(buffer, 'Url-token'); + } + + if (cp != null) { + + yield pushToken(next()); + } + + break; + } + + if (isWhiteSpace(cp)) { + + whitespace = next(); + + while (true) { + + value = peek(); + cp = value.charCodeAt(0); + + if (isWhiteSpace(cp)) { + whitespace += value; + continue; + } + + break; + } + + if (cp == null || cp == 0x29) { + continue; + } + + // bad url token + buffer += next(whitespace.length); + + do { + + value = peek(); + cp = value.charCodeAt(0); + + if (cp == null || cp == 0x29) { + break; + } + + buffer += next(); + } + + while (true); + + yield pushToken(buffer, 'Bad-url-token'); + continue; + } + + buffer += next(); + value = peek(); + } + while (true); + buffer = ''; + } + + break; + } + + yield pushToken(buffer); + buffer = ''; + break; + + case '[': + case ']': + case '{': + case '}': + case ';': + + if (buffer.length > 0) { + + yield pushToken(buffer); + buffer = ''; + } + + yield pushToken(value); + break; + + case '!': + + if (buffer.length > 0) { + + yield pushToken(buffer); + buffer = ''; + } + + const important = peek(9); + + if (important == 'important') { + + yield pushToken('', 'Important'); + next(9); + buffer = ''; + break; + } + + buffer = '!'; + break; + default: + buffer += value; + break; + } + } + + if (buffer.length > 0) { + yield pushToken(buffer); + } +} diff --git a/src/lib/parser/utils/config.js b/src/lib/parser/utils/config.js deleted file mode 100644 index 2cbe98a..0000000 --- a/src/lib/parser/utils/config.js +++ /dev/null @@ -1,2 +0,0 @@ -import config from '../../../config.json' assert { type: 'json' }; -export const getConfig = () => config; diff --git a/src/lib/parser/utils/config.ts b/src/lib/parser/utils/config.ts index fe15f0a..6904d6f 100644 --- a/src/lib/parser/utils/config.ts +++ b/src/lib/parser/utils/config.ts @@ -1,3 +1,4 @@ import config from '../../../config.json' assert {type: 'json'}; +import {PropertiesConfig} from "../../../@types"; -export const getConfig = () => config; \ No newline at end of file +export const getConfig = () => config; \ No newline at end of file diff --git a/src/lib/parser/utils/eq.js b/src/lib/parser/utils/eq.js deleted file mode 100644 index 2daefa9..0000000 --- a/src/lib/parser/utils/eq.js +++ /dev/null @@ -1,11 +0,0 @@ -export function eq(a, b) { - if ((typeof a != 'object') || typeof b != 'object') { - return a === b; - } - const k1 = Object.keys(a); - const k2 = Object.keys(b); - return k1.length == k2.length && - k1.every((key) => { - return eq(a[key], b[key]); - }); -} diff --git a/src/lib/parser/utils/eq.ts b/src/lib/parser/utils/eq.ts index 89bf0b3..abb64ff 100644 --- a/src/lib/parser/utils/eq.ts +++ b/src/lib/parser/utils/eq.ts @@ -1,16 +1,57 @@ export function eq(a: { [key: string]: any }, b: { [key: string]: any }): boolean { - if ((typeof a != 'object') || typeof b != 'object') { + if (a == null || b == null) { + + return a == b; + } + + if (typeof a != 'object' || typeof b != 'object') { return a === b; } + if (Object.getPrototypeOf(a) !== Object.getPrototypeOf(b)) { + + return false; + } + + if (Array.isArray(a)) { + + if (a.length != b.length) { + + return false; + } + + let i = 0; + + for (; i < a.length; i++) { + + if (!eq(a[i], b[i])) { + + return false; + } + } + + return true; + } + const k1: string[] = Object.keys(a); const k2: string[] = Object.keys(b); - return k1.length == k2.length && - k1.every((key) => { + if (k1.length != k2.length) { + + return false; + } + + let key; + + for (key of k1) { + + if (!eq(a[key], b[key])) { + + return false; + } + } - return eq(a[key], b[key]) - }); + return true; } \ No newline at end of file diff --git a/src/lib/parser/utils/index.js b/src/lib/parser/utils/index.js deleted file mode 100644 index ac2b9df..0000000 --- a/src/lib/parser/utils/index.js +++ /dev/null @@ -1,2 +0,0 @@ -export * from './syntax'; -export * from './config'; diff --git a/src/lib/parser/utils/syntax.js b/src/lib/parser/utils/syntax.js deleted file mode 100644 index 1b3f541..0000000 --- a/src/lib/parser/utils/syntax.js +++ /dev/null @@ -1,269 +0,0 @@ -// https://www.w3.org/TR/CSS21/syndata.html#syntax -// https://www.w3.org/TR/2021/CRD-css-syntax-3-20211224/#typedef-ident-token -// '\\' -const REVERSE_SOLIDUS = 0x5c; -export function isLength(dimension) { - return 'unit' in dimension && [ - 'q', 'cap', 'ch', 'cm', 'cqb', 'cqh', 'cqi', 'cqmax', 'cqmin', 'cqw', 'dvb', - 'dvh', 'dvi', 'dvmax', 'dvmin', 'dvw', 'em', 'ex', 'ic', 'in', 'lh', 'lvb', - 'lvh', 'lvi', 'lvmax', 'lvw', 'mm', 'pc', 'pt', 'px', 'rem', 'rlh', 'svb', - 'svh', 'svi', 'svmin', 'svw', 'vb', 'vh', 'vi', 'vmax', 'vmin', 'vw' - ].includes(dimension.unit.toLowerCase()); -} -export function isResolution(dimension) { - return 'unit' in dimension && ['dpi', 'dpcm', 'dppx', 'x'].includes(dimension.unit.toLowerCase()); -} -export function isAngle(dimension) { - return 'unit' in dimension && ['rad', 'turn', 'deg', 'grad'].includes(dimension.unit.toLowerCase()); -} -export function isTime(dimension) { - return 'unit' in dimension && ['ms', 's'].includes(dimension.unit.toLowerCase()); -} -export function isFrequency(dimension) { - return 'unit' in dimension && ['hz', 'khz'].includes(dimension.unit.toLowerCase()); -} -function isLetter(codepoint) { - // lowercase - return (codepoint >= 0x61 && codepoint <= 0x7a) || - // uppercase - (codepoint >= 0x41 && codepoint <= 0x5a); -} -function isNonAscii(codepoint) { - return codepoint >= 0x80; -} -export function isIdentStart(codepoint) { - // _ - return codepoint == 0x5f || isLetter(codepoint) || isNonAscii(codepoint); -} -export function isDigit(codepoint) { - return codepoint >= 0x30 && codepoint <= 0x39; -} -export function isIdentCodepoint(codepoint) { - // - - return codepoint == 0x2d || isDigit(codepoint) || isIdentStart(codepoint); -} -export function isIdent(name) { - const j = name.length - 1; - let i = 0; - let codepoint = name.charCodeAt(0); - // - - if (codepoint == 0x2d) { - const nextCodepoint = name.charCodeAt(1); - if (Number.isNaN(nextCodepoint)) { - return false; - } - // - - if (nextCodepoint == 0x2d) { - return true; - } - if (nextCodepoint == REVERSE_SOLIDUS) { - return name.length > 2 && !isNewLine(name.charCodeAt(2)); - } - return true; - } - if (!isIdentStart(codepoint)) { - return false; - } - while (i < j) { - i += codepoint < 0x80 ? 1 : String.fromCodePoint(codepoint).length; - codepoint = name.charCodeAt(i); - if (!isIdentCodepoint(codepoint)) { - return false; - } - } - return true; -} -export function isPseudo(name) { - if (name.charAt(0) != ':') { - return false; - } - if (name.endsWith('(')) { - return isIdent(name.charAt(1) == ':' ? name.slice(2, -1) : name.slice(1, -1)); - } - return isIdent(name.charAt(1) == ':' ? name.slice(2) : name.slice(1)); -} -export function isHash(name) { - if (name.charAt(0) != '#') { - return false; - } - if (isIdent(name.charAt(1))) { - return true; - } - return true; -} -export function isNumber(name) { - if (name.length == 0) { - return false; - } - let codepoint = name.charCodeAt(0); - let i = 0; - const j = name.length; - if (j == 1 && !isDigit(codepoint)) { - return false; - } - // '+' '-' - if ([0x2b, 0x2d].includes(codepoint)) { - i++; - } - // consume digits - while (i < j) { - codepoint = name.charCodeAt(i); - if (isDigit(codepoint)) { - i++; - continue; - } - // '.' 'E' 'e' - if (codepoint == 0x2e || codepoint == 0x45 || codepoint == 0x65) { - break; - } - return false; - } - // '.' - if (codepoint == 0x2e) { - if (!isDigit(name.charCodeAt(++i))) { - return false; - } - } - while (i < j) { - codepoint = name.charCodeAt(i); - if (isDigit(codepoint)) { - i++; - continue; - } - // 'E' 'e' - if (codepoint == 0x45 || codepoint == 0x65) { - i++; - break; - } - return false; - } - // 'E' 'e' - if (codepoint == 0x45 || codepoint == 0x65) { - if (i == j) { - return false; - } - codepoint = name.charCodeAt(i + 1); - // '+' '-' - if ([0x2b, 0x2d].includes(codepoint)) { - i++; - } - codepoint = name.charCodeAt(i + 1); - if (!isDigit(codepoint)) { - return false; - } - } - while (++i < j) { - codepoint = name.charCodeAt(i); - if (!isDigit(codepoint)) { - return false; - } - } - return true; -} -export function isDimension(name) { - let index = 0; - while (index++ < name.length) { - if (isDigit(name.charCodeAt(name.length - index))) { - index--; - break; - } - if (index == 3) { - break; - } - } - if (index == 0 || index > 3) { - return false; - } - const number = name.slice(0, -index); - return number.length > 0 && isIdentStart(name.charCodeAt(name.length - index)) && isNumber(number); -} -export function isPercentage(name) { - return name.endsWith('%') && isNumber(name.slice(0, -1)); -} -export function parseDimension(name) { - let index = 0; - while (index++ < name.length) { - if (isDigit(name.charCodeAt(name.length - index))) { - index--; - break; - } - if (index == 3) { - break; - } - } - const dimension = { typ: 'Dimension', val: name.slice(0, -index), unit: name.slice(-index) }; - if (isAngle(dimension)) { - // @ts-ignore - dimension.typ = 'Angle'; - } - else if (isLength(dimension)) { - // @ts-ignore - dimension.typ = 'Length'; - } - else if (isTime(dimension)) { - // @ts-ignore - dimension.typ = 'Time'; - } - else if (isResolution(dimension)) { - // @ts-ignore - dimension.typ = 'Resolution'; - if (dimension.unit == 'dppx') { - dimension.unit = 'x'; - } - } - else if (isFrequency(dimension)) { - // @ts-ignore - dimension.typ = 'Frequency'; - } - return dimension; -} -export function isHexColor(name) { - if (name.charAt(0) != '#' || ![4, 5, 7, 9].includes(name.length)) { - return false; - } - for (let chr of name.slice(1)) { - let codepoint = chr.charCodeAt(0); - if (!isDigit(codepoint) && - // A-F - !(codepoint >= 0x41 && codepoint <= 0x46) && - // a-f - !(codepoint >= 0x61 && codepoint <= 0x66)) { - return false; - } - } - return true; -} -export function isHexDigit(name) { - if (name.length || name.length > 6) { - return false; - } - for (let chr of name) { - let codepoint = chr.charCodeAt(0); - if (!isDigit(codepoint) && - // A F - !(codepoint >= 0x41 && codepoint <= 0x46) && - // a f - !(codepoint >= 0x61 && codepoint <= 0x66)) { - return false; - } - } - return true; -} -function isEscape(name) { - return name.charCodeAt(0) == REVERSE_SOLIDUS && !isNewLine(name.charCodeAt(1)); -} -export function isFunction(name) { - return name.endsWith('(') && isIdent(name.slice(0, -1)); -} -export function isAtKeyword(name) { - return name.charCodeAt(0) == 0x40 && isIdent(name.slice(1)); -} -export function isNewLine(codepoint) { - // \n \r \f - return codepoint == 0xa || codepoint == 0xc || codepoint == 0xd; -} -export function isWhiteSpace(codepoint) { - return codepoint == 0x9 || codepoint == 0x20 || - // isNewLine - codepoint == 0xa || codepoint == 0xc || codepoint == 0xd; -} diff --git a/src/lib/parser/utils/syntax.ts b/src/lib/parser/utils/syntax.ts index a572237..38442ba 100644 --- a/src/lib/parser/utils/syntax.ts +++ b/src/lib/parser/utils/syntax.ts @@ -5,15 +5,16 @@ import {AngleToken, DimensionToken, LengthToken} from '../../../@types'; // '\\' const REVERSE_SOLIDUS = 0x5c; +const dimensionUnits = [ + 'q', 'cap', 'ch', 'cm', 'cqb', 'cqh', 'cqi', 'cqmax', 'cqmin', 'cqw', 'dvb', + 'dvh', 'dvi', 'dvmax', 'dvmin', 'dvw', 'em', 'ex', 'ic', 'in', 'lh', 'lvb', + 'lvh', 'lvi', 'lvmax', 'lvw', 'mm', 'pc', 'pt', 'px', 'rem', 'rlh', 'svb', + 'svh', 'svi', 'svmin', 'svw', 'vb', 'vh', 'vi', 'vmax', 'vmin', 'vw' +]; export function isLength(dimension: DimensionToken): boolean { - return 'unit' in dimension && [ - 'q', 'cap', 'ch', 'cm', 'cqb', 'cqh', 'cqi', 'cqmax', 'cqmin', 'cqw', 'dvb', - 'dvh', 'dvi', 'dvmax', 'dvmin', 'dvw', 'em', 'ex', 'ic', 'in', 'lh', 'lvb', - 'lvh', 'lvi', 'lvmax', 'lvw', 'mm', 'pc', 'pt', 'px', 'rem', 'rlh', 'svb', - 'svh', 'svi', 'svmin', 'svw', 'vb', 'vh', 'vi', 'vmax', 'vmin', 'vw' - ].includes(dimension.unit.toLowerCase()); + return 'unit' in dimension && dimensionUnits.includes(dimension.unit.toLowerCase()); } export function isResolution(dimension: DimensionToken): boolean { diff --git a/src/lib/parser/utils/type.js b/src/lib/parser/utils/type.js deleted file mode 100644 index 8b82c2e..0000000 --- a/src/lib/parser/utils/type.js +++ /dev/null @@ -1,10 +0,0 @@ -export 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; -} diff --git a/src/lib/renderer/index.js b/src/lib/renderer/index.js deleted file mode 100644 index dc70a4d..0000000 --- a/src/lib/renderer/index.js +++ /dev/null @@ -1 +0,0 @@ -export * from './render'; diff --git a/src/lib/renderer/render.js b/src/lib/renderer/render.js deleted file mode 100644 index ca87b3a..0000000 --- a/src/lib/renderer/render.js +++ /dev/null @@ -1,227 +0,0 @@ -import { cmyk2hex, hsl2Hex, hwb2hex, NAMES_COLORS, rgb2Hex } from "./utils"; -export function render(data, opt = {}) { - const options = Object.assign(opt.compress ? { - indent: '', - newLine: '', - removeComments: true - } : { - indent: ' ', - newLine: '\n', - compress: false, - removeComments: false, - }, { colorConvert: true, preserveLicense: false }, opt); - function reducer(acc, curr, index, original) { - if (curr.typ == 'Comment' && options.removeComments) { - if (!options.preserveLicense || !curr.val.startsWith('/*!')) { - return acc; - } - } - return acc + renderToken(curr, options); - } - return { code: doRender(data, options, reducer, 0) }; -} -// @ts-ignore -function doRender(data, options, reducer, level = 0, indents = []) { - if (indents.length < level + 1) { - indents.push(options.indent.repeat(level)); - } - if (indents.length < level + 2) { - indents.push(options.indent.repeat(level + 1)); - } - const indent = indents[level]; - const indentSub = indents[level + 1]; - switch (data.typ) { - case 'Comment': - return options.removeComments ? '' : data.val; - case 'StyleSheet': - return data.chi.reduce((css, node) => { - const str = doRender(node, options, reducer, level, indents); - if (str === '') { - return css; - } - if (css === '') { - return str; - } - return `${css}${options.newLine}${str}`; - }, ''); - case 'AtRule': - case 'Rule': - if (data.typ == 'AtRule' && !('chi' in data)) { - return `${indent}@${data.nam} ${data.val};`; - } - // @ts-ignore - let children = data.chi.reduce((css, node) => { - let str; - if (node.typ == 'Comment') { - str = options.removeComments ? '' : node.val; - } - else if (node.typ == 'Declaration') { - str = `${node.nam}:${options.indent}${node.val.reduce(reducer, '').trimEnd()};`; - } - else if (node.typ == 'AtRule' && !('chi' in node)) { - str = `@${node.nam} ${node.val};`; - } - else { - str = doRender(node, options, reducer, level + 1, indents); - } - if (css === '') { - return str; - } - if (str === '') { - return css; - } - return `${css}${options.newLine}${indentSub}${str}`; - }, ''); - if (children.endsWith(';')) { - 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.sel + `${options.indent}{${options.newLine}` + (children === '' ? '' : indentSub + children + options.newLine) + indent + `}`; - } - return ''; -} -export function renderToken(token, options = {}) { - switch (token.typ) { - case 'Color': - if (options.compress || options.colorConvert) { - let value = token.kin == 'hex' ? token.val.toLowerCase() : ''; - if (token.val == 'rgb' || token.val == 'rgba') { - value = rgb2Hex(token); - } - else if (token.val == 'hsl' || token.val == 'hsla') { - value = hsl2Hex(token); - } - else if (token.val == 'hwb') { - value = hwb2hex(token); - } - else if (token.val == 'device-cmyk') { - value = cmyk2hex(token); - } - const named_color = NAMES_COLORS[value]; - if (value !== '') { - if (value.length == 7) { - if (value[1] == value[2] && - value[3] == value[4] && - value[5] == value[6]) { - value = `#${value[1]}${value[3]}${value[5]}`; - } - } - else if (value.length == 9) { - if (value[1] == value[2] && - value[3] == value[4] && - value[5] == value[6] && - value[7] == value[8]) { - value = `#${value[1]}${value[3]}${value[5]}${value[7]}`; - } - } - return named_color != null && named_color.length <= value.length ? named_color : value; - } - } - if (token.kin == 'hex' || token.kin == 'lit') { - return token.val; - } - case 'Start-parens': - if (!('chi' in token)) { - return '('; - } - case 'Func': - case 'UrlFunc': - case 'Pseudo-class-func': - // @ts-ignore - return ( /* options.compress && 'Pseudo-class-func' == token.typ && token.val.slice(0, 2) == '::' ? token.val.slice(1) :*/token.val ?? '') + '(' + token.chi.reduce((acc, curr) => { - if (options.removeComments && curr.typ == 'Comment') { - if (!options.preserveLicense || !curr.val.startsWith('/*!')) { - return acc; - } - } - return acc + renderToken(curr, options); - }, '') + ')'; - case 'Includes': - return '~='; - case 'Dash-match': - return '|='; - case 'Lt': - return '<'; - case 'Gt': - return '>'; - case 'End-parens': - return ')'; - case 'Attr-start': - return '['; - case 'Attr-end': - return ']'; - case 'Whitespace': - return ' '; - case 'Colon': - return ':'; - case 'Semi-colon': - return ';'; - case 'Comma': - return ','; - case 'Important': - return '!important'; - case 'Attr': - return '[' + token.chi.reduce((acc, curr) => acc + renderToken(curr, options), '') + ']'; - case 'Time': - case 'Frequency': - case 'Angle': - case 'Length': - case 'Dimension': - const val = (+token.val).toString(); - if (val === '0') { - if (token.typ == 'Time') { - return '0s'; - } - if (token.typ == 'Frequency') { - return '0Hz'; - } - // @ts-ignore - if (token.typ == 'Resolution') { - return '0x'; - } - return '0'; - } - const chr = val.charAt(0); - if (chr == '-') { - const slice = val.slice(0, 2); - if (slice == '-0') { - return (val.length == 2 ? '0' : '-' + val.slice(2)) + token.unit; - } - } - else if (chr == '0') { - return val.slice(1) + token.unit; - } - return val + token.unit; - case 'Perc': - return token.val + '%'; - case 'Number': - const num = (+token.val).toString(); - if (token.val.length < num.length) { - return token.val; - } - if (num.charAt(0) === '0' && num.length > 1) { - return num.slice(1); - } - const slice = num.slice(0, 2); - if (slice == '-0') { - return '-' + num.slice(2); - } - return num; - case 'Comment': - if (options.removeComments) { - return ''; - } - case 'Url-token': - case 'At-rule': - case 'Hash': - case 'Pseudo-class': - case 'Literal': - case 'String': - case 'Iden': - case 'Delim': - return /* options.compress && '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)}`); -} diff --git a/src/lib/renderer/render.ts b/src/lib/renderer/render.ts index cbaefa5..08a4702 100644 --- a/src/lib/renderer/render.ts +++ b/src/lib/renderer/render.ts @@ -8,11 +8,11 @@ import { RenderOptions, RenderResult, Token } from "../../@types"; -import {cmyk2hex, hsl2Hex, hwb2hex, NAMES_COLORS, rgb2Hex} from "./utils"; +import {cmyk2hex, COLORS_NAMES, hsl2Hex, hwb2hex, NAMES_COLORS, rgb2Hex} from "./utils"; export function render(data: AstNode, opt: RenderOptions = {}): RenderResult { - const options = Object.assign(opt.compress ? { + const options = Object.assign(opt.minify ?? true ? { indent: '', newLine: '', removeComments: true @@ -144,9 +144,14 @@ export function renderToken(token: Token, options: RenderOptions = {}): string { case 'Color': - if (options.compress || options.colorConvert) { + if (options.minify || options.colorConvert) { - let value: string = token.kin == 'hex' ? token.val.toLowerCase() : ''; + if (token.kin == 'lit' && token.val.toLowerCase() == 'currentcolor') { + + return 'currentcolor'; + } + + let value: string = token.kin == 'hex' ? token.val.toLowerCase() : (token.kin == 'lit' ? COLORS_NAMES[token.val.toLowerCase()] : ''); if (token.val == 'rgb' || token.val == 'rgba') { @@ -206,7 +211,7 @@ export function renderToken(token: Token, options: RenderOptions = {}): string { case 'Pseudo-class-func': // @ts-ignore - return (/* options.compress && 'Pseudo-class-func' == token.typ && token.val.slice(0, 2) == '::' ? token.val.slice(1) :*/ token.val ?? '') + '(' + token.chi.reduce((acc: string, curr: Token) => { + return (/* options.minify && 'Pseudo-class-func' == token.typ && token.val.slice(0, 2) == '::' ? token.val.slice(1) :*/ token.val ?? '') + '(' + token.chi.reduce((acc: string, curr: Token) => { if (options.removeComments && curr.typ == 'Comment') { @@ -350,7 +355,7 @@ export function renderToken(token: Token, options: RenderOptions = {}): string { case 'String': case 'Iden': case 'Delim': - return /* options.compress && 'Pseudo-class' == token.typ && '::' == token.val.slice(0, 2) ? token.val.slice(1) : */token.val; + 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)}`); diff --git a/src/lib/renderer/utils/color.js b/src/lib/renderer/utils/color.js deleted file mode 100644 index f770b88..0000000 --- a/src/lib/renderer/utils/color.js +++ /dev/null @@ -1,495 +0,0 @@ -// name to color -export const COLORS_NAMES = Object.seal({ - 'aliceblue': '#f0f8ff', - 'antiquewhite': '#faebd7', - 'aqua': '#00ffff', - 'aquamarine': '#7fffd4', - 'azure': '#f0ffff', - 'beige': '#f5f5dc', - 'bisque': '#ffe4c4', - 'black': '#000000', - 'blanchedalmond': '#ffebcd', - 'blue': '#0000ff', - 'blueviolet': '#8a2be2', - 'brown': '#a52a2a', - 'burlywood': '#deb887', - 'cadetblue': '#5f9ea0', - 'chartreuse': '#7fff00', - 'chocolate': '#d2691e', - 'coral': '#ff7f50', - 'cornflowerblue': '#6495ed', - 'cornsilk': '#fff8dc', - 'crimson': '#dc143c', - 'cyan': '#00ffff', - 'darkblue': '#00008b', - 'darkcyan': '#008b8b', - 'darkgoldenrod': '#b8860b', - 'darkgray': '#a9a9a9', - 'darkgrey': '#a9a9a9', - 'darkgreen': '#006400', - 'darkkhaki': '#bdb76b', - 'darkmagenta': '#8b008b', - 'darkolivegreen': '#556b2f', - 'darkorange': '#ff8c00', - 'darkorchid': '#9932cc', - 'darkred': '#8b0000', - 'darksalmon': '#e9967a', - 'darkseagreen': '#8fbc8f', - 'darkslateblue': '#483d8b', - 'darkslategray': '#2f4f4f', - 'darkslategrey': '#2f4f4f', - 'darkturquoise': '#00ced1', - 'darkviolet': '#9400d3', - 'deeppink': '#ff1493', - 'deepskyblue': '#00bfff', - 'dimgray': '#696969', - 'dimgrey': '#696969', - 'dodgerblue': '#1e90ff', - 'firebrick': '#b22222', - 'floralwhite': '#fffaf0', - 'forestgreen': '#228b22', - 'fuchsia': '#ff00ff', - 'gainsboro': '#dcdcdc', - 'ghostwhite': '#f8f8ff', - 'gold': '#ffd700', - 'goldenrod': '#daa520', - 'gray': '#808080', - 'grey': '#808080', - 'green': '#008000', - 'greenyellow': '#adff2f', - 'honeydew': '#f0fff0', - 'hotpink': '#ff69b4', - 'indianred': '#cd5c5c', - 'indigo': '#4b0082', - 'ivory': '#fffff0', - 'khaki': '#f0e68c', - 'lavender': '#e6e6fa', - 'lavenderblush': '#fff0f5', - 'lawngreen': '#7cfc00', - 'lemonchiffon': '#fffacd', - 'lightblue': '#add8e6', - 'lightcoral': '#f08080', - 'lightcyan': '#e0ffff', - 'lightgoldenrodyellow': '#fafad2', - 'lightgray': '#d3d3d3', - 'lightgrey': '#d3d3d3', - 'lightgreen': '#90ee90', - 'lightpink': '#ffb6c1', - 'lightsalmon': '#ffa07a', - 'lightseagreen': '#20b2aa', - 'lightskyblue': '#87cefa', - 'lightslategray': '#778899', - 'lightslategrey': '#778899', - 'lightsteelblue': '#b0c4de', - 'lightyellow': '#ffffe0', - 'lime': '#00ff00', - 'limegreen': '#32cd32', - 'linen': '#faf0e6', - 'magenta': '#ff00ff', - 'maroon': '#800000', - 'mediumaquamarine': '#66cdaa', - 'mediumblue': '#0000cd', - 'mediumorchid': '#ba55d3', - 'mediumpurple': '#9370d8', - 'mediumseagreen': '#3cb371', - 'mediumslateblue': '#7b68ee', - 'mediumspringgreen': '#00fa9a', - 'mediumturquoise': '#48d1cc', - 'mediumvioletred': '#c71585', - 'midnightblue': '#191970', - 'mintcream': '#f5fffa', - 'mistyrose': '#ffe4e1', - 'moccasin': '#ffe4b5', - 'navajowhite': '#ffdead', - 'navy': '#000080', - 'oldlace': '#fdf5e6', - 'olive': '#808000', - 'olivedrab': '#6b8e23', - 'orange': '#ffa500', - 'orangered': '#ff4500', - 'orchid': '#da70d6', - 'palegoldenrod': '#eee8aa', - 'palegreen': '#98fb98', - 'paleturquoise': '#afeeee', - 'palevioletred': '#d87093', - 'papayawhip': '#ffefd5', - 'peachpuff': '#ffdab9', - 'peru': '#cd853f', - 'pink': '#ffc0cb', - 'plum': '#dda0dd', - 'powderblue': '#b0e0e6', - 'purple': '#800080', - 'red': '#ff0000', - 'rosybrown': '#bc8f8f', - 'royalblue': '#4169e1', - 'saddlebrown': '#8b4513', - 'salmon': '#fa8072', - 'sandybrown': '#f4a460', - 'seagreen': '#2e8b57', - 'seashell': '#fff5ee', - 'sienna': '#a0522d', - 'silver': '#c0c0c0', - 'skyblue': '#87ceeb', - 'slateblue': '#6a5acd', - 'slategray': '#708090', - 'slategrey': '#708090', - 'snow': '#fffafa', - 'springgreen': '#00ff7f', - 'steelblue': '#4682b4', - 'tan': '#d2b48c', - 'teal': '#008080', - 'thistle': '#d8bfd8', - 'tomato': '#ff6347', - 'turquoise': '#40e0d0', - 'violet': '#ee82ee', - 'wheat': '#f5deb3', - 'white': '#ffffff', - 'whitesmoke': '#f5f5f5', - 'yellow': '#ffff00', - 'yellowgreen': '#9acd32', - 'rebeccapurple': '#663399', - 'transparent': '#00000000' -}); -// color to name -export const NAMES_COLORS = Object.seal({ - '#f0f8ff': 'aliceblue', - '#faebd7': 'antiquewhite', - // '#00ffff': 'aqua', - '#7fffd4': 'aquamarine', - '#f0ffff': 'azure', - '#f5f5dc': 'beige', - '#ffe4c4': 'bisque', - '#000000': 'black', - '#ffebcd': 'blanchedalmond', - '#0000ff': 'blue', - '#8a2be2': 'blueviolet', - '#a52a2a': 'brown', - '#deb887': 'burlywood', - '#5f9ea0': 'cadetblue', - '#7fff00': 'chartreuse', - '#d2691e': 'chocolate', - '#ff7f50': 'coral', - '#6495ed': 'cornflowerblue', - '#fff8dc': 'cornsilk', - '#dc143c': 'crimson', - '#00ffff': 'cyan', - '#00008b': 'darkblue', - '#008b8b': 'darkcyan', - '#b8860b': 'darkgoldenrod', - // '#a9a9a9': 'darkgray', - '#a9a9a9': 'darkgrey', - '#006400': 'darkgreen', - '#bdb76b': 'darkkhaki', - '#8b008b': 'darkmagenta', - '#556b2f': 'darkolivegreen', - '#ff8c00': 'darkorange', - '#9932cc': 'darkorchid', - '#8b0000': 'darkred', - '#e9967a': 'darksalmon', - '#8fbc8f': 'darkseagreen', - '#483d8b': 'darkslateblue', - // '#2f4f4f': 'darkslategray', - '#2f4f4f': 'darkslategrey', - '#00ced1': 'darkturquoise', - '#9400d3': 'darkviolet', - '#ff1493': 'deeppink', - '#00bfff': 'deepskyblue', - // '#696969': 'dimgray', - '#696969': 'dimgrey', - '#1e90ff': 'dodgerblue', - '#b22222': 'firebrick', - '#fffaf0': 'floralwhite', - '#228b22': 'forestgreen', - // '#ff00ff': 'fuchsia', - '#dcdcdc': 'gainsboro', - '#f8f8ff': 'ghostwhite', - '#ffd700': 'gold', - '#daa520': 'goldenrod', - // '#808080': 'gray', - '#808080': 'grey', - '#008000': 'green', - '#adff2f': 'greenyellow', - '#f0fff0': 'honeydew', - '#ff69b4': 'hotpink', - '#cd5c5c': 'indianred', - '#4b0082': 'indigo', - '#fffff0': 'ivory', - '#f0e68c': 'khaki', - '#e6e6fa': 'lavender', - '#fff0f5': 'lavenderblush', - '#7cfc00': 'lawngreen', - '#fffacd': 'lemonchiffon', - '#add8e6': 'lightblue', - '#f08080': 'lightcoral', - '#e0ffff': 'lightcyan', - '#fafad2': 'lightgoldenrodyellow', - // '#d3d3d3': 'lightgray', - '#d3d3d3': 'lightgrey', - '#90ee90': 'lightgreen', - '#ffb6c1': 'lightpink', - '#ffa07a': 'lightsalmon', - '#20b2aa': 'lightseagreen', - '#87cefa': 'lightskyblue', - // '#778899': 'lightslategray', - '#778899': 'lightslategrey', - '#b0c4de': 'lightsteelblue', - '#ffffe0': 'lightyellow', - '#00ff00': 'lime', - '#32cd32': 'limegreen', - '#faf0e6': 'linen', - '#ff00ff': 'magenta', - '#800000': 'maroon', - '#66cdaa': 'mediumaquamarine', - '#0000cd': 'mediumblue', - '#ba55d3': 'mediumorchid', - '#9370d8': 'mediumpurple', - '#3cb371': 'mediumseagreen', - '#7b68ee': 'mediumslateblue', - '#00fa9a': 'mediumspringgreen', - '#48d1cc': 'mediumturquoise', - '#c71585': 'mediumvioletred', - '#191970': 'midnightblue', - '#f5fffa': 'mintcream', - '#ffe4e1': 'mistyrose', - '#ffe4b5': 'moccasin', - '#ffdead': 'navajowhite', - '#000080': 'navy', - '#fdf5e6': 'oldlace', - '#808000': 'olive', - '#6b8e23': 'olivedrab', - '#ffa500': 'orange', - '#ff4500': 'orangered', - '#da70d6': 'orchid', - '#eee8aa': 'palegoldenrod', - '#98fb98': 'palegreen', - '#afeeee': 'paleturquoise', - '#d87093': 'palevioletred', - '#ffefd5': 'papayawhip', - '#ffdab9': 'peachpuff', - '#cd853f': 'peru', - '#ffc0cb': 'pink', - '#dda0dd': 'plum', - '#b0e0e6': 'powderblue', - '#800080': 'purple', - '#ff0000': 'red', - '#bc8f8f': 'rosybrown', - '#4169e1': 'royalblue', - '#8b4513': 'saddlebrown', - '#fa8072': 'salmon', - '#f4a460': 'sandybrown', - '#2e8b57': 'seagreen', - '#fff5ee': 'seashell', - '#a0522d': 'sienna', - '#c0c0c0': 'silver', - '#87ceeb': 'skyblue', - '#6a5acd': 'slateblue', - // '#708090': 'slategray', - '#708090': 'slategrey', - '#fffafa': 'snow', - '#00ff7f': 'springgreen', - '#4682b4': 'steelblue', - '#d2b48c': 'tan', - '#008080': 'teal', - '#d8bfd8': 'thistle', - '#ff6347': 'tomato', - '#40e0d0': 'turquoise', - '#ee82ee': 'violet', - '#f5deb3': 'wheat', - '#ffffff': 'white', - '#f5f5f5': 'whitesmoke', - '#ffff00': 'yellow', - '#9acd32': 'yellowgreen', - '#663399': 'rebeccapurple', - '#00000000': 'transparent' -}); -export function rgb2Hex(token) { - let value = '#'; - let t; - // @ts-ignore - for (let i = 0; i < 6; i += 2) { - // @ts-ignore - t = token.chi[i]; - if (t == null) { - // console.debug({token}) - } - // @ts-ignore - value += Math.round(t.typ == 'Perc' ? 255 * t.val / 100 : t.val).toString(16).padStart(2, '0'); - } - // @ts-ignore - if (token.chi.length == 7) { - // @ts-ignore - t = token.chi[6]; - // @ts-ignore - if ((t.typ == 'Number' && t.val < 1) || - // @ts-ignore - (t.typ == 'Perc' && t.val < 100)) { - // @ts-ignore - value += Math.round(255 * (t.typ == 'Perc' ? t.val / 100 : t.val)).toString(16).padStart(2, '0'); - } - } - return value; -} -export function hsl2Hex(token) { - let t; - // @ts-ignore - let h = getAngle(token.chi[0]); - // @ts-ignore - t = token.chi[2]; - // @ts-ignore - let s = t.typ == 'Perc' ? t.val / 100 : t.val; - // @ts-ignore - t = token.chi[4]; - // @ts-ignore - let l = t.typ == 'Perc' ? t.val / 100 : t.val; - let a = null; - if (token.chi?.length == 7) { - // @ts-ignore - t = token.chi[6]; - // @ts-ignore - if ((t.typ == 'Perc' && t.val < 100) || - // @ts-ignore - (t.typ == 'Number' && t.val < 1)) { - // @ts-ignore - a = (t.typ == 'Perc' ? t.val / 100 : t.val); - } - } - return `#${hsl2rgb(h, s, l, a).reduce((acc, curr) => acc + curr.toString(16).padStart(2, '0'), '')}`; -} -export function hwb2hex(token) { - let t; - // @ts-ignore - let h = getAngle(token.chi[0]); - // @ts-ignore - t = token.chi[2]; - // @ts-ignore - let white = t.typ == 'Perc' ? t.val / 100 : t.val; - // @ts-ignore - t = token.chi[4]; - // @ts-ignore - let black = t.typ == 'Perc' ? t.val / 100 : t.val; - let a = null; - if (token.chi?.length == 7) { - // @ts-ignore - t = token.chi[6]; - // @ts-ignore - if ((t.typ == 'Perc' && t.val < 100) || - // @ts-ignore - (t.typ == 'Number' && t.val < 1)) { - // @ts-ignore - a = (t.typ == 'Perc' ? t.val / 100 : t.val); - } - } - const rgb = hsl2rgb(h, 1, .5, a); - let value; - for (let i = 0; i < 3; i++) { - value = rgb[i] / 255; - value *= (1 - white - black); - value += white; - rgb[i] = Math.round(value * 255); - } - return `#${rgb.reduce((acc, curr) => acc + curr.toString(16).padStart(2, '0'), '')}`; -} -export function cmyk2hex(token) { - // @ts-ignore - let t = token.chi[0]; - // @ts-ignore - const c = t.typ == 'Perc' ? t.val / 100 : t.val; - // @ts-ignore - t = token.chi[2]; - // @ts-ignore - const m = t.typ == 'Perc' ? t.val / 100 : t.val; - // @ts-ignore - t = token.chi[4]; - // @ts-ignore - const y = t.typ == 'Perc' ? t.val / 100 : t.val; - // @ts-ignore - t = token.chi[6]; - // @ts-ignore - const k = t.typ == 'Perc' ? t.val / 100 : t.val; - const rgb = [ - Math.round(255 * (1 - Math.min(1, c * (1 - k) + k))), - Math.round(255 * (1 - Math.min(1, m * (1 - k) + k))), - Math.round(255 * (1 - Math.min(1, y * (1 - k) + k))) - ]; - // @ts-ignore - if (token.chi.length >= 9) { - // @ts-ignore - t = token.chi[8]; - // @ts-ignore - rgb.push(Math.round(255 * (t.typ == 'Perc' ? t.val / 100 : t.val))); - } - return `#${rgb.reduce((acc, curr) => acc + curr.toString(16).padStart(2, '0'), '')}`; -} -function getAngle(token) { - if (token.typ == 'Dimension') { - switch (token.unit) { - case 'deg': - // @ts-ignore - return token.val / 360; - case 'rad': - // @ts-ignore - return token.val / (2 * Math.PI); - case 'grad': - // @ts-ignore - return token.val / 400; - case 'turn': - // @ts-ignore - return +token.val; - } - } - // @ts-ignore - return token.val / 360; -} -function hsl2rgb(h, s, l, a = null) { - let v = l <= .5 ? l * (1.0 + s) : l + s - l * s; - let r = l; - let g = l; - let b = l; - if (v > 0) { - let m = l + l - v; - let sv = (v - m) / v; - h *= 6.0; - let sextant = Math.floor(h); - let fract = h - sextant; - let vsf = v * sv * fract; - let mid1 = m + vsf; - let mid2 = v - vsf; - switch (sextant) { - case 0: - r = v; - g = mid1; - b = m; - break; - case 1: - r = mid2; - g = v; - b = m; - break; - case 2: - r = m; - g = v; - b = mid1; - break; - case 3: - r = m; - g = mid2; - b = v; - break; - case 4: - r = mid1; - g = m; - b = v; - break; - case 5: - r = v; - g = m; - b = mid2; - break; - } - } - const values = [Math.round(r * 255), Math.round(g * 255), Math.round(b * 255)]; - if (a != null && a != 1) { - values.push(Math.round(a * 255)); - } - return values; -} diff --git a/src/lib/renderer/utils/color.ts b/src/lib/renderer/utils/color.ts index 63c5996..214baed 100644 --- a/src/lib/renderer/utils/color.ts +++ b/src/lib/renderer/utils/color.ts @@ -318,11 +318,6 @@ export function rgb2Hex(token: ColorToken) { // @ts-ignore t = token.chi[i]; - if (t == null) { - - // console.debug({token}) - } - // @ts-ignore value += Math.round(t.typ == 'Perc' ? 255 * t.val / 100 : t.val).toString(16).padStart(2, '0') } diff --git a/src/lib/renderer/utils/index.js b/src/lib/renderer/utils/index.js deleted file mode 100644 index 64d2585..0000000 --- a/src/lib/renderer/utils/index.js +++ /dev/null @@ -1 +0,0 @@ -export * from './color'; diff --git a/src/lib/transform.js b/src/lib/transform.js deleted file mode 100644 index b1394ca..0000000 --- a/src/lib/transform.js +++ /dev/null @@ -1,19 +0,0 @@ -import { parse } from "./parser"; -import { render } from "./renderer"; -export async function transform(css, options = {}) { - options = { compress: 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` - } - }; -} diff --git a/src/lib/transform.ts b/src/lib/transform.ts index 746743f..5389ac9 100644 --- a/src/lib/transform.ts +++ b/src/lib/transform.ts @@ -4,7 +4,7 @@ import {render} from "./renderer"; export async function transform(css: string, options: TransformOptions = {}): Promise { - options = {compress: true, removeEmpty: true, ...options}; + options = {minify: true, removeEmpty: true, ...options}; const startTime: number = performance.now(); const parseResult: ParseResult = await parse(css, options); diff --git a/src/lib/walker/index.js b/src/lib/walker/index.js deleted file mode 100644 index c6de808..0000000 --- a/src/lib/walker/index.js +++ /dev/null @@ -1 +0,0 @@ -export * from './walk'; diff --git a/src/lib/walker/index.ts b/src/lib/walker/index.ts deleted file mode 100644 index 05dbcb5..0000000 --- a/src/lib/walker/index.ts +++ /dev/null @@ -1,2 +0,0 @@ - -export * from './walk'; \ No newline at end of file diff --git a/src/lib/walker/walk.js b/src/lib/walker/walk.js deleted file mode 100644 index f11e2aa..0000000 --- a/src/lib/walker/walk.js +++ /dev/null @@ -1,12 +0,0 @@ -export function* walk(node) { - // @ts-ignore - yield* doWalk(node, null, null); -} -function* doWalk(node, parent, root) { - yield { node, parent, root }; - if ('chi' in node) { - for (const child of node.chi) { - yield* doWalk(child, node, (root == null ? node : root)); - } - } -} diff --git a/src/node/index.js b/src/node/index.js deleted file mode 100644 index ecfe257..0000000 --- a/src/node/index.js +++ /dev/null @@ -1,11 +0,0 @@ -export * from '../lib'; -export * from './load'; -export * from '../lib/fs'; -import { parse as doParse, transform as doTransform } from "../lib"; -import { load, resolve } from "../node"; -export function parse(iterator, opt = {}) { - return doParse(iterator, Object.assign(opt, { load, resolve, cwd: opt.cwd ?? process.cwd() })); -} -export function transform(css, options = {}) { - return doTransform(css, Object.assign(options, { load, resolve, cwd: options.cwd ?? process.cwd() })); -} diff --git a/src/node/load.js b/src/node/load.js deleted file mode 100644 index 2ffe04e..0000000 --- a/src/node/load.js +++ /dev/null @@ -1,12 +0,0 @@ -import { readFile } from "fs/promises"; -import { resolve, matchUrl } from "../lib/fs"; -function parseResponse(response) { - if (!response.ok) { - throw new Error(`${response.status} ${response.statusText} ${response.url}`); - } - return response.text(); -} -export async function load(url, currentFile) { - const resolved = resolve(url, currentFile); - return matchUrl.test(resolved.absolute) ? fetch(resolved.absolute).then(parseResponse) : readFile(resolved.absolute, { encoding: 'utf-8' }); -} diff --git a/src/web/index.js b/src/web/index.js deleted file mode 100644 index 17e0e68..0000000 --- a/src/web/index.js +++ /dev/null @@ -1,19 +0,0 @@ -export * from '../lib'; -export * from './load'; -export * from '../lib/fs'; -import { parse as doParse, transform as doTransform } from "../lib"; -import { dirname, load, resolve } from "./index"; -export function parse(iterator, opt = {}) { - return doParse(iterator, Object.assign(opt, { - load, - resolve, - cwd: opt.cwd ?? self.location.pathname.endsWith('/') ? self.location.pathname : dirname(self.location.pathname) - })); -} -export function transform(css, options = {}) { - return doTransform(css, Object.assign(options, { - load, - resolve, - cwd: options.cwd ?? self.location.pathname.endsWith('/') ? self.location.pathname : dirname(self.location.pathname) - })); -} diff --git a/src/web/load.js b/src/web/load.js deleted file mode 100644 index 46753fc..0000000 --- a/src/web/load.js +++ /dev/null @@ -1,17 +0,0 @@ -import { matchUrl, resolve } from "../lib/fs"; -function parseResponse(response) { - if (!response.ok) { - throw new Error(`${response.status} ${response.statusText} ${response.url}`); - } - return response.text(); -} -export async function load(url, currentFile) { - if (matchUrl.test(url)) { - return fetch(url).then(parseResponse); - } - if (matchUrl.test(currentFile)) { - return fetch(new URL(url, currentFile)).then(parseResponse); - } - // return fetch(new URL(url, new URL(currentFile, self.location.href).href)).then(parseResponse); - return fetch(resolve(url, currentFile).absolute).then(parseResponse); -} diff --git a/test/files/css/blueprint.css b/test/files/css/blueprint.css index 85dcff9..ba61681 100644 --- a/test/files/css/blueprint.css +++ b/test/files/css/blueprint.css @@ -280,7 +280,7 @@ form.inline p { margin-bottom:0; } spanning 30px, and a 10px margin between columns. If you need fewer or more columns, namespaces or semantic - element names, use the compressor script (lib/compress.rb) + element names, use the compressor script (lib/minify.rb) -------------------------------------------------------------- */ diff --git a/test/specs/block.spec.js b/test/specs/block.spec.js index ae81135..3adc06e 100644 --- a/test/specs/block.spec.js +++ b/test/specs/block.spec.js @@ -37,7 +37,7 @@ describe('parse block', function () { width: 0; }`; return transform(file, { - compress: true + minify: true }).then(result => f(result.code).equals(`.clear,.clearfix:before{width:0;height:0}`)); }); @@ -54,7 +54,7 @@ describe('parse block', function () { width: 0; }`; return transform(file, { - compress: true + minify: true }).then(result => f(result.code).equals(`.clear,.clearfix:before{width:0;height:0}`)); }); @@ -68,7 +68,7 @@ border-bottom-color: gold; border-left-color: red; `; return transform(file, { - compress: true + minify: true }).then(result => f(result.code).equals(`.test input[type=text],a{border-color:gold red}`)); }); @@ -111,7 +111,7 @@ border-left-color: red; `; return transform(file, { - compress: true + minify: true }).then(result => f(result.code).equals(`.blockquote{margin-bottom:1rem;font-size:1.25rem}.blockquote>:last-child{margin-bottom:0}.blockquote-footer{margin-top:-1rem;margin-bottom:1rem;font-size:.875em;color:#6c757d}.blockquote-footer::before{content:"— "}.img-fluid,.img-thumbnail{max-width:100%;height:auto}.img-thumbnail{padding:.25rem;background-color:var(--bs-body-bg);border:var(--bs-border-width) solid var(--bs-border-color);border-radius:var(--bs-border-radius)}`)); }); @@ -125,7 +125,7 @@ border-left-color: red; `; return transform(file, { - compress: true + minify: true }).then(result => f(result.code).equals(`.nav-pills:is(.nav-link.active,.show>.nav-link){color:var(--bs-nav-pills-link-active-color);background-color:var(--bs-nav-pills-link-active-bg)}`)); }); @@ -138,7 +138,7 @@ border-left-color: red; `; return transform(file, { - compress: true + minify: true }).then(result => f(result.code).equals(`.card{--bs-card-inner-border-radius:calc(var(--bs-border-radius) - (var(--bs-border-width)))}`)); }); @@ -153,7 +153,7 @@ border-left-color: red; `; return transform(file, { - compress: true + minify: true }).then(result => f(result.code).equals(`@media (max-width:575.98px) and (prefers-reduced-motion:reduce){.offcanvas-sm{transition:none}}`)); }); @@ -167,7 +167,7 @@ border-left-color: red; `; return transform(file, { - compress: true + minify: true }).then(result => f(result.code).equals(`:root:is(.fa-flip-both,.fa-flip-horizontal,.fa-flip-vertical,.fa-rotate-180,.fa-rotate-270,.fa-rotate-90){-webkit-filter:none;filter:none}`)); }); @@ -177,7 +177,7 @@ border-left-color: red; `; return transform(file, { - compress: true + minify: true }).then(result => f(result.code).equals(`.bs-popover-auto[data-popper-placement^=top]>.popover-arrow,.bs-popover-top>.popover-arrow{bottom:calc(-1 * (var(--bs-popover-arrow-height)) - var(--bs-popover-border-width))}`)); }); @@ -195,7 +195,7 @@ abbr[title], abbr[data-original-title], abbr>[data-original-title] { `; return transform(file, { - compress: true + 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}`)); }); }); diff --git a/test/specs/block.web-spec.js b/test/specs/block.web-spec.js index 327a1fa..73d72ad 100644 --- a/test/specs/block.web-spec.js +++ b/test/specs/block.web-spec.js @@ -47,7 +47,7 @@ describe('parse block', function () { width: 0; }`; return transform(file, { - compress: true + minify: true }).then(result => f(result.code).equals(`.clear,.clearfix:before{width:0;height:0}`)); }); it('similar rules #5', function () { @@ -63,7 +63,7 @@ describe('parse block', function () { width: 0; }`; return transform(file, { - compress: true + minify: true }).then(result => f(result.code).equals(`.clear,.clearfix:before{width:0;height:0}`)); }); it('duplicated selector components #6', function () { @@ -76,7 +76,7 @@ border-bottom-color: gold; border-left-color: red; `; return transform(file, { - compress: true + minify: true }).then(result => f(result.code).equals(`.test input[type=text],a{border-color:gold red}`)); }); it('merge selectors #7', function () { @@ -118,7 +118,7 @@ border-left-color: red; `; return transform(file, { - compress: true + minify: true }).then(result => f(result.code).equals(`.blockquote{margin-bottom:1rem;font-size:1.25rem}.blockquote>:last-child{margin-bottom:0}.blockquote-footer{margin-top:-1rem;margin-bottom:1rem;font-size:.875em;color:#6c757d}.blockquote-footer::before{content:"— "}.img-fluid,.img-thumbnail{max-width:100%;height:auto}.img-thumbnail{padding:.25rem;background-color:var(--bs-body-bg);border:var(--bs-border-width) solid var(--bs-border-color);border-radius:var(--bs-border-radius)}`)); }); }); diff --git a/test/specs/import.spec.js b/test/specs/import.spec.js index 6264a93..22c9618 100644 --- a/test/specs/import.spec.js +++ b/test/specs/import.spec.js @@ -18,7 +18,7 @@ abbr[title], abbr[data-original-title] { describe('process import', function () { it('process import #1', function () { return transform(atRule, { - compress: true, + minify: true, resolveImport: true }).then((result) => f(result.code).equals(`p{color:#8133cc26}abbr:is([title],[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}`)); }); diff --git a/test/specs/import.web-spec.js b/test/specs/import.web-spec.js index 226e829..3a0fb45 100644 --- a/test/specs/import.web-spec.js +++ b/test/specs/import.web-spec.js @@ -25,8 +25,8 @@ abbr[title], abbr[data-original-title] { describe('process import', function () { it('process import #1', function () { return transform(atRule, { - compress: true, + minify: true, resolveImport: true - }).then(result => f(result.code).equals(`p{color:#8133cc26}abbr[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}`)); + }).then(result => f(result.code).equals(`p{color:#8133cc26}abbr:is([title],[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}`)); }); }); diff --git a/test/specs/import2.spec.js b/test/specs/import2.spec.js index c03f02f..bed0906 100644 --- a/test/specs/import2.spec.js +++ b/test/specs/import2.spec.js @@ -9,7 +9,7 @@ const import1 = `@import 'https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5. describe('process import', function () { it('process import #2', function () { return readFile(dirname(new URL(import.meta.url).pathname) + '/../files/result/font-awesome-all.css', {encoding: 'utf-8'}).then(file => transform(import1, { - compress: false, + minify: false, resolveImport: true }).then((result) => f(result.code).equals(file.trimEnd()))); }); diff --git a/test/specs/import2.web-spec.js b/test/specs/import2.web-spec.js index 93bb0e4..5dd487c 100644 --- a/test/specs/import2.web-spec.js +++ b/test/specs/import2.web-spec.js @@ -17,7 +17,7 @@ describe('process import', function () { return fetch(dirname(new URL(import.meta.url).pathname) + '/../files/result/font-awesome-all.css'). then(response => response.text()). then(file => transform(import1, { - compress: false, + minify: false, resolveImport: true }).then((result) => { // const a = document.createElement('a'); diff --git a/test/specs/import3.spec.js b/test/specs/import3.spec.js index e63438d..5857c22 100644 --- a/test/specs/import3.spec.js +++ b/test/specs/import3.spec.js @@ -9,8 +9,8 @@ describe('process import', function () { it('process import #3', function () { return readFile(dirname(new URL(import.meta.url).pathname) + '/../files/result/font-awesome-line-awesome.css', { encoding: 'utf-8' }). then(file => transform(import2, { - compress: true, + minify: true, resolveImport: true - }).then((result) => f(result.code).equals(file.trimEnd()))); + }).then((result) => f(result.code).equals(file.trim()))); }); }); diff --git a/test/specs/import3.web-spec.js b/test/specs/import3.web-spec.js index acc3a8c..d6ff64a 100644 --- a/test/specs/import3.web-spec.js +++ b/test/specs/import3.web-spec.js @@ -16,7 +16,7 @@ describe('process import', function () { return fetch(dirname(new URL(import.meta.url).pathname) + '/../files/result/font-awesome-line-awesome.css'). then(response => response.text()). then(file => transform(import2, { - compress: true, + minify: true, resolveImport: true }).then((result) => { // const a = document.createElement('a'); diff --git a/test/specs/nesting.spec.js b/test/specs/nesting.spec.js index 9a6f598..3ae87c8 100644 --- a/test/specs/nesting.spec.js +++ b/test/specs/nesting.spec.js @@ -18,8 +18,7 @@ describe('CSS Nesting', function () { } `; return transform(nesting1, { - compress: true, - nestingRules: true + minify: true, nestingRules: true }).then((result) => f(result.code).equals(`.nesting{color:hotpink;>.is{color:#639;>.awesome{color:#ff1493}}}`)); }); @@ -31,9 +30,7 @@ describe('CSS Nesting', function () { `; return transform(nesting2, { - compress: true, - nestingRules: true, - resolveImport: true + minify: true, nestingRules: true, resolveImport: true }).then((result) => f(result.code).equals(`.nav-link{&:focus,&:hover{color:var(--bs-nav-link-hover-color)}}`)); }); it('nesting #3', function () { @@ -49,9 +46,7 @@ describe('CSS Nesting', function () { `; return transform(nesting3, { - compress: true, - nestingRules: true, - resolveImport: true + minify: true, nestingRules: true, resolveImport: true }).then((result) => f(result.code).equals(`.nav-link{&:focus,&:hover{color:var(--bs-nav-link-hover-color)}&:focus-visible{outline:0;box-shadow:0 0 0 .25rem #0d6efd40}}`)); }); it('nesting #4', function () { @@ -74,9 +69,7 @@ describe('CSS Nesting', function () { `; return transform(nesting3, { - compress: true, - nestingRules: true, - resolveImport: true + minify: true, nestingRules: true, resolveImport: true }).then((result) => f(result.code).equals(`.nav-link{&:focus,&:hover{color:var(--bs-nav-link-hover-color)}&:focus-visible{outline:0;box-shadow:0 0 0 .25rem #0d6efd40}&.disabled{color:var(--bs-nav-link-disabled-color);pointer-events:none;cursor:default}}`)); }); it('nesting #5', function () { @@ -104,9 +97,7 @@ describe('CSS Nesting', function () { } `; return transform(nesting3, { - compress: true, - nestingRules: true, - resolveImport: true + minify: true, nestingRules: true, resolveImport: true }).then((result) => f(result.code).equals(`.nav-link{&:focus,&:hover{color:var(--bs-nav-link-hover-color)}&:focus-visible{outline:0;box-shadow:0 0 0 .25rem #0d6efd40}&.disabled,&:disabled{color:var(--bs-nav-link-disabled-color);pointer-events:none;cursor:default}}`)); }); it('nesting #6', function () { @@ -117,9 +108,7 @@ describe('CSS Nesting', function () { } `; return transform(nesting3, { - compress: true, - nestingRules: true, - resolveImport: true + minify: true, nestingRules: true, resolveImport: true }).then((result) => f(result.code).equals(`.form-floating{>.form-control,>.form-control-plaintext,>.form-select{height:calc(3.5rem + calc(var(--bs-border-width) * 2))}}`)); }); it('nesting #7', function () { @@ -156,9 +145,7 @@ describe('CSS Nesting', function () { `; return transform(nesting3, { - compress: true, - nestingRules: true, - resolveImport: true + minify: true, nestingRules: true, resolveImport: true }).then((result) => f(result.code).equals(`.form-floating{position:relative;>.form-control,>.form-control-plaintext,>.form-select{height:calc(3.5rem + calc(var(--bs-border-width) * 2));min-height:calc(3.5rem + calc(var(--bs-border-width) * 2));line-height:1.25;z-index:2;white-space:nowrap}>label{position:absolute;top:0;left:0;z-index:2;height:100%;padding:1rem .75rem;overflow:hidden;text-align:start;text-overflow:ellipsis;white-space:nowrap;pointer-events:none;border:var(--bs-border-width) solid #0000;transform-origin:0 0;transition:opacity .1s ease-in-out,transform .1s ease-in-out}}`)); }); @@ -204,9 +191,7 @@ describe('CSS Nesting', function () { } `; return transform(nesting3, { - compress: true, - nestingRules: true, - resolveImport: true + minify: true, nestingRules: true, resolveImport: true }).then((result) => f(result.code).equals(`.card{--bs-card-spacer-y:1rem;--bs-card-spacer-x:1rem;--bs-card-title-spacer-y:.5rem;--bs-card-border-width:var(--bs-border-width);--bs-card-border-color:var(--bs-border-color-translucent);--bs-card-border-radius:var(--bs-border-radius);--bs-card-inner-border-radius:calc(var(--bs-border-radius) - (var(--bs-border-width)));--bs-card-cap-padding-y:.5rem;--bs-card-cap-padding-x:1rem;--bs-card-cap-bg:rgba(var(--bs-body-color-rgb),.03);--bs-card-bg:var(--bs-body-bg);--bs-card-img-overlay-padding:1rem;--bs-card-group-margin:.75rem;position:relative;display:flex;flex-direction:column;min-width:0;height:var(--bs-card-height);color:var(--bs-body-color);word-wrap:break-word;background-color:var(--bs-card-bg);background-clip:border-box;border:var(--bs-card-border-width) solid var(--bs-card-border-color);border-radius:var(--bs-card-border-radius);>hr{margin-right:0;margin-left:0}}`)); }); it('nesting #9', function () { @@ -215,9 +200,7 @@ describe('CSS Nesting', function () { `; return transform(nesting3, { - compress: true, - nestingRules: true, - resolveImport: true + minify: true, nestingRules: true, resolveImport: true }).then((result) => f(result.code).equals(`.tab-content{>.tab-pane{display:none}>.active{display:block}}.navbar{--bs-navbar-padding-x:0;--bs-navbar-padding-y:.5rem;--bs-navbar-color:rgba(var(--bs-emphasis-color-rgb),.65);--bs-navbar-hover-color:rgba(var(--bs-emphasis-color-rgb),.8);--bs-navbar-disabled-color:rgba(var(--bs-emphasis-color-rgb),.3);--bs-navbar-active-color:rgba(var(--bs-emphasis-color-rgb),1);--bs-navbar-brand-padding-y:.3125rem;--bs-navbar-brand-margin-end:1rem;--bs-navbar-brand-font-size:1.25rem;--bs-navbar-brand-color:rgba(var(--bs-emphasis-color-rgb),1);--bs-navbar-brand-hover-color:rgba(var(--bs-emphasis-color-rgb),1);--bs-navbar-nav-link-padding-x:.5rem;--bs-navbar-toggler-padding-y:.25rem;--bs-navbar-toggler-padding-x:.75rem;--bs-navbar-toggler-font-size:1.25rem;--bs-navbar-toggler-icon-bg:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%2833, 37, 41, 0.75%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e");--bs-navbar-toggler-border-color:rgba(var(--bs-emphasis-color-rgb),.15);--bs-navbar-toggler-border-radius:var(--bs-border-radius);--bs-navbar-toggler-focus-width:.25rem;--bs-navbar-toggler-transition:box-shadow .15s ease-in-out;position:relative;display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;padding:var(--bs-navbar-padding-y) var(--bs-navbar-padding-x);>.container,>.container-fluid,>.container-lg,>.container-md,>.container-sm,>.container-xl,>.container-xxl{display:flex;flex-wrap:inherit;align-items:center;justify-content:space-between}}`)); }); @@ -237,9 +220,7 @@ describe('CSS Nesting', function () { `; return transform(nesting3, { - compress: true, - nestingRules: true, - resolveImport: true + minify: true, nestingRules: true, resolveImport: true }).then((result) => f(result.code).equals(`.navbar-text{padding-top:.5rem;padding-bottom:.5rem;color:var(--bs-navbar-color);a{&,&:focus,&:hover{color:var(--bs-navbar-active-color)}}}`)); }); @@ -256,9 +237,7 @@ describe('CSS Nesting', function () { `; return transform(nesting3, { - compress: true, - nestingRules: true, - resolveImport: true + minify: true, nestingRules: true, resolveImport: true }).then((result) => f(result.code).equals(`.table-bordered>:not(caption)>*{border-width:var(--bs-border-width) 0;>*{border-width:0 var(--bs-border-width)}}`)); }); @@ -299,9 +278,7 @@ describe('CSS Nesting', function () { } `; return transform(nesting3, { - compress: true, - nestingRules: true, - resolveImport: true + minify: true, nestingRules: true, resolveImport: true }).then((result) => f(result.code).equals(`.dropdown-item{display:block;width:100%;padding:var(--bs-dropdown-item-padding-y) var(--bs-dropdown-item-padding-x);clear:both;font-weight:400;color:var(--bs-dropdown-link-color);text-align:inherit;text-decoration:none;white-space:nowrap;background-color:#0000;border:0;border-radius:var(--bs-dropdown-item-border-radius,0);&:focus,&:hover{color:var(--bs-dropdown-link-hover-color);background-color:var(--bs-dropdown-link-hover-bg)}&.active,&:active{color:var(--bs-dropdown-link-active-color);text-decoration:none;background-color:var(--bs-dropdown-link-active-bg)}&.disabled,&:disabled{color:var(--bs-dropdown-link-disabled-color);pointer-events:none;background-color:#0000}}`)); }); @@ -322,9 +299,7 @@ describe('CSS Nesting', function () { `; return transform(nesting3, { - compress: true, - nestingRules: true, - resolveImport: true + minify: true, nestingRules: true, resolveImport: true }).then((result) => f(result.code).equals(`[data-bs-theme=dark]{.carousel .carousel-control-next-icon,.carousel .carousel-control-prev-icon,&.carousel .carousel-control-next-icon,&.carousel .carousel-control-prev-icon{filter:invert(1)grayscale(100)}.carousel .carousel-indicators [data-bs-target],&.carousel .carousel-indicators [data-bs-target]{background-color:#000}.carousel .carousel-caption,&.carousel .carousel-caption{color:#000}}`)); }); @@ -342,9 +317,7 @@ describe('CSS Nesting', function () { } `; return transform(nesting3, { - compress: true, - nestingRules: true, - resolveImport: true + minify: true, nestingRules: true, resolveImport: true }).then((result) => f(result.code).equals(`.demo.lg{&.triangle,&.circle{opacity:.25;filter:blur(25px)}}`)); }); @@ -358,9 +331,126 @@ describe('CSS Nesting', function () { `; return transform(nesting3, { - compress: true, - nestingRules: true, - resolveImport: true + minify: true, nestingRules: true, resolveImport: true }).then((result) => f(result.code).equals(`.nav-pills{.nav-link.active,.show>.nav-link{color:var(--bs-nav-pills-link-active-color);background-color:var(--bs-nav-pills-link-active-bg)}}`)); }); + + + it('merge selectors #16', function () { + const file = ` + +a { + color: rgba(var(--bs-link-color-rgb), var(--bs-link-opacity, 1)); + text-decoration: underline; +} + a :hover { + --bs-link-color-rgb: var(--bs-link-hover-color-rgb) + } + + a span { + --bs-link-color-rgb: var(--bs-link-hover-color-rgb) + } + +`; + return transform(file, { + nestingRules: true, + minify: true + }).then(result => f(result.code).equals(`a{color:rgba(var(--bs-link-color-rgb),var(--bs-link-opacity,1));text-decoration:underline;:hover,& span{--bs-link-color-rgb:var(--bs-link-hover-color-rgb)}}`)); + }); + + // see https://www.w3.org/TR/css-nesting-1/#conditionals + + /* + +// .parent { +// color: red; +// } +// .parent +// +// /* +// Valid because it begins with a combinator, which is a +// form of relative selector. +// */ +// > .descendant { +// border: 1px solid black; +// } +// .parent +// +// /* +// Valid because it's equivalent of *.img, which is a complex +// selector. Complex selectors are also a form of relative +// selector. +// */ +// .nested { +// font-style: italic; +// } +// .parent +// +// /* Not valid. Type (element) selectors are not relative selectors. */ +// img { +// box-shadow: 0 0 10px 5px rgba(0 0 0 .5); +// } +// } +// */ + /* + .foo { + display: grid; +} + +@media (width => 30em) { + .foo { + grid-auto-flow: column; + } +} + */ + /* + @media, @supports) + +@layer + +@scope + +@container + +*/ + +// it('nesting #16', function () { +// const nesting3 = ` +// .header { +// background-color: white; +// } +// +// .dark .header { +// background-color: blue; +// } +// `; +// return transform(nesting3, { +// minify: true, nestingRules: true, resolveImport: true +// }).then((result) => f(result.code).equals(`.nav-pills{.nav-link.active,.show>.nav-link{color:var(--bs-nav-pills-link-active-color);background-color:var(--bs-nav-pills-link-active-bg)}}`)); +// }); + +// it('nesting #17', function () { +// const nesting3 = ` +// .header { +// font-size: 40px; +// } +// +// @media (max-width: 760px) { +// .header { +// font-size: 24px; +// } +// } +// `; +// return transform(nesting3, { +// minify: true, nestingRules: true, resolveImport: true +// }).then((result) => f(result.code).equals(`.header { +// font-size: 40px +// +// @media (max-width: 760px ) { +// & { +// font-size: 24px; +// } +// } +// }`)); +// }); }); diff --git a/test/specs/shorthand.spec.js b/test/specs/shorthand.spec.js index 8fe8599..3226f81 100644 --- a/test/specs/shorthand.spec.js +++ b/test/specs/shorthand.spec.js @@ -3,7 +3,7 @@ import { expect as f } from '../../node_modules/@esm-bundle/chai/esm/chai.js'; import { transform } from '../../dist/node/index.js'; const options = { - compress: true, + minify: true, removeEmpty: true }; const marginPadding = ` @@ -173,4 +173,52 @@ describe('shorthand', function () { it('background #11', function () { return transform(background3, options).then(result => f(result.code).equals('a{background:no-repeat url(../../media/examples/firefox-logo.svg) 50%/cover,#eee url(../../media/examples/lizard.png) 35%/contain}')); }); + + it('border #12', function () { + return transform(`a{ + border: #333 solid; + border-width: 2px; + }`, options).then(result => f(result.code).equals('a{border:#333 solid 2px}')); + }); + + it('border #13', function () { + return transform(` +.test input[type="text"] { + +border: #333 solid 1px; + border-bottom-width: 2px; + border-left-width: thin;; + border-right-width: thin; + border-top-width:2px;;; + + }`, options).then(result => f(result.code).equals('.test input[type=text]{border:#333 solid 2px thin}')); + }); + + it('border #13', function () { + return transform(` +.test input[type="text"] { + +border: #333 solid 1px; + border-bottom-width: 2px; + border-left-width: medium;; + border-right-width: medium; + border-top-width:2px;;; + + }`, options).then(result => f(result.code).equals('.test input[type=text]{border:#333 solid 2px medium}')); + }); + + it('border #14', function () { + return transform(` +.test input[type="text"] { + +border: #333 solid 1px; + border-bottom-width: 2px; + border-left-width: medium;; + border-right-width: medium; + border-top-width:2px;;; + border-bottom-width: medium; + border-top-width:medium;;; + + }`, options).then(result => f(result.code).equals('.test input[type=text]{border:#333 solid}')); + }); }); diff --git a/test/specs/shorthand.web-spec.js b/test/specs/shorthand.web-spec.js index 3d3c2be..d49189c 100644 --- a/test/specs/shorthand.web-spec.js +++ b/test/specs/shorthand.web-spec.js @@ -12,7 +12,7 @@ function dirname(path) { } dirname(new URL(import.meta.url).pathname) + '/../files'; const options = { - compress: true, + minify: true, removeEmpty: true }; const marginPadding = ` diff --git a/tools/shorthand.ts b/tools/shorthand.ts index ae9aa9c..c114181 100644 --- a/tools/shorthand.ts +++ b/tools/shorthand.ts @@ -3,17 +3,19 @@ import { ShorthandMapType, ShorthandType, ShorthandPropertyType, - ShorthandDef + ShorthandDef, PropertyMapType } from "../src/@types"; function createProperties(data: ShorthandPropertyType) { + const map = data.map; return { [data.shorthand]: {...data}, ...data.properties.reduce((acc, property: string) => { - return Object.assign(acc, { - [property]: { + return Object.assign(acc,{ + [property]: { + map, shorthand: data.shorthand } }); @@ -48,6 +50,31 @@ function createMap(data: ShorthandDef, fields: Array) { // @ts-ignore export const map: ShorthandMapType = [ + [ + { + shorthand: 'border', + pattern: 'border-color border-style border-width', + keywords: ['none'], + default: ['0', 'none'] + }, + [ + { + shorthand: 'border-color', + properties: { + } + }, + { + shorthand: 'border-style', + properties: { + } + }, + { + shorthand: 'border-width', + properties: { + } + } + ] + ], [ { shorthand: 'outline', @@ -60,8 +87,8 @@ export const map: ShorthandMapType = [ shorthand: 'outline-color', properties: { types: ['Color'], - default: ['currentColor', 'invert'], - keywords: ['currentColor', 'invert'], + default: ['currentColor'], + keywords: ['currentColor'], } }, { @@ -194,7 +221,8 @@ export const map: ShorthandMapType = [ } ] ], - [{ + [ + { shorthand: 'background', pattern: 'background-repeat background-color background-image background-attachment background-clip background-origin background-position background-size', keywords: ['none'], @@ -225,6 +253,7 @@ export const map: ShorthandMapType = [ properties: { types: ['Color'], default: ['transparent'], + multiple: true, keywords: [] } }, @@ -241,6 +270,7 @@ export const map: ShorthandMapType = [ properties: { types: [], default: ['scroll'], + multiple: true, keywords: ['scroll', 'fixed', 'local'] } }, @@ -249,6 +279,7 @@ export const map: ShorthandMapType = [ properties: { types: [], default: ['border-box'], + multiple: true, keywords: ['border-box', 'padding-box', 'content-box', 'text'] } }, @@ -257,6 +288,7 @@ export const map: ShorthandMapType = [ properties: { types: [], default: ['padding-box'], + multiple: true, keywords: ['border-box', 'padding-box', 'content-box'] } }, @@ -345,6 +377,7 @@ export const properties: PropertySetType = [ }, { shorthand: 'border-width', + map: 'border', properties: [ 'border-top-width', 'border-right-width', @@ -354,26 +387,49 @@ export const properties: PropertySetType = [ types: ['Length', 'Perc'], // multiple: false, // separator: null, + default: ['medium'], keywords: ['thin', 'medium', 'thick'] }, { shorthand: 'border-style', + map: 'border', properties: ['border-top-style', 'border-right-style', 'border-bottom-style', 'border-left-style'], types: [], // multiple: false, // separator: null, + default: ['none'], keywords: ['none', 'hidden', 'dotted', 'dashed', 'solid', 'double', 'groove', 'ridge', 'inset', 'outset'] }, { shorthand: 'border-color', + map: 'border', properties: ['border-top-color', 'border-right-color', 'border-bottom-color', 'border-left-color'], types: ['Color'], // multiple: false, // separator: null, + default: ['currentcolor'], keywords: [] } ].reduce((acc: PropertySetType, data) => { + if (data.map) { + + // console.debug({data}); + + // @ts-ignore + (map[data.map].properties[data.shorthand]).types = data.types; + // @ts-ignore + (map[data.map].properties[data.shorthand]).default = data.default; + // @ts-ignore + (map[data.map].properties[data.shorthand]).keywords = data.keywords; + + // @ts-ignore + // (map[data.shorthand]).types = data.types; + // @ts-ignore + // (map[data.shorthand]).keywords = data.keywords; + + } + return Object.assign(acc, createProperties(data)); }, {});