@monoharada/wcf-mcp 0.9.0 → 0.9.1

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/core/cem.mjs ADDED
@@ -0,0 +1,923 @@
1
+ /**
2
+ * core/cem.mjs — CEM index, icon catalog, accessibility, related components, and pattern helpers.
3
+ */
4
+
5
+ import { CANONICAL_PREFIX } from './constants.mjs';
6
+ import { normalizePrefix, withPrefix, toCanonicalTagName, getCategory, suggestUnknownElementTagName } from './prefix.mjs';
7
+ import { normalizeTokenIdentifier } from './tokens.mjs';
8
+
9
+ // Single-module constants (DD-14)
10
+ const A11Y_CATEGORY_LEVEL_MAP = Object.freeze({
11
+ semantics: 'A',
12
+ keyboard: 'A',
13
+ labels: 'A',
14
+ states: 'AA',
15
+ zoom: 'AA',
16
+ motion: 'AA',
17
+ callouts: 'AA',
18
+ guideline: 'A',
19
+ });
20
+
21
+ const WCAG_LEVELS = Object.freeze(new Set(['A', 'AA', 'AAA', 'all']));
22
+
23
+ // Icon alias table: common alias → canonical icon names (DD-18)
24
+ const ICON_ALIAS_TABLE = new Map([
25
+ ['x', ['close', 'cancel']],
26
+ ['trash', ['delete']],
27
+ ['pencil', ['edit']],
28
+ ['magnifying', ['search']],
29
+ ['gear', ['settings']],
30
+ ['plus', ['add']],
31
+ ['minus', ['subtract']],
32
+ ['tick', ['check', 'checkmark']],
33
+ ['alert', ['warning', 'attention']],
34
+ ['info', ['information', 'help']],
35
+ ['hamburger', ['menu']],
36
+ ['back', ['arrowBack', 'arrowLeft']],
37
+ ['forward', ['arrowForward', 'arrowRight']],
38
+ ['eye', ['visibility']],
39
+ ['user', ['person']],
40
+ ['file', ['document']],
41
+ ['bell', ['notification']],
42
+ ]);
43
+
44
+ // Interaction examples for form components (P-04 / #206)
45
+ const INTERACTION_EXAMPLES_MAP = Object.freeze({
46
+ 'dads-input-text': [
47
+ { scenario: 'Set value programmatically', trigger: 'property', code: 'el.value = "hello";' },
48
+ { scenario: 'Show validation error', trigger: 'attribute', code: 'el.error = true; el.errorText = "この項目は入力が必須です";' },
49
+ { scenario: 'Clear validation error', trigger: 'attribute', code: 'el.error = false; el.errorText = "";' },
50
+ { scenario: 'Listen to value change', trigger: 'event', code: "el.addEventListener('change', (e) => { console.log(e.target.value); });" },
51
+ ],
52
+ 'dads-textarea': [
53
+ { scenario: 'Set value programmatically', trigger: 'property', code: 'el.value = "long text...";' },
54
+ { scenario: 'Show validation error', trigger: 'attribute', code: 'el.error = true; el.errorText = "入力できる文字数を超えています";' },
55
+ { scenario: 'Listen to input event', trigger: 'event', code: "el.addEventListener('input', (e) => { console.log(e.target.value); });" },
56
+ ],
57
+ 'dads-select': [
58
+ { scenario: 'Set selected value', trigger: 'property', code: 'el.value = "option1";' },
59
+ { scenario: 'Show validation error', trigger: 'attribute', code: 'el.error = true; el.errorText = "この項目は入力が必須です";' },
60
+ { scenario: 'Listen to change event', trigger: 'event', code: "el.addEventListener('change', (e) => { console.log(e.target.value); });" },
61
+ ],
62
+ 'dads-checkbox': [
63
+ { scenario: 'Set checked state', trigger: 'property', code: 'el.checked = true;' },
64
+ { scenario: 'Show validation error', trigger: 'attribute', code: 'el.error = true; el.errorText = "この項目は入力が必須です";' },
65
+ { scenario: 'Listen to change event', trigger: 'event', code: "el.addEventListener('change', (e) => { console.log(e.target.checked); });" },
66
+ ],
67
+ 'dads-radio': [
68
+ { scenario: 'Set checked state', trigger: 'property', code: 'el.checked = true;' },
69
+ { scenario: 'Listen to change event', trigger: 'event', code: "el.addEventListener('change', (e) => { console.log(e.target.value); });" },
70
+ ],
71
+ 'dads-combobox': [
72
+ { scenario: 'Set value programmatically', trigger: 'property', code: 'el.value = "selected-option";' },
73
+ { scenario: 'Show validation error', trigger: 'attribute', code: 'el.error = true; el.errorText = "この項目は入力が必須です";' },
74
+ { scenario: 'Listen to change event', trigger: 'event', code: "el.addEventListener('change', (e) => { console.log(e.target.value); });" },
75
+ ],
76
+ 'dads-date-picker': [
77
+ { scenario: 'Set date value', trigger: 'property', code: 'el.value = "2024-01-15";' },
78
+ { scenario: 'Show validation error', trigger: 'attribute', code: 'el.error = true; el.errorText = "この項目は入力が必須です";' },
79
+ { scenario: 'Listen to change event', trigger: 'event', code: "el.addEventListener('change', (e) => { console.log(e.target.value); });" },
80
+ ],
81
+ 'dads-file-upload': [
82
+ { scenario: 'Listen to file selection', trigger: 'event', code: "el.addEventListener('change', (e) => { console.log(e.target.files); });" },
83
+ { scenario: 'Show validation error', trigger: 'attribute', code: 'el.error = true; el.errorText = "この項目は入力が必須です";' },
84
+ ],
85
+ });
86
+
87
+ // Layout behavior metadata for layout/display components (P-05 / #207)
88
+ const LAYOUT_BEHAVIOR_MAP = Object.freeze({
89
+ 'dads-layout-shell': {
90
+ responsive: {
91
+ breakpoints: { desktop: '80rem', tablet: '48rem' },
92
+ modes: ['auto', 'desktop', 'tablet', 'mobile'],
93
+ defaultMode: 'auto',
94
+ description: 'Automatically switches between desktop/tablet/mobile layouts based on viewport width when mode="auto".',
95
+ },
96
+ overflow: {
97
+ strategy: 'slot-driven',
98
+ description: 'Slots (header, sidebar, aside, footer) are auto-hidden when empty. Sidebar collapses to rail on tablet.',
99
+ },
100
+ constraints: {
101
+ patterns: ['website', 'app-shell', 'master-detail', 'left-header-pane', 'three-pane', 'three-pane-shell'],
102
+ defaultPattern: 'app-shell',
103
+ mobileSidebarOptions: ['hidden', 'top', 'bottom'],
104
+ description: 'Choose a pattern attribute to control layout structure. Pair with mode and mobile-sidebar for full control.',
105
+ },
106
+ },
107
+ 'dads-layout-sidebar': {
108
+ responsive: {
109
+ description: 'Designed to be placed inside dads-layout-shell sidebar slot. Width is controlled by the parent shell.',
110
+ },
111
+ constraints: {
112
+ description: 'Simple container for sidebar content. Use inside dads-layout-shell for responsive behavior.',
113
+ },
114
+ },
115
+ 'dads-device-mock': {
116
+ responsive: {
117
+ devices: ['desktop', 'tablet', 'mobile'],
118
+ defaultDevice: 'mobile',
119
+ description: 'Renders a device frame (desktop/tablet/mobile) around slotted content. Set device attribute to switch.',
120
+ },
121
+ constraints: {
122
+ visibleHeight: 'Use visible-height attribute to clip the mock to a specific height (e.g. "220px").',
123
+ description: 'Display-only component for previewing content in device frames. Not a layout container.',
124
+ },
125
+ },
126
+ });
127
+
128
+ /**
129
+ * Generic fallback values for common attributes when CEM default is missing.
130
+ */
131
+ const SNIPPET_FALLBACK_VALUES = {
132
+ label: 'ラベル',
133
+ name: 'field1',
134
+ value: 'サンプル値',
135
+ 'support-text': '説明テキスト',
136
+ };
137
+
138
+ export function findCustomElementDeclarations(manifest) {
139
+ const modules = Array.isArray(manifest?.modules) ? manifest.modules : [];
140
+ const decls = [];
141
+
142
+ for (const mod of modules) {
143
+ const modulePath = typeof mod?.path === 'string' ? mod.path : undefined;
144
+ const declarations = Array.isArray(mod?.declarations) ? mod.declarations : [];
145
+ for (const decl of declarations) {
146
+ if (!decl || typeof decl !== 'object') continue;
147
+ const tagName = typeof decl.tagName === 'string' ? decl.tagName : undefined;
148
+ const isCustomElement = decl.customElement === true || decl.kind === 'custom-element';
149
+ if (!isCustomElement || !tagName) continue;
150
+
151
+ decls.push({ decl, tagName: tagName.toLowerCase(), modulePath });
152
+ }
153
+ }
154
+
155
+ return decls;
156
+ }
157
+
158
+ export function buildIndexes(manifest) {
159
+ const decls = findCustomElementDeclarations(manifest);
160
+
161
+ const byTag = new Map();
162
+ const byClass = new Map();
163
+ const modulePathByTag = new Map();
164
+
165
+ for (const { decl, tagName, modulePath } of decls) {
166
+ if (!byTag.has(tagName)) byTag.set(tagName, decl);
167
+ if (typeof decl?.name === 'string' && !byClass.has(decl.name)) byClass.set(decl.name, decl);
168
+ if (!modulePathByTag.has(tagName)) modulePathByTag.set(tagName, modulePath);
169
+ }
170
+
171
+ return { byTag, byClass, modulePathByTag, decls };
172
+ }
173
+
174
+ /**
175
+ * Extracts the primary component prefix from CEM indexes.
176
+ */
177
+ export function extractPrefixFromIndexes(indexes) {
178
+ const counts = new Map();
179
+ for (const { tagName } of indexes.decls) {
180
+ const i = tagName.indexOf('-');
181
+ if (i > 0) {
182
+ const p = tagName.slice(0, i);
183
+ counts.set(p, (counts.get(p) ?? 0) + 1);
184
+ }
185
+ }
186
+ let best = CANONICAL_PREFIX;
187
+ let bestCount = 0;
188
+ for (const [p, c] of counts) {
189
+ if (c > bestCount) { best = p; bestCount = c; }
190
+ }
191
+ return best;
192
+ }
193
+
194
+ /**
195
+ * Build a full HTML page from a fragment.
196
+ */
197
+ export function buildFullPageHtml({ html, prefix, cemIndex }) {
198
+ const tagRe = /<([a-z][a-z0-9]*-[a-z0-9-]*)\b/gi;
199
+ const tags = new Set();
200
+ let m;
201
+ while ((m = tagRe.exec(html))) {
202
+ tags.add(String(m[1]).toLowerCase());
203
+ }
204
+
205
+ const importEntries = {};
206
+ for (const tag of tags) {
207
+ if (cemIndex.has(tag)) {
208
+ const suffix = tag.replace(/^[^-]+-/, '');
209
+ importEntries[tag] = `./<dir>/components/${suffix}.js`;
210
+ }
211
+ }
212
+
213
+ const importMapJson = JSON.stringify({ imports: importEntries }, null, 2);
214
+
215
+ const lines = [
216
+ '<!DOCTYPE html>',
217
+ `<html lang="ja">`,
218
+ '<head>',
219
+ ' <meta charset="utf-8">',
220
+ ' <meta name="viewport" content="width=device-width, initial-scale=1">',
221
+ ` <title>WCF Preview</title>`,
222
+ ` <link rel="stylesheet" href="./<dir>/styles/tokens.css">`,
223
+ ` <script type="importmap">`,
224
+ importMapJson,
225
+ ` </script>`,
226
+ '</head>',
227
+ '<body>',
228
+ html,
229
+ ` <script type="module" src="./<dir>/boot.js"></script>`,
230
+ '</body>',
231
+ '</html>',
232
+ ];
233
+
234
+ return { fullHtml: lines.join('\n'), importEntries };
235
+ }
236
+
237
+ export function pickDecl({ byTag, byClass }, { tagName, className, prefix }) {
238
+ if (typeof tagName === 'string' && tagName.trim() !== '') {
239
+ const canonical = toCanonicalTagName(tagName, prefix);
240
+ if (canonical && byTag.has(canonical)) return byTag.get(canonical);
241
+ }
242
+
243
+ if (typeof className === 'string' && className.trim() !== '' && byClass.has(className.trim())) {
244
+ return byClass.get(className.trim());
245
+ }
246
+
247
+ return undefined;
248
+ }
249
+
250
+ export function serializeApi(decl, modulePath, prefix) {
251
+ const tagName = typeof decl?.tagName === 'string' ? decl.tagName.toLowerCase() : undefined;
252
+ const outTag = tagName ? withPrefix(tagName, prefix) : undefined;
253
+
254
+ const attributes = Array.isArray(decl?.attributes) ? decl.attributes : [];
255
+ const slots = Array.isArray(decl?.slots) ? decl.slots : [];
256
+ const events = Array.isArray(decl?.events) ? decl.events : [];
257
+ const cssParts = Array.isArray(decl?.cssParts) ? decl.cssParts : [];
258
+ const cssProperties = Array.isArray(decl?.cssProperties) ? decl.cssProperties : [];
259
+
260
+ return {
261
+ tagName: outTag,
262
+ className: typeof decl?.name === 'string' ? decl.name : undefined,
263
+ description: typeof decl?.description === 'string' ? decl.description : undefined,
264
+ modulePath,
265
+ custom: decl?.custom,
266
+ attributes: attributes.map((a) => ({
267
+ name: a?.name,
268
+ type: a?.type?.text,
269
+ default: a?.default ?? null,
270
+ description: a?.description,
271
+ inheritedFrom: a?.inheritedFrom,
272
+ deprecated: a?.deprecated,
273
+ })),
274
+ slots: slots.map((s) => ({
275
+ name: s?.name,
276
+ description: s?.description,
277
+ })),
278
+ events: events.map((e) => ({
279
+ name: e?.name,
280
+ type: e?.type?.text,
281
+ description: e?.description,
282
+ inheritedFrom: e?.inheritedFrom,
283
+ deprecated: e?.deprecated,
284
+ })),
285
+ cssParts: cssParts.map((p) => ({
286
+ name: p?.name,
287
+ description: p?.description,
288
+ })),
289
+ cssProperties: cssProperties.map((p) => ({
290
+ name: p?.name,
291
+ default: p?.default,
292
+ description: p?.description,
293
+ })),
294
+ };
295
+ }
296
+
297
+ export function generateSnippet(api, prefix) {
298
+ const customSnippet = api.custom?.usageSnippet;
299
+ if (typeof customSnippet === 'string' && customSnippet.trim()) {
300
+ const p = normalizePrefix(prefix);
301
+ if (p !== CANONICAL_PREFIX) {
302
+ return customSnippet.replace(
303
+ new RegExp(`<\\s*(\\/?)\\s*${CANONICAL_PREFIX}-([a-z0-9-]+)(?=[\\s/>])`, 'gi'),
304
+ (_m, slash, rest) => `<${slash ?? ''}${p}-${String(rest).toLowerCase()}`,
305
+ );
306
+ }
307
+ return customSnippet;
308
+ }
309
+
310
+ const tag = api.tagName ?? withPrefix(String(api.className ?? 'dads-component'), prefix);
311
+ const attrs = Array.isArray(api.attributes) ? api.attributes : [];
312
+ const slots = Array.isArray(api.slots) ? api.slots : [];
313
+
314
+ const attrPriority = [
315
+ 'label',
316
+ 'support-text',
317
+ 'value',
318
+ 'name',
319
+ 'type',
320
+ 'variant',
321
+ 'size',
322
+ 'required',
323
+ 'disabled',
324
+ 'readonly',
325
+ ];
326
+
327
+ const attrByName = new Map(attrs.map((a) => [String(a?.name ?? ''), a]));
328
+ const lines = [];
329
+
330
+ for (const name of attrPriority) {
331
+ const a = attrByName.get(name);
332
+ if (!a) continue;
333
+ const t = String(a.type ?? '').toLowerCase();
334
+ const isBoolean = t.includes('boolean');
335
+ if (isBoolean) {
336
+ lines.push(` ${name}`);
337
+ } else {
338
+ let defaultVal;
339
+ if (typeof a.default === 'string') {
340
+ defaultVal = a.default.replace(/^['"]|['"]$/g, '');
341
+ } else if (SNIPPET_FALLBACK_VALUES[name] !== undefined) {
342
+ defaultVal = SNIPPET_FALLBACK_VALUES[name];
343
+ } else {
344
+ const enumMatch = t.match(/^'([^']+)'/);
345
+ if (enumMatch) {
346
+ defaultVal = enumMatch[1];
347
+ } else {
348
+ const desc = String(a.description ?? '');
349
+ const descEnum = desc.match(/\(([^)]+)\)/);
350
+ if (descEnum) {
351
+ const first = descEnum[1].split(/\s*[||]\s*/)[0]?.trim();
352
+ defaultVal = first || '';
353
+ } else {
354
+ defaultVal = '';
355
+ }
356
+ }
357
+ }
358
+ lines.push(` ${name}="${defaultVal}"`);
359
+ }
360
+ if (lines.length >= 4) break;
361
+ }
362
+
363
+ const open = lines.length > 0 ? `<${tag}\n${lines.join('\n')}\n>` : `<${tag}>`;
364
+ const slotNames = slots
365
+ .map((s) => String(s?.name ?? '').trim())
366
+ .filter((s) => s !== '');
367
+ const slotComment =
368
+ slotNames.length > 0 ? `\n <!-- slots: ${slotNames.join(', ')} -->\n` : '\n';
369
+
370
+ return `${open}${slotComment}</${tag}>`;
371
+ }
372
+
373
+ export function findDeclByComponentId(indexes, componentIdRaw) {
374
+ const componentId = typeof componentIdRaw === 'string' ? componentIdRaw.trim() : '';
375
+ if (!componentId) return undefined;
376
+ for (const { decl, modulePath } of indexes.decls) {
377
+ const installId = decl?.custom?.install?.id;
378
+ const inferredId = decl?.custom?.componentId;
379
+ const id = typeof installId === 'string' ? installId : typeof inferredId === 'string' ? inferredId : undefined;
380
+ if (id === componentId) return { decl, modulePath };
381
+ }
382
+ return undefined;
383
+ }
384
+
385
+ export function loadPatternRegistryShape(raw) {
386
+ if (!raw || typeof raw !== 'object') return { patterns: {} };
387
+ const patterns = raw.patterns && typeof raw.patterns === 'object' ? raw.patterns : {};
388
+ return { patterns };
389
+ }
390
+
391
+ export function resolveComponentClosure({ installRegistry }, componentIds) {
392
+ const components = installRegistry?.components && typeof installRegistry.components === 'object' ? installRegistry.components : {};
393
+ const queue = [...new Set(componentIds.map((c) => String(c ?? '').trim()).filter(Boolean))];
394
+ const out = new Set();
395
+
396
+ while (queue.length > 0) {
397
+ const id = queue.shift();
398
+ if (!id || out.has(id)) continue;
399
+ out.add(id);
400
+
401
+ const meta = components[id];
402
+ const deps = Array.isArray(meta?.deps) ? meta.deps : [];
403
+ for (const d of deps) {
404
+ const dep = String(d ?? '').trim();
405
+ if (dep && !out.has(dep)) queue.push(dep);
406
+ }
407
+ }
408
+
409
+ return [...out];
410
+ }
411
+
412
+ /**
413
+ * Build a frequency map: componentId → count of patterns that require it.
414
+ */
415
+ export function buildPatternFrequencyMap(patterns) {
416
+ const freq = new Map();
417
+ if (!patterns || typeof patterns !== 'object') return freq;
418
+ for (const pat of Object.values(patterns)) {
419
+ const requires = Array.isArray(pat?.requires) ? pat.requires : [];
420
+ for (const id of requires) {
421
+ const key = String(id ?? '').trim();
422
+ if (key) freq.set(key, (freq.get(key) ?? 0) + 1);
423
+ }
424
+ }
425
+ return freq;
426
+ }
427
+
428
+ export function buildComponentSummaries(indexes, { category, query, limit, offset, prefix, patternId, sort, patterns, installRegistry, patternFrequency } = {}) {
429
+ const p = normalizePrefix(prefix);
430
+ const q = typeof query === 'string' ? query.trim().toLowerCase() : '';
431
+ const limitExplicit = Number.isInteger(limit);
432
+ const pageSize = limitExplicit ? Math.max(1, Math.min(limit, 200)) : 20;
433
+ const pageOffset = Number.isInteger(offset) ? Math.max(0, offset) : 0;
434
+
435
+ /**
436
+ * Convert a tag from the current prefix to canonical prefix using string ops.
437
+ */
438
+ const toCanonicalTag = (tag, currentPrefix) => {
439
+ const cp = `${currentPrefix}-`;
440
+ if (tag.startsWith(cp)) {
441
+ return `${CANONICAL_PREFIX}-${tag.slice(cp.length)}`;
442
+ }
443
+ return tag;
444
+ };
445
+
446
+ let items = indexes.decls.map(({ decl, tagName, modulePath }) => ({
447
+ tagName: withPrefix(tagName, p),
448
+ className: typeof decl?.name === 'string' ? decl.name : undefined,
449
+ description: typeof decl?.description === 'string' ? decl.description : undefined,
450
+ category: getCategory(tagName),
451
+ modulePath,
452
+ }));
453
+
454
+ // patternId filter
455
+ if (typeof patternId === 'string' && patternId.trim()) {
456
+ const pats = patterns && typeof patterns === 'object' ? patterns : {};
457
+ const pat = pats[patternId.trim()];
458
+ if (pat && Array.isArray(pat.requires)) {
459
+ const requiredIds = new Set(pat.requires.map((r) => String(r ?? '').trim()).filter(Boolean));
460
+ const tags = installRegistry?.tags && typeof installRegistry.tags === 'object' ? installRegistry.tags : {};
461
+ items = items.filter((item) => {
462
+ const canonicalTag = toCanonicalTag(item.tagName, p);
463
+ const componentId = tags[canonicalTag];
464
+ return componentId && requiredIds.has(componentId);
465
+ });
466
+ } else {
467
+ items = [];
468
+ }
469
+ }
470
+
471
+ if (category) {
472
+ items = items.filter((item) => item.category === category);
473
+ }
474
+
475
+ if (q) {
476
+ items = items.filter((item) => {
477
+ const haystacks = [
478
+ item.tagName,
479
+ item.className,
480
+ item.description,
481
+ item.category,
482
+ item.modulePath,
483
+ ];
484
+ return haystacks.some((value) => String(value ?? '').toLowerCase().includes(q));
485
+ });
486
+ }
487
+
488
+ // frequency sort
489
+ if (sort === 'frequency') {
490
+ const freq = patternFrequency instanceof Map ? patternFrequency : new Map();
491
+ const tags = installRegistry?.tags && typeof installRegistry.tags === 'object' ? installRegistry.tags : {};
492
+ items = items.map((item) => {
493
+ const canonicalTag = toCanonicalTag(item.tagName, p);
494
+ const componentId = tags[canonicalTag] ?? '';
495
+ return { ...item, frequency: freq.get(componentId) ?? 0 };
496
+ });
497
+ items.sort((a, b) => b.frequency - a.frequency);
498
+ }
499
+
500
+ const total = items.length;
501
+ const paged = items.slice(pageOffset, pageOffset + pageSize);
502
+
503
+ const result = {
504
+ total,
505
+ limit: pageSize,
506
+ offset: pageOffset,
507
+ hasMore: pageOffset + paged.length < total,
508
+ items: paged,
509
+ };
510
+
511
+ // DIG-19: Add migration notice when limit is not explicitly provided
512
+ if (!limitExplicit && total > pageSize) {
513
+ result._notice = 'Default pagination changed to 20 items. Set limit:200 for all results.';
514
+ }
515
+
516
+ return result;
517
+ }
518
+
519
+ export function parseIconNamesFromDescription(description) {
520
+ if (typeof description !== 'string' || description.trim() === '') return [];
521
+
522
+ const markerMatch = description.match(/iconPathsのキー[::]\s*([^))\n]+)/u);
523
+ if (!markerMatch) return [];
524
+
525
+ return [...new Set(
526
+ markerMatch[1]
527
+ .split(/[,、]/)
528
+ .map((name) => name.trim())
529
+ .map((name) => name.replace(/[`'"]/g, ''))
530
+ .filter(Boolean),
531
+ )];
532
+ }
533
+
534
+ export function parseIconNamesFromType(typeText) {
535
+ if (typeof typeText !== 'string' || typeText.trim() === '') return [];
536
+ const out = [];
537
+ const regex = /'([^']+)'|"([^"]+)"|`([^`]+)`/g;
538
+ let match;
539
+ while ((match = regex.exec(typeText)) !== null) {
540
+ const value = match[1] ?? match[2] ?? match[3];
541
+ if (typeof value === 'string' && value.trim() !== '') out.push(value.trim());
542
+ }
543
+ return [...new Set(out)];
544
+ }
545
+
546
+ export function extractIconNames(indexes) {
547
+ const decl = indexes.byTag.get('dads-icon');
548
+ if (!decl) return [];
549
+
550
+ const attributes = Array.isArray(decl?.attributes) ? decl.attributes : [];
551
+ const nameAttr = attributes.find((attr) => String(attr?.name ?? '') === 'name');
552
+ if (!nameAttr) return [];
553
+
554
+ const fromDescription = parseIconNamesFromDescription(nameAttr?.description);
555
+ const fromType = parseIconNamesFromType(nameAttr?.type?.text);
556
+
557
+ return [...new Set([...fromDescription, ...fromType])];
558
+ }
559
+
560
+ export function buildIconCatalog(indexes, prefix) {
561
+ const p = normalizePrefix(prefix);
562
+ const tag = withPrefix('dads-icon', p);
563
+ const names = extractIconNames(indexes).sort((left, right) => left.localeCompare(right));
564
+
565
+ return names.map((name) => ({
566
+ name,
567
+ variants: ['default'],
568
+ usageExample: `<${tag} name="${name}" size="20"></${tag}>`,
569
+ }));
570
+ }
571
+
572
+ export function searchIconCatalog(indexes, { query, limit, offset, prefix } = {}) {
573
+ const q = typeof query === 'string' ? query.trim().toLowerCase() : '';
574
+ const pageSize = Number.isInteger(limit) ? Math.max(1, Math.min(limit, 100)) : 20;
575
+ const pageOffset = Number.isInteger(offset) ? Math.max(0, offset) : 0;
576
+
577
+ let icons = buildIconCatalog(indexes, prefix);
578
+ if (q) {
579
+ const searchTerms = [q];
580
+ const aliases = ICON_ALIAS_TABLE.get(q);
581
+ if (aliases) {
582
+ for (const alias of aliases) {
583
+ if (!searchTerms.includes(alias)) searchTerms.push(alias);
584
+ }
585
+ }
586
+ icons = icons.filter((icon) => {
587
+ const name = icon.name.toLowerCase();
588
+ return searchTerms.some((term) => name.includes(term));
589
+ });
590
+ }
591
+
592
+ const total = icons.length;
593
+ const paged = icons.slice(pageOffset, pageOffset + pageSize);
594
+
595
+ return {
596
+ total,
597
+ limit: pageSize,
598
+ offset: pageOffset,
599
+ hasMore: pageOffset + paged.length < total,
600
+ icons: paged,
601
+ };
602
+ }
603
+
604
+ export function buildRelatedComponentMap(installRegistry, patterns) {
605
+ const components = installRegistry?.components && typeof installRegistry.components === 'object' ? installRegistry.components : {};
606
+ const patternList = Object.values(patterns ?? {});
607
+ const related = new Map();
608
+
609
+ const addRelation = (fromId, toId, via) => {
610
+ const from = String(fromId ?? '').trim();
611
+ const to = String(toId ?? '').trim();
612
+ if (!from || !to || from === to) return;
613
+
614
+ if (!related.has(from)) related.set(from, new Map());
615
+ const relMap = related.get(from);
616
+ if (!relMap.has(to)) relMap.set(to, new Set());
617
+ relMap.get(to).add(via);
618
+ };
619
+
620
+ for (const pattern of patternList) {
621
+ const patternIdVal = String(pattern?.id ?? '').trim() || 'pattern';
622
+ const requires = [...new Set((Array.isArray(pattern?.requires) ? pattern.requires : []).map((id) => String(id ?? '').trim()).filter(Boolean))];
623
+
624
+ for (const fromId of requires) {
625
+ for (const toId of requires) {
626
+ addRelation(fromId, toId, patternIdVal);
627
+ }
628
+ }
629
+ }
630
+
631
+ for (const [componentId, meta] of Object.entries(components)) {
632
+ const deps = Array.isArray(meta?.deps) ? meta.deps : [];
633
+ for (const dep of deps) {
634
+ const depId = String(dep ?? '').trim();
635
+ addRelation(componentId, depId, 'dependency');
636
+ addRelation(depId, componentId, 'dependencyOf');
637
+ }
638
+ }
639
+
640
+ return related;
641
+ }
642
+
643
+ export function getRelatedComponentsForTag({ canonicalTagName, installRegistry, relatedMap, prefix, maxResults = 12 }) {
644
+ const tags = installRegistry?.tags && typeof installRegistry.tags === 'object' ? installRegistry.tags : {};
645
+ const components = installRegistry?.components && typeof installRegistry.components === 'object' ? installRegistry.components : {};
646
+ const componentId = typeof canonicalTagName === 'string' ? tags[canonicalTagName] : undefined;
647
+ if (typeof componentId !== 'string' || componentId === '') return [];
648
+
649
+ const relMap = relatedMap?.get(componentId);
650
+ if (!relMap) return [];
651
+
652
+ const out = [];
653
+ for (const [relatedId, via] of relMap.entries()) {
654
+ const relatedMeta = components[relatedId];
655
+ if (!relatedMeta || typeof relatedMeta !== 'object') continue;
656
+
657
+ const canonicalTags = Array.isArray(relatedMeta.tags)
658
+ ? relatedMeta.tags.map((tag) => String(tag ?? '').toLowerCase()).filter(Boolean)
659
+ : [];
660
+
661
+ out.push({
662
+ componentId: relatedId,
663
+ tagNames: canonicalTags.map((tag) => withPrefix(tag, prefix)),
664
+ via: [...via],
665
+ });
666
+ }
667
+
668
+ out.sort((left, right) => String(left.componentId).localeCompare(String(right.componentId)));
669
+ return out.slice(0, Math.max(1, maxResults));
670
+ }
671
+
672
+ export function normalizeWcagLevel(level) {
673
+ const raw = typeof level === 'string' ? level.trim().toUpperCase() : '';
674
+ if (!raw || raw === 'ALL') return 'all';
675
+ return WCAG_LEVELS.has(raw) ? raw : 'all';
676
+ }
677
+
678
+ function getWcagLevelForA11yTopic(topic) {
679
+ const key = String(topic ?? '').trim().toLowerCase();
680
+ return A11Y_CATEGORY_LEVEL_MAP[key] ?? 'A';
681
+ }
682
+
683
+ function toChecklistItemsFromCategories(categories) {
684
+ if (!categories || typeof categories !== 'object') return [];
685
+
686
+ const out = [];
687
+ for (const [topic, checks] of Object.entries(categories)) {
688
+ if (!Array.isArray(checks)) continue;
689
+ const wcagLevel = getWcagLevelForA11yTopic(topic);
690
+ for (const check of checks) {
691
+ const text = String(check ?? '').trim();
692
+ if (!text) continue;
693
+ out.push({
694
+ topic: String(topic),
695
+ wcagLevel,
696
+ check: text,
697
+ });
698
+ }
699
+ }
700
+ return out;
701
+ }
702
+
703
+ function toChecklistItemsFromCallouts(callouts) {
704
+ if (!Array.isArray(callouts)) return [];
705
+
706
+ const out = [];
707
+ for (const callout of callouts) {
708
+ const parts = [
709
+ callout?.title,
710
+ callout?.label,
711
+ callout?.description,
712
+ ...(Array.isArray(callout?.highlights) ? callout.highlights : []),
713
+ ]
714
+ .map((value) => String(value ?? '').trim())
715
+ .filter(Boolean);
716
+
717
+ if (parts.length === 0) continue;
718
+ out.push({
719
+ topic: 'callouts',
720
+ wcagLevel: getWcagLevelForA11yTopic('callouts'),
721
+ check: parts.join(' — '),
722
+ });
723
+ }
724
+ return out;
725
+ }
726
+
727
+ export function extractAccessibilityChecklist(decl, { prefix } = {}) {
728
+ const annotations = decl?.custom?.a11yAnnotations;
729
+ if (!annotations || typeof annotations !== 'object') return undefined;
730
+
731
+ const items = [
732
+ ...toChecklistItemsFromCategories(annotations.categories),
733
+ ...toChecklistItemsFromCallouts(annotations.callouts),
734
+ ];
735
+ if (items.length === 0) return undefined;
736
+
737
+ const unique = new Map();
738
+ for (const item of items) {
739
+ const key = `${item.topic}|${item.wcagLevel}|${item.check}`;
740
+ if (!unique.has(key)) unique.set(key, item);
741
+ }
742
+
743
+ return {
744
+ summary: String(annotations.summary ?? '').trim() || 'Component accessibility checklist',
745
+ version: Number.isInteger(annotations.version) ? annotations.version : 1,
746
+ totalChecks: unique.size,
747
+ items: [...unique.values()],
748
+ componentTagName:
749
+ typeof decl?.tagName === 'string' ? withPrefix(decl.tagName.toLowerCase(), prefix) : undefined,
750
+ };
751
+ }
752
+
753
+ export function buildAccessibilityIndex(indexes, guidelinesIndexData, { prefix } = {}) {
754
+ const out = [];
755
+
756
+ for (const { decl, tagName } of indexes.decls) {
757
+ const checklist = extractAccessibilityChecklist(decl, { prefix });
758
+ if (!checklist) continue;
759
+ const className = typeof decl?.name === 'string' ? decl.name : undefined;
760
+
761
+ for (const item of checklist.items) {
762
+ out.push({
763
+ source: 'component',
764
+ componentTagName: withPrefix(tagName, prefix),
765
+ componentClassName: className,
766
+ topic: item.topic,
767
+ wcagLevel: item.wcagLevel,
768
+ check: item.check,
769
+ });
770
+ }
771
+ }
772
+
773
+ const docs = Array.isArray(guidelinesIndexData?.documents)
774
+ ? guidelinesIndexData.documents.filter((doc) => doc?.topic === 'accessibility')
775
+ : [];
776
+
777
+ for (const doc of docs) {
778
+ const sections = Array.isArray(doc?.sections) ? doc.sections : [];
779
+ for (const section of sections) {
780
+ const heading = String(section?.heading ?? '').trim();
781
+ const snippet = String(section?.snippet ?? '').trim();
782
+ if (!heading && !snippet) continue;
783
+
784
+ out.push({
785
+ source: 'guideline',
786
+ documentId: String(doc?.id ?? ''),
787
+ title: String(doc?.title ?? ''),
788
+ heading,
789
+ topic: 'guideline',
790
+ wcagLevel: getWcagLevelForA11yTopic('guideline'),
791
+ check: snippet || heading,
792
+ });
793
+ }
794
+ }
795
+
796
+ return out;
797
+ }
798
+
799
+ export function queryAccessibilityIndex(
800
+ entries,
801
+ { componentTagName, topic, wcagLevel, maxResults = 20 } = {},
802
+ ) {
803
+ const normalizedTopic = String(topic ?? '').trim().toLowerCase() || 'all';
804
+ const normalizedWcagLevel = normalizeWcagLevel(wcagLevel);
805
+ const pageSize = Number.isInteger(maxResults) ? Math.max(1, Math.min(maxResults, 100)) : 20;
806
+ const source = Array.isArray(entries) ? entries : [];
807
+ const results = [];
808
+ const shouldBalanceSources = !componentTagName && normalizedTopic === 'all';
809
+ const guidelineCandidates = [];
810
+ const componentCandidates = [];
811
+ const otherCandidates = [];
812
+ let totalHits = 0;
813
+
814
+ for (const entry of source) {
815
+ if (componentTagName && entry.componentTagName !== componentTagName) continue;
816
+ if (normalizedTopic !== 'all' && String(entry.topic ?? '').toLowerCase() !== normalizedTopic) continue;
817
+ if (normalizedWcagLevel !== 'all' && String(entry.wcagLevel ?? '').toUpperCase() !== normalizedWcagLevel) continue;
818
+
819
+ totalHits += 1;
820
+ if (!shouldBalanceSources) {
821
+ if (results.length < pageSize) results.push(entry);
822
+ continue;
823
+ }
824
+
825
+ if (String(entry.source ?? '') === 'guideline') {
826
+ if (guidelineCandidates.length < pageSize) guidelineCandidates.push(entry);
827
+ } else if (String(entry.source ?? '') === 'component') {
828
+ if (componentCandidates.length < pageSize) componentCandidates.push(entry);
829
+ } else if (otherCandidates.length < pageSize) {
830
+ otherCandidates.push(entry);
831
+ }
832
+ }
833
+
834
+ if (shouldBalanceSources) {
835
+ while (results.length < pageSize) {
836
+ const beforeLength = results.length;
837
+ if (guidelineCandidates.length > 0) results.push(guidelineCandidates.shift());
838
+ if (results.length < pageSize && componentCandidates.length > 0) results.push(componentCandidates.shift());
839
+ if (results.length < pageSize && otherCandidates.length > 0) results.push(otherCandidates.shift());
840
+ if (results.length === beforeLength) break;
841
+ }
842
+ }
843
+
844
+ return {
845
+ topic: normalizedTopic,
846
+ wcagLevel: normalizedWcagLevel,
847
+ totalHits,
848
+ results,
849
+ };
850
+ }
851
+
852
+ // Helpers used by register.mjs — exported for internal use
853
+ export { INTERACTION_EXAMPLES_MAP, LAYOUT_BEHAVIOR_MAP };
854
+
855
+ export function buildComponentsResourcePayload(indexes) {
856
+ const page = buildComponentSummaries(indexes, { limit: 200 });
857
+ const componentsByCategory = {};
858
+ for (const item of page.items) {
859
+ const cat = String(item?.category ?? 'Other');
860
+ componentsByCategory[cat] = (componentsByCategory[cat] ?? 0) + 1;
861
+ }
862
+ return {
863
+ total: page.total,
864
+ componentsByCategory,
865
+ components: page.items,
866
+ };
867
+ }
868
+
869
+ export function resolveDeclByComponent(indexes, component, prefix) {
870
+ const byTagOrClass =
871
+ pickDecl(indexes, { tagName: component, prefix }) ??
872
+ pickDecl(indexes, { className: component, prefix });
873
+ if (byTagOrClass) {
874
+ const canonicalTag = typeof byTagOrClass.tagName === 'string' ? byTagOrClass.tagName.toLowerCase() : undefined;
875
+ return {
876
+ decl: byTagOrClass,
877
+ modulePath: canonicalTag ? indexes.modulePathByTag.get(canonicalTag) : undefined,
878
+ };
879
+ }
880
+
881
+ const byComponentId = findDeclByComponentId(indexes, component);
882
+ if (byComponentId) return byComponentId;
883
+
884
+ // Auto-prefix: try with canonical prefix if bare name was given (DIG-15)
885
+ const comp = typeof component === 'string' ? component.trim().toLowerCase() : '';
886
+ const p = normalizePrefix(prefix);
887
+ if (comp && !comp.startsWith(p)) {
888
+ const prefixed = `${p}-${comp}`;
889
+ const byPrefixed = pickDecl(indexes, { tagName: prefixed, prefix: p });
890
+ if (byPrefixed) {
891
+ const canonicalTag = typeof byPrefixed.tagName === 'string' ? byPrefixed.tagName.toLowerCase() : undefined;
892
+ return {
893
+ decl: byPrefixed,
894
+ modulePath: canonicalTag ? indexes.modulePathByTag.get(canonicalTag) : undefined,
895
+ };
896
+ }
897
+ }
898
+
899
+ return undefined;
900
+ }
901
+
902
+ export function buildComponentNotFoundError(component, indexes, prefix) {
903
+ const comp = typeof component === 'string' ? component.trim() : '';
904
+ const p = normalizePrefix(prefix);
905
+ const suggestions = [];
906
+
907
+ if (comp && !comp.toLowerCase().startsWith(p)) {
908
+ const prefixed = `${p}-${comp.toLowerCase()}`;
909
+ if (indexes.byTag.has(prefixed)) {
910
+ suggestions.push(prefixed);
911
+ }
912
+ }
913
+
914
+ const suggested = suggestUnknownElementTagName(comp.includes('-') ? comp : `${p}-${comp}`, indexes.byTag);
915
+ if (suggested && !suggestions.includes(suggested)) {
916
+ suggestions.push(suggested);
917
+ }
918
+
919
+ const msg = suggestions.length > 0
920
+ ? `Component not found: ${comp}. Did you mean: ${suggestions.join(', ')}?`
921
+ : `Component not found: ${comp}`;
922
+ return { content: [{ type: 'text', text: msg }], isError: true };
923
+ }