diff --git a/packages/frontend/lib/vite-plugin-create-search-index.ts b/packages/frontend/lib/vite-plugin-create-search-index.ts index 344faabe70..e2da23d19e 100644 --- a/packages/frontend/lib/vite-plugin-create-search-index.ts +++ b/packages/frontend/lib/vite-plugin-create-search-index.ts @@ -13,6 +13,27 @@ import path from 'node:path' import { hash, toBase62 } from '../vite.config'; import { createLogger } from 'vite'; +interface VueAstNode { + type: number; + tag?: string; + loc?: { + start: { offset: number, line: number, column: number }, + end: { offset: number, line: number, column: number }, + source?: string + }; + props?: Array<{ + name: string; + type: number; + value?: { content?: string }; + arg?: { content?: string }; + exp?: { content?: string; loc?: any }; + }>; + children?: VueAstNode[]; + content?: any; + __markerId?: string; + __children?: string[]; +} + export type AnalysisResult = { filePath: string; usage: SearchIndexItem[]; @@ -27,6 +48,22 @@ export type SearchIndexItem = { children?: (SearchIndexItem[] | string); } +// 関連するノードタイプの定数化 +const NODE_TYPES = { + ELEMENT: 1, + EXPRESSION: 2, + TEXT: 3, + INTERPOLATION: 5, // Mustache +}; + +// マーカー関係を表す型 +interface MarkerRelation { + parentId?: string; + markerId: string; + node: VueAstNode; +} + +// ロガーの設定 const logger = createLogger(); const loggerInfo = logger.info; const loggerWarn = logger.warn; @@ -47,258 +84,199 @@ logger.error = (msg, options) => { loggerError(msg, options); } +/** + * 解析結果をTypeScriptファイルとして出力する + */ function outputAnalysisResultAsTS(outputPath: string, analysisResults: AnalysisResult[]): void { - logger.info(`Processing ${analysisResults.length} files for output`); + logger.info(`Processing ${analysisResults.length} files for output`); - // 新しいツリー構造を構築 - const allMarkers = new Map(); + // 新しいツリー構造を構築 + const allMarkers = new Map(); - // 1. すべてのマーカーを一旦フラットに収集 - for (const file of analysisResults) { - logger.info(`Processing file: ${file.filePath} with ${file.usage.length} markers`); - for (const marker of file.usage) { - if (marker.id) { - // キーワードの処理(文字列から配列へ変換) - let keywords = marker.keywords; - if (typeof keywords === 'string' && keywords.startsWith('[') && keywords.endsWith(']')) { - try { - // JSON5解析を試みる(ただしi18n参照などがある場合は例外発生) - keywords = JSON5.parse(keywords.replace(/'/g, '"')); - } catch (e) { - // 解析に失敗した場合は文字列のままにする - logger.warn(`Keeping keywords as string expression: ${keywords}`); - } - } + // 1. すべてのマーカーを一旦フラットに収集 + for (const file of analysisResults) { + logger.info(`Processing file: ${file.filePath} with ${file.usage.length} markers`); - // 子要素の処理(文字列から配列へ変換) - let children = marker.children || []; - if (typeof children === 'string' && children.startsWith('[') && children.endsWith(']')) { - try { - // JSON5解析を試みる - children = JSON5.parse(children.replace(/'/g, '"')); - } catch (e) { - // 解析に失敗した場合は空配列に - logger.warn(`Could not parse children: ${children}, using empty array`); - children = []; - } - } + for (const marker of file.usage) { + if (marker.id) { + // キーワードとchildren処理を共通化 + const processedMarker = { + ...marker, + keywords: processMarkerProperty(marker.keywords, 'keywords'), + children: processMarkerProperty(marker.children || [], 'children') + }; - // 子マーカーの内部構造を適切に更新 - if (Array.isArray(children) && children.length > 0) { - logger.info(`Marker ${marker.id} has ${children.length} children: ${JSON.stringify(children)}`); - } + allMarkers.set(marker.id, processedMarker); + } + } + } - allMarkers.set(marker.id, { - ...marker, - keywords, - children: Array.isArray(children) ? children : [] - }); - } - } - } + logger.info(`Collected total ${allMarkers.size} unique markers`); - logger.info(`Collected total ${allMarkers.size} unique markers`); + // 2. 子マーカーIDの収集 + const childIds = collectChildIds(allMarkers); + logger.info(`Found ${childIds.size} child markers`); - // 2. 子マーカーIDの収集 - const childIds = new Set(); + // 3. ルートマーカーの特定(他の誰かの子でないマーカー) + const rootMarkers = identifyRootMarkers(allMarkers, childIds); + logger.info(`Found ${rootMarkers.length} root markers`); - allMarkers.forEach((marker, id) => { - const children = marker.children; - if (Array.isArray(children)) { - children.forEach(childId => { - if (typeof childId === 'string') { - if (!allMarkers.has(childId)) { - logger.warn(`Warning: Child marker ID ${childId} referenced but not found`); - } else { - childIds.add(childId); - } - } - }); - } - }); + // 4. 子マーカーの参照を解決 + const resolvedRootMarkers = resolveChildReferences(rootMarkers, allMarkers); - logger.info(`Found ${childIds.size} child markers`); + // 5. デバッグ情報を生成 + const { totalMarkers, totalChildren } = countMarkers(resolvedRootMarkers); + logger.info(`Total markers in tree: ${totalMarkers} (${resolvedRootMarkers.length} roots + ${totalChildren} nested children)`); - // 3. ルートマーカーの特定(他の誰かの子でないマーカー) - const rootMarkers: SearchIndexItem[] = []; + // 6. 結果をTS形式で出力 + writeOutputFile(outputPath, resolvedRootMarkers); +} - allMarkers.forEach((marker, id) => { - if (!childIds.has(id)) { - // このマーカーはルート(他の誰の子でもない) - rootMarkers.push(marker); - logger.info(`Added root marker to output: ${id} with label ${marker.label}`); - } - }); +/** + * マーカーのプロパティ(keywordsやchildren)を処理する + */ +function processMarkerProperty(propValue: any, propType: 'keywords' | 'children'): any { + // 文字列の配列表現を解析 + if (typeof propValue === 'string' && propValue.startsWith('[') && propValue.endsWith(']')) { + try { + // JSON5解析を試みる + return JSON5.parse(propValue.replace(/'/g, '"')); + } catch (e) { + // 解析に失敗した場合 + logger.warn(`Could not parse ${propType}: ${propValue}, using ${propType === 'children' ? 'empty array' : 'as is'}`); + return propType === 'children' ? [] : propValue; + } + } - logger.info(`Found ${rootMarkers.length} root markers`); + return propValue; +} - // 4. 子マーカーの参照を解決(IDから実際のオブジェクトに) - function resolveChildrenReferences(marker: SearchIndexItem): SearchIndexItem { - // マーカーのディープコピーを作成 - const resolvedMarker = { ...marker }; +/** + * 全マーカーから子IDを収集する + */ +function collectChildIds(allMarkers: Map): Set { + const childIds = new Set(); - // 子リファレンスを解決 - if (Array.isArray(marker.children)) { - const children: SearchIndexItem[] = []; + allMarkers.forEach((marker, id) => { + const children = marker.children; + if (Array.isArray(children)) { + children.forEach(childId => { + if (typeof childId === 'string') { + if (!allMarkers.has(childId)) { + logger.warn(`Warning: Child marker ID ${childId} referenced but not found`); + } else { + childIds.add(childId); + } + } + }); + } + }); - for (const childId of marker.children) { - if (typeof childId === 'string') { - const childMarker = allMarkers.get(childId); + return childIds; +} - if (childMarker) { - // 子マーカーの子も再帰的に解決 - const resolvedChild = resolveChildrenReferences(childMarker); - children.push(resolvedChild); - logger.info(`Resolved child ${childId} for parent ${marker.id}`); - } - } - } +/** + * ルートマーカー(他の子でないマーカー)を特定する + */ +function identifyRootMarkers( + allMarkers: Map, + childIds: Set +): SearchIndexItem[] { + const rootMarkers: SearchIndexItem[] = []; - // 子が存在する場合のみchildrenプロパティを設定 - if (children.length > 0) { - resolvedMarker.children = children; - } else { - // 子がない場合はchildrenプロパティを削除 - delete resolvedMarker.children; - } - } + allMarkers.forEach((marker, id) => { + if (!childIds.has(id)) { + rootMarkers.push(marker); + logger.info(`Added root marker to output: ${id} with label ${marker.label}`); + } + }); - return resolvedMarker; - } + return rootMarkers; +} - // すべてのルートマーカーに対して子の参照を解決 - const resolvedRootMarkers = rootMarkers.map(marker => { - return resolveChildrenReferences(marker); - }); +/** + * 子マーカーの参照をIDから実際のオブジェクトに解決する + */ +function resolveChildReferences( + rootMarkers: SearchIndexItem[], + allMarkers: Map +): SearchIndexItem[] { + function resolveChildrenForMarker(marker: SearchIndexItem): SearchIndexItem { + // マーカーのディープコピーを作成 + const resolvedMarker = { ...marker }; - // 特殊なプロパティ変換用の関数 - i18n参照処理を強化 - function formatSpecialProperty(key: string, value: any): string { - // 値がundefinedの場合は空文字列を返す - if (value === undefined) { - return '""'; - } + // 子リファレンスを解決 + if (Array.isArray(marker.children)) { + const children: SearchIndexItem[] = []; - // childrenが配列の場合は特別に処理 - if (key === 'children' && Array.isArray(value)) { - return customStringify(value); - } + for (const childId of marker.children) { + if (typeof childId === 'string') { + const childMarker = allMarkers.get(childId); - // keywordsが配列の場合、特別に処理 - if (key === 'keywords' && Array.isArray(value)) { - return `[${formatArrayForOutput(value)}]`; - } + if (childMarker) { + // 子マーカーの子も再帰的に解決 + const resolvedChild = resolveChildrenForMarker(childMarker); + children.push(resolvedChild); + logger.info(`Resolved child ${childId} for parent ${marker.id}`); + } + } + } - // 文字列値の場合の特別処理 - if (typeof value === 'string') { - // i18n.ts 参照を含む場合 - クォートなしでそのまま出力 - if (value.includes('i18n.ts.')) { - logger.info(`Preserving i18n reference in output: ${value}`); - return value; - } + // 子が存在する場合のみchildrenプロパティを設定 + if (children.length > 0) { + resolvedMarker.children = children; + } else { + // 子がない場合はchildrenプロパティを削除 + delete resolvedMarker.children; + } + } - // keywords が配列リテラルの形式の場合 - if (key === 'keywords' && value.startsWith('[') && value.endsWith(']')) { - return value; - } - } + return resolvedMarker; + } - // 上記以外は通常の JSON5 文字列として返す - return JSON5.stringify(value); - } + // すべてのルートマーカーの子を解決 + return rootMarkers.map(marker => resolveChildrenForMarker(marker)); +} - // オブジェクトをカスタム形式に変換する関数 - i18n参照処理を強化 - function customStringify(obj: any, depth = 0): string { - const INDENT_STR = '\t'; +/** + * マーカー数を数える(デバッグ用) + */ +function countMarkers(markers: SearchIndexItem[]): { totalMarkers: number, totalChildren: number } { + let totalMarkers = markers.length; + let totalChildren = 0; - // 配列の処理 - if (Array.isArray(obj)) { - if (obj.length === 0) return '[]'; - const indent = INDENT_STR.repeat(depth); - const childIndent = INDENT_STR.repeat(depth + 1); + function countNested(items: SearchIndexItem[]): void { + for (const marker of items) { + if (marker.children && Array.isArray(marker.children)) { + totalChildren += marker.children.length; + totalMarkers += marker.children.length; + countNested(marker.children as SearchIndexItem[]); + } + } + } - // 配列要素の処理 - const items = obj.map(item => { - // オブジェクト要素 - if (typeof item === 'object' && item !== null) { - return `${childIndent}${customStringify(item, depth + 1)}`; - } + countNested(markers); + return { totalMarkers, totalChildren }; +} - // i18n参照を含む文字列要素 - if (typeof item === 'string' && item.includes('i18n.ts.')) { - return `${childIndent}${item}`; // クォートなしでそのまま出力 - } +/** + * 最終的なTypeScriptファイルを出力 + */ +function writeOutputFile(outputPath: string, resolvedRootMarkers: SearchIndexItem[]): void { + try { + const tsOutput = generateTypeScriptCode(resolvedRootMarkers); + fs.writeFileSync(outputPath, tsOutput, 'utf-8'); + logger.info(`Successfully wrote search index to ${outputPath} with ${resolvedRootMarkers.length} root entries`); + } catch (error) { + logger.error('[create-search-index]: error writing output: ', error); + } +} - // その他の要素 - return `${childIndent}${JSON5.stringify(item)}`; - }).join(',\n'); - - return `[\n${items},\n${indent}]`; - } - - // null または非オブジェクト - if (obj === null || typeof obj !== 'object') { - return JSON5.stringify(obj); - } - - // オブジェクトの処理 - const indent = INDENT_STR.repeat(depth); - const childIndent = INDENT_STR.repeat(depth + 1); - - const entries = Object.entries(obj) - // 不要なプロパティを除去 - .filter(([key, value]) => { - if (value === undefined) return false; - if (key === 'children' && Array.isArray(value) && value.length === 0) return false; - return true; - }) - // 各プロパティを変換 - .map(([key, value]) => { - // 子要素配列の特殊処理 - if (key === 'children' && Array.isArray(value) && value.length > 0) { - return `${childIndent}${key}: ${customStringify(value, depth + 1)}`; - } - - // ラベルやその他プロパティを処理 - return `${childIndent}${key}: ${formatSpecialProperty(key, value)}`; - }); - - if (entries.length === 0) return '{}'; - return `{\n${entries.join(',\n')},\n${indent}}`; - } - - // 配列式の文字列表現を修正 - i18n参照を適切に処理 - function formatArrayForOutput(items: any[]): string { - return items.map(item => { - // i18n.ts. 参照の文字列はそのままJavaScript式として出力 - if (typeof item === 'string' && item.includes('i18n.ts.')) { - logger.info(`Preserving i18n reference in array: ${item}`); - return item; // クォートなしでそのまま - } - - // その他の値はJSON5形式で文字列化 - return JSON5.stringify(item); - }).join(', '); - } - - // 最終出力用のデバッグ情報を生成 - let totalMarkers = resolvedRootMarkers.length; - let totalChildren = 0; - - function countNestedMarkers(markers: SearchIndexItem[]): void { - for (const marker of markers) { - if (marker.children && Array.isArray(marker.children)) { - totalChildren += marker.children.length; - totalMarkers += marker.children.length; - countNestedMarkers(marker.children as SearchIndexItem[]); - } - } - } - - countNestedMarkers(resolvedRootMarkers); - logger.info(`Total markers in tree: ${totalMarkers} (${resolvedRootMarkers.length} roots + ${totalChildren} nested children)`); - - // 結果をTS形式で出力 - const tsOutput = ` +/** + * TypeScriptコード生成 + */ +function generateTypeScriptCode(resolvedRootMarkers: SearchIndexItem[]): string { + return ` /* * SPDX-FileCopyrightText: syuilo and misskey-project * SPDX-License-Identifier: AGPL-3.0-only @@ -322,555 +300,714 @@ export const searchIndexes:SearchIndexItem[] = ${customStringify(resolvedRootMar export type SearchIndex = typeof searchIndexes; `; - - try { - fs.writeFileSync(outputPath, tsOutput, 'utf-8'); - logger.info(`Successfully wrote search index to ${outputPath} with ${resolvedRootMarkers.length} root entries and ${totalChildren} nested children`); - } catch (error) { - logger.error('[create-search-index]: error: ', error); - } } -// 要素ノードからテキスト内容を抽出する関数 (Mustache構文のi18n参照にも対応) -function extractElementText(node: any): string | null { - if (!node) return null; +/** + * オブジェクトを特殊な形式の文字列に変換する + * i18n参照を保持しつつ適切な形式に変換 + */ +function customStringify(obj: any, depth = 0): string { + const INDENT_STR = '\t'; - console.log(`Extracting text from node type=${node.type}, tag=${node.tag || 'unknown'}`); + // 配列の処理 + if (Array.isArray(obj)) { + if (obj.length === 0) return '[]'; + const indent = INDENT_STR.repeat(depth); + const childIndent = INDENT_STR.repeat(depth + 1); - // Mustache構文を検出するための正規表現パターン - const mustachePattern = /^\s*{{\s*(.*?)\s*}}\s*$/; + // 配列要素の処理 + const items = obj.map(item => { + // オブジェクト要素 + if (typeof item === 'object' && item !== null) { + return `${childIndent}${customStringify(item, depth + 1)}`; + } - // childrenが配列でない場合、content直接チェック (インラインテキスト対応) - if (node.content) { - const content = node.content.trim(); - console.log(`Direct node content found: ${content}`); + // i18n参照を含む文字列要素 + if (typeof item === 'string' && item.includes('i18n.ts.')) { + return `${childIndent}${item}`; // クォートなしでそのまま出力 + } - // Mustache構文のチェック - const mustacheMatch = content.match(mustachePattern); - if (mustacheMatch && mustacheMatch[1] && mustacheMatch[1].includes('i18n.ts.')) { - const extractedContent = mustacheMatch[1].trim(); - console.log(`Extracted i18n reference from mustache: ${extractedContent}`); - return extractedContent; - } + // その他の要素 + return `${childIndent}${JSON5.stringify(item)}`; + }).join(',\n'); - // 直接i18n参照を含む場合 - if (content.includes('i18n.ts.')) { - console.log(`Direct i18n reference found: ${content}`); - return content; - } + return `[\n${items},\n${indent}]`; + } - // その他のコンテンツ - if (content) { - return content; - } - } + // null または非オブジェクト + if (obj === null || typeof obj !== 'object') { + return JSON5.stringify(obj); + } - // childrenがない場合は終了 - if (!node.children || !Array.isArray(node.children)) { - return null; - } + // オブジェクトの処理 + const indent = INDENT_STR.repeat(depth); + const childIndent = INDENT_STR.repeat(depth + 1); - // Mustacheテンプレート構文の特殊ノード検出 (type=5はインターポレーション) - for (const child of node.children) { - if (child.type === 5) { // インターポレーションノード (Mustache表現) - console.log(`Found interpolation node (Mustache): `, child); - if (child.content && child.content.type === 4 && child.content.content) { - const content = child.content.content.trim(); - console.log(`Interpolation content: ${content}`); - if (content.includes('i18n.ts.')) { - return content; - } - } else if (child.content && typeof child.content === 'object') { - // オブジェクト形式のcontentを再帰的に探索 - console.log(`Complex interpolation node:`, JSON.stringify(child.content).substring(0, 100)); - if (child.content.content) { - const content = child.content.content.trim(); - if (content.includes('i18n.ts.')) { - console.log(`Found i18n reference in complex interpolation: ${content}`); - return content; - } - } - } - } - } + const entries = Object.entries(obj) + // 不要なプロパティを除去 + .filter(([key, value]) => { + if (value === undefined) return false; + if (key === 'children' && Array.isArray(value) && value.length === 0) return false; + return true; + }) + // 各プロパティを変換 + .map(([key, value]) => { + // 子要素配列の特殊処理 + if (key === 'children' && Array.isArray(value) && value.length > 0) { + return `${childIndent}${key}: ${customStringify(value, depth + 1)}`; + } - // 最初のパスで i18n.ts. 参照パターンを持つものを探す (最優先) - for (const child of node.children) { - if (child.type === 2 && child.content) { // 式ノード - const expr = child.content.trim(); - if (expr.includes('i18n.ts.')) { - console.log(`Found i18n reference in expression node: ${expr}`); - return expr; // i18n参照を見つけたら即座に返す - } - } - } + // ラベルやその他プロパティを処理 + return `${childIndent}${key}: ${formatSpecialProperty(key, value)}`; + }); - // 2回目のパスで一般的な式を探す - for (const child of node.children) { - if (child.type === 2 && child.content) { // その他の式ノード - const expr = child.content.trim(); - console.log(`Found expression: ${expr}`); - return expr; - } - } - - // 3回目のパスでテキストノードを探す - for (const child of node.children) { - if (child.type === 3 && child.content) { // テキストノード - const text = child.content.trim(); - if (text) { - console.log(`Found text node: ${text}`); - - // Mustache構文のチェック - const mustacheMatch = text.match(mustachePattern); - if (mustacheMatch && mustacheMatch[1] && mustacheMatch[1].includes('i18n.ts.')) { - console.log(`Extracted i18n ref from text mustache: ${mustacheMatch[1]}`); - return mustacheMatch[1].trim(); - } - - return text; - } - } - } - - // 深さ優先で再帰的に探索 (子の子まで調べる) - for (const child of node.children) { - if (child.children && Array.isArray(child.children) && child.children.length > 0) { - const nestedContent = extractElementText(child); - if (nestedContent) { - console.log(`Found nested content: ${nestedContent}`); - return nestedContent; - } - } else if (child.type === 1) { // 子要素ノード - childrenがなくても内部を調査 - const nestedContent = extractElementText(child); - if (nestedContent) { - console.log(`Found content in childless element: ${nestedContent}`); - return nestedContent; - } - } - } - - return null; + if (entries.length === 0) return '{}'; + return `{\n${entries.join(',\n')},\n${indent}}`; } -// SearchLabelとSearchKeywordを探して抽出する関数 (スコープを適切に分離) -function extractLabelsAndKeywords(nodes: any[]): { label: string | null, keywords: any[] } { - let label: string | null = null; - const keywords: any[] = []; +/** + * 特殊プロパティの書式設定 + */ +function formatSpecialProperty(key: string, value: any): string { + // 値がundefinedの場合は空文字列を返す + if (value === undefined) { + return '""'; + } - console.log(`Extracting labels and keywords from ${nodes.length} nodes`); + // childrenが配列の場合は特別に処理 + if (key === 'children' && Array.isArray(value)) { + return customStringify(value); + } - // 再帰的にSearchLabelとSearchKeywordを探索(ネストされたSearchMarkerは処理しない) - function findComponents(nodes: any[]) { - for (const node of nodes) { - if (node.type === 1) { // Element node - console.log(`Checking element: ${node.tag}`); + // keywordsが配列の場合、特別に処理 + if (key === 'keywords' && Array.isArray(value)) { + return `[${formatArrayForOutput(value)}]`; + } - // SearchMarkerの場合は、その子要素は別スコープなのでスキップ - if (node.tag === 'SearchMarker') { - console.log(`Found nested SearchMarker - skipping its content to maintain scope isolation`); - continue; // このSearchMarkerの中身は処理しない (スコープ分離) - } + // 文字列値の場合の特別処理 + if (typeof value === 'string') { + // i18n.ts 参照を含む場合 - クォートなしでそのまま出力 + if (value.includes('i18n.ts.')) { + logger.info(`Preserving i18n reference in output: ${value}`); + return value; + } - // SearchLabelの処理 - if (node.tag === 'SearchLabel') { - console.log(`Found SearchLabel node, structure:`, JSON.stringify(node).substring(0, 200) + '...'); + // keywords が配列リテラルの形式の場合 + if (key === 'keywords' && value.startsWith('[') && value.endsWith(']')) { + return value; + } + } - // まず完全なノード内容の抽出を試みる - const content = extractElementText(node); - if (content) { - label = content; - console.log(`SearchLabel content extracted: ${content}`); - } else { - console.log(`SearchLabel found but extraction failed, trying direct children inspection`); + // 上記以外は通常の JSON5 文字列として返す + return JSON5.stringify(value); +} - // バックアップ: 子直接確認 - type=5のMustacheインターポレーションを重点的に確認 - if (node.children && Array.isArray(node.children)) { - for (const child of node.children) { - // Mustacheインターポレーション - if (child.type === 5 && child.content) { - // content内の式を取り出す - const expression = child.content.content || - (child.content.type === 4 ? child.content.content : null) || - JSON.stringify(child.content); +/** + * 配列式の文字列表現を生成 + */ +function formatArrayForOutput(items: any[]): string { + return items.map(item => { + // i18n.ts. 参照の文字列はそのままJavaScript式として出力 + if (typeof item === 'string' && item.includes('i18n.ts.')) { + logger.info(`Preserving i18n reference in array: ${item}`); + return item; // クォートなしでそのまま + } - console.log(`Interpolation expression: ${expression}`); - if (typeof expression === 'string' && expression.includes('i18n.ts.')) { - label = expression.trim(); - console.log(`Found i18n in interpolation: ${label}`); - break; - } - } - // 式ノード - else if (child.type === 2 && child.content && child.content.includes('i18n.ts.')) { - label = child.content.trim(); - console.log(`Found i18n in expression: ${label}`); - break; - } - // テキストノードでもMustache構文を探す - else if (child.type === 3 && child.content) { - const mustacheMatch = child.content.trim().match(/^\s*{{\s*(.*?)\s*}}\s*$/); - if (mustacheMatch && mustacheMatch[1] && mustacheMatch[1].includes('i18n.ts.')) { - label = mustacheMatch[1].trim(); - console.log(`Found i18n in text mustache: ${label}`); - break; - } - } - } - } - } - } - // SearchKeywordの処理 - else if (node.tag === 'SearchKeyword') { - console.log(`Found SearchKeyword node`); + // その他の値はJSON5形式で文字列化 + return JSON5.stringify(item); + }).join(', '); +} - // まず完全なノード内容の抽出を試みる - const content = extractElementText(node); - if (content) { - keywords.push(content); - console.log(`SearchKeyword content extracted: ${content}`); - } else { - console.log(`SearchKeyword found but extraction failed, trying direct children inspection`); +/** + * 要素ノードからテキスト内容を抽出する + * 各抽出方法を分離して可読性を向上 + */ +function extractElementText(node: VueAstNode): string | null { + if (!node) return null; - // バックアップ: 子直接確認 - type=5のMustacheインターポレーションを重点的に確認 - if (node.children && Array.isArray(node.children)) { - for (const child of node.children) { - // Mustacheインターポレーション - if (child.type === 5 && child.content) { - // content内の式を取り出す - const expression = child.content.content || - (child.content.type === 4 ? child.content.content : null) || - JSON.stringify(child.content); + console.log(`Extracting text from node type=${node.type}, tag=${node.tag || 'unknown'}`); - console.log(`Keyword interpolation: ${expression}`); - if (typeof expression === 'string' && expression.includes('i18n.ts.')) { - const keyword = expression.trim(); - keywords.push(keyword); - console.log(`Found i18n keyword in interpolation: ${keyword}`); - break; - } - } - // 式ノード - else if (child.type === 2 && child.content && child.content.includes('i18n.ts.')) { - const keyword = child.content.trim(); - keywords.push(keyword); - console.log(`Found i18n keyword in expression: ${keyword}`); - break; - } - // テキストノードでもMustache構文を探す - else if (child.type === 3 && child.content) { - const mustacheMatch = child.content.trim().match(/^\s*{{\s*(.*?)\s*}}\s*$/); - if (mustacheMatch && mustacheMatch[1] && mustacheMatch[1].includes('i18n.ts.')) { - const keyword = mustacheMatch[1].trim(); - keywords.push(keyword); - console.log(`Found i18n keyword in text mustache: ${keyword}`); - break; - } - } - } - } - } - } + // 1. 直接コンテンツの抽出を試行 + const directContent = extractDirectContent(node); + if (directContent) return directContent; - // 子要素を再帰的に調査(ただしSearchMarkerは除外) - if (node.children && Array.isArray(node.children)) { - findComponents(node.children); - } - } - } - } + // 子要素がない場合は終了 + if (!node.children || !Array.isArray(node.children)) { + return null; + } - findComponents(nodes); + // 2. インターポレーションノードを検索 + const interpolationContent = extractInterpolationContent(node.children); + if (interpolationContent) return interpolationContent; - // デバッグ情報 - console.log(`Extraction completed: label=${label}, keywords=[${keywords.join(', ')}]`); - return { label, keywords }; + // 3. 式ノードを検索 + const expressionContent = extractExpressionContent(node.children); + if (expressionContent) return expressionContent; + + // 4. テキストノードを検索 + const textContent = extractTextContent(node.children); + if (textContent) return textContent; + + // 5. 再帰的に子ノードを探索 + return extractNestedContent(node.children); +} + +/** + * ノードから直接コンテンツを抽出 + */ +function extractDirectContent(node: VueAstNode): string | null { + if (!node.content) return null; + + const content = typeof node.content === 'string' + ? node.content.trim() + : (node.content.content ? node.content.content.trim() : null); + + if (!content) return null; + + console.log(`Direct node content found: ${content}`); + + // Mustache構文のチェック + const mustachePattern = /^\s*{{\s*(.*?)\s*}}\s*$/; + const mustacheMatch = content.match(mustachePattern); + + if (mustacheMatch && mustacheMatch[1] && mustacheMatch[1].includes('i18n.ts.')) { + const extractedContent = mustacheMatch[1].trim(); + console.log(`Extracted i18n reference from mustache: ${extractedContent}`); + return extractedContent; + } + + // 直接i18n参照を含む場合 + if (content.includes('i18n.ts.')) { + console.log(`Direct i18n reference found: ${content}`); + return content; + } + + // その他のコンテンツ + return content; +} + +/** + * インターポレーションノード(Mustache)からコンテンツを抽出 + */ +function extractInterpolationContent(children: VueAstNode[]): string | null { + for (const child of children) { + if (child.type === NODE_TYPES.INTERPOLATION) { + console.log(`Found interpolation node (Mustache): `, child); + + if (child.content && child.content.type === 4 && child.content.content) { + const content = child.content.content.trim(); + console.log(`Interpolation content: ${content}`); + + if (content.includes('i18n.ts.')) { + return content; + } + } else if (child.content && typeof child.content === 'object') { + // オブジェクト形式のcontentを探索 + console.log(`Complex interpolation node:`, JSON.stringify(child.content).substring(0, 100)); + + if (child.content.content) { + const content = child.content.content.trim(); + + if (content.includes('i18n.ts.')) { + console.log(`Found i18n reference in complex interpolation: ${content}`); + return content; + } + } + } + } + } + + return null; +} + +/** + * 式ノードからコンテンツを抽出 + */ +function extractExpressionContent(children: VueAstNode[]): string | null { + // i18n.ts. 参照パターンを持つものを優先 + for (const child of children) { + if (child.type === NODE_TYPES.EXPRESSION && child.content) { + const expr = child.content.trim(); + + if (expr.includes('i18n.ts.')) { + console.log(`Found i18n reference in expression node: ${expr}`); + return expr; + } + } + } + + // その他の式 + for (const child of children) { + if (child.type === NODE_TYPES.EXPRESSION && child.content) { + const expr = child.content.trim(); + console.log(`Found expression: ${expr}`); + return expr; + } + } + + return null; +} + +/** + * テキストノードからコンテンツを抽出 + */ +function extractTextContent(children: VueAstNode[]): string | null { + for (const child of children) { + if (child.type === NODE_TYPES.TEXT && child.content) { + const text = child.content.trim(); + + if (text) { + console.log(`Found text node: ${text}`); + + // Mustache構文のチェック + const mustachePattern = /^\s*{{\s*(.*?)\s*}}\s*$/; + const mustacheMatch = text.match(mustachePattern); + + if (mustacheMatch && mustacheMatch[1] && mustacheMatch[1].includes('i18n.ts.')) { + console.log(`Extracted i18n ref from text mustache: ${mustacheMatch[1]}`); + return mustacheMatch[1].trim(); + } + + return text; + } + } + } + + return null; +} + +/** + * 子ノードを再帰的に探索してコンテンツを抽出 + */ +function extractNestedContent(children: VueAstNode[]): string | null { + for (const child of children) { + if (child.children && Array.isArray(child.children) && child.children.length > 0) { + const nestedContent = extractElementText(child); + + if (nestedContent) { + console.log(`Found nested content: ${nestedContent}`); + return nestedContent; + } + } else if (child.type === NODE_TYPES.ELEMENT) { + // childrenがなくても内部を調査 + const nestedContent = extractElementText(child); + + if (nestedContent) { + console.log(`Found content in childless element: ${nestedContent}`); + return nestedContent; + } + } + } + + return null; +} + +/** + * SearchLabelとSearchKeywordを探して抽出する関数 + */ +function extractLabelsAndKeywords(nodes: VueAstNode[]): { label: string | null, keywords: any[] } { + let label: string | null = null; + const keywords: any[] = []; + + console.log(`Extracting labels and keywords from ${nodes.length} nodes`); + + // 再帰的にSearchLabelとSearchKeywordを探索(ネストされたSearchMarkerは処理しない) + function findComponents(nodes: VueAstNode[]) { + for (const node of nodes) { + if (node.type === NODE_TYPES.ELEMENT) { + console.log(`Checking element: ${node.tag}`); + + // SearchMarkerの場合は、その子要素は別スコープなのでスキップ + if (node.tag === 'SearchMarker') { + console.log(`Found nested SearchMarker - skipping its content to maintain scope isolation`); + continue; // このSearchMarkerの中身は処理しない (スコープ分離) + } + + // SearchLabelの処理 + if (node.tag === 'SearchLabel') { + console.log(`Found SearchLabel node, structure:`, JSON.stringify(node).substring(0, 200) + '...'); + + // まず完全なノード内容の抽出を試みる + const content = extractElementText(node); + if (content) { + label = content; + console.log(`SearchLabel content extracted: ${content}`); + } else { + console.log(`SearchLabel found but extraction failed, trying direct children inspection`); + + // バックアップ: 子直接確認 - type=5のMustacheインターポレーションを重点的に確認 + if (node.children && Array.isArray(node.children)) { + for (const child of node.children) { + // Mustacheインターポレーション + if (child.type === NODE_TYPES.INTERPOLATION && child.content) { + // content内の式を取り出す + const expression = child.content.content || + (child.content.type === 4 ? child.content.content : null) || + JSON.stringify(child.content); + + console.log(`Interpolation expression: ${expression}`); + if (typeof expression === 'string' && expression.includes('i18n.ts.')) { + label = expression.trim(); + console.log(`Found i18n in interpolation: ${label}`); + break; + } + } + // 式ノード + else if (child.type === NODE_TYPES.EXPRESSION && child.content && child.content.includes('i18n.ts.')) { + label = child.content.trim(); + console.log(`Found i18n in expression: ${label}`); + break; + } + // テキストノードでもMustache構文を探す + else if (child.type === NODE_TYPES.TEXT && child.content) { + const mustacheMatch = child.content.trim().match(/^\s*{{\s*(.*?)\s*}}\s*$/); + if (mustacheMatch && mustacheMatch[1] && mustacheMatch[1].includes('i18n.ts.')) { + label = mustacheMatch[1].trim(); + console.log(`Found i18n in text mustache: ${label}`); + break; + } + } + } + } + } + } + // SearchKeywordの処理 + else if (node.tag === 'SearchKeyword') { + console.log(`Found SearchKeyword node`); + + // まず完全なノード内容の抽出を試みる + const content = extractElementText(node); + if (content) { + keywords.push(content); + console.log(`SearchKeyword content extracted: ${content}`); + } else { + console.log(`SearchKeyword found but extraction failed, trying direct children inspection`); + + // バックアップ: 子直接確認 - type=5のMustacheインターポレーションを重点的に確認 + if (node.children && Array.isArray(node.children)) { + for (const child of node.children) { + // Mustacheインターポレーション + if (child.type === NODE_TYPES.INTERPOLATION && child.content) { + // content内の式を取り出す + const expression = child.content.content || + (child.content.type === 4 ? child.content.content : null) || + JSON.stringify(child.content); + + console.log(`Keyword interpolation: ${expression}`); + if (typeof expression === 'string' && expression.includes('i18n.ts.')) { + const keyword = expression.trim(); + keywords.push(keyword); + console.log(`Found i18n keyword in interpolation: ${keyword}`); + break; + } + } + // 式ノード + else if (child.type === NODE_TYPES.EXPRESSION && child.content && child.content.includes('i18n.ts.')) { + const keyword = child.content.trim(); + keywords.push(keyword); + console.log(`Found i18n keyword in expression: ${keyword}`); + break; + } + // テキストノードでもMustache構文を探す + else if (child.type === NODE_TYPES.TEXT && child.content) { + const mustacheMatch = child.content.trim().match(/^\s*{{\s*(.*?)\s*}}\s*$/); + if (mustacheMatch && mustacheMatch[1] && mustacheMatch[1].includes('i18n.ts.')) { + const keyword = mustacheMatch[1].trim(); + keywords.push(keyword); + console.log(`Found i18n keyword in text mustache: ${keyword}`); + break; + } + } + } + } + } + } + + // 子要素を再帰的に調査(ただしSearchMarkerは除外) + if (node.children && Array.isArray(node.children)) { + findComponents(node.children); + } + } + } + } + + findComponents(nodes); + + // デバッグ情報 + console.log(`Extraction completed: label=${label}, keywords=[${keywords.join(', ')}]`); + return { label, keywords }; } function extractUsageInfoFromTemplateAst( - templateAst: any, - code: string, + templateAst: any, + code: string, ): SearchIndexItem[] { - const allMarkers: SearchIndexItem[] = []; - const markerMap = new Map(); - const childrenIds = new Set(); + const allMarkers: SearchIndexItem[] = []; + const markerMap = new Map(); + const childrenIds = new Set(); - if (!templateAst) return allMarkers; + if (!templateAst) return allMarkers; - // マーカーの基本情報を収集 - function collectMarkers(node: any, parentId: string | null = null) { - if (node.type === 1 && node.tag === 'SearchMarker') { - // マーカーID取得 - const markerIdProp = node.props?.find((p: any) => p.name === 'markerId'); - const markerId = markerIdProp?.value?.content || - node.__markerId || - `marker-${Math.random().toString(36).substring(2, 10)}`; + // マーカーの基本情報を収集 + function collectMarkers(node: any, parentId: string | null = null) { + if (node.type === 1 && node.tag === 'SearchMarker') { + // マーカーID取得 + const markerIdProp = node.props?.find((p: any) => p.name === 'markerId'); + const markerId = markerIdProp?.value?.content || + node.__markerId || + `marker-${Math.random().toString(36).substring(2, 10)}`; - // マーカー基本情報 - const markerInfo: SearchIndexItem = { - id: markerId, - children: [], - label: '', // デフォルト値 - keywords: [], - }; + // マーカー基本情報 + const markerInfo: SearchIndexItem = { + id: markerId, + children: [], + label: '', // デフォルト値 + keywords: [], + }; - // 静的プロパティを取得 - if (node.props && Array.isArray(node.props)) { - for (const prop of node.props) { - if (prop.type === 6 && prop.name && prop.name !== 'markerId') { - if (prop.name === 'path') markerInfo.path = prop.value?.content || ''; - else if (prop.name === 'icon') markerInfo.icon = prop.value?.content || ''; - else if (prop.name === 'label') markerInfo.label = prop.value?.content || ''; - } - } - } + // 静的プロパティを取得 + if (node.props && Array.isArray(node.props)) { + for (const prop of node.props) { + if (prop.type === 6 && prop.name && prop.name !== 'markerId') { + if (prop.name === 'path') markerInfo.path = prop.value?.content || ''; + else if (prop.name === 'icon') markerInfo.icon = prop.value?.content || ''; + else if (prop.name === 'label') markerInfo.label = prop.value?.content || ''; + } + } + } - // バインドプロパティを取得 - const bindings = extractNodeBindings(node); - if (bindings.path) markerInfo.path = bindings.path; - if (bindings.icon) markerInfo.icon = bindings.icon; - if (bindings.label) markerInfo.label = bindings.label; - if (bindings.children) markerInfo.children = bindings.children; - if (bindings.keywords) { - if (Array.isArray(bindings.keywords)) { - markerInfo.keywords = bindings.keywords; - } else { - markerInfo.keywords = bindings.keywords || []; - } - } + // バインドプロパティを取得 + const bindings = extractNodeBindings(node); + if (bindings.path) markerInfo.path = bindings.path; + if (bindings.icon) markerInfo.icon = bindings.icon; + if (bindings.label) markerInfo.label = bindings.label; + if (bindings.children) markerInfo.children = bindings.children; + if (bindings.keywords) { + if (Array.isArray(bindings.keywords)) { + markerInfo.keywords = bindings.keywords; + } else { + markerInfo.keywords = bindings.keywords || []; + } + } - // SearchLabelとSearchKeywordを抽出 (AST全体を探索) - if (node.children && Array.isArray(node.children)) { - console.log(`Processing marker ${markerId} for labels and keywords`); - const extracted = extractLabelsAndKeywords(node.children); + // SearchLabelとSearchKeywordを抽出 (AST全体を探索) + if (node.children && Array.isArray(node.children)) { + console.log(`Processing marker ${markerId} for labels and keywords`); + const extracted = extractLabelsAndKeywords(node.children); - // SearchLabelからのラベル取得は最優先で適用 - if (extracted.label) { - markerInfo.label = extracted.label; - console.log(`Using extracted label for ${markerId}: ${extracted.label}`); - } else if (markerInfo.label) { - console.log(`Using existing label for ${markerId}: ${markerInfo.label}`); - } else { - markerInfo.label = 'Unnamed marker'; - console.log(`No label found for ${markerId}, using default`); - } + // SearchLabelからのラベル取得は最優先で適用 + if (extracted.label) { + markerInfo.label = extracted.label; + console.log(`Using extracted label for ${markerId}: ${extracted.label}`); + } else if (markerInfo.label) { + console.log(`Using existing label for ${markerId}: ${markerInfo.label}`); + } else { + markerInfo.label = 'Unnamed marker'; + console.log(`No label found for ${markerId}, using default`); + } - // SearchKeywordからのキーワード取得を追加 - if (extracted.keywords.length > 0) { - const existingKeywords = Array.isArray(markerInfo.keywords) ? - [...markerInfo.keywords] : - (markerInfo.keywords ? [markerInfo.keywords] : []); + // SearchKeywordからのキーワード取得を追加 + if (extracted.keywords.length > 0) { + const existingKeywords = Array.isArray(markerInfo.keywords) ? + [...markerInfo.keywords] : + (markerInfo.keywords ? [markerInfo.keywords] : []); - // i18n参照のキーワードは最優先で追加 - const combinedKeywords = [...existingKeywords]; - for (const kw of extracted.keywords) { - combinedKeywords.push(kw); - console.log(`Added extracted keyword to ${markerId}: ${kw}`); - } + // i18n参照のキーワードは最優先で追加 + const combinedKeywords = [...existingKeywords]; + for (const kw of extracted.keywords) { + combinedKeywords.push(kw); + console.log(`Added extracted keyword to ${markerId}: ${kw}`); + } - markerInfo.keywords = combinedKeywords; - } - } + markerInfo.keywords = combinedKeywords; + } + } - // マーカーを登録 - markerMap.set(markerId, markerInfo); - allMarkers.push(markerInfo); + // マーカーを登録 + markerMap.set(markerId, markerInfo); + allMarkers.push(markerInfo); - // 親子関係を記録 - if (parentId) { - const parent = markerMap.get(parentId); - if (parent) { - if (!parent.children) parent.children = []; - if (Array.isArray(parent.children)) { - parent.children.push(markerId); - } else { - parent.children = [markerId]; - } - childrenIds.add(markerId); - } - } + // 親子関係を記録 + if (parentId) { + const parent = markerMap.get(parentId); + if (parent) { + if (!parent.children) parent.children = []; + if (Array.isArray(parent.children)) { + parent.children.push(markerId); + } else { + parent.children = [markerId]; + } + childrenIds.add(markerId); + } + } - // 子ノードを処理 - if (node.children && Array.isArray(node.children)) { - node.children.forEach((child: any) => { - collectMarkers(child, markerId); - }); - } + // 子ノードを処理 + if (node.children && Array.isArray(node.children)) { + node.children.forEach((child: any) => { + collectMarkers(child, markerId); + }); + } - return markerId; - } + return markerId; + } - // 子ノードを処理 - if (node.children && Array.isArray(node.children)) { - node.children.forEach((child: any) => { - collectMarkers(child, parentId); - }); - } + // 子ノードを処理 + if (node.children && Array.isArray(node.children)) { + node.children.forEach((child: any) => { + collectMarkers(child, parentId); + }); + } - return null; - } + return null; + } - // AST解析開始 - collectMarkers(templateAst); - return allMarkers; + // AST解析開始 + collectMarkers(templateAst); + return allMarkers; } // バインドプロパティの処理を修正する関数 function extractNodeBindings(node: any): Record { - const bindings: Record = {}; + const bindings: Record = {}; - if (!node.props || !Array.isArray(node.props)) return bindings; + if (!node.props || !Array.isArray(node.props)) return bindings; - // バインド式を収集 - for (const prop of node.props) { - if (prop.type === 7 && prop.name === 'bind' && prop.arg?.content) { - const propName = prop.arg.content; - const propContent = prop.exp?.content || ''; + // バインド式を収集 + for (const prop of node.props) { + if (prop.type === 7 && prop.name === 'bind' && prop.arg?.content) { + const propName = prop.arg.content; + const propContent = prop.exp?.content || ''; - logger.info(`Processing bind prop ${propName}: ${propContent}`); + logger.info(`Processing bind prop ${propName}: ${propContent}`); - // keywordsの特殊処理 - if (propName === 'keywords') { - try { - const content = propContent.trim(); + // keywordsの特殊処理 + if (propName === 'keywords') { + try { + const content = propContent.trim(); - // 配列式の場合 - if (content.startsWith('[') && content.endsWith(']')) { - // i18n参照や特殊な式を保持するため、各要素を個別に解析 - const elements = parseArrayExpression(content); - if (elements.length > 0) { - bindings.keywords = elements; - logger.info(`Parsed keywords array: ${JSON5.stringify(elements)}`); - } else { - bindings.keywords = []; - logger.info('Empty keywords array'); - } - } - // その他の式(非配列) - else if (content) { - bindings.keywords = content; // 式をそのまま保持 - logger.info(`Keeping keywords as expression: ${content}`); - } else { - bindings.keywords = []; - logger.info('No keywords provided'); - } - } catch (e) { - logger.error(`Failed to parse keywords binding: ${propContent}`, e); - // エラーが起きても何らかの値を設定 - bindings.keywords = propContent || []; - } - } - // その他のプロパティ - else if (propName === 'label') { - // ラベルの場合も式として保持 - bindings[propName] = propContent; - logger.info(`Set label from bind expression: ${propContent}`); - } - else { - bindings[propName] = propContent; - } - } - } + // 配列式の場合 + if (content.startsWith('[') && content.endsWith(']')) { + // i18n参照や特殊な式を保持するため、各要素を個別に解析 + const elements = parseArrayExpression(content); + if (elements.length > 0) { + bindings.keywords = elements; + logger.info(`Parsed keywords array: ${JSON5.stringify(elements)}`); + } else { + bindings.keywords = []; + logger.info('Empty keywords array'); + } + } + // その他の式(非配列) + else if (content) { + bindings.keywords = content; // 式をそのまま保持 + logger.info(`Keeping keywords as expression: ${content}`); + } else { + bindings.keywords = []; + logger.info('No keywords provided'); + } + } catch (e) { + logger.error(`Failed to parse keywords binding: ${propContent}`, e); + // エラーが起きても何らかの値を設定 + bindings.keywords = propContent || []; + } + } + // その他のプロパティ + else if (propName === 'label') { + // ラベルの場合も式として保持 + bindings[propName] = propContent; + logger.info(`Set label from bind expression: ${propContent}`); + } + else { + bindings[propName] = propContent; + } + } + } - return bindings; + return bindings; } // 配列式をパースする補助関数(文字列リテラル処理を改善) function parseArrayExpression(expr: string): any[] { - try { - // 単純なケースはJSON5でパースを試みる - return JSON5.parse(expr.replace(/'/g, '"')); - } catch (e) { - // 複雑なケース(i18n.ts.xxx などの式を含む場合)は手動パース - logger.info(`Complex array expression, trying manual parsing: ${expr}`); + try { + // 単純なケースはJSON5でパースを試みる + return JSON5.parse(expr.replace(/'/g, '"')); + } catch (e) { + // 複雑なケース(i18n.ts.xxx などの式を含む場合)は手動パース + logger.info(`Complex array expression, trying manual parsing: ${expr}`); - // "["と"]"を取り除く - const content = expr.substring(1, expr.length - 1).trim(); - if (!content) return []; + // "["と"]"を取り除く + const content = expr.substring(1, expr.length - 1).trim(); + if (!content) return []; - const result: any[] = []; - let currentItem = ''; - let depth = 0; - let inString = false; - let stringChar = ''; + const result: any[] = []; + let currentItem = ''; + let depth = 0; + let inString = false; + let stringChar = ''; - // カンマで区切る(ただし文字列内や入れ子の配列内のカンマは無視) - for (let i = 0; i < content.length; i++) { - const char = content[i]; + // カンマで区切る(ただし文字列内や入れ子の配列内のカンマは無視) + for (let i = 0; i < content.length; i++) { + const char = content[i]; - if (inString) { - if (char === stringChar && content[i - 1] !== '\\') { - inString = false; - } - currentItem += char; - } else if (char === '"' || char === "'") { - inString = true; - stringChar = char; - currentItem += char; - } else if (char === '[') { - depth++; - currentItem += char; - } else if (char === ']') { - depth--; - currentItem += char; - } else if (char === ',' && depth === 0) { - // 項目の区切りを検出 - const trimmed = currentItem.trim(); + if (inString) { + if (char === stringChar && content[i - 1] !== '\\') { + inString = false; + } + currentItem += char; + } else if (char === '"' || char === "'") { + inString = true; + stringChar = char; + currentItem += char; + } else if (char === '[') { + depth++; + currentItem += char; + } else if (char === ']') { + depth--; + currentItem += char; + } else if (char === ',' && depth === 0) { + // 項目の区切りを検出 + const trimmed = currentItem.trim(); - // 純粋な文字列リテラルの場合、実際の値に変換 - if ((trimmed.startsWith("'") && trimmed.endsWith("'")) || - (trimmed.startsWith('"') && trimmed.endsWith('"'))) { - try { - result.push(JSON5.parse(trimmed)); - } catch (err) { - result.push(trimmed); - } - } else { - // それ以外の式はそのまま(i18n.ts.xxx など) - result.push(trimmed); - } + // 純粋な文字列リテラルの場合、実際の値に変換 + if ((trimmed.startsWith("'") && trimmed.endsWith("'")) || + (trimmed.startsWith('"') && trimmed.endsWith('"'))) { + try { + result.push(JSON5.parse(trimmed)); + } catch (err) { + result.push(trimmed); + } + } else { + // それ以外の式はそのまま(i18n.ts.xxx など) + result.push(trimmed); + } - currentItem = ''; - } else { - currentItem += char; - } - } + currentItem = ''; + } else { + currentItem += char; + } + } - // 最後の項目を処理 - if (currentItem.trim()) { - const trimmed = currentItem.trim(); + // 最後の項目を処理 + if (currentItem.trim()) { + const trimmed = currentItem.trim(); - // 純粋な文字列リテラルの場合、実際の値に変換 - if ((trimmed.startsWith("'") && trimmed.endsWith("'")) || - (trimmed.startsWith('"') && trimmed.endsWith('"'))) { - try { - result.push(JSON5.parse(trimmed)); - } catch (err) { - result.push(trimmed); - } - } else { - // それ以外の式はそのまま(i18n.ts.xxx など) - result.push(trimmed); - } - } + // 純粋な文字列リテラルの場合、実際の値に変換 + if ((trimmed.startsWith("'") && trimmed.endsWith("'")) || + (trimmed.startsWith('"') && trimmed.endsWith('"'))) { + try { + result.push(JSON5.parse(trimmed)); + } catch (err) { + result.push(trimmed); + } + } else { + // それ以外の式はそのまま(i18n.ts.xxx など) + result.push(trimmed); + } + } - logger.info(`Parsed complex array expression: ${expr} -> ${JSON.stringify(result)}`); - return result; - } -} - -// 配列式の文字列表現を修正 - i18n参照を適切に処理 -function formatArrayForOutput(items: any[]): string { - return items.map(item => { - // i18n.ts. 参照の文字列はそのままJavaScript式として出力 - if (typeof item === 'string' && item.includes('i18n.ts.')) { - logger.info(`Preserving i18n reference in array: ${item}`); - return item; // クォートなしでそのまま - } - - // その他の値はJSON5形式で文字列化 - return JSON5.stringify(item); - }).join(', '); + logger.info(`Parsed complex array expression: ${expr} -> ${JSON.stringify(result)}`); + return result; + } } export async function analyzeVueProps(options: { @@ -941,7 +1078,7 @@ export async function analyzeVueProps(options: { interface MarkerRelation { parentId?: string; markerId: string; - node: any; + node: VueAstNode; } async function processVueFile( @@ -1059,9 +1196,9 @@ async function processVueFile( const childrenProp = parentNode.props?.find((prop: any) => prop.type === 7 && prop.name === 'bind' && prop.arg?.content === 'children'); // 親ノードの開始位置を特定 - const parentNodeStart = parentNode.loc.start.offset; + const parentNodeStart = parentNode.loc!.start.offset; const endOfParentStartTag = parentNode.children && parentNode.children.length > 0 - ? code.lastIndexOf('>', parentNode.children[0].loc.start.offset) + ? code.lastIndexOf('>', parentNode.children[0].loc!.start.offset) : code.indexOf('>', parentNodeStart); if (endOfParentStartTag === -1) continue; @@ -1072,8 +1209,8 @@ async function processVueFile( if (childrenProp) { // AST で :children 属性が検出された場合、それを更新 try { - const childrenStart = code.indexOf('[', childrenProp.exp.loc.start.offset); - const childrenEnd = code.indexOf(']', childrenProp.exp.loc.start.offset); + const childrenStart = code.indexOf('[', childrenProp.exp!.loc.start.offset); + const childrenEnd = code.indexOf(']', childrenProp.exp!.loc.start.offset); if (childrenStart !== -1 && childrenEnd !== -1) { const childrenArrayStr = code.slice(childrenStart, childrenEnd + 1); let childrenArray = JSON5.parse(childrenArrayStr.replace(/'/g, '"'));