aboutsummaryrefslogtreecommitdiffhomepage
path: root/scripts/migrate-to-question-summary.mjs
blob: 75cabd29d0c8376c9ff88c3784fc58066cab8f7a (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
import { readFileSync, writeFileSync } from "node:fs";
import { readdir } from "node:fs/promises";
import { join, extname, relative } from "node:path";

const DOCS_DIR = join(import.meta.dirname, "..", "src", "content", "docs");

/**
 * Recursively collect all .mdx files, excluding files starting with "_" or "index".
 */
async function collectMdxFiles(dir) {
  const entries = await readdir(dir, { withFileTypes: true });
  const files = [];

  for (const entry of entries) {
    if (entry.name.startsWith("_")) continue;
    const fullPath = join(dir, entry.name);
    if (entry.isDirectory()) {
      const subFiles = await collectMdxFiles(fullPath);
      files.push(...subFiles);
    } else if (extname(entry.name) === ".mdx" && entry.name !== "index.mdx") {
      files.push(fullPath);
    }
  }

  return files;
}

/**
 * Extract Q&A rows from the summary table in the MDX content.
 * Table format:
 *   | 質問 | 答弁概要(クリックで詳細) |
 *   |---|---|
 *   | ① question text | [answer text](#anchor) |
 */
function extractQATable(content) {
  // Find the summary table: starts with "| 質問 | 答弁概要"
  const tableStartRegex =
    /\| 質問 \| 答弁概要(クリックで詳細) \|\r?\n\|[-| ]+\|\r?\n/;
  const tableStartMatch = content.match(tableStartRegex);
  if (!tableStartMatch) return null;

  const tableStart = tableStartMatch.index;
  const afterHeader = content.slice(tableStart + tableStartMatch[0].length);

  // Collect table rows
  const rows = [];
  const lines = afterHeader.split(/\r?\n/);
  for (const line of lines) {
    const rowMatch = line.match(/^\| (.+?) \| \[(.+?)\]\((#.+?)\)\s*\|$/);
    if (!rowMatch) break;
    rows.push({
      question: rowMatch[1].trim(),
      answer: rowMatch[2].trim(),
      anchor: rowMatch[3].replace(/^#/, "").trim(),
    });
  }

  if (rows.length === 0) return null;

  // Calculate the end of the table (header + separator + all rows)
  const tableText = content.slice(tableStart);
  const tableEndMatch = tableText.match(
    new RegExp(
      `(?:^\\| .+? \\| \\[.+?\\]\\(#.+?\\)\\s*\\|$(?:\\r?\\n)?){${rows.length}}`,
      "m",
    ),
  );
  if (!tableEndMatch) return null;

  const tableEnd =
    tableStart + tableStartMatch[0].length + tableEndMatch[0].length;

  return {
    rows,
    tableStart,
    tableEnd,
  };
}

/**
 * Extract frontmatter fields from MDX content.
 */
function extractFrontmatter(content) {
  const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
  if (!match) return {};
  const fm = match[1];

  const titleMatch = fm.match(/^title:\s*["']?(.+?)["']?\s*$/m);
  const dateMatch = fm.match(
    /^(?:first|date):\s*["']?(\d{4}-\d{2}-\d{2})["']?\s*$/m,
  );

  return {
    title: titleMatch ? titleMatch[1].trim() : null,
    date: dateMatch ? dateMatch[1].trim() : null,
  };
}

/**
 * Ensure the import line for QuestionSummary exists in the import block.
 */
function addImport(content, importPath) {
  const importLine = `import QuestionSummary from '${importPath}';`;

  if (content.includes("QuestionSummary")) {
    return content; // Already imported
  }

  // Find the last import statement and insert after it
  const importRegex = /^(import .+)$/gm;
  const imports = [...content.matchAll(importRegex)];
  if (imports.length > 0) {
    const lastImport = imports[imports.length - 1];
    const insertPos = lastImport.index + lastImport[0].length;
    return (
      content.slice(0, insertPos) + "\n" + importLine + content.slice(insertPos)
    );
  }

  return content; // No existing imports
}

/**
 * Generate the QuestionSummary component JSX string.
 */
function generateQuestionSummary(qaRows, headline, datePublished) {
  const qaEntries = qaRows
    .map((row) => {
      return `    { question: ${JSON.stringify(row.question)}, answer: ${JSON.stringify(row.answer)}, anchor: ${JSON.stringify(row.anchor)} },`;
    })
    .join("\n");

  const props = [];
  if (headline) props.push(`  headline=${JSON.stringify(headline)}`);
  if (datePublished)
    props.push(`  datePublished=${JSON.stringify(datePublished)}`);

  return `<QuestionSummary\n${props.join("\n")}\n  qa={[\n${qaEntries}\n  ]}\n/>`;
}

async function main() {
  const files = await collectMdxFiles(DOCS_DIR);
  console.log(`Found ${files.length} MDX files to check.`);

  let migrated = 0;
  let skipped = 0;

  for (const file of files) {
    const content = readFileSync(file, "utf-8");
    const tableData = extractQATable(content);

    if (!tableData) {
      skipped++;
      continue;
    }

    const fm = extractFrontmatter(content);

    // Generate the replacement component
    const component = generateQuestionSummary(
      tableData.rows,
      fm.title,
      fm.date,
    );

    // Replace the table with the component
    let newContent =
      content.slice(0, tableData.tableStart) +
      component +
      content.slice(tableData.tableEnd);

    // Ensure the import exists
    newContent = addImport(newContent, "@/components/QuestionSummary.astro");

    writeFileSync(file, newContent, "utf-8");
    const relPath = relative(DOCS_DIR, file);
    console.log(`  Migrated: ${relPath} (${tableData.rows.length} QA pairs)`);
    migrated++;
  }

  console.log(`\nDone: ${migrated} migrated, ${skipped} skipped.`);
}

main().catch((err) => {
  console.error("Error:", err);
  process.exit(1);
});