@ncds/ui-admin-mcp 1.0.0-alpha.25 → 1.0.0-alpha.26

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.
@@ -1,33 +1 @@
1
- "use strict";
2
- /**
3
- * search_component tool — name/description/aliases 퍼지 키워드 검색 (순수 함수)
4
- *
5
- * Calculator: searchComponent — 입력(componentMap, query) → 출력(매칭 결과, 점수순 정렬)
6
- */
7
- Object.defineProperty(exports, "__esModule", { value: true });
8
- exports.searchComponent = void 0;
9
- const dataLoader_js_1 = require("../utils/dataLoader.js");
10
- const fuzzyMatch_js_1 = require("../utils/fuzzyMatch.js");
11
- const response_js_1 = require("../utils/response.js");
12
- /** search_component tool — name/description/aliases 퍼지 검색 후 점수순 반환 */
13
- const searchComponent = (componentMap, query) => {
14
- if (!query.trim())
15
- return (0, response_js_1.successResponse)([]);
16
- const scored = (0, dataLoader_js_1.getAllComponents)(componentMap)
17
- .map((c) => {
18
- // name, description, aliases 모두에 대해 퍼지 매칭 → 최고 점수 채택
19
- const targets = [c.name, c.description, ...c.aliases];
20
- const match = (0, fuzzyMatch_js_1.bestFuzzyMatch)(query, targets);
21
- return { component: c, score: match?.score ?? 0 };
22
- })
23
- .filter((item) => item.score > 0)
24
- .sort((a, b) => b.score - a.score);
25
- const results = scored.map(({ component: { name, category, description, aliases } }) => ({
26
- name,
27
- category,
28
- description,
29
- aliases,
30
- }));
31
- return (0, response_js_1.successResponse)(results);
32
- };
33
- exports.searchComponent = searchComponent;
1
+ "use strict";Object.defineProperty(exports,"__esModule",{value:!0}),exports.searchComponent=void 0;const e=require("../utils/dataLoader.js"),s=require("../utils/fuzzyMatch.js"),r=require("../utils/response.js"),o=(o,t)=>{if(!t.trim())return(0,r.successResponse)([]);const n=(0,e.getAllComponents)(o).map(e=>{const r=[e.name,e.description,...e.aliases],o=(0,s.bestFuzzyMatch)(t,r);return{component:e,score:o?.score??0}}).filter(e=>e.score>0).sort((e,s)=>s.score-e.score).map(({component:{name:e,category:s,description:r,aliases:o}})=>({name:e,category:s,description:r,aliases:o}));return(0,r.successResponse)(n)};exports.searchComponent=o;
@@ -1,19 +1 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.searchIcon = void 0;
4
- const response_js_1 = require("../utils/response.js");
5
- /** search_icon tool — 키워드로 아이콘 이름/kebab을 검색 */
6
- const searchIcon = (iconData, query) => {
7
- const lower = query.trim().toLowerCase();
8
- if (!lower)
9
- return (0, response_js_1.successResponse)([]);
10
- const results = iconData.icons
11
- .filter((icon) => icon.name.toLowerCase().includes(lower) || icon.kebab.includes(lower))
12
- .map((icon) => ({
13
- name: icon.name,
14
- kebab: icon.kebab,
15
- fill: icon.fill,
16
- }));
17
- return (0, response_js_1.successResponse)(results);
18
- };
19
- exports.searchIcon = searchIcon;
1
+ "use strict";Object.defineProperty(exports,"__esModule",{value:!0}),exports.searchIcon=void 0;const e=require("../utils/response.js"),s=(s,o)=>{const r=o.trim().toLowerCase();if(!r)return(0,e.successResponse)([]);const n=s.icons.filter(e=>e.name.toLowerCase().includes(r)||e.kebab.includes(r)).map(e=>({name:e.name,kebab:e.kebab,fill:e.fill}));return(0,e.successResponse)(n)};exports.searchIcon=s;
@@ -1,153 +1 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.buildTokenValueMap = exports.buildRootClassMap = exports.validateHtml = void 0;
4
- /**
5
- * validate_html tool — NCUA HTML BEM 검증 + 디자인 시스템 준수도 검증 진입점
6
- *
7
- * BEM 검증: utils/bemValidator.ts
8
- * Compliance 검증: utils/compliance.ts
9
- * 이 파일은 오케스트레이션만 담당한다.
10
- */
11
- const node_html_parser_1 = require("node-html-parser");
12
- const bemValidator_js_1 = require("../utils/bemValidator.js");
13
- const compliance_js_1 = require("../utils/compliance.js");
14
- const response_js_1 = require("../utils/response.js");
15
- /** validate_html tool — HTML의 NCUA BEM 클래스 정합성 + 디자인 시스템 준수도 검증 */
16
- const validateHtml = (params) => {
17
- const { componentMap, rootClassMap, html, cdnMeta, tokenData, complianceRules, tokenValueMap } = params;
18
- const trimmed = html.trim();
19
- if (trimmed.length === 0)
20
- return (0, response_js_1.successResponse)({ valid: true });
21
- const root = (0, node_html_parser_1.parse)(trimmed);
22
- const elements = root.querySelectorAll('[class]');
23
- const { errors, warnings, invalidClassesSet, hasNcuaClasses } = (0, bemValidator_js_1.collectBemErrors)({
24
- elements,
25
- rootClassMap,
26
- componentMap,
27
- });
28
- if (!hasNcuaClasses)
29
- return (0, response_js_1.successResponse)({ valid: true });
30
- if (cdnMeta)
31
- errors.push(...(0, bemValidator_js_1.checkCdnInclusion)(trimmed, cdnMeta));
32
- // P13: Vertical Table context 검증 + 페이지 prefix required 마크 검출
33
- errors.push(...collectVerticalTableContextErrors(root));
34
- warnings.push(...collectCustomRequiredMarkWarnings(root));
35
- const compliance = buildCompliance({
36
- root,
37
- componentMap,
38
- rootClassMap,
39
- warnings,
40
- tokenData,
41
- complianceRules,
42
- tokenValueMap,
43
- });
44
- return buildResponse({ errors, warnings, invalidClassesSet, root, compliance });
45
- };
46
- exports.validateHtml = validateHtml;
47
- /** compliance 검증 오케스트레이션 — tokenData 또는 complianceRules가 있을 때만 실행 */
48
- const buildCompliance = (params) => {
49
- const { root, componentMap, rootClassMap, warnings, tokenData, complianceRules, tokenValueMap } = params;
50
- if (!complianceRules && !tokenData)
51
- return undefined;
52
- const ncuaResult = complianceRules
53
- ? (0, compliance_js_1.detectNcuaOmission)({ root, complianceRules, componentMap })
54
- : { errors: [], ncuaUsage: { used: 0, available: 0 } };
55
- const tokenResult = tokenData && tokenValueMap
56
- ? (0, compliance_js_1.detectTokenIssues)({ root, tokenData, tokenValueMap })
57
- : { errors: [], tokenUsage: { correct: 0, missing: 0, invalid: 0 } };
58
- const customResult = (0, compliance_js_1.detectCustomNotSeparated)({ root, rootClassMap, warnings });
59
- return (0, compliance_js_1.buildComplianceSummary)({
60
- ncuaErrors: ncuaResult.errors,
61
- tokenErrors: tokenResult.errors,
62
- customErrors: customResult.errors,
63
- ncuaUsage: ncuaResult.ncuaUsage,
64
- tokenUsage: tokenResult.tokenUsage,
65
- customSeparation: customResult.customSeparation,
66
- });
67
- };
68
- /** P13: Vertical Table context 위반 검출.
69
- * 1) ncua-table--vertical 안 ncua-table__header-cell → horizontal 전용 오용.
70
- * 2) ncua-table 안 <tr> 에 ncua-table__row 클래스 누락. */
71
- const collectVerticalTableContextErrors = (root) => {
72
- const errors = [];
73
- // (1) vertical 안의 header-cell 검출
74
- const verticalRoots = root.querySelectorAll('.ncua-table--vertical');
75
- for (const v of verticalRoots) {
76
- const headerCells = v.querySelectorAll('.ncua-table__header-cell');
77
- for (const _ of headerCells) {
78
- errors.push({
79
- type: 'vertical_uses_horizontal_header_cell',
80
- class: 'ncua-table__header-cell',
81
- component: 'table',
82
- message: "Vertical Table 안에서 'ncua-table__header-cell' 사용은 horizontal 전용 — vertical 라벨에는 <th scope='row' class='ncua-table__cell'> 사용 (Table.Cell with isHeader=true).",
83
- suggestion: "render_to_html('table', { type: 'vertical', children: [...] }) 호출하여 정확한 BEM 출력을 받거나, <th class='ncua-table__header-cell'> 을 <th scope='row' class='ncua-table__cell'> 로 교체.",
84
- });
85
- }
86
- }
87
- // (2) ncua-table 안 <tr> 의 ncua-table__row 클래스 누락
88
- const allTables = root.querySelectorAll('.ncua-table');
89
- for (const t of allTables) {
90
- const trs = t.querySelectorAll('tr');
91
- for (const tr of trs) {
92
- const classAttr = tr.getAttribute('class') ?? '';
93
- if (!classAttr.split(/\s+/).includes('ncua-table__row')) {
94
- errors.push({
95
- type: 'missing_table_row_class',
96
- class: 'ncua-table__row',
97
- component: 'table',
98
- message: "ncua-table 안의 <tr> 에 'ncua-table__row' 클래스 누락. NCDS Table 의 모든 row 는 이 클래스가 필요.",
99
- suggestion: "render_to_html('table', ...) 출력을 그대로 사용. 수동 작성 시 모든 <tr> 에 class='ncua-table__row' 추가.",
100
- });
101
- // 같은 row 중복 보고 방지 — 첫 누락만
102
- break;
103
- }
104
- }
105
- }
106
- return errors;
107
- };
108
- /** P13: 페이지 prefix 의 *-required 클래스로 필수 마크 자체 작성 검출.
109
- * 텍스트가 정확히 `*` 한 글자이면 ncua-table__required 사용 권장. */
110
- const collectCustomRequiredMarkWarnings = (root) => {
111
- const warnings = [];
112
- const allWithClass = root.querySelectorAll('[class]');
113
- for (const el of allWithClass) {
114
- const classAttr = el.getAttribute('class') ?? '';
115
- const classes = classAttr.split(/\s+/).filter(Boolean);
116
- // ncua- 가 아닌 prefix 의 *-required 또는 __required-mark 같은 형태
117
- const matched = classes.find((c) => !c.startsWith('ncua-') && /(-required(?:-mark)?$|__required(?:-mark)?$)/.test(c));
118
- if (!matched)
119
- continue;
120
- const text = (el.text ?? '').trim();
121
- if (text !== '*')
122
- continue;
123
- warnings.push({
124
- type: 'custom_required_mark',
125
- class: matched,
126
- component: 'table',
127
- message: `페이지 prefix 클래스 '${matched}' 로 '*' 필수 마크를 자체 작성. NCDS 가 공식 element 'ncua-table__required' 를 제공.`,
128
- suggestion: `<span class='${matched}'>*</span> 를 <span class='ncua-table__required'>*</span> 로 교체 (Vertical Table 라벨 셀 안에 prepend).`,
129
- });
130
- }
131
- return warnings;
132
- };
133
- /** 최종 응답 빌드 */
134
- const buildResponse = (params) => {
135
- const { errors, warnings, invalidClassesSet, root, compliance } = params;
136
- if (errors.length === 0 && warnings.length === 0 && !compliance)
137
- return (0, response_js_1.successResponse)({ valid: true });
138
- if (errors.length === 0 && warnings.length === 0 && compliance)
139
- return (0, response_js_1.successResponse)({ valid: true, compliance });
140
- if (errors.length === 0)
141
- return (0, response_js_1.successResponse)({ valid: true, warnings, ...(compliance && { compliance }) });
142
- return (0, response_js_1.successResponse)({
143
- valid: false,
144
- errors,
145
- ...(warnings.length > 0 && { warnings }),
146
- fixed_html: (0, bemValidator_js_1.buildFixedHtml)(root, invalidClassesSet),
147
- ...(compliance && { compliance }),
148
- });
149
- };
150
- var bemValidator_js_2 = require("../utils/bemValidator.js");
151
- Object.defineProperty(exports, "buildRootClassMap", { enumerable: true, get: function () { return bemValidator_js_2.buildRootClassMap; } });
152
- var tokenValidator_js_1 = require("../utils/tokenValidator.js");
153
- Object.defineProperty(exports, "buildTokenValueMap", { enumerable: true, get: function () { return tokenValidator_js_1.buildTokenValueMap; } });
1
+ "use strict";Object.defineProperty(exports,"__esModule",{value:!0}),exports.buildTokenValueMap=exports.buildRootClassMap=exports.validateHtml=void 0;const e=require("node-html-parser"),s=require("../utils/bemValidator.js"),t=require("../utils/compliance.js"),a=require("../utils/response.js"),r=t=>{const{componentMap:r,rootClassMap:i,html:u,cdnMeta:p,tokenData:d,complianceRules:m,tokenValueMap:b}=t,_=u.trim();if(0===_.length)return(0,a.successResponse)({valid:!0});const g=(0,e.parse)(_),h=g.querySelectorAll("[class]"),{errors:f,warnings:M,invalidClassesSet:k,hasNcuaClasses:v}=(0,s.collectBemErrors)({elements:h,rootClassMap:i,componentMap:r});if(!v)return(0,a.successResponse)({valid:!0});p&&f.push(...(0,s.checkCdnInclusion)(_,p)),f.push(...l(g)),M.push(...n(g));const C=o({root:g,componentMap:r,rootClassMap:i,warnings:M,tokenData:d,complianceRules:m,tokenValueMap:b});return c({errors:f,warnings:M,invalidClassesSet:k,root:g,compliance:C})};exports.validateHtml=r;const o=e=>{const{root:s,componentMap:a,rootClassMap:r,warnings:o,tokenData:l,complianceRules:n,tokenValueMap:c}=e;if(!n&&!l)return;const i=n?(0,t.detectNcuaOmission)({root:s,complianceRules:n,componentMap:a}):{errors:[],ncuaUsage:{used:0,available:0}},u=l&&c?(0,t.detectTokenIssues)({root:s,tokenData:l,tokenValueMap:c}):{errors:[],tokenUsage:{correct:0,missing:0,invalid:0}},p=(0,t.detectCustomNotSeparated)({root:s,rootClassMap:r,warnings:o});return(0,t.buildComplianceSummary)({ncuaErrors:i.errors,tokenErrors:u.errors,customErrors:p.errors,ncuaUsage:i.ncuaUsage,tokenUsage:u.tokenUsage,customSeparation:p.customSeparation})},l=e=>{const s=[],t=e.querySelectorAll(".ncua-table--vertical");for(const e of t){const t=e.querySelectorAll(".ncua-table__header-cell");for(const e of t)s.push({type:"vertical_uses_horizontal_header_cell",class:"ncua-table__header-cell",component:"table",message:"Vertical Table 안에서 'ncua-table__header-cell' 사용은 horizontal 전용 — vertical 라벨에는 <th scope='row' class='ncua-table__cell'> 사용 (Table.Cell with isHeader=true).",suggestion:"render_to_html('table', { type: 'vertical', children: [...] }) 호출하여 정확한 BEM 출력을 받거나, <th class='ncua-table__header-cell'> 을 <th scope='row' class='ncua-table__cell'> 로 교체."})}const a=e.querySelectorAll(".ncua-table");for(const e of a){const t=e.querySelectorAll("tr");for(const e of t)if(!(e.getAttribute("class")??"").split(/\s+/).includes("ncua-table__row")){s.push({type:"missing_table_row_class",class:"ncua-table__row",component:"table",message:"ncua-table 안의 <tr> 에 'ncua-table__row' 클래스 누락. NCDS Table 의 모든 row 는 이 클래스가 필요.",suggestion:"render_to_html('table', ...) 출력을 그대로 사용. 수동 작성 시 모든 <tr> 에 class='ncua-table__row' 추가."});break}}return s},n=e=>{const s=[],t=e.querySelectorAll("[class]");for(const e of t){const t=(e.getAttribute("class")??"").split(/\s+/).filter(Boolean).find(e=>!e.startsWith("ncua-")&&/(-required(?:-mark)?$|__required(?:-mark)?$)/.test(e));t&&"*"===(e.text??"").trim()&&s.push({type:"custom_required_mark",class:t,component:"table",message:`페이지 prefix 클래스 '${t}' 로 '*' 필수 마크를 자체 작성. NCDS 가 공식 element 'ncua-table__required' 를 제공.`,suggestion:`<span class='${t}'>*</span> 를 <span class='ncua-table__required'>*</span> 로 교체 (Vertical Table 라벨 셀 안에 prepend).`})}return s},c=e=>{const{errors:t,warnings:r,invalidClassesSet:o,root:l,compliance:n}=e;return 0!==t.length||0!==r.length||n?0===t.length&&0===r.length&&n?(0,a.successResponse)({valid:!0,compliance:n}):0===t.length?(0,a.successResponse)({valid:!0,warnings:r,...n&&{compliance:n}}):(0,a.successResponse)({valid:!1,errors:t,...r.length>0&&{warnings:r},fixed_html:(0,s.buildFixedHtml)(l,o),...n&&{compliance:n}}):(0,a.successResponse)({valid:!0})};var i=require("../utils/bemValidator.js");Object.defineProperty(exports,"buildRootClassMap",{enumerable:!0,get:function(){return i.buildRootClassMap}});var u=require("../utils/tokenValidator.js");Object.defineProperty(exports,"buildTokenValueMap",{enumerable:!0,get:function(){return u.buildTokenValueMap}});
package/bin/types.js CHANGED
@@ -1,5 +1 @@
1
- "use strict";
2
- /**
3
- * MCP 서버 공통 타입 정의
4
- */
5
- Object.defineProperty(exports, "__esModule", { value: true });
1
+ "use strict";Object.defineProperty(exports,"__esModule",{value:!0});
@@ -1,210 +1 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.getClassList = exports.collectBemErrors = exports.checkCdnInclusion = exports.buildRootClassMap = exports.buildFixedHtml = void 0;
4
- // ── 상수 ─────────────────────────────────────────────────────────────────
5
- const MAX_SIMILAR_SUGGESTIONS = 3;
6
- // ── 순수 헬퍼 ────────────────────────────────────────────────────────────
7
- /** 엘리먼트에서 class 목록을 파싱 */
8
- const getClassList = (el) => el.getAttribute('class')?.split(/\s+/) ?? [];
9
- exports.getClassList = getClassList;
10
- /** root class → component name 매핑 빌드 — server.ts에서 1회 호출 */
11
- const buildRootClassMap = (componentMap) => {
12
- const rootClassMap = new Map();
13
- for (const [name, data] of componentMap) {
14
- for (const cls of data.bemClasses) {
15
- if (!cls.includes('--') && !cls.includes('__'))
16
- rootClassMap.set(cls, name);
17
- }
18
- }
19
- return rootClassMap;
20
- };
21
- exports.buildRootClassMap = buildRootClassMap;
22
- /** ncua- 클래스에서 root class를 결정 (modifier/element 제외, 가장 짧은 것) */
23
- const findRootClass = (ncuaClasses) => {
24
- const roots = ncuaClasses.filter((c) => !c.includes('--') && !c.includes('__'));
25
- if (roots.length === 0)
26
- return undefined;
27
- return roots.sort((a, b) => a.length - b.length)[0];
28
- };
29
- /** BEM 구분자(-- 또는 __)의 첫 위치를 반환. 없으면 -1 */
30
- const findFirstSeparatorIndex = (dashIdx, underIdx) => {
31
- if (dashIdx >= 0 && underIdx >= 0)
32
- return Math.min(dashIdx, underIdx);
33
- if (dashIdx >= 0)
34
- return dashIdx;
35
- return underIdx;
36
- };
37
- /** modifier/element 클래스에서 root class를 추론 */
38
- const inferRootClass = (ncuaClasses, rootClassMap) => {
39
- for (const cls of ncuaClasses) {
40
- const sepIdx = findFirstSeparatorIndex(cls.indexOf('--'), cls.indexOf('__'));
41
- if (sepIdx > 0) {
42
- const candidate = cls.substring(0, sepIdx);
43
- if (rootClassMap.has(candidate))
44
- return candidate;
45
- }
46
- }
47
- return undefined;
48
- };
49
- /** 문자열 유사도 — 공통 접두사 길이 기반 */
50
- const prefixSimilarity = (a, b) => {
51
- let i = 0;
52
- while (i < a.length && i < b.length && a[i] === b[i])
53
- i++;
54
- return i;
55
- };
56
- /** 유사도 상위 N개 제안 — modifier 오류면 modifier만, element 오류면 element만 */
57
- const suggestSimilar = (invalidClass, validClasses, max = MAX_SIMILAR_SUGGESTIONS) => {
58
- const isElement = invalidClass.includes('__');
59
- return validClasses
60
- .filter((c) => (isElement ? c.includes('__') : c.includes('--')))
61
- .map((c) => ({ class: c, score: prefixSimilarity(invalidClass, c) }))
62
- .sort((a, b) => b.score - a.score)
63
- .slice(0, max)
64
- .map((item) => item.class);
65
- };
66
- /** unknown component 에러 수집 */
67
- const reportUnknownClasses = (ncuaClasses, errors, invalidClassesSet) => {
68
- for (const cls of ncuaClasses) {
69
- errors.push({
70
- type: 'unknown_component',
71
- class: cls,
72
- component: 'unknown',
73
- message: `Unknown component class '${cls}'. No matching component found.`,
74
- suggestion: 'Use list_components to see all available components.',
75
- });
76
- invalidClassesSet.add(cls);
77
- }
78
- };
79
- /** BEM 클래스 유효성 검증 */
80
- const validateBemClasses = (ncuaClasses, validSet, componentName, bemClasses, errors, invalidClassesSet) => {
81
- for (const cls of ncuaClasses) {
82
- if (validSet.has(cls))
83
- continue;
84
- const similar = suggestSimilar(cls, bemClasses);
85
- errors.push({
86
- type: cls.includes('__') ? 'invalid_element' : 'invalid_modifier',
87
- class: cls,
88
- component: componentName,
89
- message: `Invalid BEM class '${cls}' for component '${componentName}'.`,
90
- suggestion: similar.length > 0 ? `Similar: ${similar.join(', ')}` : 'Use get_component_props to see valid BEM classes.',
91
- });
92
- invalidClassesSet.add(cls);
93
- }
94
- };
95
- // ── BEM 검증 엔트리 함수 ────────────────────────────────────────────────
96
- /** 엘리먼트별 BEM 클래스 검증 → errors + warnings + invalidClasses 수집 */
97
- const collectBemErrors = (params) => {
98
- const { elements, rootClassMap, componentMap } = params;
99
- const errors = [];
100
- const warnings = [];
101
- const invalidClassesSet = new Set();
102
- const validSetCache = new Map();
103
- let foundNcua = false;
104
- for (const el of elements) {
105
- const classes = getClassList(el);
106
- const ncuaClasses = classes.filter((c) => c.startsWith('ncua-'));
107
- if (ncuaClasses.length === 0)
108
- continue;
109
- foundNcua = true;
110
- let rootClass = findRootClass(ncuaClasses);
111
- if (!rootClass || !rootClassMap.has(rootClass)) {
112
- const inferred = inferRootClass(ncuaClasses, rootClassMap);
113
- if (inferred)
114
- rootClass = inferred;
115
- }
116
- collectMixedNamespaceWarning(classes, ncuaClasses, rootClass, rootClassMap, warnings);
117
- collectMissingRootClassError(ncuaClasses, rootClass, rootClassMap, errors);
118
- if (!rootClass || !rootClassMap.has(rootClass)) {
119
- reportUnknownClasses(ncuaClasses, errors, invalidClassesSet);
120
- continue;
121
- }
122
- // biome-ignore lint/style/noNonNullAssertion: 직전 가드(rootClassMap.has)로 존재 보장
123
- const componentName = rootClassMap.get(rootClass);
124
- const component = componentMap.get(componentName);
125
- if (!component) {
126
- // rootClassMap 에는 있는데 componentMap 에 없는 비정상 상태 (호출자가 두 Map 을 분리 빌드한 경우).
127
- // graceful skip — rootClass 가 알 수 없는 컴포넌트인 경로와 동일 처리.
128
- reportUnknownClasses(ncuaClasses, errors, invalidClassesSet);
129
- continue;
130
- }
131
- if (!validSetCache.has(componentName))
132
- validSetCache.set(componentName, new Set(component.bemClasses));
133
- validateBemClasses(ncuaClasses,
134
- // biome-ignore lint/style/noNonNullAssertion: 직전 has/set 보장
135
- validSetCache.get(componentName), componentName, component.bemClasses, errors, invalidClassesSet);
136
- }
137
- return { errors, warnings, invalidClassesSet, hasNcuaClasses: foundNcua };
138
- };
139
- exports.collectBemErrors = collectBemErrors;
140
- /** mixed_namespace 경고 수집 */
141
- const collectMixedNamespaceWarning = (classes, ncuaClasses, rootClass, rootClassMap, warnings) => {
142
- const customClasses = classes.filter((c) => !c.startsWith('ncua-'));
143
- if (customClasses.length === 0)
144
- return;
145
- // biome-ignore lint/style/noNonNullAssertion: 삼항 가드로 rootClassMap.has(rootClass) 보장
146
- const compName = rootClass && rootClassMap.has(rootClass) ? rootClassMap.get(rootClass) : 'unknown';
147
- warnings.push({
148
- type: 'mixed_namespace',
149
- class: ncuaClasses[0],
150
- component: compName,
151
- message: `Element mixes NCUA classes (${ncuaClasses.join(', ')}) with custom classes (${customClasses.join(', ')}).`,
152
- suggestion: 'Keep NCUA components isolated. Use a separate wrapper for custom styling.',
153
- });
154
- };
155
- /** missing_root_class 에러 수집 */
156
- const collectMissingRootClassError = (ncuaClasses, rootClass, rootClassMap, errors) => {
157
- const hasModifiers = ncuaClasses.some((c) => c.includes('--'));
158
- const hasRootInElement = ncuaClasses.some((c) => !c.includes('--') && !c.includes('__'));
159
- const isModifierWithoutRoot = hasModifiers && !hasRootInElement && rootClass != null && rootClassMap.has(rootClass);
160
- if (!isModifierWithoutRoot)
161
- return;
162
- errors.push({
163
- type: 'missing_root_class',
164
- // biome-ignore lint/style/noNonNullAssertion: isModifierWithoutRoot 가드에 rootClass != null 포함
165
- class: rootClass,
166
- // biome-ignore lint/style/noNonNullAssertion: 위와 동일 가드 (rootClass != null + has)
167
- component: rootClassMap.get(rootClass),
168
- message: `Root class '${rootClass}' is missing. Modifiers/elements require their root class.`,
169
- suggestion: `Add '${rootClass}' class to the same element.`,
170
- });
171
- };
172
- /** 잘못된 클래스를 제거하여 fixed_html 생성 */
173
- const buildFixedHtml = (root, invalidClassesSet) => {
174
- for (const el of root.querySelectorAll('[class]')) {
175
- const classes = getClassList(el);
176
- const filtered = classes.filter((c) => !invalidClassesSet.has(c));
177
- if (filtered.length > 0) {
178
- el.setAttribute('class', filtered.join(' '));
179
- }
180
- else {
181
- el.removeAttribute('class');
182
- }
183
- }
184
- return root.toString();
185
- };
186
- exports.buildFixedHtml = buildFixedHtml;
187
- /** CDN CSS/JS 링크 포함 여부 검증 */
188
- const checkCdnInclusion = (html, cdnMeta) => {
189
- const errors = [];
190
- if (!html.includes(cdnMeta.css)) {
191
- errors.push({
192
- type: 'missing_cdn',
193
- class: '(page-level)',
194
- component: '(page-level)',
195
- message: 'HTML contains NCUA components but is missing the required CDN CSS link.',
196
- suggestion: `Add <link href="${cdnMeta.css}" rel="stylesheet">`,
197
- });
198
- }
199
- if (!html.includes(cdnMeta.js)) {
200
- errors.push({
201
- type: 'missing_cdn',
202
- class: '(page-level)',
203
- component: '(page-level)',
204
- message: 'HTML contains NCUA components but is missing the required CDN JS link.',
205
- suggestion: `Add <script src="${cdnMeta.js}"></script>`,
206
- });
207
- }
208
- return errors;
209
- };
210
- exports.checkCdnInclusion = checkCdnInclusion;
1
+ "use strict";Object.defineProperty(exports,"__esModule",{value:!0}),exports.getClassList=exports.collectBemErrors=exports.checkCdnInclusion=exports.buildRootClassMap=exports.buildFixedHtml=void 0;const s=3,e=s=>s.getAttribute("class")?.split(/\s+/)??[];exports.getClassList=e;const t=s=>{const e=new Map;for(const[t,n]of s)for(const s of n.bemClasses)s.includes("--")||s.includes("__")||e.set(s,t);return e};exports.buildRootClassMap=t;const n=s=>{const e=s.filter(s=>!s.includes("--")&&!s.includes("__"));if(0!==e.length)return e.sort((s,e)=>s.length-e.length)[0]},o=(s,e)=>s>=0&&e>=0?Math.min(s,e):s>=0?s:e,c=(s,e)=>{for(const t of s){const s=o(t.indexOf("--"),t.indexOf("__"));if(s>0){const n=t.substring(0,s);if(e.has(n))return n}}},i=(s,e)=>{let t=0;for(;t<s.length&&t<e.length&&s[t]===e[t];)t++;return t},l=(s,e,t=3)=>{const n=s.includes("__");return e.filter(s=>n?s.includes("__"):s.includes("--")).map(e=>({class:e,score:i(s,e)})).sort((s,e)=>e.score-s.score).slice(0,t).map(s=>s.class)},r=(s,e,t)=>{for(const n of s)e.push({type:"unknown_component",class:n,component:"unknown",message:`Unknown component class '${n}'. No matching component found.`,suggestion:"Use list_components to see all available components."}),t.add(n)},a=(s,e,t,n,o,c)=>{for(const i of s){if(e.has(i))continue;const s=l(i,n);o.push({type:i.includes("__")?"invalid_element":"invalid_modifier",class:i,component:t,message:`Invalid BEM class '${i}' for component '${t}'.`,suggestion:s.length>0?`Similar: ${s.join(", ")}`:"Use get_component_props to see valid BEM classes."}),c.add(i)}},u=s=>{const{elements:t,rootClassMap:o,componentMap:i}=s,l=[],u=[],g=new Set,d=new Map;let f=!1;for(const s of t){const t=e(s),h=t.filter(s=>s.startsWith("ncua-"));if(0===h.length)continue;f=!0;let _=n(h);if(!_||!o.has(_)){const s=c(h,o);s&&(_=s)}if(p(t,h,_,o,u),m(h,_,o,l),!_||!o.has(_)){r(h,l,g);continue}const C=o.get(_),x=i.get(C);x?(d.has(C)||d.set(C,new Set(x.bemClasses)),a(h,d.get(C),C,x.bemClasses,l,g)):r(h,l,g)}return{errors:l,warnings:u,invalidClassesSet:g,hasNcuaClasses:f}};exports.collectBemErrors=u;const p=(s,e,t,n,o)=>{const c=s.filter(s=>!s.startsWith("ncua-"));if(0===c.length)return;const i=t&&n.has(t)?n.get(t):"unknown";o.push({type:"mixed_namespace",class:e[0],component:i,message:`Element mixes NCUA classes (${e.join(", ")}) with custom classes (${c.join(", ")}).`,suggestion:"Keep NCUA components isolated. Use a separate wrapper for custom styling."})},m=(s,e,t,n)=>{const o=s.some(s=>s.includes("--")),c=s.some(s=>!s.includes("--")&&!s.includes("__"));o&&!c&&null!=e&&t.has(e)&&n.push({type:"missing_root_class",class:e,component:t.get(e),message:`Root class '${e}' is missing. Modifiers/elements require their root class.`,suggestion:`Add '${e}' class to the same element.`})},g=(s,t)=>{for(const n of s.querySelectorAll("[class]")){const s=e(n).filter(s=>!t.has(s));s.length>0?n.setAttribute("class",s.join(" ")):n.removeAttribute("class")}return s.toString()};exports.buildFixedHtml=g;const d=(s,e)=>{const t=[];return s.includes(e.css)||t.push({type:"missing_cdn",class:"(page-level)",component:"(page-level)",message:"HTML contains NCUA components but is missing the required CDN CSS link.",suggestion:`Add <link href="${e.css}" rel="stylesheet">`}),s.includes(e.js)||t.push({type:"missing_cdn",class:"(page-level)",component:"(page-level)",message:"HTML contains NCUA components but is missing the required CDN JS link.",suggestion:`Add <script src="${e.js}"><\/script>`}),t};exports.checkCdnInclusion=d;
@@ -1,203 +1 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.detectTokenIssues = exports.detectNcuaOmission = exports.detectCustomNotSeparated = exports.buildComplianceSummary = void 0;
4
- const bemValidator_js_1 = require("./bemValidator.js");
5
- // ── 상수 ─────────────────────────────────────────────────────────────────
6
- const MAX_COMPLIANCE_ERRORS = 10;
7
- const NCUA_NOT_USED_MEDIUM_WEIGHT = 0.5;
8
- const COMPLIANCE_WEIGHTS = {
9
- ncua_not_used: 1.0,
10
- token_not_used: 0.5,
11
- invalid_token: 1.0,
12
- custom_not_separated: 1.0,
13
- };
14
- /** 규칙의 hints가 요소 속성과 매칭되는지 확인 */
15
- const matchesHints = (el, hints) => {
16
- if (!hints)
17
- return true;
18
- for (const hint of hints) {
19
- const attrValue = el.getAttribute(hint.attr);
20
- if (!attrValue || !hint.values.includes(attrValue.toLowerCase()))
21
- return false;
22
- }
23
- return true;
24
- };
25
- /** 태그명 → 해당 태그의 규칙 배열 인덱스 (§ 5.3 O(n) 전환) */
26
- const buildTagRuleIndex = (rules, componentMap) => {
27
- const index = new Map();
28
- for (const rule of rules) {
29
- if (!componentMap.has(rule.ncuaComponent))
30
- continue;
31
- const existing = index.get(rule.match.tag);
32
- if (existing)
33
- existing.push(rule);
34
- else
35
- index.set(rule.match.tag, [rule]);
36
- }
37
- return index;
38
- };
39
- /** ncua- root class → compliance rule의 ncuaComponent 매핑 */
40
- const buildNcuaRootToComponentMap = (complianceRules, componentMap) => {
41
- const map = new Map();
42
- for (const rule of complianceRules.patterns) {
43
- const component = componentMap.get(rule.ncuaComponent);
44
- if (!component)
45
- continue;
46
- for (const cls of component.bemClasses) {
47
- if (!cls.includes('--') && !cls.includes('__'))
48
- map.set(cls, rule.ncuaComponent);
49
- }
50
- }
51
- return map;
52
- };
53
- /** compliance rules 중 componentMap에 실제 존재하는 고유 컴포넌트 수 */
54
- const countAvailableComponents = (patterns, componentMap) => {
55
- const available = new Set();
56
- for (const rule of patterns) {
57
- if (componentMap.has(rule.ncuaComponent))
58
- available.add(rule.ncuaComponent);
59
- }
60
- return available.size;
61
- };
62
- // ── 검증 함수 ────────────────────────────────────────────────────────────
63
- /** ncua 클래스를 가진 요소에서 사용 중인 컴포넌트 수집 */
64
- const collectUsedComponents = (el, ncuaRootMap, usedComponents) => {
65
- for (const cls of (0, bemValidator_js_1.getClassList)(el)) {
66
- const comp = ncuaRootMap.get(cls);
67
- if (comp)
68
- usedComponents.add(comp);
69
- }
70
- };
71
- /** 네이티브 HTML 요소에서 ncua_not_used 에러 매칭 */
72
- const matchNativeElement = (el, tagIndex) => {
73
- const tag = el.tagName?.toLowerCase();
74
- if (!tag)
75
- return undefined;
76
- const candidateRules = tagIndex.get(tag);
77
- if (!candidateRules)
78
- return undefined;
79
- for (const rule of candidateRules) {
80
- if (!matchesHints(el, rule.match.hints))
81
- continue;
82
- return {
83
- type: 'ncua_not_used',
84
- target: `<${tag}>`,
85
- component: rule.ncuaComponent,
86
- message: `Native <${tag}> used instead of NCUA '${rule.ncuaComponent}' component.`,
87
- suggestion: `Use NCUA '${rule.ncuaComponent}' component. Call search_component('${rule.ncuaComponent}') for usage.`,
88
- ...(rule.confidence === 'medium' && { severity: 'warning' }),
89
- };
90
- }
91
- return undefined;
92
- };
93
- /** NCUA 컴포넌트 누락 감지 — 태그 인덱스로 O(n) 매칭 */
94
- const detectNcuaOmission = (params) => {
95
- const { root, complianceRules, componentMap } = params;
96
- const errors = [];
97
- const usedComponents = new Set();
98
- const tagIndex = buildTagRuleIndex(complianceRules.patterns, componentMap);
99
- const ncuaRootMap = buildNcuaRootToComponentMap(complianceRules, componentMap);
100
- for (const el of root.querySelectorAll('*')) {
101
- if ((0, bemValidator_js_1.getClassList)(el).some((c) => c.startsWith('ncua-'))) {
102
- collectUsedComponents(el, ncuaRootMap, usedComponents);
103
- continue;
104
- }
105
- const error = matchNativeElement(el, tagIndex);
106
- if (error)
107
- errors.push(error);
108
- }
109
- return {
110
- errors,
111
- ncuaUsage: {
112
- used: usedComponents.size,
113
- available: countAvailableComponents(complianceRules.patterns, componentMap),
114
- },
115
- };
116
- };
117
- exports.detectNcuaOmission = detectNcuaOmission;
118
- /** NCUA 컴포넌트 내부 커스텀 스타일 감지 */
119
- const detectCustomNotSeparated = (params) => {
120
- const { root, rootClassMap, warnings } = params;
121
- const errors = [];
122
- let violated = 0;
123
- let clean = 0;
124
- const suppressedClasses = new Set();
125
- for (const el of root.querySelectorAll('[class]')) {
126
- const classes = (0, bemValidator_js_1.getClassList)(el);
127
- const ncuaClasses = classes.filter((c) => c.startsWith('ncua-'));
128
- if (ncuaClasses.length === 0)
129
- continue;
130
- const target = ncuaClasses[0];
131
- const hasInlineStyle = !!el.getAttribute('style');
132
- const isBemInternal = ncuaClasses.some((c) => c.includes('__'));
133
- const hasCustomOverride = isBemInternal && classes.some((c) => !c.startsWith('ncua-'));
134
- if (hasInlineStyle) {
135
- violated++;
136
- errors.push(buildCustomError(target, rootClassMap, 'inline'));
137
- }
138
- else if (hasCustomOverride) {
139
- violated++;
140
- suppressedClasses.add(target);
141
- errors.push(buildCustomError(target, rootClassMap, 'override'));
142
- }
143
- else {
144
- clean++;
145
- }
146
- }
147
- suppressMixedNamespaceWarnings(warnings, suppressedClasses);
148
- return { errors, customSeparation: { clean, violated } };
149
- };
150
- exports.detectCustomNotSeparated = detectCustomNotSeparated;
151
- /** custom_not_separated 에러 빌드 */
152
- const buildCustomError = (target, rootClassMap, kind) => {
153
- const rootClass = kind === 'inline' ? target.split('--')[0].split('__')[0] : target.split('__')[0];
154
- const message = kind === 'inline'
155
- ? `Inline style on NCUA component element '${target}'. NCUA components should not have inline styles.`
156
- : `Custom class override on NCUA BEM internal element '${target}'. Do not add custom classes to BEM sub-elements.`;
157
- return {
158
- type: 'custom_not_separated',
159
- target,
160
- component: rootClassMap.get(rootClass) ?? 'unknown',
161
- message,
162
- suggestion: 'Use a separate wrapper outside the NCUA component for custom styling.',
163
- };
164
- };
165
- /** mixed_namespace warnings 중 suppress 대상을 제거 */
166
- const suppressMixedNamespaceWarnings = (warnings, suppressedClasses) => {
167
- if (suppressedClasses.size === 0)
168
- return;
169
- for (let i = warnings.length - 1; i >= 0; i--) {
170
- if (warnings[i].type === 'mixed_namespace' && suppressedClasses.has(warnings[i].class)) {
171
- warnings.splice(i, 1);
172
- }
173
- }
174
- };
175
- /** 준수도 점수 + 요약 빌드 */
176
- const buildComplianceSummary = (params) => {
177
- const { ncuaErrors, tokenErrors, customErrors, ncuaUsage, tokenUsage, customSeparation } = params;
178
- const allErrors = [...ncuaErrors, ...tokenErrors, ...customErrors];
179
- let weightedViolations = 0;
180
- for (const err of allErrors) {
181
- const isMedium = err.type === 'ncua_not_used' && err.severity === 'warning';
182
- weightedViolations += isMedium ? NCUA_NOT_USED_MEDIUM_WEIGHT : COMPLIANCE_WEIGHTS[err.type];
183
- }
184
- const ncuaNotUsedCount = ncuaErrors.length;
185
- const totalCheckpoints = ncuaUsage.used +
186
- ncuaNotUsedCount +
187
- (tokenUsage.correct + tokenUsage.missing + tokenUsage.invalid) +
188
- (customSeparation.clean + customSeparation.violated);
189
- // biome-ignore lint/style/noMagicNumbers: 점수 0~1 boundary (clamp)
190
- const score = totalCheckpoints === 0 ? 1.0 : Math.max(0, Math.min(1, 1 - weightedViolations / totalCheckpoints));
191
- return {
192
- // biome-ignore lint/style/noMagicNumbers: 2자리 소수점 반올림 (×100/100)
193
- score: Math.round(score * 100) / 100,
194
- errors: allErrors.slice(0, MAX_COMPLIANCE_ERRORS),
195
- ncuaUsage,
196
- tokenUsage,
197
- customSeparation,
198
- totalViolations: allErrors.length,
199
- };
200
- };
201
- exports.buildComplianceSummary = buildComplianceSummary;
202
- var tokenValidator_js_1 = require("./tokenValidator.js");
203
- Object.defineProperty(exports, "detectTokenIssues", { enumerable: true, get: function () { return tokenValidator_js_1.detectTokenIssues; } });
1
+ "use strict";Object.defineProperty(exports,"__esModule",{value:!0}),exports.detectTokenIssues=exports.detectNcuaOmission=exports.detectCustomNotSeparated=exports.buildComplianceSummary=void 0;const e=require("./bemValidator.js"),t=10,n=.5,o={ncua_not_used:1,token_not_used:.5,invalid_token:1,custom_not_separated:1},s=(e,t)=>{if(!t)return!0;for(const n of t){const t=e.getAttribute(n.attr);if(!t||!n.values.includes(t.toLowerCase()))return!1}return!0},r=(e,t)=>{const n=new Map;for(const o of e){if(!t.has(o.ncuaComponent))continue;const e=n.get(o.match.tag);e?e.push(o):n.set(o.match.tag,[o])}return n},a=(e,t)=>{const n=new Map;for(const o of e.patterns){const e=t.get(o.ncuaComponent);if(e)for(const t of e.bemClasses)t.includes("--")||t.includes("__")||n.set(t,o.ncuaComponent)}return n},c=(e,t)=>{const n=new Set;for(const o of e)t.has(o.ncuaComponent)&&n.add(o.ncuaComponent);return n.size},i=(t,n,o)=>{for(const s of(0,e.getClassList)(t)){const e=n.get(s);e&&o.add(e)}},u=(e,t)=>{const n=e.tagName?.toLowerCase();if(!n)return;const o=t.get(n);if(o)for(const t of o)if(s(e,t.match.hints))return{type:"ncua_not_used",target:`<${n}>`,component:t.ncuaComponent,message:`Native <${n}> used instead of NCUA '${t.ncuaComponent}' component.`,suggestion:`Use NCUA '${t.ncuaComponent}' component. Call search_component('${t.ncuaComponent}') for usage.`,..."medium"===t.confidence&&{severity:"warning"}}},l=t=>{const{root:n,complianceRules:o,componentMap:s}=t,l=[],p=new Set,m=r(o.patterns,s),d=a(o,s);for(const t of n.querySelectorAll("*")){if((0,e.getClassList)(t).some(e=>e.startsWith("ncua-"))){i(t,d,p);continue}const n=u(t,m);n&&l.push(n)}return{errors:l,ncuaUsage:{used:p.size,available:c(o.patterns,s)}}};exports.detectNcuaOmission=l;const p=t=>{const{root:n,rootClassMap:o,warnings:s}=t,r=[];let a=0,c=0;const i=new Set;for(const t of n.querySelectorAll("[class]")){const n=(0,e.getClassList)(t),s=n.filter(e=>e.startsWith("ncua-"));if(0===s.length)continue;const u=s[0],l=!!t.getAttribute("style"),p=s.some(e=>e.includes("__"))&&n.some(e=>!e.startsWith("ncua-"));l?(a++,r.push(m(u,o,"inline"))):p?(a++,i.add(u),r.push(m(u,o,"override"))):c++}return d(s,i),{errors:r,customSeparation:{clean:c,violated:a}}};exports.detectCustomNotSeparated=p;const m=(e,t,n)=>{const o="inline"===n?e.split("--")[0].split("__")[0]:e.split("__")[0],s="inline"===n?`Inline style on NCUA component element '${e}'. NCUA components should not have inline styles.`:`Custom class override on NCUA BEM internal element '${e}'. Do not add custom classes to BEM sub-elements.`;return{type:"custom_not_separated",target:e,component:t.get(o)??"unknown",message:s,suggestion:"Use a separate wrapper outside the NCUA component for custom styling."}},d=(e,t)=>{if(0!==t.size)for(let n=e.length-1;n>=0;n--)"mixed_namespace"===e[n].type&&t.has(e[n].class)&&e.splice(n,1)},f=e=>{const{ncuaErrors:s,tokenErrors:r,customErrors:a,ncuaUsage:c,tokenUsage:i,customSeparation:u}=e,l=[...s,...r,...a];let p=0;for(const e of l)p+="ncua_not_used"===e.type&&"warning"===e.severity?n:o[e.type];const m=s.length,d=c.used+m+(i.correct+i.missing+i.invalid)+(u.clean+u.violated),f=0===d?1:Math.max(0,Math.min(1,1-p/d));return{score:Math.round(100*f)/100,errors:l.slice(0,t),ncuaUsage:c,tokenUsage:i,customSeparation:u,totalViolations:l.length}};exports.buildComplianceSummary=f;var g=require("./tokenValidator.js");Object.defineProperty(exports,"detectTokenIssues",{enumerable:!0,get:function(){return g.detectTokenIssues}});