@master/css-tooling 0.0.0-site.1793659883.g69c9aebcf628
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/README.md +58 -0
- package/dist/browser.d.ts +2 -0
- package/dist/browser.js +1 -0
- package/dist/css-tree-version.d.ts +2 -0
- package/dist/css-tree-version.js +4 -0
- package/dist/css.d.ts +32 -0
- package/dist/css.js +120 -0
- package/dist/immutable.d.ts +2 -0
- package/dist/immutable.js +7 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +1 -0
- package/dist/language/binding-session.d.ts +6 -0
- package/dist/language/binding-session.js +8 -0
- package/dist/language/browser.d.ts +10 -0
- package/dist/language/browser.js +12 -0
- package/dist/language/common.d.ts +12 -0
- package/dist/language/common.js +55 -0
- package/dist/language/contracts.d.ts +197 -0
- package/dist/language/contracts.js +1 -0
- package/dist/language/index.d.ts +29 -0
- package/dist/language/index.js +39 -0
- package/dist/language/master-css.d.ts +13 -0
- package/dist/language/master-css.js +35 -0
- package/dist/language/node.d.ts +21 -0
- package/dist/language/node.js +36 -0
- package/dist/language/semantic/scopes.d.ts +55 -0
- package/dist/language/semantic/scopes.js +73 -0
- package/dist/language/semantic/types.d.ts +11 -0
- package/dist/language/semantic/types.js +1 -0
- package/dist/language/session.d.ts +37 -0
- package/dist/language/session.js +87 -0
- package/dist/language/settings.d.ts +13 -0
- package/dist/language/settings.js +19 -0
- package/dist/language/utils/mdn-css-data.d.ts +5 -0
- package/dist/language/utils/mdn-css-data.js +63 -0
- package/dist/lexer/browser.d.ts +6 -0
- package/dist/lexer/browser.js +11 -0
- package/dist/lexer/contracts.d.ts +41 -0
- package/dist/lexer/contracts.js +1 -0
- package/dist/lexer/index.d.ts +8 -0
- package/dist/lexer/index.js +11 -0
- package/dist/lexer/node.d.ts +7 -0
- package/dist/lexer/node.js +11 -0
- package/dist/lexer/session.d.ts +17 -0
- package/dist/lexer/session.js +25 -0
- package/dist/lint/analysis.d.ts +90 -0
- package/dist/lint/analysis.js +1 -0
- package/dist/lint/binding-session.d.ts +21 -0
- package/dist/lint/binding-session.js +97 -0
- package/dist/lint/constants.d.ts +7 -0
- package/dist/lint/constants.js +18 -0
- package/dist/lint/contracts.d.ts +17 -0
- package/dist/lint/contracts.js +6 -0
- package/dist/lint/diagnostic-format.d.ts +5 -0
- package/dist/lint/diagnostic-format.js +15 -0
- package/dist/lint/diagnostics.d.ts +42 -0
- package/dist/lint/diagnostics.js +1 -0
- package/dist/lint/index.d.ts +21 -0
- package/dist/lint/index.js +30 -0
- package/dist/lint/node.d.ts +6 -0
- package/dist/lint/node.js +11 -0
- package/dist/lint/rule-signatures.d.ts +23 -0
- package/dist/lint/rule-signatures.js +27 -0
- package/dist/lint/source.d.ts +70 -0
- package/dist/lint/source.js +261 -0
- package/dist/math-types.d.ts +11 -0
- package/dist/math-types.js +236 -0
- package/dist/math-validation.d.ts +7 -0
- package/dist/math-validation.js +74 -0
- package/dist/node.d.ts +12 -0
- package/dist/node.js +32 -0
- package/dist/scanner/binding-session.d.ts +29 -0
- package/dist/scanner/binding-session.js +37 -0
- package/dist/scanner/core.d.ts +130 -0
- package/dist/scanner/core.js +480 -0
- package/dist/scanner/index.d.ts +3 -0
- package/dist/scanner/index.js +3 -0
- package/dist/scanner/node.d.ts +5 -0
- package/dist/scanner/node.js +4 -0
- package/dist/scanner/options/index.d.ts +16 -0
- package/dist/scanner/options/index.js +57 -0
- package/dist/scanner/source-policy.d.ts +15 -0
- package/dist/scanner/source-policy.js +131 -0
- package/dist/source/adapters/optional-peer.d.ts +5 -0
- package/dist/source/adapters/optional-peer.js +29 -0
- package/dist/source/adapters/oxc.d.ts +2 -0
- package/dist/source/adapters/oxc.js +6 -0
- package/dist/source/adapters/svelte.d.ts +5 -0
- package/dist/source/adapters/svelte.js +82 -0
- package/dist/source/adapters/types.d.ts +11 -0
- package/dist/source/adapters/types.js +9 -0
- package/dist/source/adapters/vue.d.ts +5 -0
- package/dist/source/adapters/vue.js +50 -0
- package/dist/source/browser.d.ts +7 -0
- package/dist/source/browser.js +11 -0
- package/dist/source/contracts.d.ts +35 -0
- package/dist/source/contracts.js +1 -0
- package/dist/source/index.d.ts +8 -0
- package/dist/source/index.js +11 -0
- package/dist/source/native.d.ts +5 -0
- package/dist/source/native.js +53 -0
- package/dist/source/node.d.ts +7 -0
- package/dist/source/node.js +11 -0
- package/dist/source/session.d.ts +25 -0
- package/dist/source/session.js +37 -0
- package/dist/syntax-validation.d.ts +10 -0
- package/dist/syntax-validation.js +54 -0
- package/dist/tooling-session.d.ts +66 -0
- package/dist/tooling-session.js +188 -0
- package/dist/validator/binding-session.d.ts +17 -0
- package/dist/validator/binding-session.js +29 -0
- package/dist/validator/contracts.d.ts +24 -0
- package/dist/validator/contracts.js +1 -0
- package/dist/validator/index.d.ts +10 -0
- package/dist/validator/index.js +13 -0
- package/dist/validator/node.d.ts +6 -0
- package/dist/validator/node.js +11 -0
- package/dist/validator/types/syntax-error.d.ts +22 -0
- package/dist/validator/types/syntax-error.js +1 -0
- package/dist/value-validation.d.ts +46 -0
- package/dist/value-validation.js +162 -0
- package/package.json +127 -0
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
// src/lint/source.ts
|
|
2
|
+
const MAX_FIX_PASSES = 10;
|
|
3
|
+
const masterCSSLintRuleIds = Object.freeze([
|
|
4
|
+
"sort-classes",
|
|
5
|
+
"no-invalid-classes",
|
|
6
|
+
"no-conflicting-classes",
|
|
7
|
+
"prefer-canonical-classes",
|
|
8
|
+
"no-unapproved-raw-values",
|
|
9
|
+
"prefer-component-classes"
|
|
10
|
+
]);
|
|
11
|
+
const defaultMasterCSSLintRules = Object.freeze({
|
|
12
|
+
"sort-classes": true,
|
|
13
|
+
"no-invalid-classes": true,
|
|
14
|
+
"no-conflicting-classes": true,
|
|
15
|
+
"prefer-canonical-classes": true,
|
|
16
|
+
"no-unapproved-raw-values": false,
|
|
17
|
+
"prefer-component-classes": false
|
|
18
|
+
});
|
|
19
|
+
const languageByExtension = {
|
|
20
|
+
".astro": "astro",
|
|
21
|
+
".css": "css",
|
|
22
|
+
".htm": "html",
|
|
23
|
+
".html": "html",
|
|
24
|
+
".js": "javascript",
|
|
25
|
+
".mjs": "javascript",
|
|
26
|
+
".cjs": "javascript",
|
|
27
|
+
".jsx": "javascriptreact",
|
|
28
|
+
".less": "less",
|
|
29
|
+
".md": "markdown",
|
|
30
|
+
".mdx": "mdx",
|
|
31
|
+
".mts": "typescript",
|
|
32
|
+
".cts": "typescript",
|
|
33
|
+
".php": "html",
|
|
34
|
+
".pug": "html",
|
|
35
|
+
".scss": "scss",
|
|
36
|
+
".svelte": "svelte",
|
|
37
|
+
".ts": "typescript",
|
|
38
|
+
".tsx": "typescriptreact",
|
|
39
|
+
".vue": "vue"
|
|
40
|
+
};
|
|
41
|
+
const stylesheetLanguageIds = /* @__PURE__ */ new Set([
|
|
42
|
+
"css",
|
|
43
|
+
"scss",
|
|
44
|
+
"less"
|
|
45
|
+
]);
|
|
46
|
+
function resolveMasterCSSLintRules(option) {
|
|
47
|
+
if (!option) return { ...defaultMasterCSSLintRules };
|
|
48
|
+
const resolved = Object.fromEntries(masterCSSLintRuleIds.map((ruleId) => [ruleId, false]));
|
|
49
|
+
for (const rule of option.split(",").map((item) => item.trim()).filter(Boolean)) {
|
|
50
|
+
if (rule === "recommended") {
|
|
51
|
+
Object.assign(resolved, defaultMasterCSSLintRules);
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
if (rule === "all") {
|
|
55
|
+
for (const ruleId of masterCSSLintRuleIds) resolved[ruleId] = true;
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
if (!masterCSSLintRuleIds.includes(rule)) throw new Error(`Unknown Master CSS lint rule "${rule}".`);
|
|
59
|
+
resolved[rule] = true;
|
|
60
|
+
}
|
|
61
|
+
return resolved;
|
|
62
|
+
}
|
|
63
|
+
function resolveRules(rules) {
|
|
64
|
+
return {
|
|
65
|
+
...defaultMasterCSSLintRules,
|
|
66
|
+
...rules
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
function getLanguageId(filePath) {
|
|
70
|
+
const separator = Math.max(filePath.lastIndexOf("/"), filePath.lastIndexOf("\\"));
|
|
71
|
+
const dot = filePath.lastIndexOf(".");
|
|
72
|
+
const extension = dot > separator ? filePath.slice(dot).toLowerCase() : "";
|
|
73
|
+
return languageByExtension[extension] || "html";
|
|
74
|
+
}
|
|
75
|
+
function getFileSourceKind(languageId) {
|
|
76
|
+
return stylesheetLanguageIds.has(languageId) ? "stylesheet" : "source";
|
|
77
|
+
}
|
|
78
|
+
function detectClassListUnescape(content, range) {
|
|
79
|
+
const quote = content[range.start - 1];
|
|
80
|
+
return quote === "\"" || quote === "'" || quote === "`" ? quote : false;
|
|
81
|
+
}
|
|
82
|
+
function inferClassSourceKind(content, range) {
|
|
83
|
+
const prefix = content.slice(Math.max(0, range.start - 80), range.start);
|
|
84
|
+
return /(?:^|[\s{<])(?:class|className|class:list|:class|v-bind:class|\[class\]|\[className\]|\[ngClass\])\s*=\s*(?:"|'|`|\{[^]*$)/.test(prefix) ? "class-attribute" : "class-expression";
|
|
85
|
+
}
|
|
86
|
+
function collectClassListContexts(content, filePath, lintSession, analysis = lintSession.analyzeLintDocument(content, getLanguageId(filePath))) {
|
|
87
|
+
const positions = analysis.classPositions;
|
|
88
|
+
const contexts = /* @__PURE__ */ new Map();
|
|
89
|
+
for (const position of positions) {
|
|
90
|
+
const contextRange = position.contextRange;
|
|
91
|
+
const key = `${contextRange.start}:${contextRange.end}`;
|
|
92
|
+
const existing = contexts.get(key);
|
|
93
|
+
if (existing) {
|
|
94
|
+
existing.classNames.push(position.token);
|
|
95
|
+
existing.transformed ||= position.raw !== position.token;
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
contexts.set(key, {
|
|
99
|
+
range: contextRange,
|
|
100
|
+
text: content.slice(contextRange.start, contextRange.end),
|
|
101
|
+
sourceKind: inferClassSourceKind(content, contextRange),
|
|
102
|
+
unescape: detectClassListUnescape(content, contextRange),
|
|
103
|
+
classNames: [position.token],
|
|
104
|
+
transformed: position.raw !== position.token
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
return [...contexts.values()].map((context) => {
|
|
108
|
+
const tokens = lintSession.tokenizeClassList(context.text, context.unescape).map((item) => item.token);
|
|
109
|
+
context.transformed ||= tokens.length !== context.classNames.length || tokens.some((token, index) => token !== context.classNames[index]);
|
|
110
|
+
return context;
|
|
111
|
+
}).sort((a, b) => a.range.start - b.range.start || a.range.end - b.range.end);
|
|
112
|
+
}
|
|
113
|
+
function offsetToLocation(content, offset) {
|
|
114
|
+
let line = 1;
|
|
115
|
+
let lineStart = 0;
|
|
116
|
+
for (let index = 0; index < offset; index++) if (content[index] === "\n") {
|
|
117
|
+
line++;
|
|
118
|
+
lineStart = index + 1;
|
|
119
|
+
}
|
|
120
|
+
return {
|
|
121
|
+
line,
|
|
122
|
+
column: offset - lineStart + 1
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
function toLocationRange(content, range) {
|
|
126
|
+
return {
|
|
127
|
+
start: offsetToLocation(content, range.start),
|
|
128
|
+
end: offsetToLocation(content, range.end)
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
function createContextLintDiagnostics(context, rules, options) {
|
|
132
|
+
return Object.values(rules).some(Boolean) ? options.lintSession.analyzeLintClassList(context.text, context.classNames, {
|
|
133
|
+
disallowUnknownClass: options.ruleOptions?.["no-invalid-classes"]?.disallowUnknownClass,
|
|
134
|
+
rawValuePolicy: rules["no-unapproved-raw-values"] ? options.ruleOptions?.["no-unapproved-raw-values"] || {} : void 0,
|
|
135
|
+
canonicalOptions: rules["prefer-canonical-classes"] ? options.ruleOptions?.["prefer-canonical-classes"] || {} : void 0,
|
|
136
|
+
remoteStylingPolicy: rules["prefer-component-classes"] ? { max: options.ruleOptions?.["prefer-component-classes"]?.max } : void 0
|
|
137
|
+
}).diagnostics.filter(({ ruleId }) => rules[ruleId]).map((diagnostic) => ({
|
|
138
|
+
...diagnostic,
|
|
139
|
+
severity: options.severities?.[diagnostic.ruleId] || (diagnostic.ruleId === "no-invalid-classes" ? "error" : "warning"),
|
|
140
|
+
fix: diagnostic.fix
|
|
141
|
+
})) : [];
|
|
142
|
+
}
|
|
143
|
+
function toSourceFix(context, fix) {
|
|
144
|
+
if (context.transformed) return;
|
|
145
|
+
return {
|
|
146
|
+
kind: "class-list",
|
|
147
|
+
safety: "safe",
|
|
148
|
+
range: {
|
|
149
|
+
start: context.range.start + fix.range.start,
|
|
150
|
+
end: context.range.start + fix.range.end
|
|
151
|
+
},
|
|
152
|
+
text: fix.text,
|
|
153
|
+
description: "Replace the class list text.",
|
|
154
|
+
requiresFormatting: false
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
function toSourceDiagnostic(content, context, diagnostic) {
|
|
158
|
+
const range = context.transformed ? context.range : {
|
|
159
|
+
start: context.range.start + diagnostic.range.start,
|
|
160
|
+
end: context.range.start + diagnostic.range.end
|
|
161
|
+
};
|
|
162
|
+
const fix = diagnostic.fix && toSourceFix(context, diagnostic.fix);
|
|
163
|
+
return {
|
|
164
|
+
ruleId: diagnostic.ruleId,
|
|
165
|
+
code: diagnostic.code,
|
|
166
|
+
severity: diagnostic.severity,
|
|
167
|
+
message: diagnostic.message,
|
|
168
|
+
range,
|
|
169
|
+
loc: toLocationRange(content, range),
|
|
170
|
+
source: "Master CSS",
|
|
171
|
+
sourceKind: context.sourceKind,
|
|
172
|
+
...diagnostic.data ? { data: diagnostic.data } : {},
|
|
173
|
+
...fix ? { fixes: [fix] } : {}
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
function lintMasterCSSContent(options) {
|
|
177
|
+
const diagnostics = [];
|
|
178
|
+
const rules = resolveRules(options.rules);
|
|
179
|
+
const document = options.lintSession.analyzeLintDocument(options.content, getLanguageId(options.filePath));
|
|
180
|
+
for (const diagnostic of document.diagnostics ?? []) {
|
|
181
|
+
if (diagnostic.severity === "info") continue;
|
|
182
|
+
const range = diagnostic.range ?? {
|
|
183
|
+
start: 0,
|
|
184
|
+
end: 0
|
|
185
|
+
};
|
|
186
|
+
diagnostics.push({
|
|
187
|
+
code: diagnostic.code,
|
|
188
|
+
severity: diagnostic.severity,
|
|
189
|
+
message: diagnostic.message,
|
|
190
|
+
range,
|
|
191
|
+
loc: toLocationRange(options.content, range),
|
|
192
|
+
source: "Master CSS",
|
|
193
|
+
sourceKind: "class-expression"
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
for (const context of collectClassListContexts(options.content, options.filePath, options.lintSession, document)) diagnostics.push(...createContextLintDiagnostics(context, rules, options).map((diagnostic) => toSourceDiagnostic(options.content, context, diagnostic)));
|
|
197
|
+
const languageId = getLanguageId(options.filePath);
|
|
198
|
+
return {
|
|
199
|
+
filePath: options.filePath,
|
|
200
|
+
languageId,
|
|
201
|
+
sourceKind: getFileSourceKind(languageId),
|
|
202
|
+
diagnostics
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
function applyReplacement(content, range, text) {
|
|
206
|
+
return content.slice(0, range.start) + text + content.slice(range.end);
|
|
207
|
+
}
|
|
208
|
+
function applyFixes(content, fixes) {
|
|
209
|
+
let fixed = content;
|
|
210
|
+
for (const fix of fixes) fixed = applyReplacement(fixed, fix.range, fix.text);
|
|
211
|
+
return fixed;
|
|
212
|
+
}
|
|
213
|
+
function fixClassListText(context, rules, options) {
|
|
214
|
+
if (context.transformed) return context.text;
|
|
215
|
+
let fixed = context.text;
|
|
216
|
+
for (let pass = 0; pass < MAX_FIX_PASSES; pass++) {
|
|
217
|
+
const fix = createContextLintDiagnostics({
|
|
218
|
+
...context,
|
|
219
|
+
range: {
|
|
220
|
+
start: 0,
|
|
221
|
+
end: fixed.length
|
|
222
|
+
},
|
|
223
|
+
text: fixed,
|
|
224
|
+
classNames: options.lintSession.tokenizeClassList(fixed, context.unescape).map(({ token }) => token)
|
|
225
|
+
}, rules, options).map((diagnostic) => diagnostic.fix).find((fix) => Boolean(fix));
|
|
226
|
+
if (!fix) return fixed;
|
|
227
|
+
const next = applyReplacement(fixed, fix.range, fix.text);
|
|
228
|
+
if (next === fixed) return fixed;
|
|
229
|
+
fixed = next;
|
|
230
|
+
}
|
|
231
|
+
return fixed;
|
|
232
|
+
}
|
|
233
|
+
function fixSafeClassLists(content, filePath, rules, options) {
|
|
234
|
+
return applyFixes(content, collectClassListContexts(content, filePath, options.lintSession).map((context) => ({
|
|
235
|
+
range: context.range,
|
|
236
|
+
text: fixClassListText(context, rules, options)
|
|
237
|
+
})).filter((replacement) => content.slice(replacement.range.start, replacement.range.end) !== replacement.text).sort((a, b) => b.range.start - a.range.start || b.range.end - a.range.end).map((replacement) => ({
|
|
238
|
+
kind: "class-list",
|
|
239
|
+
safety: "safe",
|
|
240
|
+
range: replacement.range,
|
|
241
|
+
text: replacement.text,
|
|
242
|
+
description: "Replace the class list text.",
|
|
243
|
+
requiresFormatting: false
|
|
244
|
+
})));
|
|
245
|
+
}
|
|
246
|
+
function fixMasterCSSContent(options) {
|
|
247
|
+
return fixSafeClassLists(options.content, options.filePath, resolveRules(options.rules), options);
|
|
248
|
+
}
|
|
249
|
+
function summarizeMasterCSSLintFiles(files) {
|
|
250
|
+
const diagnostics = files.flatMap((file) => file.diagnostics);
|
|
251
|
+
const fixes = diagnostics.flatMap((diagnostic) => diagnostic.fixes ?? []);
|
|
252
|
+
return {
|
|
253
|
+
files: files.length,
|
|
254
|
+
diagnostics: diagnostics.length,
|
|
255
|
+
errors: diagnostics.filter((diagnostic) => diagnostic.severity === "error").length,
|
|
256
|
+
warnings: diagnostics.filter((diagnostic) => diagnostic.severity === "warning").length,
|
|
257
|
+
fixable: diagnostics.filter((diagnostic) => diagnostic.fixes?.length).length,
|
|
258
|
+
safeFixes: fixes.filter((fix) => fix.safety === "safe").length
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
export { defaultMasterCSSLintRules, fixMasterCSSContent, lintMasterCSSContent, masterCSSLintRuleIds, resolveMasterCSSLintRules, summarizeMasterCSSLintFiles };
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { CssNode } from "@eslint/css-tree";
|
|
2
|
+
declare const mathFunctions: Set<string>;
|
|
3
|
+
/** Substitute only proven result types into a disposable grammar AST. Never
|
|
4
|
+
* evaluate values, ranges, variable substitutions, or mutate emitted CSS.
|
|
5
|
+
* A nested calc may have a compound intermediate type which its parent cancels.
|
|
6
|
+
*/
|
|
7
|
+
declare function validateMathTypes(ast: CssNode, matches: (value: CssNode) => boolean): {
|
|
8
|
+
ast: CssNode;
|
|
9
|
+
status?: 'valid' | 'invalid' | 'unknown';
|
|
10
|
+
};
|
|
11
|
+
export { mathFunctions, validateMathTypes };
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
import { clone, lexer, walk } from "@eslint/css-tree";
|
|
2
|
+
// src/math-types.ts
|
|
3
|
+
const unknown = { kind: "unknown" };
|
|
4
|
+
const invalid = { kind: "invalid" };
|
|
5
|
+
const number = () => ({
|
|
6
|
+
kind: "known",
|
|
7
|
+
powers: {}
|
|
8
|
+
});
|
|
9
|
+
const dimension = (name) => ({
|
|
10
|
+
kind: "known",
|
|
11
|
+
powers: { [name]: 1 }
|
|
12
|
+
});
|
|
13
|
+
const examples = {
|
|
14
|
+
length: "px",
|
|
15
|
+
angle: "deg",
|
|
16
|
+
time: "s",
|
|
17
|
+
frequency: "hz",
|
|
18
|
+
resolution: "dppx",
|
|
19
|
+
flex: "fr"
|
|
20
|
+
};
|
|
21
|
+
const units = new Map(Object.entries(lexer.units).filter(([type]) => type in examples).flatMap(([type, names]) => names.map((unit) => [unit.toLowerCase(), type])));
|
|
22
|
+
const mathFunctions = /* @__PURE__ */ new Set([
|
|
23
|
+
"calc",
|
|
24
|
+
"min",
|
|
25
|
+
"max",
|
|
26
|
+
"clamp",
|
|
27
|
+
"mod",
|
|
28
|
+
"rem",
|
|
29
|
+
"sin",
|
|
30
|
+
"cos",
|
|
31
|
+
"tan",
|
|
32
|
+
"asin",
|
|
33
|
+
"acos",
|
|
34
|
+
"atan",
|
|
35
|
+
"atan2",
|
|
36
|
+
"pow",
|
|
37
|
+
"sqrt",
|
|
38
|
+
"hypot",
|
|
39
|
+
"exp",
|
|
40
|
+
"abs",
|
|
41
|
+
"sign",
|
|
42
|
+
"log",
|
|
43
|
+
"round"
|
|
44
|
+
]);
|
|
45
|
+
function same(a, b) {
|
|
46
|
+
return Object.keys({
|
|
47
|
+
...a.powers,
|
|
48
|
+
...b.powers
|
|
49
|
+
}).every((key) => a.powers[key] === b.powers[key]);
|
|
50
|
+
}
|
|
51
|
+
function combine(a, b, operator, hint) {
|
|
52
|
+
if (a.kind === "invalid" || b.kind === "invalid") return invalid;
|
|
53
|
+
if (a.kind === "unknown" || b.kind === "unknown") return unknown;
|
|
54
|
+
if (operator === "+" || operator === "-") {
|
|
55
|
+
if (same(a, b)) return a;
|
|
56
|
+
if (hint === void 0 && (a.powers.percentage || b.powers.percentage)) return unknown;
|
|
57
|
+
return invalid;
|
|
58
|
+
}
|
|
59
|
+
const powers = { ...a.powers };
|
|
60
|
+
for (const [dimension, exponent] of Object.entries(b.powers)) {
|
|
61
|
+
const key = dimension;
|
|
62
|
+
powers[key] = (powers[key] ?? 0) + exponent * (operator === "/" ? -1 : 1);
|
|
63
|
+
if (powers[key] === 0) delete powers[key];
|
|
64
|
+
}
|
|
65
|
+
return {
|
|
66
|
+
kind: "known",
|
|
67
|
+
powers
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
function expression(nodes, hint) {
|
|
71
|
+
let offset = 0;
|
|
72
|
+
const atom = () => {
|
|
73
|
+
const node = nodes[offset++];
|
|
74
|
+
if (!node) return invalid;
|
|
75
|
+
switch (node.type) {
|
|
76
|
+
case "Number": return number();
|
|
77
|
+
case "Dimension": return units.has(node.unit.toLowerCase()) ? dimension(units.get(node.unit.toLowerCase())) : unknown;
|
|
78
|
+
case "Percentage": return dimension(hint ?? "percentage");
|
|
79
|
+
case "Identifier": return [
|
|
80
|
+
"e",
|
|
81
|
+
"pi",
|
|
82
|
+
"infinity",
|
|
83
|
+
"-infinity",
|
|
84
|
+
"nan"
|
|
85
|
+
].includes(node.name.toLowerCase()) ? number() : unknown;
|
|
86
|
+
case "Parentheses": return expression([...node.children], hint);
|
|
87
|
+
case "Function": return inferFunction(node, hint);
|
|
88
|
+
default: return unknown;
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
const operator = () => {
|
|
92
|
+
const node = nodes[offset];
|
|
93
|
+
return node?.type === "Operator" ? node.value.trim() : "";
|
|
94
|
+
};
|
|
95
|
+
const product = () => {
|
|
96
|
+
let result = atom();
|
|
97
|
+
while (["*", "/"].includes(operator())) {
|
|
98
|
+
const op = operator();
|
|
99
|
+
offset++;
|
|
100
|
+
result = combine(result, atom(), op, hint);
|
|
101
|
+
}
|
|
102
|
+
return result;
|
|
103
|
+
};
|
|
104
|
+
let result = product();
|
|
105
|
+
let knownSum = result.kind === "known" ? result : void 0;
|
|
106
|
+
while (["+", "-"].includes(operator())) {
|
|
107
|
+
const op = operator();
|
|
108
|
+
offset++;
|
|
109
|
+
const right = product();
|
|
110
|
+
if (right.kind === "known") {
|
|
111
|
+
const sum = knownSum ? combine(knownSum, right, op, hint) : right;
|
|
112
|
+
if (sum.kind === "invalid") return invalid;
|
|
113
|
+
knownSum = sum.kind === "known" ? sum : void 0;
|
|
114
|
+
}
|
|
115
|
+
result = combine(result, right, op, hint);
|
|
116
|
+
}
|
|
117
|
+
return offset === nodes.length ? result : unknown;
|
|
118
|
+
}
|
|
119
|
+
function primitive(value) {
|
|
120
|
+
const entries = Object.entries(value.powers);
|
|
121
|
+
return !entries.length ? "number" : entries.length === 1 && entries[0][1] === 1 ? entries[0][0] : void 0;
|
|
122
|
+
}
|
|
123
|
+
function inferFunction(node, hint) {
|
|
124
|
+
const name = node.name.toLowerCase();
|
|
125
|
+
if (!mathFunctions.has(name)) return unknown;
|
|
126
|
+
const args = [[]];
|
|
127
|
+
for (const child of node.children) if (child.type === "Operator" && child.value.trim() === ",") args.push([]);
|
|
128
|
+
else args.at(-1).push(child);
|
|
129
|
+
if (name === "round" && args[0]?.length === 1 && args[0][0].type === "Identifier" && [
|
|
130
|
+
"nearest",
|
|
131
|
+
"up",
|
|
132
|
+
"down",
|
|
133
|
+
"to-zero"
|
|
134
|
+
].includes(args[0][0].name)) args.shift();
|
|
135
|
+
const values = args.map((nodes, index) => name === "clamp" && index !== 1 && nodes.length === 1 && nodes[0].type === "Identifier" && nodes[0].name === "none" ? void 0 : expression(nodes, hint)).filter((value) => value !== void 0);
|
|
136
|
+
if (values.some((value) => value.kind === "invalid")) return invalid;
|
|
137
|
+
const known = values.filter((value) => value.kind === "known");
|
|
138
|
+
const uncertain = known.length !== values.length;
|
|
139
|
+
const accepts = (types) => known.every((value) => {
|
|
140
|
+
const type = primitive(value);
|
|
141
|
+
return type !== void 0 && types.includes(type);
|
|
142
|
+
});
|
|
143
|
+
let result;
|
|
144
|
+
switch (name) {
|
|
145
|
+
case "calc": return values[0] ?? invalid;
|
|
146
|
+
case "sin":
|
|
147
|
+
case "cos":
|
|
148
|
+
case "tan":
|
|
149
|
+
result = accepts(["number", "angle"]) ? number() : invalid;
|
|
150
|
+
break;
|
|
151
|
+
case "asin":
|
|
152
|
+
case "acos":
|
|
153
|
+
case "atan":
|
|
154
|
+
result = accepts(["number"]) ? dimension("angle") : invalid;
|
|
155
|
+
break;
|
|
156
|
+
case "pow":
|
|
157
|
+
case "sqrt":
|
|
158
|
+
case "exp":
|
|
159
|
+
case "log":
|
|
160
|
+
result = accepts(["number"]) ? number() : invalid;
|
|
161
|
+
break;
|
|
162
|
+
case "sign":
|
|
163
|
+
result = known.every((value) => primitive(value) !== void 0) ? number() : invalid;
|
|
164
|
+
break;
|
|
165
|
+
case "abs":
|
|
166
|
+
result = values[0] ?? invalid;
|
|
167
|
+
break;
|
|
168
|
+
default:
|
|
169
|
+
result = known.reduce((result, value) => combine(result, value, "+", hint), known[0] ?? unknown);
|
|
170
|
+
if (name === "round" && values.length === 1 && !accepts(["number"])) result = invalid;
|
|
171
|
+
if (name === "atan2" && result.kind === "known") result = dimension("angle");
|
|
172
|
+
}
|
|
173
|
+
return result.kind === "invalid" ? invalid : uncertain ? unknown : result;
|
|
174
|
+
}
|
|
175
|
+
function representative(type) {
|
|
176
|
+
if (type === "number") return {
|
|
177
|
+
type: "Number",
|
|
178
|
+
value: "1"
|
|
179
|
+
};
|
|
180
|
+
if (type === "percentage") return {
|
|
181
|
+
type: "Percentage",
|
|
182
|
+
value: "1"
|
|
183
|
+
};
|
|
184
|
+
return {
|
|
185
|
+
type: "Dimension",
|
|
186
|
+
value: "1",
|
|
187
|
+
unit: examples[type]
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
/** Substitute only proven result types into a disposable grammar AST. Never
|
|
191
|
+
* evaluate values, ranges, variable substitutions, or mutate emitted CSS.
|
|
192
|
+
* A nested calc may have a compound intermediate type which its parent cancels.
|
|
193
|
+
*/
|
|
194
|
+
function validateMathTypes(ast, matches) {
|
|
195
|
+
const normalized = clone(ast);
|
|
196
|
+
let status = "valid";
|
|
197
|
+
let found = false;
|
|
198
|
+
walk(normalized, (node, item, list) => {
|
|
199
|
+
if (node.type !== "Function" || !mathFunctions.has(node.name.toLowerCase()) || !item || !list) return;
|
|
200
|
+
found = true;
|
|
201
|
+
const original = item.data;
|
|
202
|
+
const accepts = (type) => {
|
|
203
|
+
item.data = representative(type);
|
|
204
|
+
const result = matches(normalized);
|
|
205
|
+
item.data = original;
|
|
206
|
+
return result;
|
|
207
|
+
};
|
|
208
|
+
let hint = null;
|
|
209
|
+
let hasPercentage = false;
|
|
210
|
+
walk(node, (child) => {
|
|
211
|
+
if (child.type === "Percentage") hasPercentage = true;
|
|
212
|
+
});
|
|
213
|
+
if (hasPercentage) {
|
|
214
|
+
const bases = Object.keys(examples).filter((type) => accepts(type));
|
|
215
|
+
hint = accepts("percentage") && bases.length === 1 ? bases[0] : bases.length ? null : void 0;
|
|
216
|
+
}
|
|
217
|
+
const inferred = inferFunction(node, hint);
|
|
218
|
+
if (inferred.kind === "invalid") status = "invalid";
|
|
219
|
+
else if (inferred.kind === "unknown") {
|
|
220
|
+
if (status !== "invalid") status = "unknown";
|
|
221
|
+
} else {
|
|
222
|
+
const type = primitive(inferred);
|
|
223
|
+
if (!type) {
|
|
224
|
+
if (hasPercentage && hint === void 0) {
|
|
225
|
+
if (status !== "invalid") status = "unknown";
|
|
226
|
+
} else status = "invalid";
|
|
227
|
+
} else item.data = representative(type);
|
|
228
|
+
}
|
|
229
|
+
return walk.skip;
|
|
230
|
+
});
|
|
231
|
+
return {
|
|
232
|
+
ast: normalized,
|
|
233
|
+
status: found ? status : void 0
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
export { mathFunctions, validateMathTypes };
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { FunctionNode } from "@eslint/css-tree";
|
|
2
|
+
/** Check operator structure and arity, without inferring the result's dimension.
|
|
3
|
+
* CSS Values 4 allows dimensional division. Normalize operands for grammar-only
|
|
4
|
+
* checks rather than applying the lexer's numeric-divisor restriction.
|
|
5
|
+
*/
|
|
6
|
+
declare function mathFunctionStatus(node: FunctionNode): 'valid' | 'invalid' | 'unknown' | undefined;
|
|
7
|
+
export { mathFunctionStatus };
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { mathFunctions } from "./math-types.js";
|
|
2
|
+
import { List, clone, generate, lexer, walk } from "@eslint/css-tree";
|
|
3
|
+
// src/math-validation.ts
|
|
4
|
+
/** Check operator structure and arity, without inferring the result's dimension.
|
|
5
|
+
* CSS Values 4 allows dimensional division. Normalize operands for grammar-only
|
|
6
|
+
* checks rather than applying the lexer's numeric-divisor restriction.
|
|
7
|
+
*/
|
|
8
|
+
function mathFunctionStatus(node) {
|
|
9
|
+
const name = node.name.toLowerCase();
|
|
10
|
+
if (!mathFunctions.has(name)) return;
|
|
11
|
+
const normalized = clone(node);
|
|
12
|
+
let uncertain = false;
|
|
13
|
+
walk(normalized, (child, item, list) => {
|
|
14
|
+
if (child === normalized) return;
|
|
15
|
+
if (child.type === "Function") {
|
|
16
|
+
if (!mathFunctions.has(child.name.toLowerCase())) uncertain = true;
|
|
17
|
+
if (item && list) list.replace(item, list.createItem({
|
|
18
|
+
type: "Number",
|
|
19
|
+
value: "1"
|
|
20
|
+
}));
|
|
21
|
+
return walk.skip;
|
|
22
|
+
}
|
|
23
|
+
if ((child.type === "Dimension" || child.type === "Percentage") && item && list) list.replace(item, list.createItem({
|
|
24
|
+
type: "Number",
|
|
25
|
+
value: "1"
|
|
26
|
+
}));
|
|
27
|
+
});
|
|
28
|
+
if (uncertain) {
|
|
29
|
+
const arguments_ = [[]];
|
|
30
|
+
for (const child of node.children) if (child.type === "Operator" && child.value.trim() === ",") arguments_.push([]);
|
|
31
|
+
else arguments_.at(-1).push(child);
|
|
32
|
+
for (const argument of arguments_) {
|
|
33
|
+
const last = argument.at(-1);
|
|
34
|
+
if (last?.type === "Operator" && [
|
|
35
|
+
"+",
|
|
36
|
+
"-",
|
|
37
|
+
"*",
|
|
38
|
+
"/"
|
|
39
|
+
].includes(last.value.trim())) return "invalid";
|
|
40
|
+
let dependent = false;
|
|
41
|
+
const children = new List().fromArray(argument);
|
|
42
|
+
walk({
|
|
43
|
+
type: "Value",
|
|
44
|
+
children
|
|
45
|
+
}, (child) => {
|
|
46
|
+
if (child.type === "Function") dependent = true;
|
|
47
|
+
});
|
|
48
|
+
if (!dependent && mathFunctionStatus({
|
|
49
|
+
type: "Function",
|
|
50
|
+
name: "calc",
|
|
51
|
+
children
|
|
52
|
+
}) === "invalid") return "invalid";
|
|
53
|
+
}
|
|
54
|
+
return "unknown";
|
|
55
|
+
}
|
|
56
|
+
if (name === "log" || name === "round") {
|
|
57
|
+
const parts = [[]];
|
|
58
|
+
for (const child of normalized.children) if (child.type === "Operator" && child.value.trim() === ",") parts.push([]);
|
|
59
|
+
else parts.at(-1).push(child);
|
|
60
|
+
if (name === "round" && parts[0]?.length === 1 && parts[0][0].type === "Identifier" && [
|
|
61
|
+
"nearest",
|
|
62
|
+
"up",
|
|
63
|
+
"down",
|
|
64
|
+
"to-zero"
|
|
65
|
+
].includes(parts[0][0].name)) parts.shift();
|
|
66
|
+
if (parts.length < 1 || parts.length > 2) return "invalid";
|
|
67
|
+
return parts.some((children) => lexer.matchType("calc-sum", generate({
|
|
68
|
+
type: "Value",
|
|
69
|
+
children: new List().fromArray(children)
|
|
70
|
+
})).error) ? "invalid" : "valid";
|
|
71
|
+
}
|
|
72
|
+
return lexer.matchType(`${name}()`, generate(normalized)).error ? "invalid" : "valid";
|
|
73
|
+
}
|
|
74
|
+
export { mathFunctionStatus };
|
package/dist/node.d.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { CSS_VALUE_CHECK, cssValueStatus, validateRuleDeclarations } from "./value-validation.js";
|
|
2
|
+
import { CSS_SYNTAX_CHECK, cssSyntaxStatus } from "./syntax-validation.js";
|
|
3
|
+
import { MasterCSSToolingSession } from "./tooling-session.js";
|
|
4
|
+
import { MasterCSSManifest } from "@master/css-schema/manifest";
|
|
5
|
+
declare function createToolingSessionSync(options: {
|
|
6
|
+
readonly manifest: MasterCSSManifest;
|
|
7
|
+
/** Classes the project defines in native CSS; they report no diagnostics while they generate no rules. */
|
|
8
|
+
readonly nativeClassNames?: readonly string[];
|
|
9
|
+
}): MasterCSSToolingSession;
|
|
10
|
+
/** Versions from the artifact that actually executes project semantics. */
|
|
11
|
+
declare function getToolingBindingInfo(): import("@master/css-binding").MasterCSSBindingInfo;
|
|
12
|
+
export { CSS_SYNTAX_CHECK, CSS_VALUE_CHECK, type MasterCSSToolingSession, createToolingSessionSync, cssSyntaxStatus, cssValueStatus, getToolingBindingInfo, validateRuleDeclarations };
|
package/dist/node.js
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { bindLexerSession } from "./lexer/session.js";
|
|
2
|
+
import { CSS_SYNTAX_CHECK, cssSyntaxStatus } from "./syntax-validation.js";
|
|
3
|
+
import { CSS_VALUE_CHECK, cssValueStatus, validateRuleDeclarations } from "./value-validation.js";
|
|
4
|
+
import { bindLanguageSession } from "./language/session.js";
|
|
5
|
+
import { bindLintSession } from "./lint/binding-session.js";
|
|
6
|
+
import { bindSourceExtractor } from "./source/session.js";
|
|
7
|
+
import { bindValidatorSession } from "./validator/binding-session.js";
|
|
8
|
+
import { bindToolingSessionInternal } from "./tooling-session.js";
|
|
9
|
+
import { createToolingBindingSync } from "@master/css-binding/tooling/node";
|
|
10
|
+
// src/node.ts
|
|
11
|
+
function createToolingSessionSync(options) {
|
|
12
|
+
const tooling = createToolingBindingSync();
|
|
13
|
+
const lexer = tooling.createLexerSession();
|
|
14
|
+
const source = tooling.createSourceSession();
|
|
15
|
+
const validator = tooling.createValidatorSession(options.manifest);
|
|
16
|
+
const language = tooling.createLanguageSession(options.manifest);
|
|
17
|
+
const lint = tooling.createLintSession(options.manifest);
|
|
18
|
+
const lintValidator = tooling.createValidatorSession(options.manifest);
|
|
19
|
+
const lintLanguage = tooling.createLanguageSession(options.manifest);
|
|
20
|
+
return bindToolingSessionInternal({
|
|
21
|
+
lexer: bindLexerSession("native", lexer),
|
|
22
|
+
source: bindSourceExtractor("native", source),
|
|
23
|
+
validator: bindValidatorSession("native", validator, options.nativeClassNames),
|
|
24
|
+
lint: bindLintSession(lint, lintValidator, lintLanguage),
|
|
25
|
+
language: bindLanguageSession("native", language, options.nativeClassNames)
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
/** Versions from the artifact that actually executes project semantics. */
|
|
29
|
+
function getToolingBindingInfo() {
|
|
30
|
+
return createToolingBindingSync().info;
|
|
31
|
+
}
|
|
32
|
+
export { CSS_SYNTAX_CHECK, CSS_VALUE_CHECK, createToolingSessionSync, cssSyntaxStatus, cssValueStatus, getToolingBindingInfo, validateRuleDeclarations };
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { MasterCSSBindingLoadOptions, MasterCSSScannerSourceInput, MasterCSSScannerSourceOptions, MasterCSSScannerState, MasterCSSScannerUpdate } from "@master/css-binding/tooling";
|
|
2
|
+
import { MasterCSSManifest } from "@master/css-schema/manifest";
|
|
3
|
+
type BindingScannerUpdate = MasterCSSScannerUpdate;
|
|
4
|
+
type BindingScannerState = MasterCSSScannerState;
|
|
5
|
+
type ScannerSourceOptions = MasterCSSScannerSourceOptions;
|
|
6
|
+
type ScannerSourceInput = MasterCSSScannerSourceInput;
|
|
7
|
+
type BindingScannerBlocklist = string | Readonly<{
|
|
8
|
+
source: string;
|
|
9
|
+
flags: string;
|
|
10
|
+
}>;
|
|
11
|
+
interface BindingScannerSession {
|
|
12
|
+
readonly binding: 'native' | 'wasm';
|
|
13
|
+
extractCandidates(source: string, content: string, options?: ScannerSourceOptions): string[];
|
|
14
|
+
collectCandidates(candidates: string[]): string[];
|
|
15
|
+
filterCandidates(candidates: string[], blocklist: BindingScannerBlocklist[]): string[];
|
|
16
|
+
scanCandidates(source: string, content: string, candidates: string[], blocklist: BindingScannerBlocklist[], options?: ScannerSourceOptions): BindingScannerUpdate;
|
|
17
|
+
removeSource(source: string, options?: ScannerSourceOptions): BindingScannerUpdate;
|
|
18
|
+
reconcileSources(owner: string, inputs: readonly ScannerSourceInput[]): BindingScannerUpdate;
|
|
19
|
+
removeOwner(owner: string): BindingScannerUpdate;
|
|
20
|
+
ensureClasses(classNames: string[]): void;
|
|
21
|
+
registerNativeClasses(owner: string, classNames: string[]): boolean;
|
|
22
|
+
reset(): void;
|
|
23
|
+
state(): BindingScannerState;
|
|
24
|
+
dispose(): void;
|
|
25
|
+
}
|
|
26
|
+
declare function serializeScannerBlocklist(blocklist?: Iterable<string | RegExp>): BindingScannerBlocklist[];
|
|
27
|
+
declare function createNativeScannerSession(manifest: MasterCSSManifest): BindingScannerSession;
|
|
28
|
+
declare function createScannerSession(manifest: MasterCSSManifest, options?: MasterCSSBindingLoadOptions): Promise<BindingScannerSession>;
|
|
29
|
+
export { BindingScannerBlocklist, BindingScannerSession, BindingScannerState, BindingScannerUpdate, ScannerSourceInput, ScannerSourceOptions, createNativeScannerSession, createScannerSession, serializeScannerBlocklist };
|