@cssints/css-grammar 0.0.0-stage → 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/LICENSE +21 -0
- package/README.md +188 -2
- package/THIRD_PARTY_NOTICES.md +33 -0
- package/dist/api-query.d.mts +6 -0
- package/dist/api-query.mjs +1 -0
- package/dist/api-typed.d.mts +18 -0
- package/dist/api-typed.mjs +1 -0
- package/dist/api.d.mts +13 -0
- package/dist/api.mjs +1 -0
- package/dist/browsers.d.mts +5 -0
- package/dist/browsers.mjs +15 -0
- package/dist/check.d.mts +17 -0
- package/dist/check.mjs +85 -0
- package/dist/css.d.mts +298 -0
- package/dist/css.mjs +1 -0
- package/dist/errors.d.mts +52 -0
- package/dist/errors.mjs +15 -0
- package/dist/functions.d.mts +355 -0
- package/dist/functions.mjs +178 -0
- package/dist/keywords.d.mts +2353 -0
- package/dist/keywords.mjs +1 -0
- package/dist/longhands.d.mts +3 -0
- package/dist/longhands.mjs +841 -0
- package/dist/mdn.d.mts +894 -0
- package/dist/mdn.mjs +895 -0
- package/dist/query.d.mts +34 -0
- package/dist/query.mjs +8 -0
- package/dist/rules.d.mts +6 -0
- package/dist/rules.mjs +218 -0
- package/dist/selector.d.mts +3 -0
- package/dist/selector.mjs +79 -0
- package/dist/support.d.mts +36 -0
- package/dist/support.mjs +160 -0
- package/dist/token.d.mts +146 -0
- package/dist/token.mjs +112 -0
- package/dist/twin-core.d.mts +48 -0
- package/dist/twin-core.mjs +789 -0
- package/dist/twin.d.mts +12 -0
- package/dist/twin.mjs +38 -0
- package/dist/values.d.mts +28 -0
- package/dist/values.mjs +44 -0
- package/package.json +33 -4
package/dist/query.d.mts
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { Consume, Matches, Parse, Stuck, Unexpected } from "./css.mjs";
|
|
2
|
+
//#region src/query.d.ts
|
|
3
|
+
export declare const ENTRY: {
|
|
4
|
+
readonly media: "media-query-list";
|
|
5
|
+
readonly container: "container-condition";
|
|
6
|
+
readonly supports: "supports-condition";
|
|
7
|
+
};
|
|
8
|
+
export type QueryKind = keyof typeof ENTRY;
|
|
9
|
+
type Sp<S extends string> = Colon<S> extends (infer C extends string) ? Slash<C> : never;
|
|
10
|
+
type Colon<S extends string> = S extends `${infer A}:${infer B}` ? `${A} : ${Colon<B>}` : S;
|
|
11
|
+
type Slash<S extends string> = S extends `${infer A}/${infer B}` ? `${A} / ${Slash<B>}` : S;
|
|
12
|
+
type Trim<S extends string> = S extends ` ${infer R}` ? Trim<R> : S;
|
|
13
|
+
/** The query, or a message naming the bad token and the grammar (as `Valid` does for a property). */
|
|
14
|
+
export type ValidQuery<K extends QueryKind, S extends string> = Matches<`<${(typeof ENTRY)[K]}>`, Sp<S>> extends true ? S : S extends string ? Message<K, (typeof ENTRY)[K], S> : never;
|
|
15
|
+
type Message<K extends string, E extends string, S extends string> = `@${K}: ${Unexpected<Stuck<Trim<Sp<S>> | Consume<Parse<`<${E}>`>, Trim<Sp<S>>>>>} in "${S}", grammar <${E}>`;
|
|
16
|
+
type Check<K extends QueryKind, Q extends readonly string[]> = { [I in keyof Q]: ValidQuery<K, Q[I]>; };
|
|
17
|
+
/**
|
|
18
|
+
* A condition constructor: an object of queries gives an object of `R`, several queries a tuple of `R`, one query `R`.
|
|
19
|
+
* The one-query overload comes last: when nothing fits, TypeScript shows the message of the last overload, and the
|
|
20
|
+
* bad query of a single string is the common case. A bad query of an object is marked in place.
|
|
21
|
+
*/
|
|
22
|
+
export interface Condition<K extends QueryKind, R> {
|
|
23
|
+
<const O extends Record<string, string>>(queries: { [P in keyof O]: ValidQuery<K, O[P]>; }): { [P in keyof O]: R; };
|
|
24
|
+
<const Q extends readonly [string, string, ...string[]]>(...queries: Check<K, Q>): { [I in keyof Q]: R; };
|
|
25
|
+
<const Q extends string>(query: ValidQuery<K, Q>): R;
|
|
26
|
+
}
|
|
27
|
+
type Open = {
|
|
28
|
+
")": "(";
|
|
29
|
+
"]": "[";
|
|
30
|
+
};
|
|
31
|
+
type Balanced<S extends string, Stack extends string = ""> = S extends `${infer C}${infer R}` ? C extends "(" | "[" ? Balanced<R, `${C}${Stack}`> : C extends ")" | "]" ? Stack extends `${Open[C]}${infer Rest}` ? Balanced<R, Rest> : false : C extends '"' | "'" ? R extends `${string}${C}${infer After}` ? Balanced<After, Stack> : false : Balanced<R, Stack> : Stack extends "" ? true : false;
|
|
32
|
+
/** The selector of `global()`, or a message: not empty, brackets and quotes balanced. */
|
|
33
|
+
export type ValidSelector<S extends string> = Trim<S> extends "" ? "global(): the selector is empty" : Balanced<S> extends true ? S : `global(): unbalanced brackets or quotes in "${S}"`;
|
|
34
|
+
//#endregion
|
package/dist/query.mjs
ADDED
package/dist/rules.d.mts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { RuleError } from "./errors.mjs";
|
|
2
|
+
import { Effect } from "effect";
|
|
3
|
+
//#region src/rules.d.ts
|
|
4
|
+
export declare function violation(p: string, v: string): RuleError | undefined;
|
|
5
|
+
export declare const checkRules: (p: string, v: string) => Effect.Effect<string, RuleError, never>;
|
|
6
|
+
//#endregion
|
package/dist/rules.mjs
ADDED
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
import { RuleError } from "./errors.mjs";
|
|
2
|
+
import { matches } from "./twin-core.mjs";
|
|
3
|
+
import { Effect, Formatter } from "effect";
|
|
4
|
+
//#region src/rules.ts
|
|
5
|
+
const splitList = (v) => v.match(/(?:"[^"]*"|'[^']*'|\([^()]*(?:\([^()]*\)[^()]*)*\)|[^,"'()])+/g)?.map((s) => s.trim()) ?? [];
|
|
6
|
+
const CSS_WIDE = /* @__PURE__ */ new Set([
|
|
7
|
+
"inherit",
|
|
8
|
+
"initial",
|
|
9
|
+
"unset",
|
|
10
|
+
"revert",
|
|
11
|
+
"revert-layer",
|
|
12
|
+
"revert-rule"
|
|
13
|
+
]);
|
|
14
|
+
const strings = (v) => [...v.matchAll(/"([^"]*)"|'([^']*)'/g)].map((m) => m[1] ?? m[2]);
|
|
15
|
+
const negative = (v) => {
|
|
16
|
+
for (let p = ""; p !== v;) [p, v] = [v, v.replace(/\([^()]*\)/g, "")];
|
|
17
|
+
return /(^|[\s,])-\.?\d/.test(v);
|
|
18
|
+
};
|
|
19
|
+
const words = (v) => v.toLowerCase().split(/[\s(),]+/).filter(Boolean);
|
|
20
|
+
function gridAreas(v) {
|
|
21
|
+
const grid = strings(v).map((s) => {
|
|
22
|
+
const cells = [];
|
|
23
|
+
for (const [, name, nul, trash] of s.matchAll(/([\w\u0080-\uffff-]+)|(\.+)|\s+|(.)/gu)) {
|
|
24
|
+
if (trash) return `the string "${s}" has a trash token ${Formatter.formatJson(trash)}`;
|
|
25
|
+
if (name || nul) cells.push(name ?? "");
|
|
26
|
+
}
|
|
27
|
+
return cells;
|
|
28
|
+
});
|
|
29
|
+
const bad = grid.find((r) => typeof r === "string");
|
|
30
|
+
if (bad) return `${bad} (css-grid-2 §7.3)`;
|
|
31
|
+
const rows = grid;
|
|
32
|
+
if (rows.some((r) => !r.length || r.length !== rows[0].length)) return `every string needs the same number, at least one, of cell tokens (css-grid-2 §7.3)`;
|
|
33
|
+
const areas = /* @__PURE__ */ new Map();
|
|
34
|
+
rows.forEach((r, y) => r.forEach((n, x) => n && areas.set(n, [...areas.get(n) ?? [], [y, x]])));
|
|
35
|
+
for (const [n, cs] of areas) {
|
|
36
|
+
const ys = cs.map((c) => c[0]), xs = cs.map((c) => c[1]);
|
|
37
|
+
if ((Math.max(...ys) - Math.min(...ys) + 1) * (Math.max(...xs) - Math.min(...xs) + 1) !== cs.length) return `the area "${n}" is not a rectangle (css-grid-2 §7.3)`;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
function gridLine(v) {
|
|
41
|
+
for (const part of v.split("/")) {
|
|
42
|
+
const t = words(part);
|
|
43
|
+
const spans = t.filter((w) => w === "span").length;
|
|
44
|
+
if (spans > 1 || spans === 1 && t.length === 1) return `"${part.trim()}" uses span as a line name, which the <custom-ident> excludes (css-grid-2 §8.3)`;
|
|
45
|
+
const ints = t.filter((w) => /^[+-]?\d+$/.test(w)).map(Number);
|
|
46
|
+
if (ints.includes(0) || spans && ints.some((n) => n < 1)) return `"${part.trim()}" has an integer that is zero, or negative in a span (css-grid-2 §8.3)`;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
const subgridAutoFill = (v) => /^\s*subgrid\b/.test(v) && (v.match(/repeat\(\s*auto-fill/g) ?? []).length > 1 ? `auto-fill is only valid once on a subgrid (css-grid-2 §7.2.3.2)` : void 0;
|
|
50
|
+
const counterName = (v) => v.trim().toLowerCase() !== "none" && words(v).includes("none") ? `"none" cannot be a counter name (css-lists-3 §4)` : void 0;
|
|
51
|
+
const svgRadius = (v) => negative(v) ? `a negative radius is illegal (svg2 §7.3-7.5)` : void 0;
|
|
52
|
+
const strokeLength = (v) => negative(v) ? `negative values are invalid (fill-stroke-3 §4.2.1, §4.3.1)` : void 0;
|
|
53
|
+
function pathData(d) {
|
|
54
|
+
const NUMBER = /[+-]?(?:\d+\.?\d*|\.\d+)(?:[eE][+-]?\d+)?/y, FLAG = /[01]/y, SEP = /\s*,?\s*/y, ARGS = {
|
|
55
|
+
m: 2,
|
|
56
|
+
l: 2,
|
|
57
|
+
h: 1,
|
|
58
|
+
v: 1,
|
|
59
|
+
c: 6,
|
|
60
|
+
s: 4,
|
|
61
|
+
q: 4,
|
|
62
|
+
t: 2,
|
|
63
|
+
a: 7,
|
|
64
|
+
z: 0
|
|
65
|
+
};
|
|
66
|
+
let i = 0;
|
|
67
|
+
const eat = (r) => (r.lastIndex = i, r.test(d) && (i = r.lastIndex, true));
|
|
68
|
+
eat(SEP);
|
|
69
|
+
if (i === d.length) return true;
|
|
70
|
+
if (!/[Mm]/.test(d[i])) return false;
|
|
71
|
+
while (i < d.length) {
|
|
72
|
+
const n = ARGS[d[i++].toLowerCase()];
|
|
73
|
+
if (n === void 0) return false;
|
|
74
|
+
eat(SEP);
|
|
75
|
+
if (n === 0) continue;
|
|
76
|
+
do
|
|
77
|
+
for (let k = 0; k < n; k++) {
|
|
78
|
+
if (!eat(n === 7 && (k === 3 || k === 4) ? FLAG : NUMBER)) return false;
|
|
79
|
+
eat(SEP);
|
|
80
|
+
}
|
|
81
|
+
while (i < d.length && !/[A-Za-z]/.test(d[i]));
|
|
82
|
+
}
|
|
83
|
+
return true;
|
|
84
|
+
}
|
|
85
|
+
const openTypeTags = (v) => {
|
|
86
|
+
const bad = strings(v).find((s) => !/^[\x20-\x7e]{4}$/.test(s));
|
|
87
|
+
if (bad !== void 0) return `"${bad}" is not a four-character tag of U+20-7E (css-fonts-4 §6.12, §8.2)`;
|
|
88
|
+
};
|
|
89
|
+
const rules = {
|
|
90
|
+
"grid-template-areas": gridAreas,
|
|
91
|
+
"grid-template": gridAreas,
|
|
92
|
+
grid: gridAreas,
|
|
93
|
+
"grid-row": gridLine,
|
|
94
|
+
"grid-column": gridLine,
|
|
95
|
+
"grid-row-start": gridLine,
|
|
96
|
+
"grid-row-end": gridLine,
|
|
97
|
+
"grid-column-start": gridLine,
|
|
98
|
+
"grid-column-end": gridLine,
|
|
99
|
+
"grid-area": gridLine,
|
|
100
|
+
"grid-template-rows": subgridAutoFill,
|
|
101
|
+
"grid-template-columns": subgridAutoFill,
|
|
102
|
+
"counter-reset": counterName,
|
|
103
|
+
"counter-set": counterName,
|
|
104
|
+
"counter-increment": counterName,
|
|
105
|
+
"container-name"(v) {
|
|
106
|
+
const bad = v.trim().toLowerCase() !== "none" && words(v).find((w) => [
|
|
107
|
+
"none",
|
|
108
|
+
"and",
|
|
109
|
+
"not",
|
|
110
|
+
"or"
|
|
111
|
+
].includes(w));
|
|
112
|
+
if (bad) return `"${bad}" cannot be a container name (css-conditional-5 §5.2)`;
|
|
113
|
+
},
|
|
114
|
+
"view-transition-class"(v) {
|
|
115
|
+
if (v.trim().toLowerCase() !== "none" && words(v).includes("none")) return `"none" cannot be combined with another class (css-view-transitions-2 §10.3)`;
|
|
116
|
+
},
|
|
117
|
+
"color-scheme"(v) {
|
|
118
|
+
const t = words(v);
|
|
119
|
+
if (t.length > 1 && t.includes("normal")) return `"normal" cannot be combined with schemes (css-color-adjust-1 §2.2)`;
|
|
120
|
+
if (t.some((w, i) => w === "only" && i > 0 && i < t.length - 1) || t.filter((w) => w === "only").length > 1) return `"only" is a keyword, not a scheme name, and goes first or last (css-color-adjust-1 §2.2)`;
|
|
121
|
+
},
|
|
122
|
+
"font-feature-settings": openTypeTags,
|
|
123
|
+
"font-variation-settings": openTypeTags,
|
|
124
|
+
"font-language-override"(v) {
|
|
125
|
+
const long = strings(v).find((s) => s.length > 4);
|
|
126
|
+
if (long !== void 0) return `"${long}" is longer than an OpenType language system tag, four characters (css-fonts-4)`;
|
|
127
|
+
},
|
|
128
|
+
font: (v) => rules["font-family"](splitList(v).slice(1).join(", ")),
|
|
129
|
+
r: svgRadius,
|
|
130
|
+
rx: svgRadius,
|
|
131
|
+
ry: svgRadius,
|
|
132
|
+
"stroke-miterlimit": (v) => negative(v) ? `a negative miter limit is illegal (svg2 §13.5.5)` : void 0,
|
|
133
|
+
"stroke-width": strokeLength,
|
|
134
|
+
"stroke-dasharray": strokeLength,
|
|
135
|
+
transition(v) {
|
|
136
|
+
const items = splitList(v);
|
|
137
|
+
if (items.length > 1 && items.some((t) => t.replace(/\([^)]*\)/g, "").split(/\s+/).includes("none"))) return `"none" is only valid when it is the only transition, this list has ${items.length} (css-transitions-1 §2.5)`;
|
|
138
|
+
for (const item of items) {
|
|
139
|
+
const times = item.replace(/\([^)]*\)/g, "").split(/\s+/).filter((t) => matches("<time>", t));
|
|
140
|
+
if (times.length === 2 && times.every((t) => t.startsWith("-"))) return `one of the times in "${item}" is the duration, which cannot be negative (css-transitions-1 §2.2, §2.5)`;
|
|
141
|
+
const taken = /* @__PURE__ */ new Set();
|
|
142
|
+
for (const t of item.match(/[\w-]+\([^)]*\)|\S+/g) ?? []) {
|
|
143
|
+
const slot = ["<easing-function>", "<transition-behavior-value>"].find((g) => matches(g, t));
|
|
144
|
+
if (slot && !taken.has(slot)) taken.add(slot);
|
|
145
|
+
else if (slot && (t.includes("(") || taken.has("ident"))) return `"${t}" has no place left in "${item}": an earlier keyword took the ${slot} (css-values-4 §4.2)`;
|
|
146
|
+
else if (t === "none" || matches("<single-transition-property>", t)) taken.add("ident");
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
},
|
|
150
|
+
animation(v) {
|
|
151
|
+
const slots = [
|
|
152
|
+
"<easing-function>",
|
|
153
|
+
"<single-animation-iteration-count>",
|
|
154
|
+
"<single-animation-direction>",
|
|
155
|
+
"<single-animation-fill-mode>",
|
|
156
|
+
"<single-animation-play-state>"
|
|
157
|
+
];
|
|
158
|
+
for (const item of splitList(v)) {
|
|
159
|
+
const taken = /* @__PURE__ */ new Set();
|
|
160
|
+
for (const t of item.match(/[\w-]+\([^)]*\)|\S+/g) ?? []) {
|
|
161
|
+
const slot = slots.find((g) => matches(g, t));
|
|
162
|
+
if (!slot) continue;
|
|
163
|
+
if (!taken.has(slot)) taken.add(slot);
|
|
164
|
+
else if (t.includes("(") || taken.has("name") || !matches("<keyframes-name>", t)) return `"${t}" has no place left in "${item}": an earlier keyword took the ${slot} (css-values-4 §4.2)`;
|
|
165
|
+
else taken.add("name");
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
},
|
|
169
|
+
"font-family"(v) {
|
|
170
|
+
const items = splitList(v);
|
|
171
|
+
for (const item of items) {
|
|
172
|
+
if (/^["']/.test(item)) continue;
|
|
173
|
+
const ids = item.split(/\s+/);
|
|
174
|
+
if (ids.length === 1 && matches("<generic-family>", ids[0].toLowerCase())) continue;
|
|
175
|
+
if (items.length === 1 && ids.length === 1 && CSS_WIDE.has(ids[0].toLowerCase())) continue;
|
|
176
|
+
const bad = ids.find((id) => id.toLowerCase() === "default" || CSS_WIDE.has(id.toLowerCase()) || matches("<generic-family>", id.toLowerCase()));
|
|
177
|
+
if (bad) return `the keyword "${bad}" cannot be (part of) the unquoted family name "${item}", quote the name (css-fonts-4 §2.1.1)`;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
};
|
|
181
|
+
const fnRules = [
|
|
182
|
+
(_, v) => {
|
|
183
|
+
const m = /\b(grayscale|sepia|saturate|opacity|invert|brightness|contrast|blur)\(\s*-/.exec(v);
|
|
184
|
+
if (m) return `${m[1]}() cannot take a negative value (filter-effects-1 §6.1)`;
|
|
185
|
+
},
|
|
186
|
+
(_, v) => {
|
|
187
|
+
for (const m of v.matchAll(/\bpath\(\s*(?:(?:nonzero|evenodd)\s*,\s*)?(?:"([^"]*)"|'([^']*)')/g)) {
|
|
188
|
+
const d = m[1] ?? m[2];
|
|
189
|
+
if (!pathData(d)) return `"${d}" is not SVG path data (css-shapes-1 §3.1, svg2 §9.3.9)`;
|
|
190
|
+
}
|
|
191
|
+
},
|
|
192
|
+
(_, v) => {
|
|
193
|
+
for (const [f, n, pos] of v.matchAll(/steps\(\s*([+-]?\d+)\s*(?:,\s*([a-z-]+)\s*)?\)/g)) {
|
|
194
|
+
const min = pos === "jump-none" ? 2 : 1;
|
|
195
|
+
if (Number(n) < min) return `${f} needs at least ${min} step${min > 1 ? "s" : ""} (css-easing-2 §2.3)`;
|
|
196
|
+
}
|
|
197
|
+
},
|
|
198
|
+
(p, v) => {
|
|
199
|
+
if (v.includes("anchor-size(") && !/^((min-|max-)?(width|height|block-size|inline-size)|top|left|right|bottom|inset(-.+)?|margin(-.+)?)$/.test(p)) return `anchor-size() is only valid in sizing, inset and margin properties (css-anchor-position-1 §5)`;
|
|
200
|
+
}
|
|
201
|
+
];
|
|
202
|
+
function violation(p, v) {
|
|
203
|
+
let why = rules[p]?.(v);
|
|
204
|
+
for (const f of fnRules) why ??= f(p, v);
|
|
205
|
+
return why ? new RuleError({
|
|
206
|
+
message: `${p}: in "${v}", ${why}`,
|
|
207
|
+
property: p,
|
|
208
|
+
value: v,
|
|
209
|
+
reason: why
|
|
210
|
+
}) : void 0;
|
|
211
|
+
}
|
|
212
|
+
const checkRules = Effect.fnUntraced(function* (p, v) {
|
|
213
|
+
const error = violation(p, v);
|
|
214
|
+
if (error) return yield* error;
|
|
215
|
+
return v;
|
|
216
|
+
});
|
|
217
|
+
//#endregion
|
|
218
|
+
export { checkRules, violation };
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { InvalidValueError } from "./errors.mjs";
|
|
2
|
+
//#region src/selector.ts
|
|
3
|
+
const I = String.raw`(?:--|-?(?:[A-Za-z_\u0080-\uffff]|\\.))(?:[\w\u0080-\uffff-]|\\.)*`;
|
|
4
|
+
const STR = String.raw`"(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'`;
|
|
5
|
+
const NS = String.raw`(?:(?:${I}|\*)?\|)?`;
|
|
6
|
+
const y = (src) => new RegExp(src, "y");
|
|
7
|
+
const TYPE = y(`${NS}(?:${I}|\\*)`);
|
|
8
|
+
const SUB = y(String.raw`#${I}|\.${I}|\[\s*${NS}${I}\s*(?:[~|^$*]?=\s*(?:${STR}|${I})\s*[is]?\s*)?\]|::?${I}`);
|
|
9
|
+
const COMB = y(String.raw`(?:\|\||[>+~])\s*`);
|
|
10
|
+
const WS = y(String.raw`\s+`);
|
|
11
|
+
const close = (s, i) => {
|
|
12
|
+
for (let depth = 0, j = i; j < s.length; j++) {
|
|
13
|
+
const c = s[j];
|
|
14
|
+
if (c === "\\") j++;
|
|
15
|
+
else if (c === "\"" || c === "'") {
|
|
16
|
+
const end = new RegExp(`${c}(?:[^${c}\\\\]|\\\\.)*${c}`, "y");
|
|
17
|
+
end.lastIndex = j;
|
|
18
|
+
const m = end.exec(s);
|
|
19
|
+
if (!m) return -1;
|
|
20
|
+
j += m[0].length - 1;
|
|
21
|
+
} else if (c === "(") depth++;
|
|
22
|
+
else if (c === ")" && !--depth) return /^\(\s*\)$/.test(s.slice(i, j + 1)) ? -1 : j + 1;
|
|
23
|
+
}
|
|
24
|
+
return -1;
|
|
25
|
+
};
|
|
26
|
+
function checkSelectorNow(sel) {
|
|
27
|
+
let i = 0;
|
|
28
|
+
const eat = (re) => {
|
|
29
|
+
re.lastIndex = i;
|
|
30
|
+
const m = re.exec(sel);
|
|
31
|
+
if (m) i += m[0].length;
|
|
32
|
+
return m?.[0];
|
|
33
|
+
};
|
|
34
|
+
const stop = () => {
|
|
35
|
+
const token = /^,|^[^\s,]*/.exec(sel.slice(i))[0];
|
|
36
|
+
throw new InvalidValueError({
|
|
37
|
+
message: `selector: ${token ? `unexpected "${token}"` : "unexpected end"} in "${sel}", expected <complex-selector-list>`,
|
|
38
|
+
property: "selector",
|
|
39
|
+
value: sel,
|
|
40
|
+
position: i,
|
|
41
|
+
expected: ["<complex-selector-list>"]
|
|
42
|
+
});
|
|
43
|
+
};
|
|
44
|
+
const compound = () => {
|
|
45
|
+
const start = i;
|
|
46
|
+
let pseudoElement = false;
|
|
47
|
+
eat(TYPE);
|
|
48
|
+
for (let s; s = eat(SUB);) {
|
|
49
|
+
if (pseudoElement && s[0] !== ":") {
|
|
50
|
+
i -= s.length;
|
|
51
|
+
return stop();
|
|
52
|
+
}
|
|
53
|
+
if (s.startsWith("::")) pseudoElement = true;
|
|
54
|
+
if (sel[i] === "(" && s[0] === ":") {
|
|
55
|
+
const end = close(sel, i);
|
|
56
|
+
if (end < 0) return stop();
|
|
57
|
+
i = end;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
if (i === start) stop();
|
|
61
|
+
};
|
|
62
|
+
eat(WS);
|
|
63
|
+
for (;;) {
|
|
64
|
+
compound();
|
|
65
|
+
for (;;) {
|
|
66
|
+
const space = eat(WS);
|
|
67
|
+
if (i === sel.length) return;
|
|
68
|
+
if (sel[i] === ",") {
|
|
69
|
+
i++;
|
|
70
|
+
eat(WS);
|
|
71
|
+
break;
|
|
72
|
+
}
|
|
73
|
+
if (!eat(COMB) && !space) stop();
|
|
74
|
+
compound();
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
//#endregion
|
|
79
|
+
export { checkSelectorNow };
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { BrowsersError } from "./errors.mjs";
|
|
2
|
+
import { Effect } from "effect";
|
|
3
|
+
import { Identifier } from "@mdn/browser-compat-data";
|
|
4
|
+
//#region src/support.d.ts
|
|
5
|
+
export type Target = [agent: string, version: string];
|
|
6
|
+
export declare const targets: (query?: string | undefined, path?: string | undefined) => Effect.Effect<Target[], BrowsersError, never>;
|
|
7
|
+
declare global {
|
|
8
|
+
var __cssintsTargets: Target[] | undefined;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Sets the targets the functions here default to (cssints: those of the Vite root, before each evaluation).
|
|
12
|
+
* @param {Target[] | undefined} ts the targets, or `undefined` for those of process.cwd()
|
|
13
|
+
*/
|
|
14
|
+
export declare const useTargets: (ts: Target[] | undefined) => void;
|
|
15
|
+
export declare const get: (key: string) => Identifier | undefined;
|
|
16
|
+
export interface Warning {
|
|
17
|
+
feature: string;
|
|
18
|
+
key: string;
|
|
19
|
+
lacking: string[];
|
|
20
|
+
}
|
|
21
|
+
export declare const lackingOf: (key: string, ts?: Target[], exact?: boolean) => Target[];
|
|
22
|
+
export declare const lacking: (key: string, ts?: Target[] | undefined, exact?: boolean | undefined) => Effect.Effect<Target[], never, never>;
|
|
23
|
+
/**
|
|
24
|
+
* The BCD key of a keyword or function in a value of a property, by the lookup above.
|
|
25
|
+
* @param {string} p the property
|
|
26
|
+
* @param {string} k the keyword, or the name of the function
|
|
27
|
+
* @param {boolean} fn whether `k` is a function
|
|
28
|
+
* @returns {string | undefined} the key, or `undefined` when BCD has none
|
|
29
|
+
*/
|
|
30
|
+
export declare const tokenKey: (p: string, k: string, fn: boolean) => string | undefined;
|
|
31
|
+
export declare function supportOf(p: string, v: string, ts?: Target[]): Warning[];
|
|
32
|
+
export declare function browsers(lacking: string[]): string;
|
|
33
|
+
export declare const support: (p: string, v: string, ts?: Target[] | undefined) => Effect.Effect<Warning[], never, never>;
|
|
34
|
+
export declare const messageOf: (p: string, v: string, w: Warning) => string;
|
|
35
|
+
export declare const message: (p: string, v: string, w: Warning) => Effect.Effect<string, never, never>;
|
|
36
|
+
//#endregion
|
package/dist/support.mjs
ADDED
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import { browsers as browsers$1 } from "./browsers.mjs";
|
|
2
|
+
import { properties, syntaxes } from "./mdn.mjs";
|
|
3
|
+
import { Effect } from "effect";
|
|
4
|
+
//#region src/support.ts
|
|
5
|
+
const bcd = process.getBuiltinModule("node:module").createRequire(import.meta.url)("@mdn/browser-compat-data");
|
|
6
|
+
const props = properties;
|
|
7
|
+
const syns = syntaxes;
|
|
8
|
+
const BCD_NAME = {
|
|
9
|
+
chrome: "chrome",
|
|
10
|
+
and_chr: "chrome_android",
|
|
11
|
+
edge: "edge",
|
|
12
|
+
firefox: "firefox",
|
|
13
|
+
and_ff: "firefox_android",
|
|
14
|
+
safari: "safari",
|
|
15
|
+
ios_saf: "safari_ios",
|
|
16
|
+
opera: "opera",
|
|
17
|
+
op_mob: "opera_android",
|
|
18
|
+
samsung: "samsunginternet_android",
|
|
19
|
+
android: "webview_android",
|
|
20
|
+
ie: "ie"
|
|
21
|
+
};
|
|
22
|
+
const targets = Effect.fn("support.targets")(function* (query, path) {
|
|
23
|
+
return (yield* browsers$1(query, path)).map((t) => [t.split(" ")[0], t.split(" ")[1].split("-")[0]]).filter(([a]) => BCD_NAME[a]);
|
|
24
|
+
});
|
|
25
|
+
const DEFAULT = Effect.runSync(targets());
|
|
26
|
+
const current = () => globalThis.__cssintsTargets ?? DEFAULT;
|
|
27
|
+
/**
|
|
28
|
+
* Sets the targets the functions here default to (cssints: those of the Vite root, before each evaluation).
|
|
29
|
+
* @param {Target[] | undefined} ts the targets, or `undefined` for those of process.cwd()
|
|
30
|
+
*/
|
|
31
|
+
const useTargets = (ts) => {
|
|
32
|
+
globalThis.__cssintsTargets = ts;
|
|
33
|
+
};
|
|
34
|
+
const cmp = (a, b) => {
|
|
35
|
+
const x = a.replace("≤", "").split("."), y = b.split(".");
|
|
36
|
+
for (let i = 0; i < Math.max(x.length, y.length); i++) {
|
|
37
|
+
const d = Number(x[i] ?? 0) - Number(y[i] ?? 0);
|
|
38
|
+
if (d) return d;
|
|
39
|
+
}
|
|
40
|
+
return 0;
|
|
41
|
+
};
|
|
42
|
+
function supports(s, v, exact = false) {
|
|
43
|
+
if (!s) return true;
|
|
44
|
+
return [s].flat().some((x) => {
|
|
45
|
+
if (x.prefix || x.alternative_name || x.flags || exact && x.partial_implementation) return false;
|
|
46
|
+
const added = x.version_added;
|
|
47
|
+
if (typeof added !== "string") return added !== false;
|
|
48
|
+
if (added === "preview" || cmp(added, v) > 0) return false;
|
|
49
|
+
return !(typeof x.version_removed === "string" && cmp(x.version_removed, v) <= 0);
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
function reach(syntax, seen = /* @__PURE__ */ new Set()) {
|
|
53
|
+
for (const [, q, n] of syntax.matchAll(/<('?)([a-z-]+)(?:\(\))?'?(?: \[[^\]]*\])?>/g)) {
|
|
54
|
+
if (seen.has(q + n) || !q && n === "declaration") continue;
|
|
55
|
+
seen.add(q + n);
|
|
56
|
+
const g = q ? props[n] : syns[n] ?? syns[`${n}()`];
|
|
57
|
+
if (g) reach(g, seen);
|
|
58
|
+
}
|
|
59
|
+
return seen;
|
|
60
|
+
}
|
|
61
|
+
const get = (key) => key.split(".").reduce((n, k) => n?.[k] ?? n?.[k.toLowerCase()], bcd);
|
|
62
|
+
const lacks = (c, on, exact = false) => on.filter(([a, ver]) => !supports(c.support[BCD_NAME[a]], ver, exact));
|
|
63
|
+
const lackingOf = (key, ts = current(), exact = false) => {
|
|
64
|
+
const c = get(key)?.__compat;
|
|
65
|
+
return c ? lacks(c, ts, exact) : [];
|
|
66
|
+
};
|
|
67
|
+
const lacking = Effect.fnUntraced(function* (key, ts = current(), exact = false) {
|
|
68
|
+
return yield* Effect.sync(() => lackingOf(key, ts, exact));
|
|
69
|
+
});
|
|
70
|
+
const reached = /* @__PURE__ */ new Map();
|
|
71
|
+
const reachOf = (p) => {
|
|
72
|
+
let r = reached.get(p);
|
|
73
|
+
if (!r) {
|
|
74
|
+
const all = [...reach(props[p] ?? "")];
|
|
75
|
+
r = [all.filter((t) => get(`css.types.${t}`)), [p, ...all.filter((q) => q.startsWith("'")).map((q) => q.slice(1))]];
|
|
76
|
+
reached.set(p, r);
|
|
77
|
+
}
|
|
78
|
+
return r;
|
|
79
|
+
};
|
|
80
|
+
const tokenKeys = (owners, types, k, fn) => [
|
|
81
|
+
...owners.map((q) => `css.properties.${q}.${fn ? `${k}_function` : k}`),
|
|
82
|
+
...fn ? owners.map((q) => `css.properties.${q}.${k}`) : [],
|
|
83
|
+
...fn ? [`css.types.${k}`] : [],
|
|
84
|
+
...types.map((t) => `css.types.${t}.${k}`)
|
|
85
|
+
];
|
|
86
|
+
/**
|
|
87
|
+
* The BCD key of a keyword or function in a value of a property, by the lookup above.
|
|
88
|
+
* @param {string} p the property
|
|
89
|
+
* @param {string} k the keyword, or the name of the function
|
|
90
|
+
* @param {boolean} fn whether `k` is a function
|
|
91
|
+
* @returns {string | undefined} the key, or `undefined` when BCD has none
|
|
92
|
+
*/
|
|
93
|
+
const tokenKey = (p, k, fn) => {
|
|
94
|
+
const [types, owners] = reachOf(p);
|
|
95
|
+
return tokenKeys(owners, types, k, fn).find((x) => get(x)?.__compat);
|
|
96
|
+
};
|
|
97
|
+
const known = /* @__PURE__ */ new WeakMap();
|
|
98
|
+
function supportOf(p, v, ts = current()) {
|
|
99
|
+
let byDecl = known.get(ts);
|
|
100
|
+
if (!byDecl) known.set(ts, byDecl = /* @__PURE__ */ new Map());
|
|
101
|
+
const key = `${p}\0${v}`;
|
|
102
|
+
let out = byDecl.get(key);
|
|
103
|
+
if (!out) byDecl.set(key, out = compute(p, v, ts));
|
|
104
|
+
return out;
|
|
105
|
+
}
|
|
106
|
+
function compute(p, v, ts) {
|
|
107
|
+
const pc = get(`css.properties.${p}`)?.__compat;
|
|
108
|
+
if (!pc) return [];
|
|
109
|
+
const [types, owners] = reachOf(p);
|
|
110
|
+
const found = [];
|
|
111
|
+
const code = v.replace(/"[^"]*"|'[^']*'/g, "");
|
|
112
|
+
for (const { 0: tok, 1: fn, index } of code.matchAll(/(?<![\w.#-])-?[A-Za-z_][\w-]*(\()?/g)) {
|
|
113
|
+
const k = fn ? tok.slice(0, -1) : tok;
|
|
114
|
+
const rel = tok === "from" ? /([a-z-]+)\(\s*$/.exec(code.slice(0, index))?.[1] : void 0;
|
|
115
|
+
const key = (rel ? [`css.types.color.${rel}.relative_syntax`] : tokenKeys(owners, types, k, !!fn)).find((x) => get(x)?.__compat);
|
|
116
|
+
if (key) found.push([fn ? `${k}()` : k, key]);
|
|
117
|
+
}
|
|
118
|
+
if (/"[^"]*"|'[^']*'/.test(v)) {
|
|
119
|
+
const key = owners.map((q) => `css.properties.${q}.string`).find((x) => get(x)?.__compat);
|
|
120
|
+
if (key) found.push(["string", key]);
|
|
121
|
+
}
|
|
122
|
+
const off = lacks(pc, ts).map((t) => t.join(" "));
|
|
123
|
+
const out = off.length ? [{
|
|
124
|
+
feature: p,
|
|
125
|
+
key: `css.properties.${p}`,
|
|
126
|
+
lacking: off
|
|
127
|
+
}] : [];
|
|
128
|
+
const on = ts.filter((t) => !off.includes(t.join(" ")));
|
|
129
|
+
for (const [feature, key] of found) {
|
|
130
|
+
const c = get(key)?.__compat;
|
|
131
|
+
if (!c || out.some((w) => w.key === key)) continue;
|
|
132
|
+
const lacking = lacks(c, on).map((t) => t.join(" "));
|
|
133
|
+
if (lacking.length) out.push({
|
|
134
|
+
feature,
|
|
135
|
+
key,
|
|
136
|
+
lacking
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
return out;
|
|
140
|
+
}
|
|
141
|
+
function browsers(lacking) {
|
|
142
|
+
const by = /* @__PURE__ */ new Map();
|
|
143
|
+
for (const t of lacking) {
|
|
144
|
+
const [a, ver] = t.split(" ");
|
|
145
|
+
by.set(a, [...by.get(a) ?? [], ver]);
|
|
146
|
+
}
|
|
147
|
+
return [...by].map(([a, vs]) => {
|
|
148
|
+
vs.sort(cmp);
|
|
149
|
+
return vs.length === 1 ? `${a} ${vs[0]}` : `${a} ${vs[0]}-${vs.at(-1)}`;
|
|
150
|
+
}).join(", ");
|
|
151
|
+
}
|
|
152
|
+
const support = Effect.fnUntraced(function* (p, v, ts = current()) {
|
|
153
|
+
return yield* Effect.sync(() => supportOf(p, v, ts));
|
|
154
|
+
});
|
|
155
|
+
const messageOf = (p, v, w) => `${p}: ${w.feature === p ? "the property" : `"${w.feature}"`} in "${v}" is not supported by ${browsers(w.lacking)} (${w.key})`;
|
|
156
|
+
const message = Effect.fn("support.message")(function* (p, v, w) {
|
|
157
|
+
return yield* Effect.sync(() => messageOf(p, v, w));
|
|
158
|
+
});
|
|
159
|
+
//#endregion
|
|
160
|
+
export { browsers, get, lacking, lackingOf, message, messageOf, support, supportOf, targets, tokenKey, useTargets };
|