Skip to content

Commit 873c8e3

Browse files
authored
cascade-layer-name-parser (#755)
* cascade-layer-name-parser * fixes * integrate * fixes and integrate further * one more test * better error handling * cleanup
1 parent 41cd7b9 commit 873c8e3

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

92 files changed

+1906
-240
lines changed

package-lock.json

+27
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

+1
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
"workspaces": [
1212
"packages/css-tokenizer",
1313
"packages/css-parser-algorithms",
14+
"packages/cascade-layer-name-parser",
1415
"packages/media-query-list-parser",
1516
"packages/*",
1617
"plugins/postcss-progressive-custom-properties",
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
node_modules
2+
package-lock.json
3+
yarn.lock
4+
*.result.css
5+
*.result.css.map
6+
*.result.json
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
v16.13.1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
### 1.0.0 (Unreleased)
2+
3+
- Initial version
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
The MIT License (MIT)
2+
3+
Copyright 2022 Romain Menke, Antonio Laguna <[email protected]>
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy of
6+
this software and associated documentation files (the "Software"), to deal in
7+
the Software without restriction, including without limitation the rights to
8+
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
9+
the Software, and to permit persons to whom the Software is furnished to do so,
10+
subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
17+
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
18+
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
19+
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
20+
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
# Cascade Layer Name Parser
2+
3+
[<img alt="npm version" src="https://img.shields.io/npm/v/@csstools/cascade-layer-name-parser.svg" height="20">][npm-url]
4+
[<img alt="Build Status" src="https://github.com/csstools/postcss-plugins/workflows/test/badge.svg" height="20">][cli-url]
5+
[<img alt="Discord" src="https://shields.io/badge/Discord-5865F2?logo=discord&logoColor=white">][discord]
6+
7+
## Usage
8+
9+
Add [Cascade Layer Name Parser] to your project:
10+
11+
```bash
12+
npm install postcss @csstools/cascade-layer-name-parser @csstools/css-parser-algorithms @csstools/css-tokenizer --save-dev
13+
```
14+
15+
[Cascade Layer Name Parser] depends on our CSS tokenizer and parser algorithms.
16+
It must be used together with `@csstools/css-tokenizer` and `@csstools/css-parser-algorithms`.
17+
18+
```ts
19+
import { parse } from '@csstools/cascade-layer-name-parser';
20+
21+
const layerNames = parse('layer-name, foo.bar');
22+
layerNames.forEach((layerName) => {
23+
console.log(layerName.name()) // "foo.bar"
24+
console.log(layerName.segments()) // ["foo", "bar"]
25+
});
26+
```
27+
28+
[cli-url]: https://github.com/csstools/postcss-plugins/actions/workflows/test.yml?query=workflow/test
29+
[discord]: https://discord.gg/bUadyRwkJS
30+
[npm-url]: https://www.npmjs.com/package/@csstools/cascade-layer-name-parser
31+
32+
[Cascade Layer Name Parser]: https://github.com/csstools/postcss-plugins/tree/main/packages/cascade-layer-name-parser
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
"use strict";var e=require("@csstools/css-tokenizer"),t=require("@csstools/css-parser-algorithms");class LayerName{parts;constructor(e){this.parts=e}tokens(){return[...this.parts]}slice(t,n){const r=[];for(let t=0;t<this.parts.length;t++)this.parts[t][0]===e.TokenType.Ident&&r.push(t);const s=r.slice(t,n);return new LayerName(this.parts.slice(s[0],s[s.length-1]+1))}concat(t){const n=[e.TokenType.Delim,".",-1,-1,{value:"."}];return new LayerName([...this.parts.filter((t=>t[0]===e.TokenType.Ident||t[0]===e.TokenType.Delim)),n,...t.parts.filter((t=>t[0]===e.TokenType.Ident||t[0]===e.TokenType.Delim))])}segments(){return this.parts.filter((t=>t[0]===e.TokenType.Ident)).map((e=>e[4].value))}name(){return this.parts.filter((t=>t[0]===e.TokenType.Ident||t[0]===e.TokenType.Delim)).map((e=>e[1])).join("")}equal(e){const t=this.segments(),n=e.segments();if(t.length!==n.length)return!1;for(let e=0;e<t.length;e++){if(t[e]!==n[e])return!1}return!0}toString(){return e.stringify(...this.parts)}toJSON(){return{parts:this.parts,segments:this.segments(),name:this.name()}}}function parseFromTokens(n,r){const s=t.parseCommaSeparatedListOfComponentValues(n,{onParseError:null==r?void 0:r.onParseError}),o=(null==r?void 0:r.onParseError)??(()=>{}),genericError=e=>({message:`Invalid cascade layer name. ${e}`,start:n[0][2],end:n[n.length-1][3],state:["6.4.2. Layer Naming and Nesting","Layer name syntax","<layer-name> = <ident> [ '.' <ident> ]*"]}),a=[];for(let n=0;n<s.length;n++){const r=s[n];for(let e=0;e<r.length;e++){const n=r[e];if(!t.isTokenNode(n)&&!t.isCommentNode(n)&&!t.isWhitespaceNode(n))return o(genericError(`Invalid layer name part "${n.toString()}"`)),[]}const i=r.flatMap((e=>e.tokens()));let p,l=!1,m=!1;for(let t=0;t<i.length;t++){const n=i[t];if(n[0]!==e.TokenType.Comment&&n[0]!==e.TokenType.Whitespace&&n[0]!==e.TokenType.Ident&&(n[0]!==e.TokenType.Delim||"."!==n[4].value))return o(genericError(`Invalid character "${n[1]}"`)),[];if(!l&&n[0]===e.TokenType.Delim)return o(genericError("Layer names can not start with a dot.")),[];if(l){if(n[0]===e.TokenType.Whitespace){m=!0;continue}if(m&&n[0]===e.TokenType.Comment)continue;if(m)return o(genericError("Encountered unexpected whitespace between layer name parts.")),[];if(p[0]===e.TokenType.Ident&&n[0]===e.TokenType.Ident)return o(genericError("Layer name parts must be separated by dots.")),[];if(p[0]===e.TokenType.Delim&&n[0]===e.TokenType.Delim)return o(genericError("Layer name parts must not be empty.")),[]}n[0]===e.TokenType.Ident&&(l=!0),n[0]!==e.TokenType.Ident&&n[0]!==e.TokenType.Delim||(p=n)}if(!p)return o(genericError("Empty layer name.")),[];if(p[0]===e.TokenType.Delim)return o(genericError("Layer name must not end with a dot.")),[];a.push(new LayerName(i))}return a}exports.LayerName=LayerName,exports.addLayerToModel=function addLayerToModel(e,t){return t.forEach((t=>{const n=t.segments();e:for(let r=0;r<n.length;r++){const n=t.slice(0,r+1),s=n.segments();let o=-1,a=0;for(let t=0;t<e.length;t++){const n=e[t].segments();let r=0;t:for(let e=0;e<n.length;e++){const t=n[e],o=s[e];if(o===t&&e+1===s.length)continue e;if(o!==t){if(o!==t)break t}else r++}r>=a&&(o=t,a=r)}-1===o?e.push(n):e.splice(o+1,0,n)}})),e},exports.parse=function parse(t,n){const r=e.tokenizer({css:t},{commentsAreTokens:!0,onParseError:null==n?void 0:n.onParseError}),s=[];for(;!r.endOfFile();)s.push(r.nextToken());return s.push(r.nextToken()),parseFromTokens(s,n)},exports.parseFromTokens=parseFromTokens;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export { LayerName } from './nodes/layer-name';
2+
export { addLayerToModel } from './util/model';
3+
export { parse, parseFromTokens } from './parser/parse';
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
import{TokenType as e,stringify as t,tokenizer as n}from"@csstools/css-tokenizer";import{parseCommaSeparatedListOfComponentValues as r,isTokenNode as s,isCommentNode as a,isWhitespaceNode as o}from"@csstools/css-parser-algorithms";class LayerName{parts;constructor(e){this.parts=e}tokens(){return[...this.parts]}slice(t,n){const r=[];for(let t=0;t<this.parts.length;t++)this.parts[t][0]===e.Ident&&r.push(t);const s=r.slice(t,n);return new LayerName(this.parts.slice(s[0],s[s.length-1]+1))}concat(t){const n=[e.Delim,".",-1,-1,{value:"."}];return new LayerName([...this.parts.filter((t=>t[0]===e.Ident||t[0]===e.Delim)),n,...t.parts.filter((t=>t[0]===e.Ident||t[0]===e.Delim))])}segments(){return this.parts.filter((t=>t[0]===e.Ident)).map((e=>e[4].value))}name(){return this.parts.filter((t=>t[0]===e.Ident||t[0]===e.Delim)).map((e=>e[1])).join("")}equal(e){const t=this.segments(),n=e.segments();if(t.length!==n.length)return!1;for(let e=0;e<t.length;e++){if(t[e]!==n[e])return!1}return!0}toString(){return t(...this.parts)}toJSON(){return{parts:this.parts,segments:this.segments(),name:this.name()}}}function addLayerToModel(e,t){return t.forEach((t=>{const n=t.segments();e:for(let r=0;r<n.length;r++){const n=t.slice(0,r+1),s=n.segments();let a=-1,o=0;for(let t=0;t<e.length;t++){const n=e[t].segments();let r=0;t:for(let e=0;e<n.length;e++){const t=n[e],a=s[e];if(a===t&&e+1===s.length)continue e;if(a!==t){if(a!==t)break t}else r++}r>=o&&(a=t,o=r)}-1===a?e.push(n):e.splice(a+1,0,n)}})),e}function parseFromTokens(t,n){const i=r(t,{onParseError:null==n?void 0:n.onParseError}),l=(null==n?void 0:n.onParseError)??(()=>{}),genericError=e=>({message:`Invalid cascade layer name. ${e}`,start:t[0][2],end:t[t.length-1][3],state:["6.4.2. Layer Naming and Nesting","Layer name syntax","<layer-name> = <ident> [ '.' <ident> ]*"]}),m=[];for(let t=0;t<i.length;t++){const n=i[t];for(let e=0;e<n.length;e++){const t=n[e];if(!s(t)&&!a(t)&&!o(t))return l(genericError(`Invalid layer name part "${t.toString()}"`)),[]}const r=n.flatMap((e=>e.tokens()));let c,u=!1,p=!1;for(let t=0;t<r.length;t++){const n=r[t];if(n[0]!==e.Comment&&n[0]!==e.Whitespace&&n[0]!==e.Ident&&(n[0]!==e.Delim||"."!==n[4].value))return l(genericError(`Invalid character "${n[1]}"`)),[];if(!u&&n[0]===e.Delim)return l(genericError("Layer names can not start with a dot.")),[];if(u){if(n[0]===e.Whitespace){p=!0;continue}if(p&&n[0]===e.Comment)continue;if(p)return l(genericError("Encountered unexpected whitespace between layer name parts.")),[];if(c[0]===e.Ident&&n[0]===e.Ident)return l(genericError("Layer name parts must be separated by dots.")),[];if(c[0]===e.Delim&&n[0]===e.Delim)return l(genericError("Layer name parts must not be empty.")),[]}n[0]===e.Ident&&(u=!0),n[0]!==e.Ident&&n[0]!==e.Delim||(c=n)}if(!c)return l(genericError("Empty layer name.")),[];if(c[0]===e.Delim)return l(genericError("Layer name must not end with a dot.")),[];m.push(new LayerName(r))}return m}function parse(e,t){const r=n({css:e},{commentsAreTokens:!0,onParseError:null==t?void 0:t.onParseError}),s=[];for(;!r.endOfFile();)s.push(r.nextToken());return s.push(r.nextToken()),parseFromTokens(s,t)}export{LayerName,addLayerToModel,parse,parseFromTokens};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import { CSSToken } from '@csstools/css-tokenizer';
2+
export declare class LayerName {
3+
parts: Array<CSSToken>;
4+
constructor(parts: Array<CSSToken>);
5+
tokens(): Array<CSSToken>;
6+
slice(start: number, end: number): LayerName;
7+
concat(other: LayerName): LayerName;
8+
segments(): Array<string>;
9+
name(): string;
10+
equal(other: LayerName): boolean;
11+
toString(): string;
12+
toJSON(): {
13+
parts: CSSToken[];
14+
segments: string[];
15+
name: string;
16+
};
17+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import { ParserError } from '@csstools/css-parser-algorithms/dist/interfaces/error';
2+
import { CSSToken } from '@csstools/css-tokenizer';
3+
import { LayerName } from '../nodes/layer-name';
4+
export type Options = {
5+
onParseError?: (error: ParserError) => void;
6+
};
7+
export declare function parseFromTokens(tokens: Array<CSSToken>, options?: Options): LayerName[];
8+
export declare function parse(source: string, options?: Options): LayerName[];
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
import { LayerName } from '../nodes/layer-name';
2+
export declare function addLayerToModel(layers: Array<LayerName>, currentLayerNames: Array<LayerName>): LayerName[];
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
{
2+
"name": "@csstools/cascade-layer-name-parser",
3+
"description": "Parse CSS Cascade Layer names.",
4+
"version": "1.0.0",
5+
"contributors": [
6+
{
7+
"name": "Antonio Laguna",
8+
"email": "[email protected]",
9+
"url": "https://antonio.laguna.es"
10+
},
11+
{
12+
"name": "Romain Menke",
13+
"email": "[email protected]"
14+
}
15+
],
16+
"license": "MIT",
17+
"funding": {
18+
"type": "opencollective",
19+
"url": "https://opencollective.com/csstools"
20+
},
21+
"engines": {
22+
"node": "^14 || ^16 || >=18"
23+
},
24+
"main": "dist/index.cjs",
25+
"module": "dist/index.mjs",
26+
"types": "dist/index.d.ts",
27+
"exports": {
28+
".": {
29+
"import": "./dist/index.mjs",
30+
"require": "./dist/index.cjs",
31+
"default": "./dist/index.mjs"
32+
}
33+
},
34+
"files": [
35+
"CHANGELOG.md",
36+
"LICENSE.md",
37+
"README.md",
38+
"dist"
39+
],
40+
"peerDependencies": {
41+
"@csstools/css-parser-algorithms": "^1.0.0",
42+
"@csstools/css-tokenizer": "^1.0.0"
43+
},
44+
"scripts": {
45+
"prebuild": "npm run clean",
46+
"build": "rollup -c ../../rollup/default.mjs",
47+
"clean": "node -e \"fs.rmSync('./dist', { recursive: true, force: true }); fs.mkdirSync('./dist');\"",
48+
"lint": "npm run lint:eslint && npm run lint:package-json",
49+
"lint:eslint": "eslint ./src --ext .js --ext .ts --ext .mjs --no-error-on-unmatched-pattern",
50+
"lint:package-json": "node ../../.github/bin/format-package-json.mjs",
51+
"prepublishOnly": "npm run clean && npm run build && npm run test",
52+
"stryker": "stryker run --logLevel error",
53+
"test": "node ./test/test.mjs",
54+
"test:exports": "node ./test/_import.mjs && node ./test/_require.cjs",
55+
"test:rewrite-expects": "REWRITE_EXPECTS=true node ./test/test.mjs"
56+
},
57+
"homepage": "https://github.com/csstools/postcss-plugins/tree/main/packages/cascade-layer-name-parser#readme",
58+
"repository": {
59+
"type": "git",
60+
"url": "https://github.com/csstools/postcss-plugins.git",
61+
"directory": "packages/cascade-layer-name-parser"
62+
},
63+
"bugs": "https://github.com/csstools/postcss-plugins/issues",
64+
"keywords": [
65+
"cascade-layer",
66+
"css",
67+
"parser"
68+
],
69+
"volta": {
70+
"extends": "../../package.json"
71+
}
72+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export { LayerName } from './nodes/layer-name';
2+
export { addLayerToModel } from './util/model';
3+
export { parse, parseFromTokens } from './parser/parse';
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
import { TokenDelim } from '@csstools/css-tokenizer';
2+
import { CSSToken, stringify, TokenIdent, TokenType } from '@csstools/css-tokenizer';
3+
4+
export class LayerName {
5+
parts: Array<CSSToken>;
6+
7+
constructor(parts: Array<CSSToken>) {
8+
this.parts = parts;
9+
}
10+
11+
tokens(): Array<CSSToken> {
12+
return [
13+
...this.parts,
14+
];
15+
}
16+
17+
slice(start: number, end: number): LayerName {
18+
const indices = [];
19+
for (let i = 0; i < this.parts.length; i++) {
20+
if (this.parts[i][0] === TokenType.Ident) {
21+
indices.push(i);
22+
}
23+
}
24+
25+
const slice = indices.slice(start, end);
26+
return new LayerName(this.parts.slice(slice[0], slice[slice.length-1]+1));
27+
}
28+
29+
concat(other: LayerName): LayerName {
30+
const dot: TokenDelim = [
31+
TokenType.Delim,
32+
'.',
33+
-1,
34+
-1,
35+
{ value: '.' },
36+
];
37+
38+
return new LayerName([
39+
...this.parts.filter((x) => {
40+
return x[0] === TokenType.Ident || x[0] === TokenType.Delim;
41+
}),
42+
dot,
43+
...other.parts.filter((x) => {
44+
return x[0] === TokenType.Ident || x[0] === TokenType.Delim;
45+
}),
46+
]);
47+
}
48+
49+
segments(): Array<string> {
50+
return this.parts.filter((x) => {
51+
return x[0] === TokenType.Ident;
52+
}).map((x: TokenIdent) => {
53+
return x[4].value;
54+
});
55+
}
56+
57+
name(): string {
58+
return this.parts.filter((x) => {
59+
return x[0] === TokenType.Ident || x[0] === TokenType.Delim;
60+
}).map((x: TokenIdent) => {
61+
return x[1];
62+
}).join('');
63+
}
64+
65+
equal(other: LayerName): boolean {
66+
const a = this.segments();
67+
const b = other.segments();
68+
if (a.length !== b.length) {
69+
return false;
70+
}
71+
72+
for (let i = 0; i < a.length; i++) {
73+
const aa = a[i];
74+
const bb = b[i];
75+
if (aa !== bb) {
76+
return false;
77+
}
78+
}
79+
80+
return true;
81+
}
82+
83+
toString(): string {
84+
return stringify(...this.parts);
85+
}
86+
87+
toJSON() {
88+
return {
89+
parts: this.parts,
90+
segments: this.segments(),
91+
name: this.name(),
92+
};
93+
}
94+
}

0 commit comments

Comments
 (0)