@ismail-elkorchi/css-parser 0.1.1 → 0.2.0
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 +64 -78
- package/dist/internal/cssom/declarations.d.ts +36 -0
- package/dist/internal/cssom/declarations.js +177 -0
- package/dist/internal/generated/css-data.d.ts +2 -0
- package/dist/internal/generated/css-data.js +16229 -0
- package/dist/internal/grammar/catalog-types.d.ts +26 -0
- package/dist/internal/grammar/value-definition.d.ts +63 -0
- package/dist/internal/grammar/value-definition.js +439 -0
- package/dist/internal/properties/matcher.d.ts +29 -0
- package/dist/internal/properties/matcher.js +791 -0
- package/dist/internal/properties/registry.d.ts +24 -0
- package/dist/internal/properties/registry.js +52 -0
- package/dist/internal/selectors/matcher.d.ts +96 -0
- package/dist/internal/selectors/matcher.js +616 -0
- package/dist/internal/selectors/parser.d.ts +2 -0
- package/dist/internal/selectors/parser.js +702 -0
- package/dist/internal/selectors/specificity.d.ts +3 -0
- package/dist/internal/selectors/specificity.js +77 -0
- package/dist/internal/selectors/types.d.ts +110 -0
- package/dist/internal/syntax/ast.d.ts +70 -0
- package/dist/internal/syntax/ast.js +1 -0
- package/dist/internal/syntax/characters.d.ts +8 -0
- package/dist/internal/syntax/characters.js +45 -0
- package/dist/internal/syntax/encoding.d.ts +15 -0
- package/dist/internal/syntax/encoding.js +161 -0
- package/dist/internal/syntax/input.d.ts +23 -0
- package/dist/internal/syntax/input.js +184 -0
- package/dist/internal/syntax/parser.d.ts +26 -0
- package/dist/internal/syntax/parser.js +581 -0
- package/dist/internal/syntax/resources.d.ts +28 -0
- package/dist/internal/syntax/resources.js +135 -0
- package/dist/internal/syntax/serialize.d.ts +10 -0
- package/dist/internal/syntax/serialize.js +630 -0
- package/dist/internal/syntax/token-stream.d.ts +16 -0
- package/dist/internal/syntax/token-stream.js +66 -0
- package/dist/internal/syntax/tokenizer.d.ts +21 -0
- package/dist/internal/syntax/tokenizer.js +571 -0
- package/dist/internal/syntax/tokens.d.ts +115 -0
- package/dist/internal/syntax/tokens.js +1 -0
- package/dist/internal/syntax/types.d.ts +46 -0
- package/dist/internal/syntax/types.js +1 -0
- package/dist/mod.d.ts +7 -1
- package/dist/mod.js +7 -1
- package/dist/public/edits.d.ts +12 -0
- package/dist/public/edits.js +195 -0
- package/dist/public/mod.d.ts +13 -69
- package/dist/public/mod.js +12 -1794
- package/dist/public/parse.d.ts +37 -0
- package/dist/public/parse.js +298 -0
- package/dist/public/traversal.d.ts +13 -0
- package/dist/public/traversal.js +96 -0
- package/dist/public/types.d.ts +78 -264
- package/package.json +32 -56
- package/THIRD_PARTY_NOTICES.md +0 -19
- package/dist/internal/csstree-runtime.d.ts +0 -20
- package/dist/internal/csstree-runtime.js +0 -21
- package/dist/internal/encoding/mod.d.ts +0 -1
- package/dist/internal/encoding/mod.js +0 -1
- package/dist/internal/encoding/sniff.d.ts +0 -14
- package/dist/internal/encoding/sniff.js +0 -95
- package/dist/internal/serializer/mod.d.ts +0 -1
- package/dist/internal/serializer/mod.js +0 -1
- package/dist/internal/serializer/serialize.d.ts +0 -3
- package/dist/internal/serializer/serialize.js +0 -89
- package/dist/internal/tokenizer/mod.d.ts +0 -2
- package/dist/internal/tokenizer/mod.js +0 -1
- package/dist/internal/tokenizer/tokenize.d.ts +0 -2
- package/dist/internal/tokenizer/tokenize.js +0 -39
- package/dist/internal/tokenizer/tokens.d.ts +0 -23
- package/dist/internal/tree/build.d.ts +0 -2
- package/dist/internal/tree/build.js +0 -85
- package/dist/internal/tree/mod.d.ts +0 -2
- package/dist/internal/tree/mod.js +0 -1
- package/dist/internal/tree/types.d.ts +0 -25
- package/dist/internal/vendor/csstree/LICENSE +0 -19
- package/dist/internal/vendor/csstree/csstree.esm.js +0 -12
- package/dist/internal/version.d.ts +0 -1
- package/dist/internal/version.js +0 -1
- package/dist/public/index.d.ts +0 -1
- package/dist/public/index.js +0 -1
- /package/dist/internal/{tokenizer/tokens.js → grammar/catalog-types.js} +0 -0
- /package/dist/internal/{tree → selectors}/types.js +0 -0
package/README.md
CHANGED
|
@@ -1,112 +1,98 @@
|
|
|
1
1
|
# @ismail-elkorchi/css-parser
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
## When To Use
|
|
8
|
-
|
|
9
|
-
- You need deterministic CSS parse/serialize output.
|
|
10
|
-
- You need selector querying without browser-engine side effects.
|
|
11
|
-
- You need explicit resource budgets for untrusted stylesheets.
|
|
12
|
-
|
|
13
|
-
## When Not To Use
|
|
14
|
-
|
|
15
|
-
- You need browser layout/cascade computation.
|
|
16
|
-
- You need stylesheet sanitization and policy enforcement in one step.
|
|
17
|
-
- You need script or DOM runtime semantics.
|
|
3
|
+
A CSS syntax parser, serializer, CSSOM declaration model, property validator,
|
|
4
|
+
and Selectors Level 4 engine for Node.js, Deno, Bun, and modern browsers. It has
|
|
5
|
+
no runtime dependencies.
|
|
18
6
|
|
|
19
7
|
## Install
|
|
20
8
|
|
|
21
|
-
```
|
|
9
|
+
```sh
|
|
22
10
|
npm install @ismail-elkorchi/css-parser
|
|
23
11
|
```
|
|
24
12
|
|
|
25
|
-
```
|
|
13
|
+
```sh
|
|
26
14
|
deno add jsr:@ismail-elkorchi/css-parser
|
|
27
15
|
```
|
|
28
16
|
|
|
29
|
-
##
|
|
17
|
+
## Parse and serialize CSS
|
|
30
18
|
|
|
31
19
|
```ts
|
|
32
|
-
import {
|
|
33
|
-
```
|
|
20
|
+
import { parseStylesheet, serialize } from "@ismail-elkorchi/css-parser";
|
|
34
21
|
|
|
35
|
-
|
|
36
|
-
|
|
22
|
+
const result = parseStylesheet(".card { color: red; margin: 1rem; }");
|
|
23
|
+
if (!result.ok) {
|
|
24
|
+
throw new Error(result.errors.map((error) => error.message).join("\n"));
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
console.log(serialize(result.value));
|
|
37
28
|
```
|
|
38
29
|
|
|
39
|
-
|
|
30
|
+
Parse results are discriminated by `ok`. Successful results contain `value`,
|
|
31
|
+
recoverable `errors`, and measured `usage`. Resource or abort limits throw
|
|
32
|
+
`SyntaxResourceError` or `SyntaxAbortError`.
|
|
40
33
|
|
|
41
|
-
|
|
34
|
+
The package provides the CSS Syntax parsing entrypoints for stylesheets,
|
|
35
|
+
stylesheet contents, block contents, rules, declarations, component values, and
|
|
36
|
+
comma-separated component values. See the [API guide](./docs/api.md) for their
|
|
37
|
+
exact names and result types.
|
|
42
38
|
|
|
43
|
-
|
|
44
|
-
import { parse } from "@ismail-elkorchi/css-parser";
|
|
39
|
+
## Parse bytes and streams
|
|
45
40
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
```
|
|
49
|
-
|
|
50
|
-
### Example 2: Parse streaming input
|
|
41
|
+
`parseStylesheetBytes()` and `parseStylesheetStream()` apply the CSS encoding
|
|
42
|
+
detection order and include the selected encoding in their result.
|
|
51
43
|
|
|
52
44
|
```ts
|
|
53
|
-
import {
|
|
54
|
-
|
|
55
|
-
const
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
45
|
+
import { parseStylesheetStream } from "@ismail-elkorchi/css-parser";
|
|
46
|
+
|
|
47
|
+
const response = await fetch("https://example.test/site.css");
|
|
48
|
+
if (response.body === null) throw new Error("Response has no body");
|
|
49
|
+
|
|
50
|
+
const result = await parseStylesheetStream(response.body, {
|
|
51
|
+
limits: {
|
|
52
|
+
maxInputBytes: 1_000_000,
|
|
53
|
+
maxBufferedBytes: 64_000,
|
|
54
|
+
maxTokens: 100_000,
|
|
55
|
+
maxNodes: 50_000,
|
|
56
|
+
maxDepth: 128,
|
|
57
|
+
maxSteps: 1_000_000
|
|
59
58
|
}
|
|
60
59
|
});
|
|
61
|
-
|
|
62
|
-
const tree = await parseStream(stream, { budgets: { maxInputBytes: 4096, maxBufferedBytes: 512 } });
|
|
63
|
-
console.log(tree.kind);
|
|
64
60
|
```
|
|
65
61
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
import { compileSelectorList, querySelectorAll } from "@ismail-elkorchi/css-parser";
|
|
70
|
-
|
|
71
|
-
const selector = compileSelectorList(".card");
|
|
72
|
-
console.log(selector.supported, querySelectorAll(selector, { kind: "document", children: [] }).length);
|
|
73
|
-
```
|
|
62
|
+
Stream decoding is incremental; tree construction starts after the decoded
|
|
63
|
+
stylesheet reaches EOF. `tokenizeBytes()` and `tokenizeStream()` expose the same
|
|
64
|
+
encoding and resource accounting for tokenization.
|
|
74
65
|
|
|
75
|
-
|
|
66
|
+
## CSSOM declarations and selectors
|
|
76
67
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
const tree = parse(".card { color: red; }");
|
|
81
|
-
console.log(extractRenderSignals(tree).length >= 0);
|
|
82
|
-
```
|
|
83
|
-
|
|
84
|
-
Run packaged examples:
|
|
85
|
-
|
|
86
|
-
```bash
|
|
87
|
-
npm run examples:run
|
|
88
|
-
```
|
|
68
|
+
`CssDeclarationBlock` implements declaration parsing, priority, property-name,
|
|
69
|
+
mutation, and serialization semantics. `resolveCssProperty()` and
|
|
70
|
+
`validateCssPropertyValue()` use pinned WebRef grammar data.
|
|
89
71
|
|
|
90
|
-
|
|
72
|
+
`matchSelectorList()` and `querySelectorList()` require an explicit typed
|
|
73
|
+
environment for tree access, namespaces, document mode, attributes, and dynamic
|
|
74
|
+
pseudo-classes. Their results preserve `match`, `no-match`, and `unknown`
|
|
75
|
+
outcomes instead of guessing. See [Selector matching](./docs/selectors.md).
|
|
91
76
|
|
|
92
|
-
|
|
77
|
+
## Traversal and source edits
|
|
93
78
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
| Bun | Supported |
|
|
99
|
-
| Browser (evergreen) | Supported |
|
|
79
|
+
`walkCss()`, `findNodeById()`, and `findNodesByKind()` operate on the typed
|
|
80
|
+
structural syntax tree. `computePatch()` plans source-preserving remove, replace,
|
|
81
|
+
and boundary insert edits against node spans; `applyPatch()` verifies and
|
|
82
|
+
applies the resulting plan.
|
|
100
83
|
|
|
101
|
-
|
|
84
|
+
Parsing and serialization are structural operations. They do not sanitize CSS,
|
|
85
|
+
resolve the cascade, or perform layout.
|
|
102
86
|
|
|
103
|
-
|
|
104
|
-
- configure strict budgets,
|
|
105
|
-
- handle `BudgetExceededError` explicitly,
|
|
106
|
-
- apply separate policy checks before execution or rendering.
|
|
87
|
+
## Project information
|
|
107
88
|
|
|
108
|
-
|
|
89
|
+
- [API guide](./docs/api.md)
|
|
90
|
+
- [Selector matching](./docs/selectors.md)
|
|
91
|
+
- [Runnable examples](./examples/)
|
|
92
|
+
- [Development](./docs/development.md)
|
|
93
|
+
- [Security policy](./SECURITY.md)
|
|
94
|
+
- [Changelog](./CHANGELOG.md)
|
|
109
95
|
|
|
110
|
-
-
|
|
111
|
-
|
|
112
|
-
|
|
96
|
+
Node.js 20, 22, and 24 are tested. Cross-runtime qualification covers Node.js,
|
|
97
|
+
Deno, Bun, and Chromium. Release qualification also compares CSSOM
|
|
98
|
+
serialization with Chromium, Firefox, and WebKit.
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { PropertyValidationOptions } from "../properties/matcher.js";
|
|
2
|
+
import type { SyntaxParserOptions } from "../syntax/parser.js";
|
|
3
|
+
export interface CssDeclarationBlockOptions {
|
|
4
|
+
readonly parser?: SyntaxParserOptions;
|
|
5
|
+
readonly validation?: PropertyValidationOptions;
|
|
6
|
+
}
|
|
7
|
+
export interface CssomDeclaration {
|
|
8
|
+
readonly name: string;
|
|
9
|
+
readonly value: string;
|
|
10
|
+
readonly important: boolean;
|
|
11
|
+
}
|
|
12
|
+
export type CssDeclarationMutation = {
|
|
13
|
+
readonly status: "set";
|
|
14
|
+
readonly declaration: CssomDeclaration;
|
|
15
|
+
readonly previousValue: string;
|
|
16
|
+
} | {
|
|
17
|
+
readonly status: "removed";
|
|
18
|
+
readonly previousValue: string;
|
|
19
|
+
} | {
|
|
20
|
+
readonly status: "ignored";
|
|
21
|
+
readonly reason: "invalid-priority" | "unknown-property" | "invalid-value";
|
|
22
|
+
};
|
|
23
|
+
export declare class CssDeclarationBlock {
|
|
24
|
+
#private;
|
|
25
|
+
private constructor();
|
|
26
|
+
static parse(input: string, options?: CssDeclarationBlockOptions): CssDeclarationBlock;
|
|
27
|
+
static empty(options?: CssDeclarationBlockOptions): CssDeclarationBlock;
|
|
28
|
+
get length(): number;
|
|
29
|
+
get declarations(): readonly CssomDeclaration[];
|
|
30
|
+
get cssText(): string;
|
|
31
|
+
item(index: number): string;
|
|
32
|
+
getPropertyValue(name: string): string;
|
|
33
|
+
getPropertyPriority(name: string): "" | "important";
|
|
34
|
+
setProperty(name: string, value: string, priority?: string): CssDeclarationMutation;
|
|
35
|
+
removeProperty(name: string): string;
|
|
36
|
+
}
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import { validateCssPropertyValue } from "../properties/matcher.js";
|
|
2
|
+
import { resolveCssProperty } from "../properties/registry.js";
|
|
3
|
+
import { parseCssBlockContents, parseCssDeclaration } from "../syntax/parser.js";
|
|
4
|
+
import { serializeCssSyntax } from "../syntax/serialize.js";
|
|
5
|
+
function lowerAscii(value) {
|
|
6
|
+
return value.replace(/[A-Z]/gu, (character) => character.toLowerCase());
|
|
7
|
+
}
|
|
8
|
+
function propertyIdentity(name) {
|
|
9
|
+
const property = resolveCssProperty(name);
|
|
10
|
+
if (property === null)
|
|
11
|
+
return null;
|
|
12
|
+
if (property.kind === "custom") {
|
|
13
|
+
return Object.freeze({ key: property.name, name: property.name });
|
|
14
|
+
}
|
|
15
|
+
return Object.freeze({
|
|
16
|
+
key: lowerAscii(property.name),
|
|
17
|
+
name: property.name
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
function significant(values) {
|
|
21
|
+
return values.filter((value) => value.kind !== "whitespace");
|
|
22
|
+
}
|
|
23
|
+
function serializeCssomValue(declaration) {
|
|
24
|
+
if (declaration.name.startsWith("--")) {
|
|
25
|
+
return declaration.originalText ?? "";
|
|
26
|
+
}
|
|
27
|
+
let result = "";
|
|
28
|
+
for (const value of significant(declaration.value)) {
|
|
29
|
+
const serialized = serializeCssSyntax(value);
|
|
30
|
+
if (value.kind === "comma") {
|
|
31
|
+
result = result.trimEnd();
|
|
32
|
+
result += ",";
|
|
33
|
+
}
|
|
34
|
+
else {
|
|
35
|
+
if (result.length > 0)
|
|
36
|
+
result += " ";
|
|
37
|
+
result += serialized;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return result.trim();
|
|
41
|
+
}
|
|
42
|
+
function normalizedDeclaration(declaration, options) {
|
|
43
|
+
const validation = validateCssPropertyValue(declaration, options.validation);
|
|
44
|
+
if (validation.status === "invalid")
|
|
45
|
+
return null;
|
|
46
|
+
const identity = propertyIdentity(declaration.name);
|
|
47
|
+
if (identity === null)
|
|
48
|
+
return null;
|
|
49
|
+
return Object.freeze({
|
|
50
|
+
key: identity.key,
|
|
51
|
+
declaration: Object.freeze({
|
|
52
|
+
name: identity.name,
|
|
53
|
+
value: serializeCssomValue(declaration),
|
|
54
|
+
important: declaration.important
|
|
55
|
+
})
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
function parseMutationDeclaration(name, value, important, options) {
|
|
59
|
+
const source = `${name}:${value}${important ? "!important" : ""}`;
|
|
60
|
+
const result = parseCssDeclaration(source, options.parser);
|
|
61
|
+
return result.ok ? normalizedDeclaration(result.value, options) : null;
|
|
62
|
+
}
|
|
63
|
+
function priorityStatus(priority) {
|
|
64
|
+
const normalized = lowerAscii(priority.trim());
|
|
65
|
+
if (normalized === "")
|
|
66
|
+
return false;
|
|
67
|
+
if (normalized === "important")
|
|
68
|
+
return true;
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
export class CssDeclarationBlock {
|
|
72
|
+
#options;
|
|
73
|
+
#entries = new Map();
|
|
74
|
+
constructor(options) {
|
|
75
|
+
this.#options = options;
|
|
76
|
+
}
|
|
77
|
+
static parse(input, options = {}) {
|
|
78
|
+
const block = new CssDeclarationBlock(options);
|
|
79
|
+
const parsed = parseCssBlockContents(input, options.parser);
|
|
80
|
+
if (!parsed.ok)
|
|
81
|
+
return block;
|
|
82
|
+
for (const item of parsed.value) {
|
|
83
|
+
if (item.kind !== "declaration")
|
|
84
|
+
continue;
|
|
85
|
+
const normalized = normalizedDeclaration(item, options);
|
|
86
|
+
if (normalized === null)
|
|
87
|
+
continue;
|
|
88
|
+
block.#setNormalized(normalized);
|
|
89
|
+
}
|
|
90
|
+
return block;
|
|
91
|
+
}
|
|
92
|
+
static empty(options = {}) {
|
|
93
|
+
return new CssDeclarationBlock(options);
|
|
94
|
+
}
|
|
95
|
+
get length() {
|
|
96
|
+
return this.#entries.size;
|
|
97
|
+
}
|
|
98
|
+
get declarations() {
|
|
99
|
+
return Object.freeze([...this.#entries.values()]);
|
|
100
|
+
}
|
|
101
|
+
get cssText() {
|
|
102
|
+
return [...this.#entries.values()]
|
|
103
|
+
.map((declaration) => `${declaration.name}: ${declaration.value}` +
|
|
104
|
+
`${declaration.important ? " !important" : ""};`)
|
|
105
|
+
.join(" ");
|
|
106
|
+
}
|
|
107
|
+
item(index) {
|
|
108
|
+
if (!Number.isInteger(index) || index < 0)
|
|
109
|
+
return "";
|
|
110
|
+
return [...this.#entries.values()][index]?.name ?? "";
|
|
111
|
+
}
|
|
112
|
+
getPropertyValue(name) {
|
|
113
|
+
const identity = propertyIdentity(name);
|
|
114
|
+
return identity === null
|
|
115
|
+
? ""
|
|
116
|
+
: this.#entries.get(identity.key)?.value ?? "";
|
|
117
|
+
}
|
|
118
|
+
getPropertyPriority(name) {
|
|
119
|
+
const identity = propertyIdentity(name);
|
|
120
|
+
return identity !== null && this.#entries.get(identity.key)?.important === true
|
|
121
|
+
? "important"
|
|
122
|
+
: "";
|
|
123
|
+
}
|
|
124
|
+
setProperty(name, value, priority = "") {
|
|
125
|
+
const important = priorityStatus(priority);
|
|
126
|
+
if (important === null) {
|
|
127
|
+
return Object.freeze({
|
|
128
|
+
status: "ignored",
|
|
129
|
+
reason: "invalid-priority"
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
if (value.length === 0) {
|
|
133
|
+
const previousValue = this.removeProperty(name);
|
|
134
|
+
return Object.freeze({
|
|
135
|
+
status: "removed",
|
|
136
|
+
previousValue
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
const identity = propertyIdentity(name);
|
|
140
|
+
if (identity === null) {
|
|
141
|
+
return Object.freeze({
|
|
142
|
+
status: "ignored",
|
|
143
|
+
reason: "unknown-property"
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
const normalized = parseMutationDeclaration(name, value, important, this.#options);
|
|
147
|
+
if (normalized === null) {
|
|
148
|
+
return Object.freeze({
|
|
149
|
+
status: "ignored",
|
|
150
|
+
reason: "invalid-value"
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
const previousValue = this.#entries.get(identity.key)?.value ?? "";
|
|
154
|
+
this.#entries.set(normalized.key, normalized.declaration);
|
|
155
|
+
return Object.freeze({
|
|
156
|
+
status: "set",
|
|
157
|
+
declaration: normalized.declaration,
|
|
158
|
+
previousValue
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
removeProperty(name) {
|
|
162
|
+
const identity = propertyIdentity(name);
|
|
163
|
+
if (identity === null)
|
|
164
|
+
return "";
|
|
165
|
+
const previous = this.#entries.get(identity.key);
|
|
166
|
+
this.#entries.delete(identity.key);
|
|
167
|
+
return previous?.value ?? "";
|
|
168
|
+
}
|
|
169
|
+
#setNormalized(normalized) {
|
|
170
|
+
const previous = this.#entries.get(normalized.key);
|
|
171
|
+
if (previous?.important === true &&
|
|
172
|
+
!normalized.declaration.important) {
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
this.#entries.set(normalized.key, normalized.declaration);
|
|
176
|
+
}
|
|
177
|
+
}
|