@ismail-elkorchi/css-parser 0.1.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/LICENSE +21 -0
- package/README.md +100 -0
- package/THIRD_PARTY_NOTICES.md +19 -0
- package/dist/internal/csstree-runtime.d.ts +20 -0
- package/dist/internal/csstree-runtime.js +21 -0
- package/dist/internal/encoding/mod.d.ts +1 -0
- package/dist/internal/encoding/mod.js +1 -0
- package/dist/internal/encoding/sniff.d.ts +14 -0
- package/dist/internal/encoding/sniff.js +95 -0
- package/dist/internal/serializer/mod.d.ts +1 -0
- package/dist/internal/serializer/mod.js +1 -0
- package/dist/internal/serializer/serialize.d.ts +3 -0
- package/dist/internal/serializer/serialize.js +89 -0
- package/dist/internal/tokenizer/mod.d.ts +2 -0
- package/dist/internal/tokenizer/mod.js +1 -0
- package/dist/internal/tokenizer/tokenize.d.ts +2 -0
- package/dist/internal/tokenizer/tokenize.js +39 -0
- package/dist/internal/tokenizer/tokens.d.ts +23 -0
- package/dist/internal/tokenizer/tokens.js +1 -0
- package/dist/internal/tree/build.d.ts +2 -0
- package/dist/internal/tree/build.js +85 -0
- package/dist/internal/tree/mod.d.ts +2 -0
- package/dist/internal/tree/mod.js +1 -0
- package/dist/internal/tree/types.d.ts +25 -0
- package/dist/internal/tree/types.js +1 -0
- package/dist/internal/vendor/csstree/LICENSE +19 -0
- package/dist/internal/vendor/csstree/csstree.esm.js +12 -0
- package/dist/internal/version.d.ts +1 -0
- package/dist/internal/version.js +1 -0
- package/dist/mod.d.ts +1 -0
- package/dist/mod.js +1 -0
- package/dist/public/index.d.ts +1 -0
- package/dist/public/index.js +1 -0
- package/dist/public/mod.d.ts +35 -0
- package/dist/public/mod.js +1740 -0
- package/dist/public/types.d.ts +279 -0
- package/dist/public/types.js +1 -0
- package/package.json +87 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Ismail Elkorchi
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, 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,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
# @ismail-elkorchi/css-parser
|
|
2
|
+
|
|
3
|
+
Deterministic CSS parsing and selector evaluation for automation pipelines that need stable output across Node, Deno, Bun, and modern browsers.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @ismail-elkorchi/css-parser
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
deno add jsr:@ismail-elkorchi/css-parser
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
```txt
|
|
16
|
+
import { parse } from "jsr:@ismail-elkorchi/css-parser";
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Success Path
|
|
20
|
+
|
|
21
|
+
```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
|
+
});
|
|
45
|
+
|
|
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
|
+
};
|
|
51
|
+
|
|
52
|
+
console.log(querySelectorAll(selector, root).length);
|
|
53
|
+
console.log(serialize(parsed));
|
|
54
|
+
console.log(serialize(streamed));
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Runnable examples:
|
|
58
|
+
|
|
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
|
|
68
|
+
|
|
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.
|
|
72
|
+
|
|
73
|
+
## When Not To Use
|
|
74
|
+
|
|
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.
|
|
78
|
+
|
|
79
|
+
## Security Note
|
|
80
|
+
|
|
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).
|
|
82
|
+
|
|
83
|
+
## Runtime Compatibility
|
|
84
|
+
|
|
85
|
+
- Node.js (current LTS and current stable)
|
|
86
|
+
- Deno (stable)
|
|
87
|
+
- Bun (stable)
|
|
88
|
+
- Modern evergreen browsers (smoke-tested)
|
|
89
|
+
|
|
90
|
+
## No Runtime Dependencies
|
|
91
|
+
|
|
92
|
+
No runtime dependencies are used by production parser code.
|
|
93
|
+
|
|
94
|
+
## Docs Map
|
|
95
|
+
|
|
96
|
+
- [Documentation index](./docs/index.md)
|
|
97
|
+
|
|
98
|
+
## Release Trigger
|
|
99
|
+
|
|
100
|
+
See [RELEASING.md](./RELEASING.md) for required secrets, trigger methods, and post-publish checks.
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# Third-party notices
|
|
2
|
+
|
|
3
|
+
This repository vendors third-party sources and datasets for deterministic runtime behavior and conformance evaluation.
|
|
4
|
+
|
|
5
|
+
## CSSTree
|
|
6
|
+
- Source: https://github.com/csstree/csstree
|
|
7
|
+
- Package/version: `css-tree@3.1.0`
|
|
8
|
+
- Vendored files: `src/internal/vendor/csstree/csstree.esm.js`
|
|
9
|
+
- License: MIT (`src/internal/vendor/csstree/LICENSE`)
|
|
10
|
+
|
|
11
|
+
## CSSWG draft snapshots
|
|
12
|
+
- Source: https://drafts.csswg.org/
|
|
13
|
+
- Vendored files: `vendor/specs/csswg-drafts/*.html`
|
|
14
|
+
- License: W3C/CSSWG publication terms per draft
|
|
15
|
+
|
|
16
|
+
## Web Platform Tests (WPT)
|
|
17
|
+
- Source: https://github.com/web-platform-tests/wpt
|
|
18
|
+
- Vendored sparse checkout: `vendor/wpt`
|
|
19
|
+
- License: upstream WPT repository license
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export type CssTreeParseContext = "stylesheet" | "atrule" | "atrulePrelude" | "mediaQueryList" | "mediaQuery" | "condition" | "rule" | "selectorList" | "selector" | "block" | "declarationList" | "declaration" | "value";
|
|
2
|
+
export interface CssTreeParseError {
|
|
3
|
+
readonly message: string;
|
|
4
|
+
readonly formattedMessage?: string;
|
|
5
|
+
readonly offset?: number;
|
|
6
|
+
readonly line?: number;
|
|
7
|
+
readonly column?: number;
|
|
8
|
+
}
|
|
9
|
+
export interface CssTreeParseOptions {
|
|
10
|
+
readonly context?: CssTreeParseContext;
|
|
11
|
+
readonly positions?: boolean;
|
|
12
|
+
readonly onParseError?: (error: CssTreeParseError) => void;
|
|
13
|
+
}
|
|
14
|
+
export declare function parse(source: string, options?: CssTreeParseOptions): unknown;
|
|
15
|
+
export declare function generate(ast: unknown): string;
|
|
16
|
+
export declare function tokenize(source: string, callback: (type: number, start: number, end: number) => void): void;
|
|
17
|
+
export declare function toPlainObject(ast: unknown): unknown;
|
|
18
|
+
export declare function fromPlainObject(ast: Record<string, unknown>): unknown;
|
|
19
|
+
export declare const tokenNames: readonly string[];
|
|
20
|
+
export declare const CSSTREE_VERSION: string;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// Vendored CSSTree runtime entrypoint.
|
|
2
|
+
// @ts-expect-error Vendored JS module does not include local .d.ts in this repository.
|
|
3
|
+
import * as runtime from "./vendor/csstree/csstree.esm.js";
|
|
4
|
+
const csstree = runtime;
|
|
5
|
+
export function parse(source, options) {
|
|
6
|
+
return csstree.parse(source, options);
|
|
7
|
+
}
|
|
8
|
+
export function generate(ast) {
|
|
9
|
+
return csstree.generate(ast);
|
|
10
|
+
}
|
|
11
|
+
export function tokenize(source, callback) {
|
|
12
|
+
csstree.tokenize(source, callback);
|
|
13
|
+
}
|
|
14
|
+
export function toPlainObject(ast) {
|
|
15
|
+
return csstree.toPlainObject(ast);
|
|
16
|
+
}
|
|
17
|
+
export function fromPlainObject(ast) {
|
|
18
|
+
return csstree.fromPlainObject(ast);
|
|
19
|
+
}
|
|
20
|
+
export const tokenNames = csstree.tokenNames;
|
|
21
|
+
export const CSSTREE_VERSION = csstree.version;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { decodeCssBytes, sniffCssEncoding, type EncodingSniffOptions, type EncodingSniffResult } from "./sniff.js";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { decodeCssBytes, sniffCssEncoding } from "./sniff.js";
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export interface EncodingSniffOptions {
|
|
2
|
+
readonly transportEncodingLabel?: string;
|
|
3
|
+
readonly maxPrescanBytes?: number;
|
|
4
|
+
readonly defaultEncoding?: string;
|
|
5
|
+
}
|
|
6
|
+
export interface EncodingSniffResult {
|
|
7
|
+
readonly encoding: string;
|
|
8
|
+
readonly source: "bom" | "transport" | "charset" | "default";
|
|
9
|
+
}
|
|
10
|
+
export declare function sniffCssEncoding(bytes: Uint8Array, options?: EncodingSniffOptions): EncodingSniffResult;
|
|
11
|
+
export declare function decodeCssBytes(bytes: Uint8Array, options?: EncodingSniffOptions): {
|
|
12
|
+
text: string;
|
|
13
|
+
sniff: EncodingSniffResult;
|
|
14
|
+
};
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
const WINDOWS_1252_ALIASES = new Set([
|
|
2
|
+
"iso-8859-1",
|
|
3
|
+
"iso8859-1",
|
|
4
|
+
"latin1",
|
|
5
|
+
"latin-1",
|
|
6
|
+
"us-ascii"
|
|
7
|
+
]);
|
|
8
|
+
function detectBom(bytes) {
|
|
9
|
+
if (bytes.length >= 3 && bytes[0] === 0xef && bytes[1] === 0xbb && bytes[2] === 0xbf) {
|
|
10
|
+
return "utf-8";
|
|
11
|
+
}
|
|
12
|
+
if (bytes.length >= 2 && bytes[0] === 0xfe && bytes[1] === 0xff) {
|
|
13
|
+
return "utf-16be";
|
|
14
|
+
}
|
|
15
|
+
if (bytes.length >= 2 && bytes[0] === 0xff && bytes[1] === 0xfe) {
|
|
16
|
+
return "utf-16le";
|
|
17
|
+
}
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
function stripQuotes(value) {
|
|
21
|
+
const trimmed = value.trim();
|
|
22
|
+
if ((trimmed.startsWith("\"") && trimmed.endsWith("\"")) ||
|
|
23
|
+
(trimmed.startsWith("'") && trimmed.endsWith("'"))) {
|
|
24
|
+
return trimmed.slice(1, -1).trim();
|
|
25
|
+
}
|
|
26
|
+
return trimmed;
|
|
27
|
+
}
|
|
28
|
+
function canonicalizeLabel(label) {
|
|
29
|
+
const normalized = stripQuotes(label).toLowerCase();
|
|
30
|
+
if (normalized.length === 0) {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
if (WINDOWS_1252_ALIASES.has(normalized)) {
|
|
34
|
+
return "windows-1252";
|
|
35
|
+
}
|
|
36
|
+
try {
|
|
37
|
+
const resolved = new TextDecoder(normalized).encoding.toLowerCase();
|
|
38
|
+
if (resolved === "iso-8859-1") {
|
|
39
|
+
return "windows-1252";
|
|
40
|
+
}
|
|
41
|
+
return resolved;
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
function decodeLatin1(bytes) {
|
|
48
|
+
let out = "";
|
|
49
|
+
for (let index = 0; index < bytes.length; index += 1) {
|
|
50
|
+
const value = bytes[index];
|
|
51
|
+
if (value !== undefined) {
|
|
52
|
+
out += String.fromCharCode(value);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return out;
|
|
56
|
+
}
|
|
57
|
+
function sniffCharsetRule(bytes, maxPrescanBytes) {
|
|
58
|
+
const scanSize = Math.min(bytes.length, maxPrescanBytes);
|
|
59
|
+
let scan = decodeLatin1(bytes.subarray(0, scanSize));
|
|
60
|
+
if (scan.charCodeAt(0) === 0xfeff) {
|
|
61
|
+
scan = scan.slice(1);
|
|
62
|
+
}
|
|
63
|
+
const match = scan.match(/^\s*@charset\s+("([^"]+)"|'([^']+)')\s*;/i);
|
|
64
|
+
if (!match) {
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
const label = match[2] ?? match[3] ?? "";
|
|
68
|
+
return canonicalizeLabel(label);
|
|
69
|
+
}
|
|
70
|
+
export function sniffCssEncoding(bytes, options = {}) {
|
|
71
|
+
const defaultEncoding = canonicalizeLabel(options.defaultEncoding ?? "utf-8") ?? "utf-8";
|
|
72
|
+
const bom = detectBom(bytes);
|
|
73
|
+
if (bom) {
|
|
74
|
+
return { encoding: bom, source: "bom" };
|
|
75
|
+
}
|
|
76
|
+
if (options.transportEncodingLabel) {
|
|
77
|
+
const transport = canonicalizeLabel(options.transportEncodingLabel);
|
|
78
|
+
if (transport) {
|
|
79
|
+
return { encoding: transport, source: "transport" };
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
const charset = sniffCharsetRule(bytes, options.maxPrescanBytes ?? 1024);
|
|
83
|
+
if (charset) {
|
|
84
|
+
return { encoding: charset, source: "charset" };
|
|
85
|
+
}
|
|
86
|
+
return { encoding: defaultEncoding, source: "default" };
|
|
87
|
+
}
|
|
88
|
+
export function decodeCssBytes(bytes, options = {}) {
|
|
89
|
+
const sniff = sniffCssEncoding(bytes, options);
|
|
90
|
+
const decoder = new TextDecoder(sniff.encoding);
|
|
91
|
+
return {
|
|
92
|
+
text: decoder.decode(bytes),
|
|
93
|
+
sniff
|
|
94
|
+
};
|
|
95
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { sanitizeCssNode, serializeTreeNode } from "./serialize.js";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { sanitizeCssNode, serializeTreeNode } from "./serialize.js";
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { fromPlainObject, generate } from "../csstree-runtime.js";
|
|
2
|
+
const METADATA_KEYS = new Set(["id", "span", "spanProvenance"]);
|
|
3
|
+
function isRecord(value) {
|
|
4
|
+
return value !== null && typeof value === "object";
|
|
5
|
+
}
|
|
6
|
+
function hasLeadingWhitespace(value) {
|
|
7
|
+
if (value.length === 0) {
|
|
8
|
+
return false;
|
|
9
|
+
}
|
|
10
|
+
const first = value.charCodeAt(0);
|
|
11
|
+
return first === 0x20 || first === 0x09 || first === 0x0a || first === 0x0d || first === 0x0c;
|
|
12
|
+
}
|
|
13
|
+
function normalizeCustomPropertyValue(valueNode) {
|
|
14
|
+
const valueType = valueNode["type"];
|
|
15
|
+
if (valueType === "Raw") {
|
|
16
|
+
const rawValue = valueNode["value"];
|
|
17
|
+
if (typeof rawValue !== "string" || rawValue.length === 0 || hasLeadingWhitespace(rawValue)) {
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
valueNode["value"] = ` ${rawValue}`;
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
if (valueType !== "Value") {
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
const children = valueNode["children"];
|
|
27
|
+
if (!Array.isArray(children) || children.length === 0) {
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
const [first] = children;
|
|
31
|
+
if (isRecord(first) && first["type"] === "WhiteSpace") {
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
children.unshift({
|
|
35
|
+
type: "WhiteSpace",
|
|
36
|
+
loc: null,
|
|
37
|
+
value: " "
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
function normalizeCustomPropertySpacing(node) {
|
|
41
|
+
if (Array.isArray(node)) {
|
|
42
|
+
for (const entry of node) {
|
|
43
|
+
normalizeCustomPropertySpacing(entry);
|
|
44
|
+
}
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
if (!isRecord(node)) {
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
if (node["type"] === "Declaration") {
|
|
51
|
+
const property = node["property"];
|
|
52
|
+
const value = node["value"];
|
|
53
|
+
if (typeof property === "string" && property.startsWith("--") && isRecord(value)) {
|
|
54
|
+
normalizeCustomPropertyValue(value);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
for (const entry of Object.values(node)) {
|
|
58
|
+
normalizeCustomPropertySpacing(entry);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
function sanitizeForRuntime(value) {
|
|
62
|
+
if (Array.isArray(value)) {
|
|
63
|
+
return value.map((entry) => sanitizeForRuntime(entry));
|
|
64
|
+
}
|
|
65
|
+
if (!isRecord(value)) {
|
|
66
|
+
return value;
|
|
67
|
+
}
|
|
68
|
+
const next = {};
|
|
69
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
70
|
+
if (METADATA_KEYS.has(key)) {
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
next[key] = sanitizeForRuntime(entry);
|
|
74
|
+
}
|
|
75
|
+
return next;
|
|
76
|
+
}
|
|
77
|
+
export function sanitizeCssNode(node) {
|
|
78
|
+
const sanitized = sanitizeForRuntime(node);
|
|
79
|
+
if (!isRecord(sanitized)) {
|
|
80
|
+
throw new Error("CSS node must be an object");
|
|
81
|
+
}
|
|
82
|
+
return sanitized;
|
|
83
|
+
}
|
|
84
|
+
export function serializeTreeNode(node) {
|
|
85
|
+
const sanitized = sanitizeCssNode(node);
|
|
86
|
+
normalizeCustomPropertySpacing(sanitized);
|
|
87
|
+
const runtimeNode = fromPlainObject(sanitized);
|
|
88
|
+
return generate(runtimeNode);
|
|
89
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { tokenize } from "./tokenize.js";
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { tokenNames, tokenize as tokenizeWithCssTree } from "../csstree-runtime.js";
|
|
2
|
+
function normalizeTokenKind(rawKind) {
|
|
3
|
+
return rawKind.endsWith("-token") ? rawKind.slice(0, -6) : rawKind;
|
|
4
|
+
}
|
|
5
|
+
export function tokenize(input, options = {}) {
|
|
6
|
+
const startedAt = Date.now();
|
|
7
|
+
const tokens = [];
|
|
8
|
+
const errors = [];
|
|
9
|
+
tokenizeWithCssTree(input, (type, start, end) => {
|
|
10
|
+
const rawKind = tokenNames[type] ?? `token-${String(type)}`;
|
|
11
|
+
const kind = normalizeTokenKind(rawKind);
|
|
12
|
+
tokens.push({
|
|
13
|
+
type,
|
|
14
|
+
kind,
|
|
15
|
+
rawKind,
|
|
16
|
+
value: input.slice(start, end),
|
|
17
|
+
start,
|
|
18
|
+
end
|
|
19
|
+
});
|
|
20
|
+
});
|
|
21
|
+
const maxTokens = options.budgets?.maxTokens;
|
|
22
|
+
if (maxTokens !== undefined && tokens.length > maxTokens) {
|
|
23
|
+
errors.push({
|
|
24
|
+
code: "max-tokens-exceeded",
|
|
25
|
+
index: tokens[maxTokens]?.start ?? input.length
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
const maxTimeMs = options.budgets?.maxTimeMs;
|
|
29
|
+
if (maxTimeMs !== undefined && Date.now() - startedAt > maxTimeMs) {
|
|
30
|
+
errors.push({
|
|
31
|
+
code: "soft-time-budget-exceeded",
|
|
32
|
+
index: input.length
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
return {
|
|
36
|
+
tokens: Object.freeze(tokens),
|
|
37
|
+
errors: Object.freeze(errors)
|
|
38
|
+
};
|
|
39
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export interface CssTokenInternal {
|
|
2
|
+
readonly type: number;
|
|
3
|
+
readonly kind: string;
|
|
4
|
+
readonly rawKind: string;
|
|
5
|
+
readonly value: string;
|
|
6
|
+
readonly start: number;
|
|
7
|
+
readonly end: number;
|
|
8
|
+
}
|
|
9
|
+
export interface TokenizerParseError {
|
|
10
|
+
readonly code: string;
|
|
11
|
+
readonly index: number;
|
|
12
|
+
}
|
|
13
|
+
export interface TokenizerBudgets {
|
|
14
|
+
readonly maxTokens?: number;
|
|
15
|
+
readonly maxTimeMs?: number;
|
|
16
|
+
}
|
|
17
|
+
export interface TokenizeOptions {
|
|
18
|
+
readonly budgets?: TokenizerBudgets;
|
|
19
|
+
}
|
|
20
|
+
export interface TokenizeResult {
|
|
21
|
+
readonly tokens: readonly CssTokenInternal[];
|
|
22
|
+
readonly errors: readonly TokenizerParseError[];
|
|
23
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { parse as parseCssTree, toPlainObject } from "../csstree-runtime.js";
|
|
2
|
+
const SUPPORTED_CONTEXTS = new Set([
|
|
3
|
+
"stylesheet",
|
|
4
|
+
"atrule",
|
|
5
|
+
"atrulePrelude",
|
|
6
|
+
"mediaQueryList",
|
|
7
|
+
"mediaQuery",
|
|
8
|
+
"condition",
|
|
9
|
+
"rule",
|
|
10
|
+
"selectorList",
|
|
11
|
+
"selector",
|
|
12
|
+
"block",
|
|
13
|
+
"declarationList",
|
|
14
|
+
"declaration",
|
|
15
|
+
"value"
|
|
16
|
+
]);
|
|
17
|
+
function isRecord(value) {
|
|
18
|
+
return value !== null && typeof value === "object";
|
|
19
|
+
}
|
|
20
|
+
function toTreeError(error) {
|
|
21
|
+
return {
|
|
22
|
+
message: error.message,
|
|
23
|
+
...(typeof error.formattedMessage === "string" ? { formattedMessage: error.formattedMessage } : {}),
|
|
24
|
+
...(typeof error.offset === "number" ? { offset: error.offset } : {}),
|
|
25
|
+
...(typeof error.line === "number" ? { line: error.line } : {}),
|
|
26
|
+
...(typeof error.column === "number" ? { column: error.column } : {})
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
function fallbackRoot(context, input) {
|
|
30
|
+
if (context === "stylesheet") {
|
|
31
|
+
return {
|
|
32
|
+
type: "StyleSheet",
|
|
33
|
+
loc: null,
|
|
34
|
+
children: [
|
|
35
|
+
{
|
|
36
|
+
type: "Raw",
|
|
37
|
+
loc: null,
|
|
38
|
+
value: input
|
|
39
|
+
}
|
|
40
|
+
]
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
return {
|
|
44
|
+
type: "Raw",
|
|
45
|
+
loc: null,
|
|
46
|
+
value: input
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
export function buildTreeFromCss(input, options = {}) {
|
|
50
|
+
const context = options.context ?? "stylesheet";
|
|
51
|
+
if (!SUPPORTED_CONTEXTS.has(context)) {
|
|
52
|
+
throw new Error(`Unsupported parse context: ${context}`);
|
|
53
|
+
}
|
|
54
|
+
const errors = [];
|
|
55
|
+
try {
|
|
56
|
+
const parsed = parseCssTree(input, {
|
|
57
|
+
context,
|
|
58
|
+
positions: options.captureSpans ?? false,
|
|
59
|
+
onParseError(error) {
|
|
60
|
+
const next = toTreeError(error);
|
|
61
|
+
errors.push(next);
|
|
62
|
+
options.onParseError?.(next);
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
const plain = toPlainObject(parsed);
|
|
66
|
+
if (!isRecord(plain) || typeof plain["type"] !== "string") {
|
|
67
|
+
throw new Error("CSSTree parse result was not a valid AST node");
|
|
68
|
+
}
|
|
69
|
+
return {
|
|
70
|
+
root: plain,
|
|
71
|
+
errors
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
catch (error) {
|
|
75
|
+
const fallbackError = {
|
|
76
|
+
message: error instanceof Error ? error.message : String(error)
|
|
77
|
+
};
|
|
78
|
+
errors.push(fallbackError);
|
|
79
|
+
options.onParseError?.(fallbackError);
|
|
80
|
+
return {
|
|
81
|
+
root: fallbackRoot(context, input),
|
|
82
|
+
errors
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { buildTreeFromCss } from "./build.js";
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { CssTreeParseContext } from "../csstree-runtime.js";
|
|
2
|
+
export interface TreeSpan {
|
|
3
|
+
readonly start: number;
|
|
4
|
+
readonly end: number;
|
|
5
|
+
}
|
|
6
|
+
export interface CssAstNode {
|
|
7
|
+
readonly type: string;
|
|
8
|
+
readonly [key: string]: unknown;
|
|
9
|
+
}
|
|
10
|
+
export interface TreeBuilderError {
|
|
11
|
+
readonly message: string;
|
|
12
|
+
readonly formattedMessage?: string;
|
|
13
|
+
readonly offset?: number;
|
|
14
|
+
readonly line?: number;
|
|
15
|
+
readonly column?: number;
|
|
16
|
+
}
|
|
17
|
+
export interface TreeBuildOptions {
|
|
18
|
+
readonly context?: CssTreeParseContext;
|
|
19
|
+
readonly captureSpans?: boolean;
|
|
20
|
+
readonly onParseError?: (error: TreeBuilderError) => void;
|
|
21
|
+
}
|
|
22
|
+
export interface TreeBuildResult {
|
|
23
|
+
readonly root: CssAstNode;
|
|
24
|
+
readonly errors: readonly TreeBuilderError[];
|
|
25
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
Copyright (C) 2016-2024 by Roman Dvornov
|
|
2
|
+
|
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
4
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
5
|
+
in the Software without restriction, including without limitation the rights
|
|
6
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
7
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
8
|
+
furnished to do so, subject to the following conditions:
|
|
9
|
+
|
|
10
|
+
The above copyright notice and this permission notice shall be included in
|
|
11
|
+
all copies or substantial portions of the Software.
|
|
12
|
+
|
|
13
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
14
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
15
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
16
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
17
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
18
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
19
|
+
THE SOFTWARE.
|