@cssints/css-grammar 0.0.0-stage → 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 +178 -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 +28 -0
- package/dist/support.mjs +148 -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,28 @@
|
|
|
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
|
+
export declare function supportOf(p: string, v: string, ts?: Target[]): Warning[];
|
|
24
|
+
export declare function browsers(lacking: string[]): string;
|
|
25
|
+
export declare const support: (p: string, v: string, ts?: Target[] | undefined) => Effect.Effect<Warning[], never, never>;
|
|
26
|
+
export declare const messageOf: (p: string, v: string, w: Warning) => string;
|
|
27
|
+
export declare const message: (p: string, v: string, w: Warning) => Effect.Effect<string, never, never>;
|
|
28
|
+
//#endregion
|
package/dist/support.mjs
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
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 known = /* @__PURE__ */ new WeakMap();
|
|
81
|
+
function supportOf(p, v, ts = current()) {
|
|
82
|
+
let byDecl = known.get(ts);
|
|
83
|
+
if (!byDecl) known.set(ts, byDecl = /* @__PURE__ */ new Map());
|
|
84
|
+
const key = `${p}\0${v}`;
|
|
85
|
+
let out = byDecl.get(key);
|
|
86
|
+
if (!out) byDecl.set(key, out = compute(p, v, ts));
|
|
87
|
+
return out;
|
|
88
|
+
}
|
|
89
|
+
function compute(p, v, ts) {
|
|
90
|
+
const pc = get(`css.properties.${p}`)?.__compat;
|
|
91
|
+
if (!pc) return [];
|
|
92
|
+
const [types, owners] = reachOf(p);
|
|
93
|
+
const found = [];
|
|
94
|
+
const code = v.replace(/"[^"]*"|'[^']*'/g, "");
|
|
95
|
+
for (const { 0: tok, 1: fn, index } of code.matchAll(/(?<![\w.#-])-?[A-Za-z_][\w-]*(\()?/g)) {
|
|
96
|
+
const k = fn ? tok.slice(0, -1) : tok;
|
|
97
|
+
const rel = tok === "from" ? /([a-z-]+)\(\s*$/.exec(code.slice(0, index))?.[1] : void 0;
|
|
98
|
+
const key = (rel ? [`css.types.color.${rel}.relative_syntax`] : [
|
|
99
|
+
...owners.map((q) => `css.properties.${q}.${fn ? `${k}_function` : k}`),
|
|
100
|
+
...fn ? owners.map((q) => `css.properties.${q}.${k}`) : [],
|
|
101
|
+
...fn ? [`css.types.${k}`] : [],
|
|
102
|
+
...types.map((t) => `css.types.${t}.${k}`)
|
|
103
|
+
]).find((x) => get(x)?.__compat);
|
|
104
|
+
if (key) found.push([fn ? `${k}()` : k, key]);
|
|
105
|
+
}
|
|
106
|
+
if (/"[^"]*"|'[^']*'/.test(v)) {
|
|
107
|
+
const key = owners.map((q) => `css.properties.${q}.string`).find((x) => get(x)?.__compat);
|
|
108
|
+
if (key) found.push(["string", key]);
|
|
109
|
+
}
|
|
110
|
+
const off = lacks(pc, ts).map((t) => t.join(" "));
|
|
111
|
+
const out = off.length ? [{
|
|
112
|
+
feature: p,
|
|
113
|
+
key: `css.properties.${p}`,
|
|
114
|
+
lacking: off
|
|
115
|
+
}] : [];
|
|
116
|
+
const on = ts.filter((t) => !off.includes(t.join(" ")));
|
|
117
|
+
for (const [feature, key] of found) {
|
|
118
|
+
const c = get(key)?.__compat;
|
|
119
|
+
if (!c || out.some((w) => w.key === key)) continue;
|
|
120
|
+
const lacking = lacks(c, on).map((t) => t.join(" "));
|
|
121
|
+
if (lacking.length) out.push({
|
|
122
|
+
feature,
|
|
123
|
+
key,
|
|
124
|
+
lacking
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
return out;
|
|
128
|
+
}
|
|
129
|
+
function browsers(lacking) {
|
|
130
|
+
const by = /* @__PURE__ */ new Map();
|
|
131
|
+
for (const t of lacking) {
|
|
132
|
+
const [a, ver] = t.split(" ");
|
|
133
|
+
by.set(a, [...by.get(a) ?? [], ver]);
|
|
134
|
+
}
|
|
135
|
+
return [...by].map(([a, vs]) => {
|
|
136
|
+
vs.sort(cmp);
|
|
137
|
+
return vs.length === 1 ? `${a} ${vs[0]}` : `${a} ${vs[0]}-${vs.at(-1)}`;
|
|
138
|
+
}).join(", ");
|
|
139
|
+
}
|
|
140
|
+
const support = Effect.fnUntraced(function* (p, v, ts = current()) {
|
|
141
|
+
return yield* Effect.sync(() => supportOf(p, v, ts));
|
|
142
|
+
});
|
|
143
|
+
const messageOf = (p, v, w) => `${p}: ${w.feature === p ? "the property" : `"${w.feature}"`} in "${v}" is not supported by ${browsers(w.lacking)} (${w.key})`;
|
|
144
|
+
const message = Effect.fn("support.message")(function* (p, v, w) {
|
|
145
|
+
return yield* Effect.sync(() => messageOf(p, v, w));
|
|
146
|
+
});
|
|
147
|
+
//#endregion
|
|
148
|
+
export { browsers, get, lacking, lackingOf, message, messageOf, support, supportOf, targets, useTargets };
|
package/dist/token.d.mts
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { Consume, Matches, Parse, Stuck, Unexpected } from "./css.mjs";
|
|
2
|
+
import { CSSStyleValue } from "./values.mjs";
|
|
3
|
+
//#region src/token.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* Per token type, by the name of `token.<name>`:
|
|
6
|
+
* - `syntax`: the `@property` syntax, `<syntax>`, checked by the twin and by `ValidSyntax`;
|
|
7
|
+
* - `sentinels`: the typed-value types the token stands for in a property value (`⟨length⟩`, api-typed.ts);
|
|
8
|
+
* - `probes`: literals of the type that a build-time check puts in place of the token (one per sentinel), since the
|
|
9
|
+
* recognizer reads text and a token is `var(--name)`;
|
|
10
|
+
* - `grammar`: the grammar of a value when it is not `<syntax>` (a list of shadows);
|
|
11
|
+
* - `property`: the `@property` syntax when `<syntax>` is not one `@property` takes: `*`, any value, which a browser
|
|
12
|
+
* does not check or animate (an easing function, a shadow).
|
|
13
|
+
*/
|
|
14
|
+
export declare const TOKEN_TYPES: {
|
|
15
|
+
readonly color: {
|
|
16
|
+
readonly syntax: "color";
|
|
17
|
+
readonly sentinels: readonly ["color"];
|
|
18
|
+
readonly probes: readonly ["red"];
|
|
19
|
+
};
|
|
20
|
+
readonly length: {
|
|
21
|
+
readonly syntax: "length";
|
|
22
|
+
readonly sentinels: readonly ["length"];
|
|
23
|
+
readonly probes: readonly ["1px"];
|
|
24
|
+
};
|
|
25
|
+
readonly percentage: {
|
|
26
|
+
readonly syntax: "percentage";
|
|
27
|
+
readonly sentinels: readonly ["percentage"];
|
|
28
|
+
readonly probes: readonly ["1%"];
|
|
29
|
+
};
|
|
30
|
+
readonly lengthPercentage: {
|
|
31
|
+
readonly syntax: "length-percentage";
|
|
32
|
+
readonly sentinels: readonly ["length", "percentage"];
|
|
33
|
+
readonly probes: readonly ["1px", "1%"];
|
|
34
|
+
};
|
|
35
|
+
readonly number: {
|
|
36
|
+
readonly syntax: "number";
|
|
37
|
+
readonly sentinels: readonly ["number"];
|
|
38
|
+
readonly probes: readonly ["1"];
|
|
39
|
+
};
|
|
40
|
+
readonly integer: {
|
|
41
|
+
readonly syntax: "integer";
|
|
42
|
+
readonly sentinels: readonly ["integer"];
|
|
43
|
+
readonly probes: readonly ["1"];
|
|
44
|
+
};
|
|
45
|
+
readonly angle: {
|
|
46
|
+
readonly syntax: "angle";
|
|
47
|
+
readonly sentinels: readonly ["angle"];
|
|
48
|
+
readonly probes: readonly ["1deg"];
|
|
49
|
+
};
|
|
50
|
+
readonly time: {
|
|
51
|
+
readonly syntax: "time";
|
|
52
|
+
readonly sentinels: readonly ["time"];
|
|
53
|
+
readonly probes: readonly ["1s"];
|
|
54
|
+
};
|
|
55
|
+
readonly customIdent: {
|
|
56
|
+
readonly syntax: "custom-ident";
|
|
57
|
+
readonly sentinels: readonly ["custom-ident"];
|
|
58
|
+
readonly probes: readonly ["x"];
|
|
59
|
+
};
|
|
60
|
+
readonly resolution: {
|
|
61
|
+
readonly syntax: "resolution";
|
|
62
|
+
readonly sentinels: readonly ["resolution"];
|
|
63
|
+
readonly probes: readonly ["1dppx"];
|
|
64
|
+
};
|
|
65
|
+
readonly url: {
|
|
66
|
+
readonly syntax: "url";
|
|
67
|
+
readonly sentinels: readonly ["url"];
|
|
68
|
+
readonly probes: readonly ["url(x)"];
|
|
69
|
+
};
|
|
70
|
+
readonly image: {
|
|
71
|
+
readonly syntax: "image";
|
|
72
|
+
readonly sentinels: readonly ["image"];
|
|
73
|
+
readonly probes: readonly ["linear-gradient(red, blue)"];
|
|
74
|
+
};
|
|
75
|
+
readonly transformList: {
|
|
76
|
+
readonly syntax: "transform-list";
|
|
77
|
+
readonly sentinels: readonly ["transform-list"];
|
|
78
|
+
readonly probes: readonly ["translateX(1px)"];
|
|
79
|
+
};
|
|
80
|
+
readonly easingFunction: {
|
|
81
|
+
readonly syntax: "easing-function";
|
|
82
|
+
readonly sentinels: readonly ["easing-function"];
|
|
83
|
+
readonly probes: readonly ["ease"];
|
|
84
|
+
readonly property: "*";
|
|
85
|
+
};
|
|
86
|
+
readonly shadow: {
|
|
87
|
+
readonly syntax: "shadow";
|
|
88
|
+
readonly sentinels: readonly ["shadow"];
|
|
89
|
+
readonly probes: readonly ["0 0 red"];
|
|
90
|
+
readonly grammar: "<shadow>#";
|
|
91
|
+
readonly property: "*";
|
|
92
|
+
};
|
|
93
|
+
};
|
|
94
|
+
export type TokenType = keyof typeof TOKEN_TYPES;
|
|
95
|
+
/**
|
|
96
|
+
* The grammar of a value of a token type.
|
|
97
|
+
* @param {string} syntax the `syntax` of the token type
|
|
98
|
+
* @returns {string} `<syntax>`, or the type's `grammar`
|
|
99
|
+
*/
|
|
100
|
+
export declare const grammarOf: (syntax: string) => string;
|
|
101
|
+
/**
|
|
102
|
+
* DTCG `$type` to token type (createTokens, docs/API.md "Themes"). `fontWeight` is a number: its keywords become numbers.
|
|
103
|
+
* The other DTCG types (`fontFamily`, `strokeStyle`, `border`, `transition`, `gradient`, `typography`) are an error.
|
|
104
|
+
*/
|
|
105
|
+
export declare const DTCG_TYPES: {
|
|
106
|
+
readonly color: "color";
|
|
107
|
+
readonly dimension: "length";
|
|
108
|
+
readonly number: "number";
|
|
109
|
+
readonly duration: "time";
|
|
110
|
+
readonly fontWeight: "number";
|
|
111
|
+
readonly cubicBezier: "easingFunction";
|
|
112
|
+
readonly shadow: "shadow";
|
|
113
|
+
};
|
|
114
|
+
export type DtcgType = keyof typeof DTCG_TYPES;
|
|
115
|
+
export type Syntax<K extends TokenType> = (typeof TOKEN_TYPES)[K]["syntax"];
|
|
116
|
+
/** The grammar of a value of the token type `K` (`grammarOf`). */
|
|
117
|
+
export type Grammar<K extends TokenType> = (typeof TOKEN_TYPES)[K] extends {
|
|
118
|
+
readonly grammar: infer G extends string;
|
|
119
|
+
} ? G : `<${Syntax<K>}>`;
|
|
120
|
+
type Trim<S extends string> = S extends ` ${infer R}` ? Trim<R> : S;
|
|
121
|
+
/** The value, or a message naming the bad token, like `Valid` for a property. */
|
|
122
|
+
export type ValidSyntax<K extends string, S extends string, G extends string = `<${K}>`> = Matches<G, S> extends true ? S : S extends string ? `${K}: ${Unexpected<Stuck<Trim<S> | Consume<Parse<G>, Trim<S>>>>} in "${S}", grammar ${G}` : never;
|
|
123
|
+
/** `ValidSyntax` for a value of the token type `K`. */
|
|
124
|
+
export type ValidToken<K extends TokenType, S extends string> = ValidSyntax<Syntax<K>, S, Grammar<K>>;
|
|
125
|
+
/**
|
|
126
|
+
* A token: a Typed OM value of its type whose text is `var(--name)`. `Settable` is whether it has a name. `V` is the value of
|
|
127
|
+
* the definition as text, inferred by `createTokens` (`"#545434"`, `"{color.base}"` for an alias), `string` where nothing
|
|
128
|
+
* says; a covariant phantom, so a token of any `V` is a `Token<K, S>`.
|
|
129
|
+
*/
|
|
130
|
+
export interface Token<K extends TokenType = TokenType, Settable extends boolean = boolean, V extends string = string> extends CSSStyleValue<(typeof TOKEN_TYPES)[K]["sentinels"][number]> {
|
|
131
|
+
readonly "~token": K;
|
|
132
|
+
readonly "~settable": Settable;
|
|
133
|
+
readonly "~value": V;
|
|
134
|
+
}
|
|
135
|
+
/** `--name`, and whether the variable inherits (default true). */
|
|
136
|
+
export interface TokenName {
|
|
137
|
+
var: string;
|
|
138
|
+
inherit?: boolean;
|
|
139
|
+
}
|
|
140
|
+
/** An inline style: what `style()` and `token.set()` give, for a `style` prop. */
|
|
141
|
+
export type StyleObject = Record<string, string>;
|
|
142
|
+
/** `token.<type>(initial, name?)`: with a name the token can be set, without one it is private. */
|
|
143
|
+
export type TokenFn<K extends TokenType> = <const I extends string, const N extends string | TokenName | undefined = undefined>(initial: ValidToken<K, I>, name?: N) => Token<K, [N] extends [undefined] ? false : true>;
|
|
144
|
+
/** `token.set(token, value)`: a style object with one custom property. A private token cannot be set. */
|
|
145
|
+
export type TokenSetFn = <const K extends TokenType, const V extends string>(token: Token<K, true>, value: ValidToken<K, V>) => StyleObject;
|
|
146
|
+
//#endregion
|