@tbela99/css-parser 0.0.1-rc1 → 0.0.1-rc3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,4 +1,4 @@
1
- [![cov](https://tbela99.github.io/css-parser/badges/coverage.svg)](https://github.com/tbela99/css-parser/actions)
1
+ [![npm](https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fraw.githubusercontent.com%2Ftbela99%2Fcss-parser%2Fmaster%2Fpackage.json&query=version&logo=npm&label=npm&link=https%3A%2F%2Fwww.npmjs.com%2Fpackage%2F%40tbela99%2Fcss-parser)](https://www.npmjs.com/package/@tbela99/css-parser) [![cov](https://tbela99.github.io/css-parser/badges/coverage.svg)](https://github.com/tbela99/css-parser/actions)
2
2
 
3
3
  # css-parser
4
4
 
@@ -12,15 +12,15 @@ $ npm install @tbela99/css-parser
12
12
 
13
13
  ### Features
14
14
 
15
- - [x] fault tolerant parser, will try to fix invalid tokens according to the CSS syntax module 3 recommendations.
16
- - [x] efficient minification, see benchmark.
17
- - [x] replace @import at-rules with actual css content of the imported rule
18
- - [x] CSS nesting: automatically create nested rules.
19
- - [x] works the same way in node and web browser
15
+ - fault tolerant parser, will try to fix invalid tokens according to the CSS syntax module 3 recommendations.
16
+ - efficient minification, see [benchmark](https://tbela99.github.io/css-parser/benchmark/index.html)
17
+ - replace @import at-rules with actual css content of the imported rule
18
+ - automatically generate nested css rules
19
+ - works the same way in node and web browser
20
20
 
21
21
  ### Performance
22
22
 
23
- - [x] flatten @import
23
+ - flatten @import
24
24
 
25
25
  ## Transform
26
26
 
@@ -50,7 +50,7 @@ Include ParseOptions and RenderOptions
50
50
 
51
51
  - src: string, optional. css file location to be used with sourcemap.
52
52
  - minify: boolean, optional. default to _true_. optimize ast.
53
- - nestingRules: boolean, optional. automatically nest rules.
53
+ - nestingRules: boolean, optional. automatically generated nested rules.
54
54
  - removeEmpty: boolean, remove empty nodes from the ast.
55
55
  - location: boolean, optional. includes node location in the ast, required for sourcemap generation.
56
56
  - cwd: string, optional. the current working directory. when specified url() are resolved using this value
@@ -79,7 +79,7 @@ parse(css, parseOptions = {})
79
79
 
80
80
  ````javascript
81
81
 
82
- const {ast, errors} = await parse(css);
82
+ const {ast, errors, stats} = await parse(css);
83
83
  ````
84
84
 
85
85
  ## Rendering
@@ -96,7 +96,7 @@ render(ast, RenderOptions = {});
96
96
  import {render} from '@tbela99/css-parser';
97
97
 
98
98
  // minified
99
- const {code} = render(ast, {minify: true});
99
+ const {code, stats} = render(ast, {minify: true});
100
100
 
101
101
  console.log(code);
102
102
  ```
@@ -160,6 +160,62 @@ Single JavaScript file
160
160
  <script src="dist/index-umd-web.js"></script>
161
161
  ```
162
162
 
163
+ ## Example
164
+
165
+ ### Automatic CSS Nesting
166
+
167
+ CSS
168
+
169
+ ```css
170
+
171
+ table.colortable td {
172
+ text-align:center;
173
+ }
174
+ table.colortable td.c {
175
+ text-transform:uppercase;
176
+ }
177
+ table.colortable td:first-child, table.colortable td:first-child+td {
178
+ border:1px solid black;
179
+ }
180
+ table.colortable th {
181
+ text-align:center;
182
+ background:black;
183
+ color:white;
184
+ }
185
+ ```
186
+
187
+ Javascript
188
+ ```javascript
189
+ import {parse, render} from '@tbela99/css-parser';
190
+
191
+
192
+ const options = {minify: true, nestingRules: true};
193
+
194
+ const {code} = await parse(css, options).then(result => render(result.ast, {minify: false}));
195
+ //
196
+ console.debug(code);
197
+ ```
198
+
199
+ Result
200
+ ```css
201
+ table.colortable {
202
+ & td {
203
+ text-align: center;
204
+ &.c {
205
+ text-transform: uppercase
206
+ }
207
+ &:first-child,&:first-child+td {
208
+ border: 1px solid #000
209
+ }
210
+ }
211
+ & th {
212
+ text-align: center;
213
+ background: #000;
214
+ color: #fff
215
+ }
216
+ }
217
+ ```
218
+
163
219
  ## AST
164
220
 
165
221
  ### Comment
@@ -188,4 +244,4 @@ Single JavaScript file
188
244
  ### AtRuleStyleSheet
189
245
 
190
246
  - typ: string 'Stylesheet'
191
- - chi: array of children
247
+ - chi: array of children
@@ -93,10 +93,7 @@
93
93
  if (name.charAt(0) != '#') {
94
94
  return false;
95
95
  }
96
- if (isIdent(name.charAt(1))) {
97
- return true;
98
- }
99
- return true;
96
+ return isIdent(name.charAt(1));
100
97
  }
101
98
  function isNumber(name) {
102
99
  if (name.length == 0) {
@@ -1072,6 +1069,21 @@
1072
1069
 
1073
1070
  const getConfig = () => config$1;
1074
1071
 
1072
+ const funcList = ['clamp', 'calc'];
1073
+ function matchType(val, properties) {
1074
+ if (val.typ == 'Iden' && properties.keywords.includes(val.val) ||
1075
+ (properties.types.includes(val.typ))) {
1076
+ return true;
1077
+ }
1078
+ if (val.typ == 'Number' && val.val == '0') {
1079
+ return properties.types.some(type => type == 'Length' || type == 'Angle');
1080
+ }
1081
+ if (val.typ == 'Func' && funcList.includes(val.val)) {
1082
+ return val.chi.every((t => ['Literal', 'Comma', 'Whitespace', 'Start-parens', 'End-parens'].includes(t.typ) || matchType(t, properties)));
1083
+ }
1084
+ return false;
1085
+ }
1086
+
1075
1087
  // name to color
1076
1088
  const COLORS_NAMES = Object.seal({
1077
1089
  'aliceblue': '#f0f8ff',
@@ -1566,6 +1578,7 @@
1566
1578
  }
1567
1579
 
1568
1580
  function render(data, opt = {}) {
1581
+ const startTime = performance.now();
1569
1582
  const options = Object.assign(opt.minify ?? true ? {
1570
1583
  indent: '',
1571
1584
  newLine: '',
@@ -1584,7 +1597,9 @@
1584
1597
  }
1585
1598
  return acc + renderToken(curr, options);
1586
1599
  }
1587
- return { code: doRender(data, options, reducer, 0) };
1600
+ return { code: doRender(data, options, reducer, 0), stats: {
1601
+ total: `${(performance.now() - startTime).toFixed(2)}ms`
1602
+ } };
1588
1603
  }
1589
1604
  // @ts-ignore
1590
1605
  function doRender(data, options, reducer, level = 0, indents = []) {
@@ -1597,6 +1612,8 @@
1597
1612
  const indent = indents[level];
1598
1613
  const indentSub = indents[level + 1];
1599
1614
  switch (data.typ) {
1615
+ case 'Declaration':
1616
+ return `${data.nam}:${options.indent}${data.val.reduce((acc, curr) => acc + renderToken(curr), '')}`;
1600
1617
  case 'Comment':
1601
1618
  return options.removeComments ? '' : data.val;
1602
1619
  case 'StyleSheet':
@@ -1613,7 +1630,7 @@
1613
1630
  case 'AtRule':
1614
1631
  case 'Rule':
1615
1632
  if (data.typ == 'AtRule' && !('chi' in data)) {
1616
- return `${indent}@${data.nam} ${data.val};`;
1633
+ return `${indent}@${data.nam}${data.val === '' ? '' : options.indent || ' '}${data.val};`;
1617
1634
  }
1618
1635
  // @ts-ignore
1619
1636
  let children = data.chi.reduce((css, node) => {
@@ -1622,10 +1639,14 @@
1622
1639
  str = options.removeComments ? '' : node.val;
1623
1640
  }
1624
1641
  else if (node.typ == 'Declaration') {
1642
+ if (node.val.length == 0) {
1643
+ console.error(`invalid declaration`, node);
1644
+ return '';
1645
+ }
1625
1646
  str = `${node.nam}:${options.indent}${node.val.reduce(reducer, '').trimEnd()};`;
1626
1647
  }
1627
1648
  else if (node.typ == 'AtRule' && !('chi' in node)) {
1628
- str = `@${node.nam} ${node.val};`;
1649
+ str = `${data.val === '' ? '' : options.indent || ' '}${data.val};`;
1629
1650
  }
1630
1651
  else {
1631
1652
  str = doRender(node, options, reducer, level + 1, indents);
@@ -1642,7 +1663,7 @@
1642
1663
  children = children.slice(0, -1);
1643
1664
  }
1644
1665
  if (data.typ == 'AtRule') {
1645
- return `@${data.nam}${data.val ? ' ' + data.val + options.indent : ''}{${options.newLine}` + (children === '' ? '' : indentSub + children + options.newLine) + indent + `}`;
1666
+ return `@${data.nam}${data.val === '' ? '' : options.indent || ' '}${data.val}${options.indent}{${options.newLine}` + (children === '' ? '' : indentSub + children + options.newLine) + indent + `}`;
1646
1667
  }
1647
1668
  return data.sel + `${options.indent}{${options.newLine}` + (children === '' ? '' : indentSub + children + options.newLine) + indent + `}`;
1648
1669
  }
@@ -1792,7 +1813,9 @@
1792
1813
  case 'Delim':
1793
1814
  return /* options.minify && 'Pseudo-class' == token.typ && '::' == token.val.slice(0, 2) ? token.val.slice(1) : */ token.val;
1794
1815
  }
1795
- throw new Error(`unexpected token ${JSON.stringify(token, null, 1)}`);
1816
+ console.error(`unexpected token ${JSON.stringify(token, null, 1)}`);
1817
+ // throw new Error(`unexpected token ${JSON.stringify(token, null, 1)}`);
1818
+ return '';
1796
1819
  }
1797
1820
 
1798
1821
  function eq(a, b) {
@@ -1998,17 +2021,6 @@
1998
2021
  }
1999
2022
  }
2000
2023
 
2001
- function matchType(val, properties) {
2002
- if (val.typ == 'Iden' && properties.keywords.includes(val.val) ||
2003
- (properties.types.includes(val.typ))) {
2004
- return true;
2005
- }
2006
- if (val.typ == 'Number' && val.val == '0') {
2007
- return properties.types.some(type => type == 'Length' || type == 'Angle');
2008
- }
2009
- return false;
2010
- }
2011
-
2012
2024
  const propertiesConfig = getConfig();
2013
2025
  class PropertyMap {
2014
2026
  config;
@@ -2023,6 +2035,9 @@
2023
2035
  this.pattern = config.pattern.split(/\s/);
2024
2036
  }
2025
2037
  add(declaration) {
2038
+ for (const val of declaration.val) {
2039
+ Object.defineProperty(val, 'propertyName', { enumerable: false, writable: true, value: declaration.nam });
2040
+ }
2026
2041
  if (declaration.nam == this.config.shorthand) {
2027
2042
  this.declarations = new Map;
2028
2043
  this.declarations.set(declaration.nam, declaration);
@@ -2056,7 +2071,7 @@
2056
2071
  i--;
2057
2072
  continue;
2058
2073
  }
2059
- if (matchType(acc[i], props)) {
2074
+ if (('propertyName' in acc[i] && acc[i].propertyName == property) || matchType(acc[i], props)) {
2060
2075
  if ('prefix' in props && props.previous != null && !(props.previous in tokens)) {
2061
2076
  return acc;
2062
2077
  }
@@ -2190,10 +2205,12 @@
2190
2205
  }
2191
2206
  else {
2192
2207
  let count = 0;
2208
+ let match;
2193
2209
  const separator = this.config.separator;
2194
2210
  const tokens = {};
2195
2211
  // @ts-ignore
2196
- /* const valid: string[] =*/ Object.entries(this.config.properties).reduce((acc, curr) => {
2212
+ /* const valid: string[] =*/
2213
+ Object.entries(this.config.properties).reduce((acc, curr) => {
2197
2214
  if (!this.declarations.has(curr[0])) {
2198
2215
  if (curr[1].required) {
2199
2216
  acc.push(curr[0]);
@@ -2202,33 +2219,39 @@
2202
2219
  }
2203
2220
  let current = 0;
2204
2221
  const props = this.config.properties[curr[0]];
2205
- const declaration = this.declarations.get(curr[0]);
2206
- // @ts-ignore
2207
- for (const val of (declaration instanceof PropertySet ? [...declaration][0] : declaration).val) {
2208
- if (separator != null && separator.typ == val.typ && eq(separator, val)) {
2209
- current++;
2210
- if (tokens[curr[0]].length == current) {
2211
- tokens[curr[0]].push([]);
2222
+ const properties = this.declarations.get(curr[0]);
2223
+ for (const declaration of [(properties instanceof PropertySet ? [...properties][0] : properties)]) {
2224
+ // @ts-ignore
2225
+ for (const val of declaration.val) {
2226
+ if (separator != null && separator.typ == val.typ && eq(separator, val)) {
2227
+ current++;
2228
+ if (tokens[curr[0]].length == current) {
2229
+ tokens[curr[0]].push([]);
2230
+ }
2231
+ continue;
2212
2232
  }
2213
- continue;
2214
- }
2215
- if (val.typ == 'Whitespace' || val.typ == 'Comment') {
2216
- continue;
2217
- }
2218
- if (props.multiple && props.separator != null && props.separator.typ == val.typ && eq(props.separator, val)) {
2219
- continue;
2220
- }
2221
- if (matchType(val, curr[1])) {
2222
- if (!(curr[0] in tokens)) {
2223
- tokens[curr[0]] = [[]];
2233
+ if (val.typ == 'Whitespace' || val.typ == 'Comment') {
2234
+ continue;
2235
+ }
2236
+ if (props.multiple && props.separator != null && props.separator.typ == val.typ && eq(props.separator, val)) {
2237
+ continue;
2238
+ }
2239
+ match = matchType(val, curr[1]);
2240
+ if (isShorthand) {
2241
+ isShorthand = match;
2242
+ }
2243
+ if (('propertyName' in val && val.propertyName == property) || match) {
2244
+ if (!(curr[0] in tokens)) {
2245
+ tokens[curr[0]] = [[]];
2246
+ }
2247
+ // is default value
2248
+ tokens[curr[0]][current].push(val);
2249
+ // continue;
2250
+ }
2251
+ else {
2252
+ acc.push(curr[0]);
2253
+ break;
2224
2254
  }
2225
- // is default value
2226
- tokens[curr[0]][current].push(val);
2227
- // continue;
2228
- }
2229
- else {
2230
- acc.push(curr[0]);
2231
- break;
2232
2255
  }
2233
2256
  }
2234
2257
  if (count == 0) {
@@ -2237,7 +2260,10 @@
2237
2260
  return acc;
2238
2261
  }, []);
2239
2262
  count++;
2240
- if (!Object.values(tokens).every(v => v.length == count)) {
2263
+ if (!isShorthand || Object.entries(this.config.properties).some(entry => {
2264
+ // missing required property
2265
+ return entry[1].required && !(entry[0] in tokens);
2266
+ }) || !Object.values(tokens).every(v => v.length == count)) {
2241
2267
  // @ts-ignore
2242
2268
  iterable = this.declarations.values();
2243
2269
  }
@@ -2581,7 +2607,7 @@
2581
2607
  // @ts-ignore
2582
2608
  return null;
2583
2609
  }
2584
- return { result, node1: exchanged ? node2 : node1, node2: exchanged ? node2 : node2 };
2610
+ return { result, node1: exchanged ? node2 : node1, node2: exchanged ? node1 : node2 };
2585
2611
  }
2586
2612
  function matchSelectors(selector1, selector2, parentType) {
2587
2613
  let match = [[]];
@@ -2769,11 +2795,27 @@
2769
2795
  if (node.typ == 'AtRule' && node.nam == 'font-face') {
2770
2796
  continue;
2771
2797
  }
2772
- if (node.typ == 'AtRule' && node.val == 'all') {
2798
+ if (node.typ == 'AtRule') {
2799
+ if (node.nam == 'media' && node.val == 'all') {
2800
+ // @ts-ignore
2801
+ ast.chi?.splice(i, 1, ...node.chi);
2802
+ i--;
2803
+ continue;
2804
+ }
2805
+ // console.debug({previous, node});
2773
2806
  // @ts-ignore
2774
- ast.chi?.splice(i, 1, ...node.chi);
2775
- i--;
2776
- continue;
2807
+ if (previous?.typ == 'AtRule' &&
2808
+ previous.nam == node.nam &&
2809
+ previous.val == node.val) {
2810
+ if ('chi' in node) {
2811
+ // @ts-ignore
2812
+ previous.chi.push(...node.chi);
2813
+ }
2814
+ // else {
2815
+ ast?.chi?.splice(i--, 1);
2816
+ continue;
2817
+ // }
2818
+ }
2777
2819
  }
2778
2820
  // @ts-ignore
2779
2821
  if (node.typ == 'Rule') {
@@ -3288,10 +3330,11 @@
3288
3330
  }
3289
3331
  buffer += quoteStr;
3290
3332
  while (value = peek()) {
3291
- if (ind >= iterator.length) {
3292
- yield pushToken(buffer, hasNewLine ? 'Bad-string' : 'Unclosed-string');
3293
- break;
3294
- }
3333
+ // if (ind >= iterator.length) {
3334
+ //
3335
+ // yield pushToken(buffer, hasNewLine ? 'Bad-string' : 'Unclosed-string');
3336
+ // break;
3337
+ // }
3295
3338
  if (value == '\\') {
3296
3339
  const sequence = peek(6);
3297
3340
  let escapeSequence = '';
@@ -3314,7 +3357,7 @@
3314
3357
  // not hex or new line
3315
3358
  // @ts-ignore
3316
3359
  if (i == 1 && !isNewLine(codepoint)) {
3317
- buffer += sequence[i];
3360
+ buffer += value + sequence[i];
3318
3361
  next(2);
3319
3362
  continue;
3320
3363
  }
@@ -3334,11 +3377,12 @@
3334
3377
  continue;
3335
3378
  }
3336
3379
  // buffer += value;
3337
- if (ind >= iterator.length) {
3338
- // drop '\\' at the end
3339
- yield pushToken(buffer);
3340
- break;
3341
- }
3380
+ // if (ind >= iterator.length) {
3381
+ //
3382
+ // // drop '\\' at the end
3383
+ // yield pushToken(buffer);
3384
+ // break;
3385
+ // }
3342
3386
  buffer += next(2);
3343
3387
  continue;
3344
3388
  }
@@ -3507,7 +3551,7 @@
3507
3551
  buffer = '';
3508
3552
  break;
3509
3553
  }
3510
- buffer += value;
3554
+ buffer += prev() + value;
3511
3555
  break;
3512
3556
  case '"':
3513
3557
  case "'":
@@ -3709,6 +3753,7 @@
3709
3753
  * @param opt
3710
3754
  */
3711
3755
  async function parse$1(iterator, opt = {}) {
3756
+ const startTime = performance.now();
3712
3757
  const errors = [];
3713
3758
  const options = {
3714
3759
  src: '',
@@ -3847,7 +3892,7 @@
3847
3892
  src: options.resolve(url, options.src).absolute
3848
3893
  }));
3849
3894
  });
3850
- bytesIn += root.bytesIn;
3895
+ bytesIn += root.stats.bytesIn;
3851
3896
  if (root.ast.chi.length > 0) {
3852
3897
  context.chi.push(...root.ast.chi);
3853
3898
  }
@@ -3893,13 +3938,6 @@
3893
3938
  // rule
3894
3939
  if (delim.typ == 'Block-start') {
3895
3940
  const position = map.get(tokens[0]);
3896
- // if (context.typ == 'Rule') {
3897
- //
3898
- // if (tokens[0]?.typ == 'Iden') {
3899
- // errors.push({action: 'drop', message: 'invalid nesting rule', location: {src, ...position}});
3900
- // return null;
3901
- // }
3902
- // }
3903
3941
  const uniq = new Map;
3904
3942
  parseTokens(tokens, { minify: options.minify }).reduce((acc, curr, index, array) => {
3905
3943
  if (curr.typ == 'Whitespace') {
@@ -4075,21 +4113,30 @@
4075
4113
  if (tokens.length > 0) {
4076
4114
  await parseNode(tokens);
4077
4115
  }
4116
+ const endParseTime = performance.now();
4078
4117
  if (options.minify) {
4079
4118
  if (ast.chi.length > 0) {
4080
4119
  minify(ast, options, true);
4081
4120
  }
4082
4121
  }
4083
- return { ast, errors, bytesIn };
4122
+ const endTime = performance.now();
4123
+ return {
4124
+ ast, errors, stats: {
4125
+ bytesIn,
4126
+ parse: `${(endParseTime - startTime).toFixed(2)}ms`,
4127
+ minify: `${(endTime - endParseTime).toFixed(2)}ms`,
4128
+ total: `${(endTime - startTime).toFixed(2)}ms`
4129
+ }
4130
+ };
4084
4131
  }
4085
4132
  function parseString(src, options = { location: false }) {
4086
- return [...tokenize(src)].map(t => {
4133
+ return parseTokens([...tokenize(src)].map(t => {
4087
4134
  const token = getTokenType(t.token, t.hint);
4088
4135
  if (options.location) {
4089
4136
  Object.assign(token, { loc: t.position });
4090
4137
  }
4091
4138
  return token;
4092
- });
4139
+ }));
4093
4140
  }
4094
4141
  function getTokenType(val, hint) {
4095
4142
  if (val === '' && hint == null) {
@@ -4431,19 +4478,17 @@
4431
4478
  async function transform$1(css, options = {}) {
4432
4479
  options = { minify: true, removeEmpty: true, ...options };
4433
4480
  const startTime = performance.now();
4434
- const parseResult = await parse$1(css, options);
4435
- const renderTime = performance.now();
4436
- const rendered = render(parseResult.ast, options);
4437
- const endTime = performance.now();
4438
- return {
4439
- ...parseResult, ...rendered, stats: {
4440
- bytesIn: parseResult.bytesIn,
4441
- bytesOut: rendered.code.length,
4442
- parse: `${(renderTime - startTime).toFixed(2)}ms`,
4443
- render: `${(endTime - renderTime).toFixed(2)}ms`,
4444
- total: `${(endTime - startTime).toFixed(2)}ms`
4445
- }
4446
- };
4481
+ return parse$1(css, options).then((parseResult) => {
4482
+ const rendered = render(parseResult.ast, options);
4483
+ return {
4484
+ ...parseResult, ...rendered, stats: {
4485
+ bytesOut: rendered.code.length,
4486
+ ...parseResult.stats,
4487
+ render: rendered.stats.total,
4488
+ total: `${(performance.now() - startTime).toFixed(2)}ms`
4489
+ }
4490
+ };
4491
+ });
4447
4492
  }
4448
4493
 
4449
4494
  const matchUrl = /^(https?:)?\/\//;
@@ -4602,6 +4647,7 @@
4602
4647
  exports.isTime = isTime;
4603
4648
  exports.isWhiteSpace = isWhiteSpace;
4604
4649
  exports.load = load;
4650
+ exports.matchType = matchType;
4605
4651
  exports.matchUrl = matchUrl;
4606
4652
  exports.minify = minify;
4607
4653
  exports.minifyRule = minifyRule;