@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.
Files changed (82) hide show
  1. package/README.md +64 -78
  2. package/dist/internal/cssom/declarations.d.ts +36 -0
  3. package/dist/internal/cssom/declarations.js +177 -0
  4. package/dist/internal/generated/css-data.d.ts +2 -0
  5. package/dist/internal/generated/css-data.js +16229 -0
  6. package/dist/internal/grammar/catalog-types.d.ts +26 -0
  7. package/dist/internal/grammar/value-definition.d.ts +63 -0
  8. package/dist/internal/grammar/value-definition.js +439 -0
  9. package/dist/internal/properties/matcher.d.ts +29 -0
  10. package/dist/internal/properties/matcher.js +791 -0
  11. package/dist/internal/properties/registry.d.ts +24 -0
  12. package/dist/internal/properties/registry.js +52 -0
  13. package/dist/internal/selectors/matcher.d.ts +96 -0
  14. package/dist/internal/selectors/matcher.js +616 -0
  15. package/dist/internal/selectors/parser.d.ts +2 -0
  16. package/dist/internal/selectors/parser.js +702 -0
  17. package/dist/internal/selectors/specificity.d.ts +3 -0
  18. package/dist/internal/selectors/specificity.js +77 -0
  19. package/dist/internal/selectors/types.d.ts +110 -0
  20. package/dist/internal/syntax/ast.d.ts +70 -0
  21. package/dist/internal/syntax/ast.js +1 -0
  22. package/dist/internal/syntax/characters.d.ts +8 -0
  23. package/dist/internal/syntax/characters.js +45 -0
  24. package/dist/internal/syntax/encoding.d.ts +15 -0
  25. package/dist/internal/syntax/encoding.js +161 -0
  26. package/dist/internal/syntax/input.d.ts +23 -0
  27. package/dist/internal/syntax/input.js +184 -0
  28. package/dist/internal/syntax/parser.d.ts +26 -0
  29. package/dist/internal/syntax/parser.js +581 -0
  30. package/dist/internal/syntax/resources.d.ts +28 -0
  31. package/dist/internal/syntax/resources.js +135 -0
  32. package/dist/internal/syntax/serialize.d.ts +10 -0
  33. package/dist/internal/syntax/serialize.js +630 -0
  34. package/dist/internal/syntax/token-stream.d.ts +16 -0
  35. package/dist/internal/syntax/token-stream.js +66 -0
  36. package/dist/internal/syntax/tokenizer.d.ts +21 -0
  37. package/dist/internal/syntax/tokenizer.js +571 -0
  38. package/dist/internal/syntax/tokens.d.ts +115 -0
  39. package/dist/internal/syntax/tokens.js +1 -0
  40. package/dist/internal/syntax/types.d.ts +46 -0
  41. package/dist/internal/syntax/types.js +1 -0
  42. package/dist/mod.d.ts +7 -1
  43. package/dist/mod.js +7 -1
  44. package/dist/public/edits.d.ts +12 -0
  45. package/dist/public/edits.js +195 -0
  46. package/dist/public/mod.d.ts +13 -69
  47. package/dist/public/mod.js +12 -1794
  48. package/dist/public/parse.d.ts +37 -0
  49. package/dist/public/parse.js +298 -0
  50. package/dist/public/traversal.d.ts +13 -0
  51. package/dist/public/traversal.js +96 -0
  52. package/dist/public/types.d.ts +78 -264
  53. package/package.json +32 -56
  54. package/THIRD_PARTY_NOTICES.md +0 -19
  55. package/dist/internal/csstree-runtime.d.ts +0 -20
  56. package/dist/internal/csstree-runtime.js +0 -21
  57. package/dist/internal/encoding/mod.d.ts +0 -1
  58. package/dist/internal/encoding/mod.js +0 -1
  59. package/dist/internal/encoding/sniff.d.ts +0 -14
  60. package/dist/internal/encoding/sniff.js +0 -95
  61. package/dist/internal/serializer/mod.d.ts +0 -1
  62. package/dist/internal/serializer/mod.js +0 -1
  63. package/dist/internal/serializer/serialize.d.ts +0 -3
  64. package/dist/internal/serializer/serialize.js +0 -89
  65. package/dist/internal/tokenizer/mod.d.ts +0 -2
  66. package/dist/internal/tokenizer/mod.js +0 -1
  67. package/dist/internal/tokenizer/tokenize.d.ts +0 -2
  68. package/dist/internal/tokenizer/tokenize.js +0 -39
  69. package/dist/internal/tokenizer/tokens.d.ts +0 -23
  70. package/dist/internal/tree/build.d.ts +0 -2
  71. package/dist/internal/tree/build.js +0 -85
  72. package/dist/internal/tree/mod.d.ts +0 -2
  73. package/dist/internal/tree/mod.js +0 -1
  74. package/dist/internal/tree/types.d.ts +0 -25
  75. package/dist/internal/vendor/csstree/LICENSE +0 -19
  76. package/dist/internal/vendor/csstree/csstree.esm.js +0 -12
  77. package/dist/internal/version.d.ts +0 -1
  78. package/dist/internal/version.js +0 -1
  79. package/dist/public/index.d.ts +0 -1
  80. package/dist/public/index.js +0 -1
  81. /package/dist/internal/{tokenizer/tokens.js → grammar/catalog-types.js} +0 -0
  82. /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
