import type { Validator, LintResult, ValidationContext } from '../../../core/types.js'; /** * テキスト教材の目次プレースホルダー検証バリデーター */ export class TocValidator implements Validator { readonly id = 'text/toc'; readonly name = '目次プレースホルダー検証'; // {{ toc }} または {{toc}} にマッチする正規表現 private readonly tocPattern = /\{\{\s*toc\s*\}\}/; validate(context: ValidationContext): LintResult[] { const results: LintResult[] = []; const { content } = context; // コンテンツ内で {{ toc }} を検索 const tocMatch = content.match(this.tocPattern); if (!tocMatch) { results.push({ ruleId: `${this.id}/missing`, ruleName: '目次プレースホルダー必須', severity: 'error', message: '目次プレースホルダー「{{ toc }}」がありません', detail: 'テキスト教材にはfrontmatter直後に目次プレースホルダーが必要です。これにより自動的に目次が生成されます。', suggestion: 'frontmatterの直後に以下を追加してください:\n\n{{ toc }}', location: { line: 1, column: 1 }, }); return results; } // 位置チェック: frontmatter直後かどうか // frontmatterの終了位置を見つける const lines = content.split('\n'); let frontmatterEndLine = 0; let inFrontmatter = false; let frontmatterCount = 0; for (let i = 0; i < lines.length; i++) { const line = lines[i].trim(); if (line === '---') { frontmatterCount++; if (frontmatterCount === 1) { inFrontmatter = true; } else if (frontmatterCount === 2) { frontmatterEndLine = i + 1; // 1-indexed inFrontmatter = false; break; } } } if (frontmatterEndLine === 0) { // frontmatterがない場合は位置チェックをスキップ return results; } // {{ toc }}の行番号を取得 let tocLine = 0; for (let i = 0; i < lines.length; i++) { if (this.tocPattern.test(lines[i])) { tocLine = i + 1; // 1-indexed break; } } // frontmatter終了後、最初の非空行が{{ toc }}かどうかチェック let firstContentLine = 0; for (let i = frontmatterEndLine; i < lines.length; i++) { if (lines[i].trim() !== '') { firstContentLine = i + 1; // 1-indexed break; } } if (tocLine !== firstContentLine) { results.push({ ruleId: `${this.id}/wrong-position`, ruleName: '目次プレースホルダー位置', severity: 'warning', message: '「{{ toc }}」がfrontmatter直後にありません', detail: '目次プレースホルダーはfrontmatterの直後(空行を除く最初のコンテンツ)に配置する必要があります。', suggestion: '{{ toc }} をfrontmatterの直後に移動してください。\n\n---\ntitle: タイトル\n---\n\n{{ toc }}\n\n# 見出し', location: { line: tocLine, column: 1 }, }); } return results; } }