diff --git a/dist/index.d.ts b/dist/index.d.ts index 28a42d9..0b56d3c 100644 --- a/dist/index.d.ts +++ b/dist/index.d.ts @@ -180,7 +180,7 @@ interface Position { interface Location { sta: Position; - end: Position; + // end: Position; src: string; } diff --git a/dist/index.js b/dist/index.js index 0cab091..c42595d 100644 --- a/dist/index.js +++ b/dist/index.js @@ -6,22 +6,16 @@ // https://www.w3.org/TR/CSS21/syndata.html#syntax // https://www.w3.org/TR/2021/CRD-css-syntax-3-20211224/#typedef-ident-token - // export const num = `(((\\+|-)?(?=\\d*[.eE])([0-9]+\\.?[0-9]*|\\.[0-9]+)([eE](\\+|-)?[0-9]+)?)|(\\d+|(\\d*\\.\\d+)))`; - // export const nl = `\n|\r\n|\r|\f`; - // export const nonascii = `[^\u{0}-\u{0ed}]`; - // export const unicode = `\\[0-9a-f]{1,6}(\r\n|[ \n\r\t\f])?`; - // export const escape = `(${unicode})|(\\[^\n\r\f0-9a-f])`; - // export const nmstart = `[_a-z]|${nonascii}|${escape}`; - // export const nmchar = `[_a-z0-9-]|${nonascii}|${escape}` - // export const ident = `[-]{0,2}(${nmstart})(${nmchar})*`; - // export const string1 = `\"([^\n\r\f\\"]|\\${nl}|${escape})*\"`; - // export const string2 = `\'([^\n\r\f\\']|\\${nl}|${escape})*\'`; - // export const string = `(${string1})|(${string2})`; - // - // const name = `${nmchar}+`; - // const hash = `#${name}`; // '\\' const REVERSE_SOLIDUS = 0x5c; + function isLengthUnit(dimension) { + return [ + '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); + } function isLetter(codepoint) { // lowercase return (codepoint >= 0x61 && codepoint <= 0x7a) || @@ -561,9 +555,6 @@ 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'); } @@ -907,6 +898,9 @@ case 'Important': return '!important'; case 'Dimension': + if (token.val === '0' && isLengthUnit(token)) { + return '0'; + } return token.val + token.unit; case 'Perc': return token.val + '%'; @@ -962,11 +956,12 @@ lin: 1, col: 1 }, - end: { - ind: -1, - lin: 1, - col: 0 - }, + // end: { + // + // ind: -1, + // lin: 1, + // col: 0 + // }, src: '' }; } @@ -1813,10 +1808,10 @@ const previousNode = stack.pop(); // @ts-ignore context = stack[stack.length - 1] || root; - if (options.location && context != root) { - // @ts-ignore - context.loc.end = { ind, lin, col: col == 0 ? 1 : col }; - } + // if (options.location && context != root) { + // @ts-ignore + // context.loc.end = {ind, lin, col: col == 0 ? 1 : col} + // } // @ts-ignore if (options.removeEmpty && previousNode != null && previousNode.chi.length == 0 && context.chi[context.chi.length - 1] == previousNode) { context.chi.pop(); @@ -1826,10 +1821,10 @@ buffer = ''; } // @ts-ignore - if (node != null && options.location && ['}', ';'].includes(value) && context.chi[context.chi.length - 1].loc.end == null) { - // @ts-ignore - context.chi[context.chi.length - 1].loc.end = { ind, lin, col }; - } + // if (node != null && options.location && ['}', ';'].includes(value) && context.chi[context.chi.length - 1].loc.end == null) { + // @ts-ignore + // context.chi[context.chi.length - 1].loc.end = {ind, lin, col}; + // } break; case '!': if (buffer.length > 0) { @@ -1856,19 +1851,27 @@ if (buffer.length > 0) { pushToken(getType(buffer)); } + if (tokens.length > 0) { + parseNode(tokens); + } + // console.debug({tokens}); // pushToken({typ: 'EOF'}); // - if (col == 0) { - col = 1; - } - if (options.location) { - // @ts-ignore - root.loc.end = { ind, lin, col }; - for (const context of stack) { - // @ts-ignore - context.loc.end = { ind, lin, col }; - } - } + // if (col == 0) { + // + // col = 1; + // } + // if (options.location) { + // + // // @ts-ignore + // root.loc.end = {ind, lin, col}; + // + // for (const context of stack) { + // + // // @ts-ignore + // context.loc.end = {ind, lin, col}; + // } + // } return root; } function getBlockType(chr) { @@ -1890,6 +1893,232 @@ throw new Error(`unhandled token: '${chr}'`); } + 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]); + }); + } + + 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 (this.declarations.has(this.config.shorthand)) { + let isValid = true; + const tokens = []; + for (let token of this.declarations.get(this.config.shorthand).val) { + if (this.config.types.includes(token.typ)) { + tokens.push(token); + continue; + } + if (token.typ != 'Whitespace' && token.typ != 'Comment') { + isValid = false; + break; + } + } + if (!isValid || tokens.length == 0) { + this.declarations.set(declaration.nam, declaration); + } + else { + this.declarations.delete(this.config.shorthand); + this.config.properties.forEach((property, index) => { + while (index >= tokens.length) { + index = Math.floor(index / 2); + } + this.declarations.set(property, { + typ: 'Declaration', + nam: property, + val: [tokens[index]].map((o) => { + return { ...o }; + }) + }); + }); + } + } + this.declarations.set(declaration.nam, declaration); + } + return this; + } + [Symbol.iterator]() { + let iterator; + const declarations = this.declarations; + if (declarations.size < this.config.properties.length) { + iterator = declarations.values(); + } + else { + const value = this.config.properties.reduce((acc, curr) => { + acc.val.push(...this.declarations.get(curr).val); + return acc; + }, { + typ: 'Declaration', + nam: this.config.shorthand, + val: [] + }); + let i = this.config.properties.length; + while (--i) { + const t = value.val[i]; + const k = value.val[Math.floor((i - 1) / 2)]; + if (t.val == k.val && t.val == '0') { + if ((t.typ == 'Number' && isLengthUnit(k)) || + (k.typ == 'Number' && isLengthUnit(t)) || + (isLengthUnit(k) || isLengthUnit(t))) { + value.val.splice(i, 1); + continue; + } + } + if (eq(t, k)) { + value.val.splice(i, 1); + continue; + } + break; + } + if (value.val.length > 1) { + const k = value.val.length * 2; + i = 0; + while (i < k) { + value.val.splice(i + 1, 0, { typ: 'Whitespace' }); + i += 2; + } + } + iterator = [value][Symbol.iterator](); + return { + next() { + return iterator.next(); + } + }; + } + return { + next() { + return iterator.next(); + } + }; + } + } + + var properties = { + margin: { + shorthand: "margin", + properties: [ + "margin-top", + "margin-right", + "margin-bottom", + "margin-left" + ], + types: [ + "Dimension", + "Number", + "Perc" + ] + }, + "margin-top": { + shorthand: "margin" + }, + "margin-right": { + shorthand: "margin" + }, + "margin-bottom": { + shorthand: "margin" + }, + "margin-left": { + shorthand: "margin" + }, + padding: { + shorthand: "padding", + properties: [ + "padding-top", + "padding-right", + "padding-bottom", + "padding-left" + ], + types: [ + "Dimension", + "Number", + "Perc" + ] + }, + "padding-top": { + shorthand: "padding" + }, + "padding-right": { + shorthand: "padding" + }, + "padding-bottom": { + shorthand: "padding" + }, + "padding-left": { + shorthand: "padding" + } + }; + var config$1 = { + properties: properties + }; + + const getConfig = () => config$1; + + const config = getConfig(); + class PropertyList { + declarations; + constructor() { + this.declarations = new Map; + } + add(declaration) { + if (declaration.typ != 'Declaration') { + this.declarations.set(this.declarations.size.toString(), declaration); + return this; + } + const propertyName = declaration.nam; + if (propertyName in config.properties) { + const shorthand = config.properties[propertyName].shorthand; + if (!this.declarations.has(shorthand)) { + this.declarations.set(shorthand, new PropertySet(config.properties[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) { + if (value.value instanceof PropertySet) { + 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; + } + }; + } + } + function parse(css, opt = {}) { const errors = []; const options = { @@ -1897,7 +2126,7 @@ location: false, processImport: false, deduplicate: false, - removeEmpty: false, + removeEmpty: true, ...opt }; if (css.length == 0) { @@ -1912,92 +2141,75 @@ return { ast, errors }; } function deduplicate(ast) { - if ('chi' in ast) { + // @ts-ignore + if (('chi' in ast) && ast.chi?.length > 0) { // @ts-ignore - let i = ast.chi.length; + let i = 0; let previous; let node; - while (i--) { + let nodeIndex; + // @ts-ignore + for (; i < ast.chi.length; i++) { // @ts-ignore node = ast.chi[i]; - // @ts-ignore if (node.typ == 'Comment') { continue; } - if (node.typ == 'AtRule' && node.nam == 'media' && node.val == 'all') { - // merge only if the previous rule contains only declarations - let shouldMerge = true; + if (node.typ == 'AtRule' && node.val == 'all') { // @ts-ignore - let i = node.chi.length; - while (i--) { - // @ts-ignore - if (node.chi[i].typ == 'Comment') { - continue; - } - // @ts-ignore - shouldMerge = node.chi[i].typ == 'Declaration'; - break; - } - if (!shouldMerge) { - continue; - } - // @ts-ignore - ast.chi.splice(i, 1, ...node.chi); - // @ts-ignore - i += node.chi.length; + ast.chi?.splice(i, 1, ...node.chi); + i--; continue; } - // @ts-ignore - if (node.typ == previous?.typ) { - if ((node.typ == 'Rule' && node.sel == previous.sel) || - (node.typ == 'AtRule' && - node.nam == previous.nam && - node.val == previous.val)) { - if ('chi' in node) { + if (('chi' in node)) { + // @ts-ignore + if (previous != null && previous.typ == node.typ) { + // @ts-ignore + if ((node.typ == 'Rule' && node.sel == previous.sel) || + // @ts-ignore + (node.typ == 'AtRule') && node.val == previous.val) { let shouldMerge = true; // @ts-ignore - let i = node.chi.length; - while (i--) { + let k = previous.chi.length; + while (k--) { // @ts-ignore - if (node.chi[i].typ == 'Comment') { + if (previous.chi[k].typ == 'Comment') { continue; } // @ts-ignore - shouldMerge = node.chi[i].typ == 'Declaration'; + shouldMerge = previous.chi[k].typ == 'Declaration'; break; } - if (!shouldMerge) { + if (shouldMerge) { + // @ts-ignore + node.chi.unshift(...previous.chi); + // @ts-ignore + ast.chi.splice(nodeIndex, 1); + i--; + previous = node; + nodeIndex = i; continue; } - // @ts-ignore - previous.chi = node.chi.concat(...(previous.chi || [])); - } - // @ts-ignore - ast.chi.splice(i, 1); - if (!('chi' in previous)) { - continue; } + } + // @ts-ignore + if (previous != null && previous != node && 'chi' in previous) { // @ts-ignore - if (previous.typ == 'Rule' || previous.chi.some(n => n.typ == 'Declaration')) { + if (previous.chi.some(n => n.typ == 'Declaration')) { deduplicateRule(previous); } else { deduplicate(previous); } - continue; - } - else if (node.typ == 'Declaration' && node.nam == previous.nam) { - // @ts-ignore - ast.chi.splice(i, 1); - continue; } } previous = node; - if (!('chi' in node)) { - continue; - } + nodeIndex = i; + } + // @ts-ignore + if (node != null && ('chi' in node)) { // @ts-ignore - if (node.typ == 'AtRule' || node.chi.some(n => n.typ == 'Declaration')) { + if (node.chi.some(n => n.typ == 'Declaration')) { deduplicateRule(node); } else { @@ -2011,25 +2223,24 @@ if (!('chi' in ast) || ast.chi?.length == 0) { return ast; } - const map = new Map; // @ts-ignore - let i = ast.chi.length; - let node; - while (i--) { - // @ts-ignore - node = ast.chi[i]; - if (node.typ != 'Declaration') { - continue; - } + const j = ast.chi.length; + let k = 0; + const properties = new PropertyList(); + for (; k < j; k++) { // @ts-ignore - if (map.has(node.nam)) { + if ('Comment' == ast.chi[k].typ || 'Declaration' == ast.chi[k].typ) { // @ts-ignore - ast.chi.splice(i, 1); + properties.add(ast.chi[k]); continue; } - // @ts-ignore - map.set(node.nam, node); + break; } + // @ts-ignore + ast.chi = [...properties].concat(ast.chi.slice(k)); + // @ts-ignore + // ast.chi.splice(0, k - 1, ...properties); + // console.debug({k, removed}); return ast; } diff --git a/dist/index.mjs b/dist/index.mjs index f482062..bc1949f 100644 --- a/dist/index.mjs +++ b/dist/index.mjs @@ -1,21 +1,15 @@ // https://www.w3.org/TR/CSS21/syndata.html#syntax // https://www.w3.org/TR/2021/CRD-css-syntax-3-20211224/#typedef-ident-token -// export const num = `(((\\+|-)?(?=\\d*[.eE])([0-9]+\\.?[0-9]*|\\.[0-9]+)([eE](\\+|-)?[0-9]+)?)|(\\d+|(\\d*\\.\\d+)))`; -// export const nl = `\n|\r\n|\r|\f`; -// export const nonascii = `[^\u{0}-\u{0ed}]`; -// export const unicode = `\\[0-9a-f]{1,6}(\r\n|[ \n\r\t\f])?`; -// export const escape = `(${unicode})|(\\[^\n\r\f0-9a-f])`; -// export const nmstart = `[_a-z]|${nonascii}|${escape}`; -// export const nmchar = `[_a-z0-9-]|${nonascii}|${escape}` -// export const ident = `[-]{0,2}(${nmstart})(${nmchar})*`; -// export const string1 = `\"([^\n\r\f\\"]|\\${nl}|${escape})*\"`; -// export const string2 = `\'([^\n\r\f\\']|\\${nl}|${escape})*\'`; -// export const string = `(${string1})|(${string2})`; -// -// const name = `${nmchar}+`; -// const hash = `#${name}`; // '\\' const REVERSE_SOLIDUS = 0x5c; +function isLengthUnit(dimension) { + return [ + '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); +} function isLetter(codepoint) { // lowercase return (codepoint >= 0x61 && codepoint <= 0x7a) || @@ -555,9 +549,6 @@ function rgb2Hex(token) { 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'); } @@ -901,6 +892,9 @@ function renderToken(token, options = {}) { case 'Important': return '!important'; case 'Dimension': + if (token.val === '0' && isLengthUnit(token)) { + return '0'; + } return token.val + token.unit; case 'Perc': return token.val + '%'; @@ -956,11 +950,12 @@ function tokenize(iterator, errors, options) { lin: 1, col: 1 }, - end: { - ind: -1, - lin: 1, - col: 0 - }, + // end: { + // + // ind: -1, + // lin: 1, + // col: 0 + // }, src: '' }; } @@ -1807,10 +1802,10 @@ function tokenize(iterator, errors, options) { const previousNode = stack.pop(); // @ts-ignore context = stack[stack.length - 1] || root; - if (options.location && context != root) { - // @ts-ignore - context.loc.end = { ind, lin, col: col == 0 ? 1 : col }; - } + // if (options.location && context != root) { + // @ts-ignore + // context.loc.end = {ind, lin, col: col == 0 ? 1 : col} + // } // @ts-ignore if (options.removeEmpty && previousNode != null && previousNode.chi.length == 0 && context.chi[context.chi.length - 1] == previousNode) { context.chi.pop(); @@ -1820,10 +1815,10 @@ function tokenize(iterator, errors, options) { buffer = ''; } // @ts-ignore - if (node != null && options.location && ['}', ';'].includes(value) && context.chi[context.chi.length - 1].loc.end == null) { - // @ts-ignore - context.chi[context.chi.length - 1].loc.end = { ind, lin, col }; - } + // if (node != null && options.location && ['}', ';'].includes(value) && context.chi[context.chi.length - 1].loc.end == null) { + // @ts-ignore + // context.chi[context.chi.length - 1].loc.end = {ind, lin, col}; + // } break; case '!': if (buffer.length > 0) { @@ -1850,19 +1845,27 @@ function tokenize(iterator, errors, options) { if (buffer.length > 0) { pushToken(getType(buffer)); } + if (tokens.length > 0) { + parseNode(tokens); + } + // console.debug({tokens}); // pushToken({typ: 'EOF'}); // - if (col == 0) { - col = 1; - } - if (options.location) { - // @ts-ignore - root.loc.end = { ind, lin, col }; - for (const context of stack) { - // @ts-ignore - context.loc.end = { ind, lin, col }; - } - } + // if (col == 0) { + // + // col = 1; + // } + // if (options.location) { + // + // // @ts-ignore + // root.loc.end = {ind, lin, col}; + // + // for (const context of stack) { + // + // // @ts-ignore + // context.loc.end = {ind, lin, col}; + // } + // } return root; } function getBlockType(chr) { @@ -1884,6 +1887,232 @@ function getBlockType(chr) { throw new Error(`unhandled token: '${chr}'`); } +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]); + }); +} + +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 (this.declarations.has(this.config.shorthand)) { + let isValid = true; + const tokens = []; + for (let token of this.declarations.get(this.config.shorthand).val) { + if (this.config.types.includes(token.typ)) { + tokens.push(token); + continue; + } + if (token.typ != 'Whitespace' && token.typ != 'Comment') { + isValid = false; + break; + } + } + if (!isValid || tokens.length == 0) { + this.declarations.set(declaration.nam, declaration); + } + else { + this.declarations.delete(this.config.shorthand); + this.config.properties.forEach((property, index) => { + while (index >= tokens.length) { + index = Math.floor(index / 2); + } + this.declarations.set(property, { + typ: 'Declaration', + nam: property, + val: [tokens[index]].map((o) => { + return { ...o }; + }) + }); + }); + } + } + this.declarations.set(declaration.nam, declaration); + } + return this; + } + [Symbol.iterator]() { + let iterator; + const declarations = this.declarations; + if (declarations.size < this.config.properties.length) { + iterator = declarations.values(); + } + else { + const value = this.config.properties.reduce((acc, curr) => { + acc.val.push(...this.declarations.get(curr).val); + return acc; + }, { + typ: 'Declaration', + nam: this.config.shorthand, + val: [] + }); + let i = this.config.properties.length; + while (--i) { + const t = value.val[i]; + const k = value.val[Math.floor((i - 1) / 2)]; + if (t.val == k.val && t.val == '0') { + if ((t.typ == 'Number' && isLengthUnit(k)) || + (k.typ == 'Number' && isLengthUnit(t)) || + (isLengthUnit(k) || isLengthUnit(t))) { + value.val.splice(i, 1); + continue; + } + } + if (eq(t, k)) { + value.val.splice(i, 1); + continue; + } + break; + } + if (value.val.length > 1) { + const k = value.val.length * 2; + i = 0; + while (i < k) { + value.val.splice(i + 1, 0, { typ: 'Whitespace' }); + i += 2; + } + } + iterator = [value][Symbol.iterator](); + return { + next() { + return iterator.next(); + } + }; + } + return { + next() { + return iterator.next(); + } + }; + } +} + +var properties = { + margin: { + shorthand: "margin", + properties: [ + "margin-top", + "margin-right", + "margin-bottom", + "margin-left" + ], + types: [ + "Dimension", + "Number", + "Perc" + ] + }, + "margin-top": { + shorthand: "margin" + }, + "margin-right": { + shorthand: "margin" + }, + "margin-bottom": { + shorthand: "margin" + }, + "margin-left": { + shorthand: "margin" + }, + padding: { + shorthand: "padding", + properties: [ + "padding-top", + "padding-right", + "padding-bottom", + "padding-left" + ], + types: [ + "Dimension", + "Number", + "Perc" + ] + }, + "padding-top": { + shorthand: "padding" + }, + "padding-right": { + shorthand: "padding" + }, + "padding-bottom": { + shorthand: "padding" + }, + "padding-left": { + shorthand: "padding" + } +}; +var config$1 = { + properties: properties +}; + +const getConfig = () => config$1; + +const config = getConfig(); +class PropertyList { + declarations; + constructor() { + this.declarations = new Map; + } + add(declaration) { + if (declaration.typ != 'Declaration') { + this.declarations.set(this.declarations.size.toString(), declaration); + return this; + } + const propertyName = declaration.nam; + if (propertyName in config.properties) { + const shorthand = config.properties[propertyName].shorthand; + if (!this.declarations.has(shorthand)) { + this.declarations.set(shorthand, new PropertySet(config.properties[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) { + if (value.value instanceof PropertySet) { + 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; + } + }; + } +} + function parse(css, opt = {}) { const errors = []; const options = { @@ -1891,7 +2120,7 @@ function parse(css, opt = {}) { location: false, processImport: false, deduplicate: false, - removeEmpty: false, + removeEmpty: true, ...opt }; if (css.length == 0) { @@ -1906,92 +2135,75 @@ function parse(css, opt = {}) { return { ast, errors }; } function deduplicate(ast) { - if ('chi' in ast) { + // @ts-ignore + if (('chi' in ast) && ast.chi?.length > 0) { // @ts-ignore - let i = ast.chi.length; + let i = 0; let previous; let node; - while (i--) { + let nodeIndex; + // @ts-ignore + for (; i < ast.chi.length; i++) { // @ts-ignore node = ast.chi[i]; - // @ts-ignore if (node.typ == 'Comment') { continue; } - if (node.typ == 'AtRule' && node.nam == 'media' && node.val == 'all') { - // merge only if the previous rule contains only declarations - let shouldMerge = true; + if (node.typ == 'AtRule' && node.val == 'all') { // @ts-ignore - let i = node.chi.length; - while (i--) { - // @ts-ignore - if (node.chi[i].typ == 'Comment') { - continue; - } - // @ts-ignore - shouldMerge = node.chi[i].typ == 'Declaration'; - break; - } - if (!shouldMerge) { - continue; - } - // @ts-ignore - ast.chi.splice(i, 1, ...node.chi); - // @ts-ignore - i += node.chi.length; + ast.chi?.splice(i, 1, ...node.chi); + i--; continue; } - // @ts-ignore - if (node.typ == previous?.typ) { - if ((node.typ == 'Rule' && node.sel == previous.sel) || - (node.typ == 'AtRule' && - node.nam == previous.nam && - node.val == previous.val)) { - if ('chi' in node) { + if (('chi' in node)) { + // @ts-ignore + if (previous != null && previous.typ == node.typ) { + // @ts-ignore + if ((node.typ == 'Rule' && node.sel == previous.sel) || + // @ts-ignore + (node.typ == 'AtRule') && node.val == previous.val) { let shouldMerge = true; // @ts-ignore - let i = node.chi.length; - while (i--) { + let k = previous.chi.length; + while (k--) { // @ts-ignore - if (node.chi[i].typ == 'Comment') { + if (previous.chi[k].typ == 'Comment') { continue; } // @ts-ignore - shouldMerge = node.chi[i].typ == 'Declaration'; + shouldMerge = previous.chi[k].typ == 'Declaration'; break; } - if (!shouldMerge) { + if (shouldMerge) { + // @ts-ignore + node.chi.unshift(...previous.chi); + // @ts-ignore + ast.chi.splice(nodeIndex, 1); + i--; + previous = node; + nodeIndex = i; continue; } - // @ts-ignore - previous.chi = node.chi.concat(...(previous.chi || [])); - } - // @ts-ignore - ast.chi.splice(i, 1); - if (!('chi' in previous)) { - continue; } + } + // @ts-ignore + if (previous != null && previous != node && 'chi' in previous) { // @ts-ignore - if (previous.typ == 'Rule' || previous.chi.some(n => n.typ == 'Declaration')) { + if (previous.chi.some(n => n.typ == 'Declaration')) { deduplicateRule(previous); } else { deduplicate(previous); } - continue; - } - else if (node.typ == 'Declaration' && node.nam == previous.nam) { - // @ts-ignore - ast.chi.splice(i, 1); - continue; } } previous = node; - if (!('chi' in node)) { - continue; - } + nodeIndex = i; + } + // @ts-ignore + if (node != null && ('chi' in node)) { // @ts-ignore - if (node.typ == 'AtRule' || node.chi.some(n => n.typ == 'Declaration')) { + if (node.chi.some(n => n.typ == 'Declaration')) { deduplicateRule(node); } else { @@ -2005,25 +2217,24 @@ function deduplicateRule(ast) { if (!('chi' in ast) || ast.chi?.length == 0) { return ast; } - const map = new Map; // @ts-ignore - let i = ast.chi.length; - let node; - while (i--) { - // @ts-ignore - node = ast.chi[i]; - if (node.typ != 'Declaration') { - continue; - } + const j = ast.chi.length; + let k = 0; + const properties = new PropertyList(); + for (; k < j; k++) { // @ts-ignore - if (map.has(node.nam)) { + if ('Comment' == ast.chi[k].typ || 'Declaration' == ast.chi[k].typ) { // @ts-ignore - ast.chi.splice(i, 1); + properties.add(ast.chi[k]); continue; } - // @ts-ignore - map.set(node.nam, node); + break; } + // @ts-ignore + ast.chi = [...properties].concat(ast.chi.slice(k)); + // @ts-ignore + // ast.chi.splice(0, k - 1, ...properties); + // console.debug({k, removed}); return ast; } diff --git a/package.json b/package.json index efe1f77..bd57507 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ }, "devDependencies": { "@esm-bundle/chai": "^4.3.4-fix.0", + "@rollup/plugin-json": "^6.0.0", "@rollup/plugin-node-resolve": "^15.0.1", "@rollup/plugin-terser": "^0.4.3", "@rollup/plugin-typescript": "^11.0.0", diff --git a/rollup.config.mjs b/rollup.config.mjs index 678f292..3e3de22 100644 --- a/rollup.config.mjs +++ b/rollup.config.mjs @@ -3,6 +3,7 @@ import typescript from "@rollup/plugin-typescript"; import nodeResolve from "@rollup/plugin-node-resolve"; import glob from "glob"; import terser from "@rollup/plugin-terser"; +import json from "@rollup/plugin-json"; export default [...await new Promise((resolve, reject) => { @@ -16,7 +17,7 @@ export default [...await new Promise((resolve, reject) => { resolve(files.map(input => { return { input, - plugins: [nodeResolve(), typescript()], + plugins: [nodeResolve(), json(), typescript()], output: { banner: `/* generate from ${input} */`, @@ -32,7 +33,7 @@ export default [...await new Promise((resolve, reject) => { })].concat([ { input: 'src/index.ts', - plugins: [nodeResolve(), typescript()], + plugins: [nodeResolve(), json(), typescript()], output: [ { file: './dist/index.mjs', @@ -47,7 +48,7 @@ export default [...await new Promise((resolve, reject) => { }, { input: 'src/index.ts', - plugins: [nodeResolve(), typescript(), terser()], + plugins: [nodeResolve(), typescript(), terser(), json()], output: [ { file: './dist/index.min.mjs', diff --git a/src/@types/index.d.ts b/src/@types/index.d.ts index 8c78695..57ac89a 100644 --- a/src/@types/index.d.ts +++ b/src/@types/index.d.ts @@ -54,7 +54,7 @@ export interface Position { export interface Location { sta: Position; - end: Position; + // end: Position; src: string; } diff --git a/src/config.json b/src/config.json index e88887e..cb8ed63 100644 --- a/src/config.json +++ b/src/config.json @@ -1,38 +1,56 @@ { - "properties": { - "margin": { - "shorthand": "margin", - "properties": [ "margin-top", "margin-left", "margin-bottom", "margin-right" ], - "types": [ "Dimension", "Number", "Perc" ] - }, - "margin-top": { - "shorthand": "margin" - }, - "margin-left": { - "shorthand": "margin" - }, - "margin-bottom": { - "shorthand": "margin" - }, - "margin-right": { - "shorthand": "margin" - }, - "padding": { - "shorthand": "padding", - "properties": [ "padding-top", "padding-left", "padding-bottom", "padding-right" ], - "types": [ "Dimension", "Number", "Perc" ] - }, - "padding-top": { - "shorthand": "padding" - }, - "padding-left": { - "shorthand": "padding" - }, - "padding-bottom": { - "shorthand": "padding" - }, - "padding-right": { - "shorthand": "padding" - } + "properties": { + "margin": { + "shorthand": "margin", + "properties": [ + "margin-top", + "margin-right", + "margin-bottom", + "margin-left" + ], + "types": [ + "Dimension", + "Number", + "Perc" + ] + }, + "margin-top": { + "shorthand": "margin" + }, + "margin-right": { + "shorthand": "margin" + }, + "margin-bottom": { + "shorthand": "margin" + }, + "margin-left": { + "shorthand": "margin" + }, + "padding": { + "shorthand": "padding", + "properties": [ + "padding-top", + "padding-right", + "padding-bottom", + "padding-left" + ], + "types": [ + "Dimension", + "Number", + "Perc" + ] + }, + "padding-top": { + "shorthand": "padding" + }, + "padding-right": { + "shorthand": "padding" + }, + "padding-bottom": { + "shorthand": "padding" + }, + "padding-left": { + "shorthand": "padding" } + } } diff --git a/src/parser/declaration/index.ts b/src/parser/declaration/index.ts index e69de29..0118b0f 100644 --- a/src/parser/declaration/index.ts +++ b/src/parser/declaration/index.ts @@ -0,0 +1,4 @@ + +export * from './list'; +export * from './set'; +export * from './map'; \ No newline at end of file diff --git a/src/parser/declaration/list.ts b/src/parser/declaration/list.ts index 35d696c..12599f0 100644 --- a/src/parser/declaration/list.ts +++ b/src/parser/declaration/list.ts @@ -1,41 +1,77 @@ import {AstDeclaration, AstNode} from "../../@types"; - -import config from '../../config.json' assert {type: 'json'}; import {PropertySet} from "./set"; +import {getConfig} from "../utils/config"; + +const config = getConfig(); export class PropertyList { - private declarations: Map; + protected declarations: Map; constructor() { - this.declarations = new Map; + this.declarations = new Map; } add(declaration: AstNode) { if (declaration.typ != 'Declaration') { - this.declarations.set(this.declarations.size, declaration); + this.declarations.set(this.declarations.size.toString(), declaration); return this; } const propertyName: string = (declaration).nam; - if (propertyName in config) { + if (propertyName in config.properties) { - const shorthand = config.properties[propertyName].shorthand; + const shorthand = config.properties[propertyName].shorthand; if (!this.declarations.has(shorthand)) { - this.declarations.set(shorthand, new PropertySet(config[propertyName])); + this.declarations.set(shorthand, new PropertySet(config.properties[shorthand])); } - this.declarations.get(shorthand) + (this.declarations.get(shorthand)).add(declaration); + return this; } this.declarations.set(propertyName, declaration); - return this; } + + [Symbol.iterator]() { + + let iterator: IterableIterator = this.declarations.values(); + const iterators: Array> = []; + + return { + next() { + + let value: IteratorResult = iterator.next(); + + while ((value.done && iterators.length > 0) || + value.value instanceof PropertySet) { + + if (value.value instanceof PropertySet) { + + 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; + } + } + } } \ No newline at end of file diff --git a/src/parser/declaration/map.ts b/src/parser/declaration/map.ts new file mode 100644 index 0000000..2de77a6 --- /dev/null +++ b/src/parser/declaration/map.ts @@ -0,0 +1,139 @@ +import {AstDeclaration, ShorthandPropertyType, Token} from "../../@types"; +import {eq} from "../utils/eq"; + + +export class PropertyMap { + + protected config: ShorthandPropertyType; + protected declarations: Map; + + constructor(config: ShorthandPropertyType) { + + this.config = config; + this.declarations = new Map; + } + + add(declaration: AstDeclaration) { + + if (declaration.nam == this.config.shorthand) { + + this.declarations.clear(); + this.declarations.set(declaration.nam, declaration); + } else { + + // expand shorthand + if (this.declarations.has(this.config.shorthand)) { + + let isValid: boolean = true; + const tokens: Token[] = []; + + for (let token of this.declarations.get(this.config.shorthand).val) { + + if (this.config.types.includes(token.typ)) { + + tokens.push(token); + continue; + } + + if (token.typ != 'Whitespace' && token.typ != 'Comment') { + + isValid = false; + break; + } + } + + if (!isValid || tokens.length == 0) { + + this.declarations.set(declaration.nam, declaration); + } else { + + this.declarations.delete(this.config.shorthand); + this.config.properties.forEach((property: string, index: number) => { + + while (index >= tokens.length) { + + index = Math.floor(index / 2); + } + + this.declarations.set(property, { + typ: 'Declaration', + nam: property, + val: [tokens[index]].map((o: Token) => { + + return {...o} + }) + }) + }) + } + } + + this.declarations.set(declaration.nam, declaration); + } + + return this; + } + + [Symbol.iterator]() { + + let iterator; + const declarations = this.declarations; + + if (declarations.size < this.config.properties.length) { + + iterator = declarations.values(); + } else { + + const value = this.config.properties.reduce((acc, curr) => { + + acc.val.push(...this.declarations.get(curr).val) + + return acc + }, { + + typ: 'Declaration', + nam: this.config.shorthand, + val: [] + }); + + let i = this.config.properties.length; + + while (--i > 0) { + + if (eq(value.val[i], value.val[Math.floor((i - 1) / 2)])) { + + value.val.splice(i, 1); + continue; + } + + break; + } + + const k: number = value.val.length * 2; + + i = 0; + + while (i < k) { + + value.val.splice(i + 1, 0, {typ: 'Whitespace'}); + i += 2; + } + + iterator = [value][Symbol.iterator](); + + return { + + next() { + + return iterator.next(); + } + } + } + + return { + next() { + + return iterator.next(); + } + } + } +} \ No newline at end of file diff --git a/src/parser/declaration/set.ts b/src/parser/declaration/set.ts index fac3424..0cf640d 100644 --- a/src/parser/declaration/set.ts +++ b/src/parser/declaration/set.ts @@ -1,9 +1,12 @@ -import {AstDeclaration, ShorthandPropertyType, Token} from "../../@types"; +import {AstDeclaration, DimensionToken, NumberToken, ShorthandPropertyType, Token} from "../../@types"; +import {eq} from "../utils/eq"; +import {isLengthUnit} from "../utils"; + export class PropertySet { - private config: ShorthandPropertyType; - private declarations: Map; + protected config: ShorthandPropertyType; + protected declarations: Map; constructor(config: ShorthandPropertyType) { @@ -13,43 +16,141 @@ export class PropertySet { add(declaration: AstDeclaration) { - if (declaration.nam == this.config.shorthand) { + if (declaration.nam == this.config.shorthand) { this.declarations.clear(); + this.declarations.set(declaration.nam, declaration); + } else { - let isValid = true; - const tokens: Token[] = []; + // expand shorthand + if (this.declarations.has(this.config.shorthand)) { - for (let token of declaration.val) { + let isValid: boolean = true; + const tokens: Token[] = []; - if (this.config.types.includes(token.typ)) { + for (let token of this.declarations.get(this.config.shorthand).val) { - tokens.push(token); - continue; + if (this.config.types.includes(token.typ)) { + + tokens.push(token); + continue; + } + + if (token.typ != 'Whitespace' && token.typ != 'Comment') { + + isValid = false; + break; + } } - if (token.typ != 'Whitespace' && token.typ != 'Comment') { + if (!isValid || tokens.length == 0) { + + this.declarations.set(declaration.nam, declaration); + } else { - isValid = false; - break; + this.declarations.delete(this.config.shorthand); + this.config.properties.forEach((property: string, index: number) => { + + while (index >= tokens.length) { + + index = Math.floor(index / 2); + } + + this.declarations.set(property, { + typ: 'Declaration', + nam: property, + val: [tokens[index]].map((o: Token) => { + + return {...o} + }) + }) + }) } } - if (!isValid) { + this.declarations.set(declaration.nam, declaration); + } + + return this; + } + + [Symbol.iterator]() { + + let iterator; + const declarations = this.declarations; + + if (declarations.size < this.config.properties.length) { + + iterator = declarations.values(); + } else { + + const value = this.config.properties.reduce((acc, curr) => { + + acc.val.push(...this.declarations.get(curr).val) + + return acc + }, { + + typ: 'Declaration', + nam: this.config.shorthand, + val: [] + }); + + let i = this.config.properties.length; - this.declarations.set( declaration.nam, declaration); + while (--i) { + + const t = value.val[i]; + const k = value.val[Math.floor((i - 1) / 2)]; + + if (t.val == k.val && t.val == '0') { + + if ((t.typ == 'Number' && isLengthUnit( k)) || + (k.typ == 'Number' && isLengthUnit( t)) || + (isLengthUnit(k) || isLengthUnit(t))) { + + value.val.splice(i, 1); + continue; + } + } + + if (eq(t, k)) { + + value.val.splice(i, 1); + continue; + } + + break; + } + + if (value.val.length > 1) { + + const k: number = value.val.length * 2; + i = 0; + + while (i < k) { + + value.val.splice(i + 1, 0, {typ: 'Whitespace'}); + i += 2; + } } - else { + iterator = [value][Symbol.iterator](); + + return { + next() { + + return iterator.next(); + } } } - else { + return { + next() { - this.declarations.set( declaration.nam, declaration); + return iterator.next(); + } } - - return this; } } \ No newline at end of file diff --git a/src/parser/parse.ts b/src/parser/parse.ts index a7b2a0f..cf369ae 100644 --- a/src/parser/parse.ts +++ b/src/parser/parse.ts @@ -1,11 +1,11 @@ import { - AstAtRule, AstDeclaration, - AstNode, AstRule, + AstAtRule, AstNode, AstRule, AstRuleStyleSheet, ErrorDescription, - ParserOptions, Token + ParserOptions } from "../@types"; import {tokenize} from "./tokenize"; +import {PropertyList} from "./declaration"; export function parse(css: string, opt: ParserOptions = {}): { ast: AstRuleStyleSheet; errors: ErrorDescription[] } { @@ -15,7 +15,7 @@ export function parse(css: string, opt: ParserOptions = {}): { ast: AstRuleStyle location: false, processImport: false, deduplicate: false, - removeEmpty: false, + removeEmpty: true, ...opt }; @@ -38,128 +38,98 @@ export function parse(css: string, opt: ParserOptions = {}): { ast: AstRuleStyle export function deduplicate(ast: AstNode) { - if ('chi' in ast) { + // @ts-ignore + if (('chi' in ast) && ast.chi?.length > 0) { // @ts-ignore - let i: number = ast.chi.length; + let i: number = 0; let previous: AstNode; let node: AstNode; + let nodeIndex: number; - while (i--) { + // @ts-ignore + for (; i < ast.chi.length; i++) { // @ts-ignore - node = ast.chi[i]; + node = ast.chi[i]; - // @ts-ignore if (node.typ == 'Comment') { continue; } - if (node.typ == 'AtRule' && (node).nam == 'media' && (node).val == 'all') { + if (node.typ == 'AtRule' && (node).val == 'all') { - // merge only if the previous rule contains only declarations - let shouldMerge = true; // @ts-ignore - let i = node.chi.length; - - while (i--) { - - // @ts-ignore - if (node.chi[i].typ == 'Comment') { - - continue; - } - - // @ts-ignore - shouldMerge = node.chi[i].typ == 'Declaration'; - break; - } - - if (!shouldMerge) { - - continue; - } - - // @ts-ignore - ast.chi.splice(i, 1, ...node.chi); - // @ts-ignore - i += node.chi.length; + ast.chi?.splice(i, 1, ...(node).chi); + i--; continue; } - // @ts-ignore - if (node.typ == previous?.typ) { + if (('chi' in node)) { - if ((node.typ == 'Rule' && (node).sel == (previous).sel) || - (node.typ == 'AtRule' && - (node).nam == (previous).nam && - (node).val == (previous).val - )) { + // @ts-ignore + if (previous != null && previous.typ == node.typ) { - if ('chi' in node) { + // @ts-ignore + if ((node.typ == 'Rule' && (node).sel == (previous).sel) || + // @ts-ignore + (node.typ == 'AtRule') && (node).val == (previous).val) { let shouldMerge = true; // @ts-ignore - let i: number = node.chi.length; + let k = previous.chi.length; - while (i--) { + while (k--) { // @ts-ignore - if (node.chi[i].typ == 'Comment') { + if (previous.chi[k].typ == 'Comment') { continue; } // @ts-ignore - shouldMerge = node.chi[i].typ == 'Declaration'; + shouldMerge = previous.chi[k].typ == 'Declaration'; break; } - if (!shouldMerge) { + if (shouldMerge) { + // @ts-ignore + node.chi.unshift(...previous.chi); + // @ts-ignore + ast.chi.splice(nodeIndex, 1); + i--; + previous = node; + nodeIndex = i; continue; } - - // @ts-ignore - previous.chi = node.chi.concat(...(previous.chi || [])); } + } - // @ts-ignore - ast.chi.splice(i, 1); - - if (!('chi' in previous)) { - - continue; - } + // @ts-ignore + if (previous != null && previous != node && 'chi' in previous) { // @ts-ignore - if (previous.typ == 'Rule' || previous.chi.some(n => n.typ == 'Declaration')) { + if (previous.chi.some(n => n.typ == 'Declaration')) { deduplicateRule(previous); } else { deduplicate(previous); } - - continue; - } else if (node.typ == 'Declaration' && (node).nam == (previous).nam) { - - // @ts-ignore - ast.chi.splice(i, 1); - continue; } } previous = node; + nodeIndex = i; + } - if (!('chi' in node)) { - - continue; - } + // @ts-ignore + if (node != null && ('chi' in node)) { // @ts-ignore - if (node.typ == 'AtRule' || node.chi.some(n => n.typ == 'Declaration')) { + if (node.chi.some(n => n.typ == 'Declaration')) { deduplicateRule(node); } else { @@ -179,33 +149,31 @@ export function deduplicateRule(ast: AstNode): AstNode { return ast; } - const map: Map = new Map; - // @ts-ignore - let i: number = ast.chi.length; - let node: AstNode; - - while (i--) { + const j: number = ast.chi.length; + let k: number = 0; - // @ts-ignore - node = ast.chi[i]; - - if (node.typ != 'Declaration') { + const properties: PropertyList = new PropertyList(); - continue; - } + for (; k < j; k++) { // @ts-ignore - if (map.has(node.nam)) { + if ('Comment' == ast.chi[k].typ || 'Declaration' == ast.chi[k].typ) { // @ts-ignore - ast.chi.splice(i, 1); + properties.add(ast.chi[k]); continue; } - // @ts-ignore - map.set(node.nam, node); + break; } + // @ts-ignore + ast.chi = [...properties].concat(ast.chi.slice(k)); + + // @ts-ignore + // ast.chi.splice(0, k - 1, ...properties); + + // console.debug({k, removed}); return ast; } \ No newline at end of file diff --git a/src/parser/tokenize.ts b/src/parser/tokenize.ts index 8f928ef..1f63d09 100644 --- a/src/parser/tokenize.ts +++ b/src/parser/tokenize.ts @@ -61,12 +61,12 @@ export function tokenize(iterator: string, errors: ErrorDescription[], options: col: 1 }, - end: { - - ind: -1, - lin: 1, - col: 0 - }, + // end: { + // + // ind: -1, + // lin: 1, + // col: 0 + // }, src: '' } } @@ -1337,11 +1337,11 @@ export function tokenize(iterator: string, errors: ErrorDescription[], options: // @ts-ignore context = stack[stack.length - 1] || root; - if (options.location && context != root) { + // if (options.location && context != root) { // @ts-ignore - context.loc.end = {ind, lin, col: col == 0 ? 1 : col} - } + // context.loc.end = {ind, lin, col: col == 0 ? 1 : col} + // } // @ts-ignore if (options.removeEmpty && previousNode != null && previousNode.chi.length == 0 && context.chi[context.chi.length - 1] == previousNode) { @@ -1355,11 +1355,11 @@ export function tokenize(iterator: string, errors: ErrorDescription[], options: } // @ts-ignore - if (node != null && options.location && ['}', ';'].includes(value) && context.chi[context.chi.length - 1].loc.end == null) { + // if (node != null && options.location && ['}', ';'].includes(value) && context.chi[context.chi.length - 1].loc.end == null) { // @ts-ignore - context.chi[context.chi.length - 1].loc.end = {ind, lin, col}; - } + // context.chi[context.chi.length - 1].loc.end = {ind, lin, col}; + // } break; @@ -1402,24 +1402,31 @@ export function tokenize(iterator: string, errors: ErrorDescription[], options: pushToken(getType(buffer)); } - // pushToken({typ: 'EOF'}); - // - if (col == 0) { + if (tokens.length > 0) { - col = 1; + parseNode(tokens); } - if (options.location) { - - // @ts-ignore - root.loc.end = {ind, lin, col}; + // console.debug({tokens}); - for (const context of stack) { + // pushToken({typ: 'EOF'}); + // + // if (col == 0) { + // + // col = 1; + // } - // @ts-ignore - context.loc.end = {ind, lin, col}; - } - } + // if (options.location) { + // + // // @ts-ignore + // root.loc.end = {ind, lin, col}; + // + // for (const context of stack) { + // + // // @ts-ignore + // context.loc.end = {ind, lin, col}; + // } + // } return root; } diff --git a/src/parser/utils/config.ts b/src/parser/utils/config.ts new file mode 100644 index 0000000..1eae50d --- /dev/null +++ b/src/parser/utils/config.ts @@ -0,0 +1,3 @@ +import config from '../../config.json' assert {type: 'json'}; + +export const getConfig = () => config; \ No newline at end of file diff --git a/src/parser/utils/eq.ts b/src/parser/utils/eq.ts new file mode 100644 index 0000000..89bf0b3 --- /dev/null +++ b/src/parser/utils/eq.ts @@ -0,0 +1,16 @@ +export function eq(a: { [key: string]: any }, b: { [key: string]: any }): boolean { + + if ((typeof a != 'object') || typeof b != 'object') { + + return a === b; + } + + const k1: string[] = Object.keys(a); + const k2: string[] = Object.keys(b); + + return k1.length == k2.length && + k1.every((key) => { + + return eq(a[key], b[key]) + }); +} \ No newline at end of file diff --git a/src/parser/utils/syntax.ts b/src/parser/utils/syntax.ts index 96dc12c..e192cb1 100644 --- a/src/parser/utils/syntax.ts +++ b/src/parser/utils/syntax.ts @@ -1,29 +1,22 @@ - // https://www.w3.org/TR/CSS21/syndata.html#syntax // https://www.w3.org/TR/2021/CRD-css-syntax-3-20211224/#typedef-ident-token -import exp from "constants"; -import {DimensionToken} from "../../@types"; - -// export const num = `(((\\+|-)?(?=\\d*[.eE])([0-9]+\\.?[0-9]*|\\.[0-9]+)([eE](\\+|-)?[0-9]+)?)|(\\d+|(\\d*\\.\\d+)))`; -// export const nl = `\n|\r\n|\r|\f`; -// export const nonascii = `[^\u{0}-\u{0ed}]`; -// export const unicode = `\\[0-9a-f]{1,6}(\r\n|[ \n\r\t\f])?`; -// export const escape = `(${unicode})|(\\[^\n\r\f0-9a-f])`; -// export const nmstart = `[_a-z]|${nonascii}|${escape}`; -// export const nmchar = `[_a-z0-9-]|${nonascii}|${escape}` -// export const ident = `[-]{0,2}(${nmstart})(${nmchar})*`; -// export const string1 = `\"([^\n\r\f\\"]|\\${nl}|${escape})*\"`; -// export const string2 = `\'([^\n\r\f\\']|\\${nl}|${escape})*\'`; -// export const string = `(${string1})|(${string2})`; -// -// const name = `${nmchar}+`; -// const hash = `#${name}`; +import {DimensionToken} from '../../@types'; // '\\' const REVERSE_SOLIDUS = 0x5c; -function isLetter(codepoint:number) { +export function isLengthUnit(dimension: DimensionToken): boolean { + + return [ + '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); +} + +function isLetter(codepoint: number) { // lowercase return (codepoint >= 0x61 && codepoint <= 0x7a) || @@ -133,7 +126,7 @@ export function isHash(name: string): boolean { return true; } -export function isNumber (name: string): boolean { +export function isNumber(name: string): boolean { if (name.length == 0) { @@ -145,7 +138,7 @@ export function isNumber (name: string): boolean { const j = name.length; // '+' '-' - if ([0x2b, 0x2d].includes(codepoint)){ + if ([0x2b, 0x2d].includes(codepoint)) { i++; } @@ -155,7 +148,7 @@ export function isNumber (name: string): boolean { codepoint = name.codePointAt(i); - if(isDigit(codepoint)) { + if (isDigit(codepoint)) { i++; continue; @@ -183,7 +176,7 @@ export function isNumber (name: string): boolean { codepoint = name.codePointAt(i); - if(isDigit(codepoint)) { + if (isDigit(codepoint)) { continue; } @@ -225,7 +218,7 @@ export function isNumber (name: string): boolean { codepoint = name.codePointAt(i); - if(!isDigit(codepoint)) { + if (!isDigit(codepoint)) { return false; } @@ -238,7 +231,7 @@ export function isDimension(name: string) { let index: number = 0; - while (index ++ < name.length) { + while (index++ < name.length) { if (isDigit(name.codePointAt(name.length - index))) { @@ -270,7 +263,7 @@ export function parseDimension(name: string): DimensionToken { let index: number = 0; - while (index ++ < name.length) { + while (index++ < name.length) { if (isDigit(name.codePointAt(name.length - index))) { @@ -296,7 +289,7 @@ export function isHexColor(name: string) { for (let chr of name.slice(1)) { - let codepoint = chr.codePointAt(0); + let codepoint = chr.codePointAt(0); if (!isDigit(codepoint) && // A-F @@ -313,14 +306,14 @@ export function isHexColor(name: string) { export function isHexDigit(name: string) { - if (name.length || name.length > 6) { + if (name.length || name.length > 6) { return false; } for (let chr of name) { - let codepoint = chr.codePointAt(0); + let codepoint = chr.codePointAt(0); if (!isDigit(codepoint) && // A F diff --git a/src/renderer/renderer.ts b/src/renderer/renderer.ts index 7c42065..04ddcde 100644 --- a/src/renderer/renderer.ts +++ b/src/renderer/renderer.ts @@ -9,6 +9,7 @@ import { Token } from "../@types"; import {cmyk2hex, hsl2Hex, hwb2hex, NAMES_COLORS, rgb2Hex} from "./utils"; +import {isLengthUnit} from "../parser/utils"; const indents: string[] = []; @@ -27,7 +28,7 @@ export function render(data: AstNode, opt: RenderOptions = {}) { colorConvert: true }, opt); - function reducer (acc: string, curr: Token): string { + function reducer(acc: string, curr: Token): string { if (curr.typ == 'Comment' && options.removeComments) { @@ -91,26 +92,20 @@ function doRender(data: AstNode, options: RenderOptions, reducer: Function, leve } // @ts-ignore - let children: string = ( data).chi.reduce((css: string, node: AstNode) => { + let children: string = (data).chi.reduce((css: string, node: AstNode) => { let str: string; if (node.typ == 'Comment') { str = options.removeComments ? '' : (node).val; - } - - else if (node.typ == 'Declaration') { + } else if (node.typ == 'Declaration') { str = `${(node).nam}:${options.indent}${(node).val.reduce(<() => string>reducer, '').trimEnd()};`; - } - - else if (node.typ == 'AtRule' && !('chi' in node)) { + } else if (node.typ == 'AtRule' && !('chi' in node)) { str = `@${(node).nam} ${(node).val};`; - } - - else { + } else { str = doRender(node, options, reducer, level + 1); } @@ -127,7 +122,7 @@ function doRender(data: AstNode, options: RenderOptions, reducer: Function, leve if (str !== '') - return `${css}${options.newLine}${indentSub}${str}`; + return `${css}${options.newLine}${indentSub}${str}`; }, ''); if (children.endsWith(';')) { @@ -137,10 +132,10 @@ function doRender(data: AstNode, options: RenderOptions, reducer: Function, leve if (data.typ == 'AtRule') { - return `@${(data).nam} ${(data).val ? (data).val + options.indent : ''}{${options.newLine}` + (children === '' ? '' : indentSub + children + options.newLine) + indent + `}` + return `@${(data).nam} ${(data).val ? (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 (data).sel + `${options.indent}{${options.newLine}` + (children === '' ? '' : indentSub + children + options.newLine) + indent + `}` } return ''; @@ -148,7 +143,7 @@ function doRender(data: AstNode, options: RenderOptions, reducer: Function, leve export function renderToken(token: Token, options: RenderOptions = {}) { - switch (token.typ ) { + switch (token.typ) { case 'Color': @@ -160,19 +155,13 @@ export function renderToken(token: Token, options: RenderOptions = {}) { if (token.val == 'rgb' || token.val == 'rgba') { value = rgb2Hex(token); - } - - else if (token.val == 'hsl' || token.val == 'hsla') { + } else if (token.val == 'hsl' || token.val == 'hsla') { value = hsl2Hex(token); - } - - else if (token.val == 'hwb') { + } else if (token.val == 'hwb') { value = hwb2hex(token); - } - - else if (token.val == 'device-cmyk') { + } else if (token.val == 'device-cmyk') { value = cmyk2hex(token); } @@ -184,14 +173,12 @@ export function renderToken(token: Token, options: RenderOptions = {}) { if (value.length == 7) { if (value[1] == value[2] && - value[3] == value[4] && - value[5] == value[6]) { + value[3] == value[4] && + value[5] == value[6]) { value = `#${value[1]}${value[3]}${value[5]}`; } - } - - else if (value.length == 9) { + } else if (value.length == 9) { if (value[1] == value[2] && value[3] == value[4] && @@ -212,6 +199,7 @@ export function renderToken(token: Token, options: RenderOptions = {}) { } case 'Func': + // @ts-ignore return token.val + '(' + token.chi.reduce((acc: string, curr: Token) => { @@ -265,9 +253,16 @@ export function renderToken(token: Token, options: RenderOptions = {}) { return '!important'; case 'Dimension': + + if (token.val === '0' && isLengthUnit(token)) { + + return '0'; + } + return token.val + (token).unit; case 'Perc': + return token.val + '%'; case 'Comment': diff --git a/src/renderer/utils/color.ts b/src/renderer/utils/color.ts index 8041f62..2565b3c 100644 --- a/src/renderer/utils/color.ts +++ b/src/renderer/utils/color.ts @@ -320,7 +320,7 @@ export function rgb2Hex(token: ColorToken) { if (t == null) { - console.debug({token}) + // 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/test/files/json/invalid-1.json b/test/files/json/invalid-1.json index 8086fac..82a191e 100644 --- a/test/files/json/invalid-1.json +++ b/test/files/json/invalid-1.json @@ -2,117 +2,110 @@ "typ": "StyleSheet", "chi": [ { - "typ": "AtRule", - "nam": "media", - "val": "all", + "typ": "Rule", + "sel": ":root,[data-color-mode=\"light\"][data-light-theme=\"light\"],[data-color-mode=\"dark\"][data-dark-theme=\"light\"]", "chi": [ { - "typ": "Rule", - "sel": ":root,[data-color-mode=\"light\"][data-light-theme=\"light\"],[data-color-mode=\"dark\"][data-dark-theme=\"light\"]", - "chi": [ + "typ": "Declaration", + "nam": "--color-canvas-default-transparent", + "val": [ { - "typ": "Declaration", - "nam": "--color-canvas-default-transparent", - "val": [ + "typ": "Color", + "val": "rgba", + "chi": [ { - "typ": "Color", - "val": "rgba", - "chi": [ - { - "typ": "Number", - "val": "255" - }, - { - "typ": "Comma" - }, - { - "typ": "Number", - "val": "255" - }, - { - "typ": "Comma" - }, - { - "typ": "Number", - "val": "255" - }, - { - "typ": "Comma" - }, - { - "typ": "Number", - "val": "0" - } - ], - "kin": "rgba" - } - ] - }, - { - "typ": "Declaration", - "nam": "--color-page-header-bg", - "val": [ + "typ": "Number", + "val": "255" + }, { - "typ": "Color", - "val": "#f6f8fa", - "kin": "hex" - } - ] - }, - { - "typ": "Declaration", - "nam": "--color-marketing-icon-primary", - "val": [ + "typ": "Comma" + }, { - "typ": "Color", - "val": "#218bff", - "kin": "hex" - } - ] - }, - { - "typ": "Declaration", - "nam": "--color-marketing-icon-secondary", - "val": [ + "typ": "Number", + "val": "255" + }, { - "typ": "Color", - "val": "#54aeff", - "kin": "hex" - } - ] - }, - { - "typ": "Declaration", - "nam": "--color-diff-blob-addition-num-text", - "val": [ + "typ": "Comma" + }, { - "typ": "Color", - "val": "#24292f", - "kin": "hex" - } - ] - }, - { - "typ": "Declaration", - "nam": "--color-diff-blob-addition-fg", - "val": [ + "typ": "Number", + "val": "255" + }, { - "typ": "Color", - "val": "#24292f", - "kin": "hex" - } - ] - }, - { - "typ": "Declaration", - "nam": "--color-diff-blob-addition-num-bg", - "val": [ + "typ": "Comma" + }, { - "typ": "Color", - "val": "#ccffd8", - "kin": "hex" + "typ": "Number", + "val": "0" } - ] + ], + "kin": "rgba" + } + ] + }, + { + "typ": "Declaration", + "nam": "--color-page-header-bg", + "val": [ + { + "typ": "Color", + "val": "#f6f8fa", + "kin": "hex" + } + ] + }, + { + "typ": "Declaration", + "nam": "--color-marketing-icon-primary", + "val": [ + { + "typ": "Color", + "val": "#218bff", + "kin": "hex" + } + ] + }, + { + "typ": "Declaration", + "nam": "--color-marketing-icon-secondary", + "val": [ + { + "typ": "Color", + "val": "#54aeff", + "kin": "hex" + } + ] + }, + { + "typ": "Declaration", + "nam": "--color-diff-blob-addition-num-text", + "val": [ + { + "typ": "Color", + "val": "#24292f", + "kin": "hex" + } + ] + }, + { + "typ": "Declaration", + "nam": "--color-diff-blob-addition-fg", + "val": [ + { + "typ": "Color", + "val": "#24292f", + "kin": "hex" + } + ] + }, + { + "typ": "Declaration", + "nam": "--color-diff-blob-addition-num-bg", + "val": [ + { + "typ": "Color", + "val": "#ccffd8", + "kin": "hex" } ] } diff --git a/test/files/json/invalid-2.json b/test/files/json/invalid-2.json index 8086fac..82a191e 100644 --- a/test/files/json/invalid-2.json +++ b/test/files/json/invalid-2.json @@ -2,117 +2,110 @@ "typ": "StyleSheet", "chi": [ { - "typ": "AtRule", - "nam": "media", - "val": "all", + "typ": "Rule", + "sel": ":root,[data-color-mode=\"light\"][data-light-theme=\"light\"],[data-color-mode=\"dark\"][data-dark-theme=\"light\"]", "chi": [ { - "typ": "Rule", - "sel": ":root,[data-color-mode=\"light\"][data-light-theme=\"light\"],[data-color-mode=\"dark\"][data-dark-theme=\"light\"]", - "chi": [ + "typ": "Declaration", + "nam": "--color-canvas-default-transparent", + "val": [ { - "typ": "Declaration", - "nam": "--color-canvas-default-transparent", - "val": [ + "typ": "Color", + "val": "rgba", + "chi": [ { - "typ": "Color", - "val": "rgba", - "chi": [ - { - "typ": "Number", - "val": "255" - }, - { - "typ": "Comma" - }, - { - "typ": "Number", - "val": "255" - }, - { - "typ": "Comma" - }, - { - "typ": "Number", - "val": "255" - }, - { - "typ": "Comma" - }, - { - "typ": "Number", - "val": "0" - } - ], - "kin": "rgba" - } - ] - }, - { - "typ": "Declaration", - "nam": "--color-page-header-bg", - "val": [ + "typ": "Number", + "val": "255" + }, { - "typ": "Color", - "val": "#f6f8fa", - "kin": "hex" - } - ] - }, - { - "typ": "Declaration", - "nam": "--color-marketing-icon-primary", - "val": [ + "typ": "Comma" + }, { - "typ": "Color", - "val": "#218bff", - "kin": "hex" - } - ] - }, - { - "typ": "Declaration", - "nam": "--color-marketing-icon-secondary", - "val": [ + "typ": "Number", + "val": "255" + }, { - "typ": "Color", - "val": "#54aeff", - "kin": "hex" - } - ] - }, - { - "typ": "Declaration", - "nam": "--color-diff-blob-addition-num-text", - "val": [ + "typ": "Comma" + }, { - "typ": "Color", - "val": "#24292f", - "kin": "hex" - } - ] - }, - { - "typ": "Declaration", - "nam": "--color-diff-blob-addition-fg", - "val": [ + "typ": "Number", + "val": "255" + }, { - "typ": "Color", - "val": "#24292f", - "kin": "hex" - } - ] - }, - { - "typ": "Declaration", - "nam": "--color-diff-blob-addition-num-bg", - "val": [ + "typ": "Comma" + }, { - "typ": "Color", - "val": "#ccffd8", - "kin": "hex" + "typ": "Number", + "val": "0" } - ] + ], + "kin": "rgba" + } + ] + }, + { + "typ": "Declaration", + "nam": "--color-page-header-bg", + "val": [ + { + "typ": "Color", + "val": "#f6f8fa", + "kin": "hex" + } + ] + }, + { + "typ": "Declaration", + "nam": "--color-marketing-icon-primary", + "val": [ + { + "typ": "Color", + "val": "#218bff", + "kin": "hex" + } + ] + }, + { + "typ": "Declaration", + "nam": "--color-marketing-icon-secondary", + "val": [ + { + "typ": "Color", + "val": "#54aeff", + "kin": "hex" + } + ] + }, + { + "typ": "Declaration", + "nam": "--color-diff-blob-addition-num-text", + "val": [ + { + "typ": "Color", + "val": "#24292f", + "kin": "hex" + } + ] + }, + { + "typ": "Declaration", + "nam": "--color-diff-blob-addition-fg", + "val": [ + { + "typ": "Color", + "val": "#24292f", + "kin": "hex" + } + ] + }, + { + "typ": "Declaration", + "nam": "--color-diff-blob-addition-num-bg", + "val": [ + { + "typ": "Color", + "val": "#ccffd8", + "kin": "hex" } ] } diff --git a/test/files/json/small.json b/test/files/json/small.json index 343db96..3b05769 100644 --- a/test/files/json/small.json +++ b/test/files/json/small.json @@ -10,139 +10,120 @@ "val": "/*@media all {\n*/" }, { - "typ": "AtRule", - "nam": "media", - "val": "all", + "typ": "Rule", + "sel": ":root,[data-color-mode=\"light\"][data-light-theme=\"light\"],[data-color-mode=\"dark\"][data-dark-theme=\"light\"]", "chi": [ { - "typ": "Rule", - "sel": ":root,[data-color-mode=\"light\"][data-light-theme=\"light\"],[data-color-mode=\"dark\"][data-dark-theme=\"light\"]", - "chi": [] - } - ] - }, - { - "typ": "AtRule", - "nam": "media", - "val": "all", - "chi": [ + "typ": "Declaration", + "nam": "-webkit-text-size-adjust", + "val": [ + { + "typ": "Perc", + "val": "100" + } + ] + }, { - "typ": "Rule", - "sel": ":root,[data-color-mode=\"light\"][data-light-theme=\"light\"],[data-color-mode=\"dark\"][data-dark-theme=\"light\"]", - "chi": [ + "typ": "Declaration", + "nam": "--color-canvas-default-transparent", + "val": [ { - "typ": "Declaration", - "nam": "-webkit-text-size-adjust", - "val": [ + "typ": "Color", + "val": "rgba", + "chi": [ { - "typ": "Perc", - "val": "100" - } - ] - }, - { - "typ": "Declaration", - "nam": "--color-canvas-default-transparent", - "val": [ + "typ": "Number", + "val": "255" + }, { - "typ": "Color", - "val": "rgba", - "chi": [ - { - "typ": "Number", - "val": "255" - }, - { - "typ": "Comma" - }, - { - "typ": "Number", - "val": "255" - }, - { - "typ": "Comma" - }, - { - "typ": "Number", - "val": "255" - }, - { - "typ": "Comma" - }, - { - "typ": "Number", - "val": "0" - } - ], - "kin": "rgba" - } - ] - }, - { - "typ": "Declaration", - "nam": "--color-page-header-bg", - "val": [ + "typ": "Comma" + }, { - "typ": "Color", - "val": "#f6f8fa", - "kin": "hex" - } - ] - }, - { - "typ": "Declaration", - "nam": "--color-marketing-icon-primary", - "val": [ + "typ": "Number", + "val": "255" + }, { - "typ": "Color", - "val": "#218bff", - "kin": "hex" - } - ] - }, - { - "typ": "Declaration", - "nam": "--color-marketing-icon-secondary", - "val": [ + "typ": "Comma" + }, { - "typ": "Color", - "val": "#54aeff", - "kin": "hex" - } - ] - }, - { - "typ": "Declaration", - "nam": "--color-diff-blob-addition-num-text", - "val": [ + "typ": "Number", + "val": "255" + }, { - "typ": "Color", - "val": "#24292f", - "kin": "hex" - } - ] - }, - { - "typ": "Declaration", - "nam": "--color-diff-blob-addition-fg", - "val": [ + "typ": "Comma" + }, { - "typ": "Color", - "val": "#24292f", - "kin": "hex" + "typ": "Number", + "val": "0" } - ] - }, + ], + "kin": "rgba" + } + ] + }, + { + "typ": "Declaration", + "nam": "--color-page-header-bg", + "val": [ { - "typ": "Declaration", - "nam": "--color-diff-blob-addition-num-bg", - "val": [ - { - "typ": "Color", - "val": "#ccffd8", - "kin": "hex" - } - ] + "typ": "Color", + "val": "#f6f8fa", + "kin": "hex" + } + ] + }, + { + "typ": "Declaration", + "nam": "--color-marketing-icon-primary", + "val": [ + { + "typ": "Color", + "val": "#218bff", + "kin": "hex" + } + ] + }, + { + "typ": "Declaration", + "nam": "--color-marketing-icon-secondary", + "val": [ + { + "typ": "Color", + "val": "#54aeff", + "kin": "hex" + } + ] + }, + { + "typ": "Declaration", + "nam": "--color-diff-blob-addition-num-text", + "val": [ + { + "typ": "Color", + "val": "#24292f", + "kin": "hex" + } + ] + }, + { + "typ": "Declaration", + "nam": "--color-diff-blob-addition-fg", + "val": [ + { + "typ": "Color", + "val": "#24292f", + "kin": "hex" + } + ] + }, + { + "typ": "Declaration", + "nam": "--color-diff-blob-addition-num-bg", + "val": [ + { + "typ": "Color", + "val": "#ccffd8", + "kin": "hex" } ] } diff --git a/test/files/json/smalli.json b/test/files/json/smalli.json index 214458e..723906c 100644 --- a/test/files/json/smalli.json +++ b/test/files/json/smalli.json @@ -15,127 +15,120 @@ "val": "/* @media all {\n*/" }, { - "typ": "AtRule", - "nam": "media", - "val": "all", + "typ": "Rule", + "sel": "div>:is(.inert),:root,[data-color-mode=\"light\"][data-light-theme=\"light\"],[data-color-mode=\"dark\"][data-dark-theme=\"light\"]", "chi": [ { - "typ": "Rule", - "sel": "div>:is(.inert),:root,[data-color-mode=\"light\"][data-light-theme=\"light\"],[data-color-mode=\"dark\"][data-dark-theme=\"light\"]", - "chi": [ + "typ": "Declaration", + "nam": "-webkit-text-size-adjust", + "val": [ { - "typ": "Declaration", - "nam": "-webkit-text-size-adjust", - "val": [ - { - "typ": "Perc", - "val": "100" - } - ] - }, + "typ": "Perc", + "val": "100" + } + ] + }, + { + "typ": "Declaration", + "nam": "--color-canvas-default-transparent", + "val": [ { - "typ": "Declaration", - "nam": "--color-canvas-default-transparent", - "val": [ + "typ": "Color", + "val": "rgba", + "chi": [ { - "typ": "Color", - "val": "rgba", - "chi": [ - { - "typ": "Number", - "val": "255" - }, - { - "typ": "Comma" - }, - { - "typ": "Number", - "val": "255" - }, - { - "typ": "Comma" - }, - { - "typ": "Number", - "val": "255" - }, - { - "typ": "Comma" - }, - { - "typ": "Number", - "val": ".2" - } - ], - "kin": "rgba" - } - ] - }, - { - "typ": "Declaration", - "nam": "--color-page-header-bg", - "val": [ + "typ": "Number", + "val": "255" + }, { - "typ": "Color", - "val": "#f6f8fa", - "kin": "hex" - } - ] - }, - { - "typ": "Declaration", - "nam": "--color-marketing-icon-primary", - "val": [ + "typ": "Comma" + }, { - "typ": "Color", - "val": "#218bff", - "kin": "hex" - } - ] - }, - { - "typ": "Declaration", - "nam": "--color-marketing-icon-secondary", - "val": [ + "typ": "Number", + "val": "255" + }, { - "typ": "Color", - "val": "#54aeff", - "kin": "hex" - } - ] - }, - { - "typ": "Declaration", - "nam": "--color-diff-blob-addition-num-text", - "val": [ + "typ": "Comma" + }, { - "typ": "Color", - "val": "#24292f", - "kin": "hex" - } - ] - }, - { - "typ": "Declaration", - "nam": "--color-diff-blob-addition-fg", - "val": [ + "typ": "Number", + "val": "255" + }, { - "typ": "Color", - "val": "#24292f", - "kin": "hex" - } - ] - }, - { - "typ": "Declaration", - "nam": "--color-diff-blob-addition-num-bg", - "val": [ + "typ": "Comma" + }, { - "typ": "Color", - "val": "#ccffd8", - "kin": "hex" + "typ": "Number", + "val": ".2" } - ] + ], + "kin": "rgba" + } + ] + }, + { + "typ": "Declaration", + "nam": "--color-page-header-bg", + "val": [ + { + "typ": "Color", + "val": "#f6f8fa", + "kin": "hex" + } + ] + }, + { + "typ": "Declaration", + "nam": "--color-marketing-icon-primary", + "val": [ + { + "typ": "Color", + "val": "#218bff", + "kin": "hex" + } + ] + }, + { + "typ": "Declaration", + "nam": "--color-marketing-icon-secondary", + "val": [ + { + "typ": "Color", + "val": "#54aeff", + "kin": "hex" + } + ] + }, + { + "typ": "Declaration", + "nam": "--color-diff-blob-addition-num-text", + "val": [ + { + "typ": "Color", + "val": "#24292f", + "kin": "hex" + } + ] + }, + { + "typ": "Declaration", + "nam": "--color-diff-blob-addition-fg", + "val": [ + { + "typ": "Color", + "val": "#24292f", + "kin": "hex" + } + ] + }, + { + "typ": "Declaration", + "nam": "--color-diff-blob-addition-num-bg", + "val": [ + { + "typ": "Color", + "val": "#ccffd8", + "kin": "hex" } ] } diff --git a/test/js/block.test.mjs b/test/js/block.test.mjs index e76f679..6254373 100644 --- a/test/js/block.test.mjs +++ b/test/js/block.test.mjs @@ -574,22 +574,16 @@ var o=e("type-detect");function r(){this._key="chai/deep-eql__"+Math.random()+Da // https://www.w3.org/TR/CSS21/syndata.html#syntax // https://www.w3.org/TR/2021/CRD-css-syntax-3-20211224/#typedef-ident-token -// export const num = `(((\\+|-)?(?=\\d*[.eE])([0-9]+\\.?[0-9]*|\\.[0-9]+)([eE](\\+|-)?[0-9]+)?)|(\\d+|(\\d*\\.\\d+)))`; -// export const nl = `\n|\r\n|\r|\f`; -// export const nonascii = `[^\u{0}-\u{0ed}]`; -// export const unicode = `\\[0-9a-f]{1,6}(\r\n|[ \n\r\t\f])?`; -// export const escape = `(${unicode})|(\\[^\n\r\f0-9a-f])`; -// export const nmstart = `[_a-z]|${nonascii}|${escape}`; -// export const nmchar = `[_a-z0-9-]|${nonascii}|${escape}` -// export const ident = `[-]{0,2}(${nmstart})(${nmchar})*`; -// export const string1 = `\"([^\n\r\f\\"]|\\${nl}|${escape})*\"`; -// export const string2 = `\'([^\n\r\f\\']|\\${nl}|${escape})*\'`; -// export const string = `(${string1})|(${string2})`; -// -// const name = `${nmchar}+`; -// const hash = `#${name}`; // '\\' const REVERSE_SOLIDUS = 0x5c; +function isLengthUnit(dimension) { + return [ + '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); +} function isLetter(codepoint) { // lowercase return (codepoint >= 0x61 && codepoint <= 0x7a) || @@ -1129,9 +1123,6 @@ function rgb2Hex(token) { 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'); } @@ -1390,6 +1381,9 @@ function renderToken(token, options = {}) { case 'Important': return '!important'; case 'Dimension': + if (token.val === '0' && isLengthUnit(token)) { + return '0'; + } return token.val + token.unit; case 'Perc': return token.val + '%'; @@ -1445,11 +1439,12 @@ function tokenize(iterator, errors, options) { lin: 1, col: 1 }, - end: { - ind: -1, - lin: 1, - col: 0 - }, + // end: { + // + // ind: -1, + // lin: 1, + // col: 0 + // }, src: '' }; } @@ -2296,10 +2291,10 @@ function tokenize(iterator, errors, options) { const previousNode = stack.pop(); // @ts-ignore context = stack[stack.length - 1] || root; - if (options.location && context != root) { - // @ts-ignore - context.loc.end = { ind, lin, col: col == 0 ? 1 : col }; - } + // if (options.location && context != root) { + // @ts-ignore + // context.loc.end = {ind, lin, col: col == 0 ? 1 : col} + // } // @ts-ignore if (options.removeEmpty && previousNode != null && previousNode.chi.length == 0 && context.chi[context.chi.length - 1] == previousNode) { context.chi.pop(); @@ -2309,10 +2304,10 @@ function tokenize(iterator, errors, options) { buffer = ''; } // @ts-ignore - if (node != null && options.location && ['}', ';'].includes(value) && context.chi[context.chi.length - 1].loc.end == null) { - // @ts-ignore - context.chi[context.chi.length - 1].loc.end = { ind, lin, col }; - } + // if (node != null && options.location && ['}', ';'].includes(value) && context.chi[context.chi.length - 1].loc.end == null) { + // @ts-ignore + // context.chi[context.chi.length - 1].loc.end = {ind, lin, col}; + // } break; case '!': if (buffer.length > 0) { @@ -2339,19 +2334,27 @@ function tokenize(iterator, errors, options) { if (buffer.length > 0) { pushToken(getType(buffer)); } + if (tokens.length > 0) { + parseNode(tokens); + } + // console.debug({tokens}); // pushToken({typ: 'EOF'}); // - if (col == 0) { - col = 1; - } - if (options.location) { - // @ts-ignore - root.loc.end = { ind, lin, col }; - for (const context of stack) { - // @ts-ignore - context.loc.end = { ind, lin, col }; - } - } + // if (col == 0) { + // + // col = 1; + // } + // if (options.location) { + // + // // @ts-ignore + // root.loc.end = {ind, lin, col}; + // + // for (const context of stack) { + // + // // @ts-ignore + // context.loc.end = {ind, lin, col}; + // } + // } return root; } function getBlockType(chr) { @@ -2373,6 +2376,232 @@ function getBlockType(chr) { throw new Error(`unhandled token: '${chr}'`); } +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]); + }); +} + +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 (this.declarations.has(this.config.shorthand)) { + let isValid = true; + const tokens = []; + for (let token of this.declarations.get(this.config.shorthand).val) { + if (this.config.types.includes(token.typ)) { + tokens.push(token); + continue; + } + if (token.typ != 'Whitespace' && token.typ != 'Comment') { + isValid = false; + break; + } + } + if (!isValid || tokens.length == 0) { + this.declarations.set(declaration.nam, declaration); + } + else { + this.declarations.delete(this.config.shorthand); + this.config.properties.forEach((property, index) => { + while (index >= tokens.length) { + index = Math.floor(index / 2); + } + this.declarations.set(property, { + typ: 'Declaration', + nam: property, + val: [tokens[index]].map((o) => { + return { ...o }; + }) + }); + }); + } + } + this.declarations.set(declaration.nam, declaration); + } + return this; + } + [Symbol.iterator]() { + let iterator; + const declarations = this.declarations; + if (declarations.size < this.config.properties.length) { + iterator = declarations.values(); + } + else { + const value = this.config.properties.reduce((acc, curr) => { + acc.val.push(...this.declarations.get(curr).val); + return acc; + }, { + typ: 'Declaration', + nam: this.config.shorthand, + val: [] + }); + let i = this.config.properties.length; + while (--i) { + const t = value.val[i]; + const k = value.val[Math.floor((i - 1) / 2)]; + if (t.val == k.val && t.val == '0') { + if ((t.typ == 'Number' && isLengthUnit(k)) || + (k.typ == 'Number' && isLengthUnit(t)) || + (isLengthUnit(k) || isLengthUnit(t))) { + value.val.splice(i, 1); + continue; + } + } + if (eq(t, k)) { + value.val.splice(i, 1); + continue; + } + break; + } + if (value.val.length > 1) { + const k = value.val.length * 2; + i = 0; + while (i < k) { + value.val.splice(i + 1, 0, { typ: 'Whitespace' }); + i += 2; + } + } + iterator = [value][Symbol.iterator](); + return { + next() { + return iterator.next(); + } + }; + } + return { + next() { + return iterator.next(); + } + }; + } +} + +var properties = { + margin: { + shorthand: "margin", + properties: [ + "margin-top", + "margin-right", + "margin-bottom", + "margin-left" + ], + types: [ + "Dimension", + "Number", + "Perc" + ] + }, + "margin-top": { + shorthand: "margin" + }, + "margin-right": { + shorthand: "margin" + }, + "margin-bottom": { + shorthand: "margin" + }, + "margin-left": { + shorthand: "margin" + }, + padding: { + shorthand: "padding", + properties: [ + "padding-top", + "padding-right", + "padding-bottom", + "padding-left" + ], + types: [ + "Dimension", + "Number", + "Perc" + ] + }, + "padding-top": { + shorthand: "padding" + }, + "padding-right": { + shorthand: "padding" + }, + "padding-bottom": { + shorthand: "padding" + }, + "padding-left": { + shorthand: "padding" + } +}; +var config$1 = { + properties: properties +}; + +const getConfig = () => config$1; + +const config = getConfig(); +class PropertyList { + declarations; + constructor() { + this.declarations = new Map; + } + add(declaration) { + if (declaration.typ != 'Declaration') { + this.declarations.set(this.declarations.size.toString(), declaration); + return this; + } + const propertyName = declaration.nam; + if (propertyName in config.properties) { + const shorthand = config.properties[propertyName].shorthand; + if (!this.declarations.has(shorthand)) { + this.declarations.set(shorthand, new PropertySet(config.properties[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) { + if (value.value instanceof PropertySet) { + 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; + } + }; + } +} + function parse(css, opt = {}) { const errors = []; const options = { @@ -2380,7 +2609,7 @@ function parse(css, opt = {}) { location: false, processImport: false, deduplicate: false, - removeEmpty: false, + removeEmpty: true, ...opt }; if (css.length == 0) { @@ -2395,92 +2624,75 @@ function parse(css, opt = {}) { return { ast, errors }; } function deduplicate(ast) { - if ('chi' in ast) { + // @ts-ignore + if (('chi' in ast) && ast.chi?.length > 0) { // @ts-ignore - let i = ast.chi.length; + let i = 0; let previous; let node; - while (i--) { + let nodeIndex; + // @ts-ignore + for (; i < ast.chi.length; i++) { // @ts-ignore node = ast.chi[i]; - // @ts-ignore if (node.typ == 'Comment') { continue; } - if (node.typ == 'AtRule' && node.nam == 'media' && node.val == 'all') { - // merge only if the previous rule contains only declarations - let shouldMerge = true; + if (node.typ == 'AtRule' && node.val == 'all') { // @ts-ignore - let i = node.chi.length; - while (i--) { - // @ts-ignore - if (node.chi[i].typ == 'Comment') { - continue; - } - // @ts-ignore - shouldMerge = node.chi[i].typ == 'Declaration'; - break; - } - if (!shouldMerge) { - continue; - } - // @ts-ignore - ast.chi.splice(i, 1, ...node.chi); - // @ts-ignore - i += node.chi.length; + ast.chi?.splice(i, 1, ...node.chi); + i--; continue; } - // @ts-ignore - if (node.typ == previous?.typ) { - if ((node.typ == 'Rule' && node.sel == previous.sel) || - (node.typ == 'AtRule' && - node.nam == previous.nam && - node.val == previous.val)) { - if ('chi' in node) { + if (('chi' in node)) { + // @ts-ignore + if (previous != null && previous.typ == node.typ) { + // @ts-ignore + if ((node.typ == 'Rule' && node.sel == previous.sel) || + // @ts-ignore + (node.typ == 'AtRule') && node.val == previous.val) { let shouldMerge = true; // @ts-ignore - let i = node.chi.length; - while (i--) { + let k = previous.chi.length; + while (k--) { // @ts-ignore - if (node.chi[i].typ == 'Comment') { + if (previous.chi[k].typ == 'Comment') { continue; } // @ts-ignore - shouldMerge = node.chi[i].typ == 'Declaration'; + shouldMerge = previous.chi[k].typ == 'Declaration'; break; } - if (!shouldMerge) { + if (shouldMerge) { + // @ts-ignore + node.chi.unshift(...previous.chi); + // @ts-ignore + ast.chi.splice(nodeIndex, 1); + i--; + previous = node; + nodeIndex = i; continue; } - // @ts-ignore - previous.chi = node.chi.concat(...(previous.chi || [])); - } - // @ts-ignore - ast.chi.splice(i, 1); - if (!('chi' in previous)) { - continue; } + } + // @ts-ignore + if (previous != null && previous != node && 'chi' in previous) { // @ts-ignore - if (previous.typ == 'Rule' || previous.chi.some(n => n.typ == 'Declaration')) { + if (previous.chi.some(n => n.typ == 'Declaration')) { deduplicateRule(previous); } else { deduplicate(previous); } - continue; - } - else if (node.typ == 'Declaration' && node.nam == previous.nam) { - // @ts-ignore - ast.chi.splice(i, 1); - continue; } } previous = node; - if (!('chi' in node)) { - continue; - } + nodeIndex = i; + } + // @ts-ignore + if (node != null && ('chi' in node)) { // @ts-ignore - if (node.typ == 'AtRule' || node.chi.some(n => n.typ == 'Declaration')) { + if (node.chi.some(n => n.typ == 'Declaration')) { deduplicateRule(node); } else { @@ -2494,25 +2706,24 @@ function deduplicateRule(ast) { if (!('chi' in ast) || ast.chi?.length == 0) { return ast; } - const map = new Map; // @ts-ignore - let i = ast.chi.length; - let node; - while (i--) { - // @ts-ignore - node = ast.chi[i]; - if (node.typ != 'Declaration') { - continue; - } + const j = ast.chi.length; + let k = 0; + const properties = new PropertyList(); + for (; k < j; k++) { // @ts-ignore - if (map.has(node.nam)) { + if ('Comment' == ast.chi[k].typ || 'Declaration' == ast.chi[k].typ) { // @ts-ignore - ast.chi.splice(i, 1); + properties.add(ast.chi[k]); continue; } - // @ts-ignore - map.set(node.nam, node); + break; } + // @ts-ignore + ast.chi = [...properties].concat(ast.chi.slice(k)); + // @ts-ignore + // ast.chi.splice(0, k - 1, ...properties); + // console.debug({k, removed}); return ast; } diff --git a/test/specs/shorthand.test.ts b/test/specs/shorthand.test.ts new file mode 100644 index 0000000..f1f2088 --- /dev/null +++ b/test/specs/shorthand.test.ts @@ -0,0 +1,47 @@ +import {expect} from "@esm-bundle/chai"; +import {readFile} from "fs/promises"; +import {parse, render} from "../../src"; +import {dirname} from "path"; + +const dir = dirname(new URL(import.meta.url).pathname) + '/../files'; + +const marginPadding = ` + +.test { +margin: 10px 0 10px 5px; +top: 4px; +padding: 2px 0 0 0; +padding-left: 25px; +padding-right: 25px; +padding-top: 25px; +} + +.test { +margin-right: 0px; +padding-right: 0; +padding-top: 0 +} + +.test { + +margin-bottom: 0px; +text-align: justify; +} + +.test { + +padding-left: 0px; +margin-top: 0px; +`; + +describe('shorthand', function () { + + it('margin padding', async function () { + + expect(render(parse(marginPadding, { + deduplicate: true, + removeEmpty: true + }).ast, {compress: true})).equals('.test{margin:0 0 0 5px;top:4px;padding:0;text-align:justify}') + }); + +}); \ No newline at end of file diff --git a/test/units.ts b/test/units.ts new file mode 100644 index 0000000..b3e759e --- /dev/null +++ b/test/units.ts @@ -0,0 +1,18 @@ +import {isLengthUnit} from "../src/parser/utils"; +import {DimensionToken} from "../src/@types"; + + +const t = { + typ: "Dimension", + val: "0", + unit: "px" + }; + + const k = { + typ: "Number", + val: "0" + }; + + console.debug(isLengthUnit(t)); + console.debug(t.typ == 'Number' && isLengthUnit( k)); + console.debug(k.typ == 'Number' && isLengthUnit( t)); \ No newline at end of file diff --git a/tools/properties.ts b/tools/properties.ts index 3faba0b..b4febde 100644 --- a/tools/properties.ts +++ b/tools/properties.ts @@ -1,12 +1,14 @@ import {PropertySetType, TokenType} from "../src/@types"; -function createProperties(shorthand: string, properties: string[], types: TokenType[]) { +function createProperties(shorthand: string, properties: string[], types: TokenType[], multiple: boolean, separator) { return Object.assign({ [shorthand]: { shorthand, properties, - types + types, + multiple, + separator: separator == undefined ? null : separator } }, properties.reduce((acc, property: string) => { @@ -23,17 +25,25 @@ export const properties: PropertySetType = [ [ 'margin', - ['margin-top', 'margin-left', 'margin-bottom', 'margin-right'], - ['Dimension', 'Number', 'Perc'] + ['margin-top', 'margin-right', 'margin-bottom', 'margin-left'], + ['Dimension', 'Number', 'Perc'], + false ], [ 'padding', - ['padding-top', 'padding-left', 'padding-bottom', 'padding-right'], - ['Dimension', 'Number', 'Perc'] + ['padding-top', 'padding-right', 'padding-bottom', 'padding-left'], + ['Dimension', 'Number', 'Perc'], + false + ], + [ + 'border-radius', + ['border-top-left-radius', 'border-top-right-radius', 'border-bottom-right-radius', 'border-bottom-left-radius'], + ['Dimension', 'Number', 'Perc'], + false ] - ].reduce((acc, data) => { + ].reduce((acc, data: Array) => { return Object.assign(acc, createProperties(...data)); }, {}); -console.debug({properties}) \ No newline at end of file +console.debug(JSON.stringify({properties}, null, 1)); \ No newline at end of file