@monoharada/wcf-mcp 0.1.0 → 0.1.2
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 +83 -10
- package/bin.mjs +108 -2
- package/core.mjs +1643 -0
- package/data/custom-elements.json +13055 -6859
- package/data/design-tokens.json +2422 -0
- package/data/guidelines-index.json +5914 -0
- package/data/install-registry.json +52 -0
- package/package.json +2 -1
- package/server.mjs +26 -624
- package/validator.mjs +174 -5
package/validator.mjs
CHANGED
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
* Only includes the two functions used by the MCP server:
|
|
4
4
|
* - collectCemCustomElements
|
|
5
5
|
* - validateTextAgainstCem
|
|
6
|
+
* - detectTokenMisuseInInlineStyles
|
|
7
|
+
* - detectAccessibilityMisuseInMarkup
|
|
6
8
|
*/
|
|
7
9
|
|
|
8
10
|
const GLOBAL_ATTR_ALLOW_PREFIXES = Object.freeze(['aria-', 'data-']);
|
|
@@ -25,6 +27,15 @@ const GLOBAL_ATTR_ALLOW_SET = Object.freeze(
|
|
|
25
27
|
);
|
|
26
28
|
|
|
27
29
|
const FORBIDDEN_ATTR_SET = Object.freeze(new Set(['placeholder']));
|
|
30
|
+
const TOKEN_MISUSE_STYLE_PROPS = Object.freeze(new Set([
|
|
31
|
+
'color',
|
|
32
|
+
'background-color',
|
|
33
|
+
'padding',
|
|
34
|
+
'padding-top',
|
|
35
|
+
'padding-right',
|
|
36
|
+
'padding-bottom',
|
|
37
|
+
'padding-left',
|
|
38
|
+
]));
|
|
28
39
|
|
|
29
40
|
function isForbiddenAttr(attrName) {
|
|
30
41
|
return FORBIDDEN_ATTR_SET.has(attrName.toLowerCase());
|
|
@@ -96,7 +107,29 @@ function makeRange(lineStarts, startIndex, endIndex) {
|
|
|
96
107
|
return { start, end };
|
|
97
108
|
}
|
|
98
109
|
|
|
110
|
+
function normalizeStyleValue(value) {
|
|
111
|
+
return String(value ?? '').trim().toLowerCase().replace(/\s+/g, ' ');
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function parseInlineStyleAttribute(attrChunk) {
|
|
115
|
+
const styleMatch = /\bstyle\s*=\s*("([^"]*)"|'([^']*)')/i.exec(attrChunk);
|
|
116
|
+
if (!styleMatch) return undefined;
|
|
117
|
+
|
|
118
|
+
const quoted = styleMatch[1] ?? '';
|
|
119
|
+
const styleValue = styleMatch[2] ?? styleMatch[3] ?? '';
|
|
120
|
+
if (!styleValue) return undefined;
|
|
121
|
+
|
|
122
|
+
return {
|
|
123
|
+
styleValue,
|
|
124
|
+
styleValueOffsetInAttr: styleMatch.index + quoted.indexOf(styleValue),
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
99
128
|
function parseAttributeNames(rawAttrs) {
|
|
129
|
+
return parseAttributes(rawAttrs).map(({ name, offset }) => ({ name, offset }));
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function parseAttributes(rawAttrs) {
|
|
100
133
|
/** @type {{ name: string, offset: number }[]} */
|
|
101
134
|
const out = [];
|
|
102
135
|
|
|
@@ -121,9 +154,7 @@ function parseAttributeNames(rawAttrs) {
|
|
|
121
154
|
}
|
|
122
155
|
|
|
123
156
|
const name = rawAttrs.slice(nameStart, i);
|
|
124
|
-
|
|
125
|
-
out.push({ name, offset: nameStart });
|
|
126
|
-
}
|
|
157
|
+
let value = '';
|
|
127
158
|
|
|
128
159
|
while (i < len && isSpace(rawAttrs.charCodeAt(i))) i += 1;
|
|
129
160
|
|
|
@@ -135,16 +166,24 @@ function parseAttributeNames(rawAttrs) {
|
|
|
135
166
|
const quote = rawAttrs[i];
|
|
136
167
|
if (quote === '"' || quote === "'") {
|
|
137
168
|
i += 1;
|
|
169
|
+
const valueStart = i;
|
|
138
170
|
while (i < len && rawAttrs[i] !== quote) i += 1;
|
|
171
|
+
value = rawAttrs.slice(valueStart, i);
|
|
139
172
|
if (i < len) i += 1;
|
|
140
173
|
} else {
|
|
174
|
+
const valueStart = i;
|
|
141
175
|
while (i < len) {
|
|
142
176
|
const cc = rawAttrs[i];
|
|
143
177
|
if (cc === '>' || cc === '/' || isSpace(rawAttrs.charCodeAt(i))) break;
|
|
144
178
|
i += 1;
|
|
145
179
|
}
|
|
180
|
+
value = rawAttrs.slice(valueStart, i);
|
|
146
181
|
}
|
|
147
182
|
}
|
|
183
|
+
|
|
184
|
+
if (name && !name.startsWith('${') && !name.includes('{') && !name.includes('}')) {
|
|
185
|
+
out.push({ name, offset: nameStart, value });
|
|
186
|
+
}
|
|
148
187
|
}
|
|
149
188
|
|
|
150
189
|
return out;
|
|
@@ -176,13 +215,13 @@ export function validateTextAgainstCem({
|
|
|
176
215
|
|
|
177
216
|
const tagOffset = m.index + 1;
|
|
178
217
|
const attrChunk = String(m[2] ?? '');
|
|
218
|
+
const rawAttrsStart = m.index + 1 + tag.length;
|
|
179
219
|
|
|
180
220
|
const attrNames = parseAttributeNames(attrChunk);
|
|
181
221
|
for (const { name, offset } of attrNames) {
|
|
182
222
|
const attrName = name.toLowerCase();
|
|
183
223
|
if (!isForbiddenAttr(attrName)) continue;
|
|
184
224
|
|
|
185
|
-
const rawAttrsStart = m.index + 1 + tag.length;
|
|
186
225
|
const startIndex = rawAttrsStart + offset;
|
|
187
226
|
const endIndex = startIndex + attrName.length;
|
|
188
227
|
const range = makeRange(lineStarts, startIndex, endIndex);
|
|
@@ -220,7 +259,6 @@ export function validateTextAgainstCem({
|
|
|
220
259
|
if (shouldSkipAttr(attrName)) continue;
|
|
221
260
|
if (meta.attributes.has(attrName)) continue;
|
|
222
261
|
|
|
223
|
-
const rawAttrsStart = m.index + 1 + tag.length;
|
|
224
262
|
const startIndex = rawAttrsStart + offset;
|
|
225
263
|
const endIndex = startIndex + attrName.length;
|
|
226
264
|
const range = makeRange(lineStarts, startIndex, endIndex);
|
|
@@ -238,3 +276,134 @@ export function validateTextAgainstCem({
|
|
|
238
276
|
|
|
239
277
|
return diagnostics;
|
|
240
278
|
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* @param {{
|
|
282
|
+
* filePath?: string;
|
|
283
|
+
* text: string;
|
|
284
|
+
* valueToToken?: Map<string, string>;
|
|
285
|
+
* severity?: string;
|
|
286
|
+
* }} params
|
|
287
|
+
*/
|
|
288
|
+
export function detectTokenMisuseInInlineStyles({
|
|
289
|
+
filePath = '<input>',
|
|
290
|
+
text,
|
|
291
|
+
valueToToken = new Map(),
|
|
292
|
+
severity = 'warning',
|
|
293
|
+
}) {
|
|
294
|
+
const diagnostics = [];
|
|
295
|
+
if (!(valueToToken instanceof Map) || valueToToken.size === 0) return diagnostics;
|
|
296
|
+
|
|
297
|
+
const lineStarts = computeLineIndex(text);
|
|
298
|
+
const tagRe = /<([a-z][a-z0-9-]*)\b([^<>]*?)>/gi;
|
|
299
|
+
let m;
|
|
300
|
+
|
|
301
|
+
while ((m = tagRe.exec(text))) {
|
|
302
|
+
const tag = String(m[1] ?? '').toLowerCase();
|
|
303
|
+
const attrChunk = String(m[2] ?? '');
|
|
304
|
+
const inlineStyle = parseInlineStyleAttribute(attrChunk);
|
|
305
|
+
if (!inlineStyle) continue;
|
|
306
|
+
|
|
307
|
+
const { styleValue, styleValueOffsetInAttr } = inlineStyle;
|
|
308
|
+
const rawAttrsStart = m.index + 1 + tag.length;
|
|
309
|
+
|
|
310
|
+
const declarationRe = /([a-z-]+)\s*:\s*([^;]+)/gi;
|
|
311
|
+
let d;
|
|
312
|
+
while ((d = declarationRe.exec(styleValue))) {
|
|
313
|
+
const prop = String(d[1] ?? '').trim().toLowerCase();
|
|
314
|
+
if (!TOKEN_MISUSE_STYLE_PROPS.has(prop)) continue;
|
|
315
|
+
|
|
316
|
+
const valueRaw = String(d[2] ?? '').trim();
|
|
317
|
+
if (!valueRaw || /^var\(/i.test(valueRaw)) continue;
|
|
318
|
+
|
|
319
|
+
const normalizedValue = normalizeStyleValue(valueRaw);
|
|
320
|
+
const cssVariable = valueToToken.get(normalizedValue);
|
|
321
|
+
if (!cssVariable) continue;
|
|
322
|
+
|
|
323
|
+
const valueOffsetInDecl = d[0].indexOf(d[2]);
|
|
324
|
+
const valueOffsetInStyle = d.index + Math.max(0, valueOffsetInDecl);
|
|
325
|
+
const startIndex = rawAttrsStart + styleValueOffsetInAttr + valueOffsetInStyle;
|
|
326
|
+
const endIndex = startIndex + d[2].length;
|
|
327
|
+
const range = makeRange(lineStarts, startIndex, endIndex);
|
|
328
|
+
|
|
329
|
+
diagnostics.push({
|
|
330
|
+
file: filePath,
|
|
331
|
+
range,
|
|
332
|
+
severity,
|
|
333
|
+
code: 'tokenMisuse',
|
|
334
|
+
message: `Use var(${cssVariable}) instead of ${valueRaw} for ${prop}`,
|
|
335
|
+
tagName: tag,
|
|
336
|
+
attrName: 'style',
|
|
337
|
+
hint: `Replace ${prop}: ${valueRaw} with ${prop}: var(${cssVariable})`,
|
|
338
|
+
});
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
return diagnostics;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* @param {{
|
|
347
|
+
* filePath?: string;
|
|
348
|
+
* text: string;
|
|
349
|
+
* severity?: string;
|
|
350
|
+
* }} params
|
|
351
|
+
*/
|
|
352
|
+
export function detectAccessibilityMisuseInMarkup({
|
|
353
|
+
filePath = '<input>',
|
|
354
|
+
text,
|
|
355
|
+
severity = 'warning',
|
|
356
|
+
}) {
|
|
357
|
+
const diagnostics = [];
|
|
358
|
+
const lineStarts = computeLineIndex(text);
|
|
359
|
+
const tagRe = /<([a-z][a-z0-9-]*)\b([^<>]*?)>/gi;
|
|
360
|
+
let m;
|
|
361
|
+
|
|
362
|
+
while ((m = tagRe.exec(text))) {
|
|
363
|
+
const tag = String(m[1] ?? '').toLowerCase();
|
|
364
|
+
const attrChunk = String(m[2] ?? '');
|
|
365
|
+
const rawAttrsStart = m.index + 1 + tag.length;
|
|
366
|
+
|
|
367
|
+
const attrNames = parseAttributeNames(attrChunk);
|
|
368
|
+
for (const { name, offset } of attrNames) {
|
|
369
|
+
const attrName = String(name ?? '').toLowerCase();
|
|
370
|
+
if (attrName !== 'aria-live') continue;
|
|
371
|
+
|
|
372
|
+
const startIndex = rawAttrsStart + offset;
|
|
373
|
+
const endIndex = startIndex + attrName.length;
|
|
374
|
+
const range = makeRange(lineStarts, startIndex, endIndex);
|
|
375
|
+
diagnostics.push({
|
|
376
|
+
file: filePath,
|
|
377
|
+
range,
|
|
378
|
+
severity,
|
|
379
|
+
code: 'ariaLiveNotRecommended',
|
|
380
|
+
message: 'Avoid aria-live in component markup; use static text with aria-describedby instead.',
|
|
381
|
+
tagName: tag,
|
|
382
|
+
attrName: 'aria-live',
|
|
383
|
+
hint: 'Remove aria-live and connect error/support text via aria-describedby.',
|
|
384
|
+
});
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
const roleAttr = parseAttributes(attrChunk).find(({ name }) => String(name ?? '').toLowerCase() === 'role');
|
|
388
|
+
const roleValue = String(roleAttr?.value ?? '').trim().toLowerCase();
|
|
389
|
+
if (roleAttr && roleValue === 'alert') {
|
|
390
|
+
const attrName = 'role';
|
|
391
|
+
const roleOffsetInChunk = roleAttr.offset;
|
|
392
|
+
const startIndex = rawAttrsStart + roleOffsetInChunk;
|
|
393
|
+
const endIndex = startIndex + attrName.length;
|
|
394
|
+
const range = makeRange(lineStarts, startIndex, endIndex);
|
|
395
|
+
diagnostics.push({
|
|
396
|
+
file: filePath,
|
|
397
|
+
range,
|
|
398
|
+
severity,
|
|
399
|
+
code: 'roleAlertNotRecommended',
|
|
400
|
+
message: 'Avoid role=\"alert\" in component markup; prefer static error text and aria-describedby.',
|
|
401
|
+
tagName: tag,
|
|
402
|
+
attrName,
|
|
403
|
+
hint: 'Replace role=\"alert\" with non-live text associated to the control.',
|
|
404
|
+
});
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
return diagnostics;
|
|
409
|
+
}
|