- Deterministic CSS parsing and selector utilities for automation across Node, Deno, Bun, and modern browsers.
4
-
5
- No runtime dependencies: this package ships with zero runtime dependencies.
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
- ```bash
9
+ ```sh
22
10
  npm install @ismail-elkorchi/css-parser
23
11
  ```
24
12
 
25
- ```bash
13
+ ```sh
26
14
  deno add jsr:@ismail-elkorchi/css-parser
27
15
  ```
28
16
 
29
- ## Import
17
+ ## Parse and serialize CSS
30
18
 
31
19
  ```ts
32
- import { parse } from "@ismail-elkorchi/css-parser";
33
- ```
20
+ import { parseStylesheet, serialize } from "@ismail-elkorchi/css-parser";
34
21
 
35
- ```txt
36
- import { parse } from "jsr:@ismail-elkorchi/css-parser";
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
- ## Copy/Paste Examples
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
- ### Example 1: Parse CSS
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
- ```ts
44
- import { parse } from "@ismail-elkorchi/css-parser";
39
+ ## Parse bytes and streams
45
40
 
46
- const tree = parse(".card { color: red; }");
47
- console.log(tree.kind);
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 { parseStream } from "@ismail-elkorchi/css-parser";
54
-
55
- const stream = new ReadableStream({
56
- start(controller) {
57
- controller.enqueue(new TextEncoder().encode(".a{display:block}"));
58
- controller.close();
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
- ### Example 3: Query selectors
67
-
68
- ```ts
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
- ### Example 4: Extract render signals
66
+ ## CSSOM declarations and selectors
76
67
 
77
- ```ts
78
- import { extractRenderSignals, parse } from "@ismail-elkorchi/css-parser";
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
- ## Compatibility
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
- Runtime compatibility matrix:
77
+ ## Traversal and source edits
93
78
 
94
- | Runtime | Status |
95
- | --- | --- |
96
- | Node.js | Supported |
97
- | Deno | Supported |
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
- ## Security and Safety Notes
84
+ Parsing and serialization are structural operations. They do not sanitize CSS,
85
+ resolve the cascade, or perform layout.
102
86
 
103
- Parsing is not sanitization. For untrusted input:
104
- - configure strict budgets,
105
- - handle `BudgetExceededError` explicitly,
106
- - apply separate policy checks before execution or rendering.
87
+ ## Project information
107
88
 
108
- ## Documentation
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
- - [Docs index](./docs/index.md)
111
- - [First parse success tutorial](./docs/tutorial/first-parse.md)
112
- - [Options reference](./docs/reference/options.md)
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
+ }
@@ -0,0 +1,2 @@
1
+ import type { CssWebrefData } from "../grammar/catalog-types.js";
2
+ export declare const CSS_WEBREF_DATA: CssWebrefData;