import type { Validator, LintResult, ValidationContext } from '../../../core/types.js'; import { visit } from 'unist-util-visit'; import yaml from 'js-yaml'; /** * テキスト教材のFrontmatter検証バリデーター */ export class FrontmatterValidator implements Validator { readonly id = 'text/frontmatter'; readonly name = 'Frontmatter検証'; validate(context: ValidationContext): LintResult[] { const results: LintResult[] = []; let foundFrontmatter = false; let frontmatterData: Record = {}; let frontmatterLine: number | undefined; // ASTからfrontmatterノードを検索 visit(context.tree, 'yaml', (node: { value: string; position?: { start: { line: number; column: number } } }) => { foundFrontmatter = true; frontmatterLine = node.position?.start.line; try { const parsed = yaml.load(node.value); if (parsed && typeof parsed === 'object') { frontmatterData = parsed as Record; } } catch { results.push({ ruleId: `${this.id}/invalid-yaml`, ruleName: 'frontmatter YAML構文', severity: 'error', message: 'frontmatterのYAML構文が不正です', detail: 'YAML構文エラーがあります。インデント、コロン、引用符を確認してください。', suggestion: 'YAMLの構文を確認し、修正してください。\n\n正しい形式の例:\n---\ntitle: "章タイトル"\ndraft: false\n---', location: frontmatterLine ? { line: frontmatterLine, column: 1 } : undefined, }); } }); // frontmatterがない場合 if (!foundFrontmatter) { results.push({ ruleId: `${this.id}/missing`, ruleName: 'frontmatter必須', severity: 'error', message: 'frontmatterがありません', detail: 'Markdownファイルの先頭にYAML形式のfrontmatterが必要です。frontmatterは文書のメタデータ(タイトル、公開状態など)を定義します。', suggestion: 'ファイルの先頭に以下のfrontmatterを追加してください:\n\n---\ntitle: [章のタイトル]\ndraft: false\n---', location: { line: 1, column: 1 }, }); return results; } // titleフィールドチェック if (!frontmatterData.title) { results.push({ ruleId: `${this.id}/missing-title`, ruleName: 'frontmatter titleフィールド必須', severity: 'error', message: 'frontmatterに「title」フィールドがありません', detail: 'titleフィールドは章のタイトルを定義し、目次や検索で使用されます。', suggestion: 'frontmatterに以下を追加してください:\n\ntitle: [章のタイトル]', location: frontmatterLine ? { line: frontmatterLine, column: 1 } : undefined, context: { expected: 'title フィールドを含むオブジェクト', }, }); } // draftフィールドチェック if (!('draft' in frontmatterData)) { results.push({ ruleId: `${this.id}/missing-draft`, ruleName: 'frontmatter draftフィールド必須', severity: 'error', message: 'frontmatterに「draft」フィールドがありません', detail: 'draftフィールドは文書の公開状態を定義します。falseで公開、trueで下書き状態となります。', suggestion: 'frontmatterに以下を追加してください:\n\ndraft: false', location: frontmatterLine ? { line: frontmatterLine, column: 1 } : undefined, }); } return results; } }