import { LintResult } from '../linter.js'; import { Root } from 'mdast'; import { visit } from 'unist-util-visit'; export function checkStructure(tree: Root): LintResult[] { const results: LintResult[] = []; // Collect all headers const headers: { depth: number; text: string; line: number }[] = []; visit(tree, 'heading', (node: any) => { const text = node.children.map((c: any) => c.value).join(''); headers.push({ depth: node.depth, text: text, line: node.position?.start.line || 0 }); }); // Rule 4: "### 学習目標" exists const goalsHeaderIndex = headers.findIndex(h => h.depth === 3 && h.text === '学習目標'); if (goalsHeaderIndex === -1) { results.push({ ruleId: '4', message: 'Missing "### 学習目標" section' }); } // Rule 6: "## まとめ" exists at the end // It should be the *last* H2 header, or at least exist as an H2. const summaryHeaderIndex = headers.findIndex(h => h.depth === 2 && h.text === 'まとめ'); if (summaryHeaderIndex === -1) { results.push({ ruleId: '6', message: 'Missing "## まとめ" section' }); } else { // Check if it's the last H2? // The rule says "最後に `## まとめ` がある" (Summary is at the end). // We can check if there are any H2s after it. const h2sAfterSummary = headers.slice(summaryHeaderIndex + 1).filter(h => h.depth === 2); if (h2sAfterSummary.length > 0) { results.push({ ruleId: '6', message: '"## まとめ" must be the last H2 section' }); } } // Rule 5: Learning items are H2 (`##`) // Basically, checks that other main sections are H2. // And "### 学習目標" is an exception (H3). // This rule is a bit broad. It probably implies that the main content structure should use H2 for items. // If we identify "Learning Items" as sections between Goals and Summary. if (goalsHeaderIndex !== -1 && summaryHeaderIndex !== -1) { if (goalsHeaderIndex > summaryHeaderIndex) { results.push({ ruleId: '3', message: 'Structure invalid: "学習目標" appears after "まとめ"' }); } else { // Check items between goals and summary // They should be H2? // Actually, "Overview" is before Goals. // Goals (H3) -> Items (H2) -> Summary (H2). // If there is an H2 *before* Goals, that might be "Overview"?? // But Overview usually doesn't have a header in the description: "2. その章の概要文章". // So no header for Overview. // Check for any H2 between start and Goals? // If strict, maybe no H2 allowed before Goals? const h2BeforeGoals = headers.filter(h => h.depth === 2 && headers.indexOf(h) < goalsHeaderIndex); if (h2BeforeGoals.length > 0) { // Maybe warning? Or maybe strict. // The rule says "Overview -> Goals". If Overview has H2, it's fine? // But "学習項目はすべて `##` のH2見出し" implies H2s are Learning Items. // If Overview is H2, is it a Learning Item? No. // Let's assume Overview text has NO header. // So any H2 before Goals is suspicious if Goals is H3. // But wait, H1 is title. // Let's assume NO H2 before Goals. // results.push({ ruleId: '3', message: 'Found H2 section before "### 学習目標". Overview should not have a header or check structure.' }); } // Check sections between Goals (H3) and Summary (H2) // We expect H2s here. // We can iterate headers between goalsHeaderIndex and summaryHeaderIndex for (let i = goalsHeaderIndex + 1; i < summaryHeaderIndex; i++) { const h = headers[i]; // If it's a main section, it should be H2. // Subsections (H3, H4) are allowed inside items. // But we shouldn't have H1. if (h.depth === 1) { results.push({ ruleId: '5', message: 'H1 header found within content. Only one H1 (Title) allowed (handled by frontmatter usually, or top level).' }); } // If we see H2, it's a learning item. Good. } } } return results; }