import type { LintResult, Severity } from '../core/types.js'; import type { LintUseCaseOutput } from '../usecases/LintUseCase.js'; /** * フォーマッターインターフェース */ export interface Formatter { format(output: LintUseCaseOutput): string; } /** * 結果のサマリー */ interface ResultSummary { total: number; errors: number; warnings: number; infos: number; } /** * サマリーを計算する */ function calculateSummary(results: LintResult[]): ResultSummary { return { total: results.length, errors: results.filter(r => r.severity === 'error').length, warnings: results.filter(r => r.severity === 'warning').length, infos: results.filter(r => r.severity === 'info').length, }; } /** * 位置情報を文字列にフォーマット */ function formatLocation(result: LintResult): string { if (!result.location) { return ''; } const { line, column } = result.location; if (line !== undefined && column !== undefined) { return `${line}:${column}`; } if (line !== undefined) { return `${line}`; } return ''; } const SEPARATOR = '━'.repeat(60); /** * コンソール出力用フォーマッター(messages.md仕様準拠) */ export class ConsoleFormatter implements Formatter { format(output: LintUseCaseOutput): string { const { results, metadata } = output; const lines: string[] = []; const summary = calculateSummary(results); // ヘッダー lines.push(''); lines.push(SEPARATOR); lines.push(`📄 文書タイプ: ${metadata.documentTypeName || 'テキスト教材'}`); lines.push(`📁 ファイル: ${metadata.filePath}`); lines.push(SEPARATOR); lines.push(''); if (results.length === 0) { lines.push('✓ 問題は見つかりませんでした (0 件)'); return lines.join('\n'); } // サマリー(先頭) lines.push(this.formatSummaryLine(summary)); lines.push(''); // 各結果を出力 for (const result of results) { lines.push(SEPARATOR); lines.push(''); this.formatResult(result, lines); } // フッター lines.push(SEPARATOR); lines.push(''); lines.push(`📊 チェック完了: ${this.formatSummary(summary)}`); return lines.join('\n'); } private formatResult(result: LintResult, lines: string[]): void { const icon = this.getSeverityIcon(result.severity); const location = formatLocation(result); // メインライン: アイコン [ruleId] メッセージ lines.push(`${icon} [${result.ruleId}] ${result.message}`); lines.push(''); // 位置情報 if (location) { lines.push(` 📍 位置: ${location}行目`); lines.push(''); } // 詳細 if (result.detail) { lines.push(' 📝 説明:'); for (const line of result.detail.split('\n')) { lines.push(` ${line}`); } lines.push(''); } // 提案 if (result.suggestion) { lines.push(' 💡 修正方法:'); for (const line of result.suggestion.split('\n')) { lines.push(` ${line}`); } lines.push(''); } } private getSeverityIcon(severity: Severity): string { switch (severity) { case 'error': return '✖'; case 'warning': return '⚠'; case 'info': return 'ℹ'; } } private formatSummaryLine(summary: ResultSummary): string { return `✖ エラー: ${summary.errors}件 | ⚠ 警告: ${summary.warnings}件 | ℹ 情報: ${summary.infos}件`; } private formatSummary(summary: ResultSummary): string { const parts: string[] = []; if (summary.errors > 0) { parts.push(`エラー: ${summary.errors}`); } if (summary.warnings > 0) { parts.push(`警告: ${summary.warnings}`); } if (summary.infos > 0) { parts.push(`情報: ${summary.infos}`); } if (parts.length === 0) { return '問題は見つかりませんでした'; } return `${summary.total}件の問題が見つかりました(${parts.join(', ')})`; } } /** * JsonFormatterのオプション */ export interface JsonFormatterOptions { /** 整形して出力するか(デフォルト: true) */ pretty?: boolean; } /** * JSON出力用フォーマッター */ export class JsonFormatter implements Formatter { private readonly pretty: boolean; constructor(options: JsonFormatterOptions = {}) { this.pretty = options.pretty ?? true; } format(output: LintUseCaseOutput): string { const { results, metadata } = output; const summary = calculateSummary(results); const jsonOutput = { filePath: metadata.filePath, documentType: metadata.documentType || 'text', documentTypeName: metadata.documentTypeName || 'テキスト教材', success: results.length === 0, summary, results, }; if (this.pretty) { return JSON.stringify(jsonOutput, null, 2); } return JSON.stringify(jsonOutput); } }