@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.
Files changed (82) hide show
  1. package/README.md +67 -69
  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 -35
  47. package/dist/public/mod.js +12 -1740
  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 -53
  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,100 +1,98 @@
1
1
  # @ismail-elkorchi/css-parser
2
2
 
3
- Deterministic CSS parsing and selector evaluation for automation pipelines that need stable output across Node, Deno, Bun, and modern browsers.
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
- ```bash
9
+ ```sh
8
10
  npm install @ismail-elkorchi/css-parser
9
11
  ```
10
12
 
11
- ```bash
13
+ ```sh
12
14
  deno add jsr:@ismail-elkorchi/css-parser
13
15
  ```
14
16
 
15
- ```txt
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 selector = compileSelectorList("#content .card");
47
- const root = {
48
- kind: "document",
49
- children: [{ kind: "element", tagName: "main", attributes: [{ name: "id", value: "content" }], children: [] }]
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(querySelectorAll(selector, root).length);
53
- console.log(serialize(parsed));
54
- console.log(serialize(streamed));
27
+ console.log(serialize(result.value));
55
28
  ```
56
29
 
57
- Runnable 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`.
58
33
 
59
- ```bash
60
- npm run examples:run
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
- - You need deterministic CSS parse/serialize behavior for repeatable tooling.
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
- ## When Not To Use
41
+ `parseStylesheetBytes()` and `parseStylesheetStream()` apply the CSS encoding
42
+ detection order and include the selected encoding in their result.
74
43
 
75
- - You need a browser layout engine.
76
- - You need CSS sanitization or policy enforcement beyond parsing.
77
- - You need runtime execution of scripts or DOM behavior.
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
- ## Security Note
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
- Use explicit budgets for untrusted input and fail closed on `BudgetExceededError`. Parsing validates syntax structure, not trust or safety policy. See [SECURITY.md](./SECURITY.md).
66
+ ## CSSOM declarations and selectors
82
67
 
83
- ## Runtime Compatibility
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
- - Node.js (current LTS and current stable)
86
- - Deno (stable)
87
- - Bun (stable)
88
- - Modern evergreen browsers (smoke-tested)
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
- ## No Runtime Dependencies
77
+ ## Traversal and source edits
91
78
 
92
- No runtime dependencies are used by production parser code.
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
- ## Docs Map
84
+ Parsing and serialization are structural operations. They do not sanitize CSS,
85
+ resolve the cascade, or perform layout.
95
86
 
96
- - [Documentation index](./docs/index.md)
87
+ ## Project information
97
88
 
98
- ## Release Trigger
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
- See [RELEASING.md](./RELEASING.md) for required secrets, trigger methods, and post-publish checks.
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;