@ismail-elkorchi/css-parser 0.1.0 → 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 +67 -69
- 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 -35
- package/dist/public/mod.js +12 -1740
- 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 -53
- 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,100 +1,98 @@
|
|
|
1
1
|
# @ismail-elkorchi/css-parser
|
|
2
2
|
|
|
3
|
-
|
|
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.
|
|
4
6
|
|
|
5
7
|
## Install
|
|
6
8
|
|
|
7
|
-
```
|
|
9
|
+
```sh
|
|
8
10
|
npm install @ismail-elkorchi/css-parser
|
|
9
11
|
```
|
|
10
12
|
|
|
11
|
-
```
|
|
13
|
+
```sh
|
|
12
14
|
deno add jsr:@ismail-elkorchi/css-parser
|
|
13
15
|
```
|
|
14
16
|
|
|
15
|
-
|
|
16
|
-
import { parse } from "jsr:@ismail-elkorchi/css-parser";
|
|
17
|
-
```
|
|
18
|
-
|
|
19
|
-
## Success Path
|
|
17
|
+
## Parse and serialize CSS
|
|
20
18
|
|
|
21
19
|
```ts
|
|
22
|
-
import {
|
|
23
|
-
compileSelectorList,
|
|
24
|
-
parse,
|
|
25
|
-
parseStream,
|
|
26
|
-
querySelectorAll,
|
|
27
|
-
serialize
|
|
28
|
-
} from "@ismail-elkorchi/css-parser";
|
|
29
|
-
|
|
30
|
-
const css = ".card { color: red; } #content .card { margin: 1px; }";
|
|
31
|
-
const parsed = parse(css, {
|
|
32
|
-
budgets: { maxInputBytes: 4096, maxNodes: 256, maxDepth: 32 }
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
const stream = new ReadableStream({
|
|
36
|
-
start(controller) {
|
|
37
|
-
controller.enqueue(new TextEncoder().encode(".a{display:block}"));
|
|
38
|
-
controller.close();
|
|
39
|
-
}
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
const streamed = await parseStream(stream, {
|
|
43
|
-
budgets: { maxInputBytes: 4096, maxBufferedBytes: 256, maxNodes: 256 }
|
|
44
|
-
});
|
|
20
|
+
import { parseStylesheet, serialize } from "@ismail-elkorchi/css-parser";
|
|
45
21
|
|
|
46
|
-
const
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
};
|
|
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
|
+
}
|
|
51
26
|
|
|
52
|
-
console.log(
|
|
53
|
-
console.log(serialize(parsed));
|
|
54
|
-
console.log(serialize(streamed));
|
|
27
|
+
console.log(serialize(result.value));
|
|
55
28
|
```
|
|
56
29
|
|
|
57
|
-
|
|
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`.
|
|
58
33
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
## Options / API Reference
|
|
64
|
-
|
|
65
|
-
- [Options and API reference](./docs/reference/options.md)
|
|
66
|
-
|
|
67
|
-
## When To Use
|
|
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.
|
|
68
38
|
|
|
69
|
-
|
|
70
|
-
- You need parse budgets to bound runtime work and memory growth.
|
|
71
|
-
- You need selector matching utilities in the same package as parsing.
|
|
39
|
+
## Parse bytes and streams
|
|
72
40
|
|
|
73
|
-
|
|
41
|
+
`parseStylesheetBytes()` and `parseStylesheetStream()` apply the CSS encoding
|
|
42
|
+
detection order and include the selected encoding in their result.
|
|
74
43
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
44
|
+
```ts
|
|
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
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
```
|
|
78
61
|
|
|
79
|
-
|
|
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.
|
|
80
65
|
|
|
81
|
-
|
|
66
|
+
## CSSOM declarations and selectors
|
|
82
67
|
|
|
83
|
-
|
|
68
|
+
`CssDeclarationBlock` implements declaration parsing, priority, property-name,
|
|
69
|
+
mutation, and serialization semantics. `resolveCssProperty()` and
|
|
70
|
+
`validateCssPropertyValue()` use pinned WebRef grammar data.
|
|
84
71
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
-
|
|
88
|
-
|
|
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).
|
|
89
76
|
|
|
90
|
-
##
|
|
77
|
+
## Traversal and source edits
|
|
91
78
|
|
|
92
|
-
|
|
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.
|
|
93
83
|
|
|
94
|
-
|
|
84
|
+
Parsing and serialization are structural operations. They do not sanitize CSS,
|
|
85
|
+
resolve the cascade, or perform layout.
|
|
95
86
|
|
|
96
|
-
|
|
87
|
+
## Project information
|
|
97
88
|
|
|
98
|
-
|
|
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)
|
|
99
95
|
|
|
100
|
-
|
|
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
|
+
}
|