const ALLOWED_ATTRS = ['data-dev-name', 'key', 'id', 'name', 'type', 'icon', 'role']; const SNIPPET_MAX = 24; export interface SignatureNodeChild { type: 'text' | 'interpolation' | 'element' | 'comment'; content?: string; } export interface SignatureNodeProp { type: 'attribute' | 'directive'; name: string; value?: string; arg?: string; exp?: string; } export interface SignatureNode { tag: string; props: SignatureNodeProp[]; children: SignatureNodeChild[]; } export interface SignatureContext { filePath: string; // absolute or relative — anything load-time-stable appRoot: string; // e.g. 'app/', used by normalizeFilePath ancestorChain: string[]; // ['DeclaringComponent', 'div', 'nav'] sameTagOrdinal: number; // 0-based among siblings of same tag in same parent } export function normalizeFilePath(filePath: string, appRoot: string): string { const normalized = filePath.replace(/\\/g, '/').replace(/\.vue$/, ''); const root = appRoot.replace(/\\/g, '/').replace(/\/$/, '') + '/'; return normalized.startsWith(root) ? normalized.slice(root.length) : normalized; } export function extractStaticText(node: SignatureNode): string | null { const parts: string[] = []; for (const child of node.children) { if (child.type === 'text' && child.content) { parts.push(child.content); } } if (parts.length === 0) return null; const merged = parts.join(' ').replace(/\s+/g, ' ').trim().toLowerCase(); if (!merged) return null; return merged.slice(0, SNIPPET_MAX); } export function extractDistinctiveAttrs(node: SignatureNode): string { const pairs: string[] = []; for (const prop of node.props) { if (prop.type !== 'attribute') continue; // skip directives / v-bind if (!ALLOWED_ATTRS.includes(prop.name)) continue; if (prop.value == null) continue; pairs.push(`${prop.name}=${prop.value}`); } return pairs.sort().join(','); } function findDevName(node: SignatureNode): string | null { for (const prop of node.props) { if (prop.type === 'attribute' && prop.name === 'data-dev-name' && prop.value) { return prop.value; } } return null; } export function computeSignature(node: SignatureNode, ctx: SignatureContext): string { const path = normalizeFilePath(ctx.filePath, ctx.appRoot); const devName = findDevName(node); if (devName) { return `${path}::data-dev-name::${devName}`; } const ancestors = ctx.ancestorChain.join('>'); const attrs = extractDistinctiveAttrs(node); const text = extractStaticText(node) ?? ''; return `${path}::${ancestors}::${node.tag}[${attrs}]::${text}::${ctx.sameTagOrdinal}`; }