67798ba feat: 重构 split_commands.js 输出多版本 en.md
 scripts/split_commands.js   | 143 +++++++++++++++++++++++++++++++++++---------
 test/split_commands.test.js |  67 +++++++++++++++++++++
 2 files changed, 181 insertions(+), 29 deletions(-)
diff --git a/scripts/split_commands.js b/scripts/split_commands.js
index 2dc492d..1356f34 100644
--- a/scripts/split_commands.js
+++ b/scripts/split_commands.js
@@ -1,52 +1,62 @@
 const fs = require('fs');
 const path = require('path');
+const matter = require('gray-matter');
 
-const schemaPath = path.join(__dirname, '../data/vivado_api_schema.json');
-const rawData = fs.readFileSync(schemaPath, 'utf8');
-
-let inString = false;
-let cleanedData = '';
-for (let i = 0; i < rawData.length; i++) {
-    let char = rawData[i];
-    if (char === '"' && rawData[i - 1] !== '\\') inString = !inString;
-    if (inString) {
-        if (char === '\n') cleanedData += '\\n';
-        else if (char === '\r') cleanedData += '\\r';
-        else if (char === '\t') cleanedData += '\\t';
-        else cleanedData += char;
-    } else {
-        cleanedData += char;
+const dataDir = path.join(__dirname, '..', 'data');
+const commandsDir = path.join(dataDir, 'commands');
+
+function readSchemaJson(filePath) {
+    const rawData = fs.readFileSync(filePath, 'utf8');
+    let inString = false;
+    let cleanedData = '';
+    for (let i = 0; i < rawData.length; i++) {
+        let char = rawData[i];
+        if (char === '"' && rawData[i - 1] !== '\\') inString = !inString;
+        if (inString) {
+            if (char === '\n') cleanedData += '\\n';
+            else if (char === '\r') cleanedData += '\\r';
+            else if (char === '\t') cleanedData += '\\t';
+            else cleanedData += char;
+        } else {
+            cleanedData += char;
+        }
     }
+    return JSON.parse(cleanedData);
 }
 
-const commands = JSON.parse(cleanedData);
-const outputDir = path.join(__dirname, '../data/commands');
-if (!fs.existsSync(outputDir)) fs.mkdirSync(outputDir, { recursive: true });
+function parseCommand(cmdObj) {
+    const cmdName = cmdObj.name || cmdObj.command;
+    if (!cmdName) return null;
+
+    if (cmdObj.full_help !== undefined) {
+        return {
+            name: cmdName,
+            short_description: cmdObj.short_description || "",
+            full_help: cmdObj.full_help || "",
+            arguments: cmdObj.arguments || []
+        };
+    }
 
-commands.forEach(cmdObj => {
-    const cmdName = cmdObj.command;
     const fullText = cmdObj.description || "";
-    
     let shortDesc = "";
     const descMatch = fullText.match(/Description:\s*\n([\s\S]*?)(?=\n\nSyntax:|\n\nUsage:|$)/);
     if (descMatch) {
         shortDesc = descMatch[1].trim().replace(/\n/g, ' ');
     }
 
     let argsArray = [];
     const usageMatch = fullText.match(/Usage:\s*\n([\s\S]*?)(?=\n\nCategories:|$)/);
-    
+
     if (usageMatch) {
         const usageText = usageMatch[1];
-        const lines = usageText.split(/\r?\n/); 
-        
+        const lines = usageText.split(/\r?\n/);
         let currentArg = null;
 
         for (let line of lines) {
             if (line.includes('---') || line.match(/^\s*Name\s+Description/)) {
                 continue;
             }
 
             const flagMatch = line.match(/^\s*\[?(-[a-zA-Z0-9_]+)\]?\s+(.*)/);
             const positionalMatch = line.match(/^\s*\[?(<[a-zA-Z0-9_]+>)\]?\s+(.*)/);
 
@@ -57,22 +67,97 @@ commands.forEach(cmdObj => {
                 };
                 argsArray.push(currentArg);
             } else if (positionalMatch) {
                 currentArg = null;
             } else if (currentArg && line.trim().length > 0) {
                 currentArg.description += " " + line.trim();
             }
         }
     }
 
-    const commandData = {
+    return {
         name: cmdName,
         short_description: shortDesc,
         full_help: fullText,
-        custom_snippet: null, 
         arguments: argsArray
     };
+}
+
+function processSchemas(versionSchemas) {
+    const commandsMap = new Map();
+
+    for (const { version, schema } of versionSchemas) {
+        for (const rawCmd of schema) {
+            const cmd = parseCommand(rawCmd);
+            if (!cmd) continue;
+
+            if (!commandsMap.has(cmd.name)) {
+                commandsMap.set(cmd.name, {
+                    versions: [version],
+                    latestCmd: cmd,
+                    latestVersion: version
+                });
+            } else {
+                const entry = commandsMap.get(cmd.name);
+                if (!entry.versions.includes(version)) {
+                    entry.versions.push(version);
+                }
+                if (version > entry.latestVersion) {
+                    entry.latestVersion = version;
+                    entry.latestCmd = cmd;
+                }
+            }
+        }
+    }
+
+    for (const data of commandsMap.values()) {
+        data.versions.sort();
+    }
 
-    fs.writeFileSync(path.join(outputDir, `${cmdName}.json`), JSON.stringify(commandData, null, 2));
-});
+    return commandsMap;
+}
+
+function runSplitCommands() {
+    const files = fs.readdirSync(dataDir).filter(f => f.startsWith('vivado_api_') && f.endsWith('.json'));
+    const versionSchemas = [];
+
+    for (const file of files) {
+        const versionMatch = file.match(/vivado_api_(.+)\.json/);
+        if (!versionMatch) continue;
+        const version = versionMatch[1];
+        const schema = readSchemaJson(path.join(dataDir, file));
+        versionSchemas.push({ version, schema });
+    }
+
+    const commandsMap = processSchemas(versionSchemas);
+
+    if (!fs.existsSync(commandsDir)) fs.mkdirSync(commandsDir, { recursive: true });
+
+    for (const [cmdName, data] of commandsMap.entries()) {
+        const cmd = data.latestCmd;
+        const cmdDir = path.join(commandsDir, cmdName);
+        if (!fs.existsSync(cmdDir)) fs.mkdirSync(cmdDir, { recursive: true });
+
+        const frontmatter = {
+            versions: data.versions,
+            short_description: cmd.short_description || "",
+            arguments: cmd.arguments || []
+        };
+
+        const mdContent = matter.stringify(cmd.full_help || cmdName, frontmatter);
+        const enMdPath = path.join(cmdDir, 'en.md');
+        fs.writeFileSync(enMdPath, mdContent);
+    }
+
+    console.log(`Generated ${commandsMap.size} command directories.`);
+}
+
+if (require.main === module) {
+    runSplitCommands();
+}
 
-console.log(`🎉 跨行提取与空数组 Bug 修复完毕！`);
\ No newline at end of file
+module.exports = {
+    readSchemaJson,
+    parseCommand,
+    processSchemas,
+    runSplitCommands
+};
\ No newline at end of file
diff --git a/test/split_commands.test.js b/test/split_commands.test.js
new file mode 100644
index 0000000..aa5ae56
--- /dev/null
+++ b/test/split_commands.test.js
@@ -0,0 +1,67 @@
+const test = require('node:test');
+const assert = require('node:assert/strict');
+const fs = require('fs');
+const path = require('path');
+const os = require('os');
+
+const { readSchemaJson, parseCommand, processSchemas } = require('../scripts/split_commands.js');
+
+test('readSchemaJson handles raw control characters in string literals', () => {
+    const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'vivado-test-'));
+    const filePath = path.join(tmpDir, 'test_schema.json');
+    const rawContent = '[\n  {\n    "command": "test_cmd",\n    "description": "line1\nline2"\n  }\n]';
+    fs.writeFileSync(filePath, rawContent, 'utf8');
+
+    const result = readSchemaJson(filePath);
+    assert.equal(result.length, 1);
+    assert.equal(result[0].command, 'test_cmd');
+    assert.equal(result[0].description, 'line1\nline2');
+
+    fs.rmSync(tmpDir, { recursive: true, force: true });
+});
+
+test('parseCommand extracts short_description and arguments correctly', () => {
+    const rawCmd = {
+        command: "add_cells_to_pblock",
+        description: "add_cells_to_pblock\n\nDescription: \nAdd cells to a Pblock\n\nSyntax: \nadd_cells_to_pblock [-top]\n\nUsage: \n  Name  Description\n  -------------------\n  [-top] Add top instance\n\nCategories: \nFloorplan\n"
+    };
+
+    const parsed = parseCommand(rawCmd);
+    assert.equal(parsed.name, "add_cells_to_pblock");
+    assert.equal(parsed.short_description, "Add cells to a Pblock");
+    assert.deepEqual(parsed.arguments, [{ flag: "-top", description: "Add top instance" }]);
+    assert.equal(parsed.full_help, rawCmd.description);
+});
+
+test('processSchemas aggregates multi-version metadata and selects latestCmd', () => {
+    const versionSchemas = [
+        {
+            version: '2023.2',
+            schema: [
+                { command: 'cmd1', description: 'Description:\nOld cmd1' },
+                { command: 'cmd2', description: 'Description:\nCmd2 in 2023' }
+            ]
+        },
+        {
+            version: '2024.2',
+            schema: [
+                { command: 'cmd1', description: 'Description:\nNew cmd1' },
+                { command: 'cmd3', description: 'Description:\nCmd3 in 2024' }
+            ]
+        }
+    ];
+
+    const commandsMap = processSchemas(versionSchemas);
+
+    assert.equal(commandsMap.size, 3);
+
+    const cmd1 = commandsMap.get('cmd1');
+    assert.deepEqual(cmd1.versions, ['2023.2', '2024.2']);
+    assert.equal(cmd1.latestCmd.short_description, 'New cmd1');
+
+    const cmd2 = commandsMap.get('cmd2');
+    assert.deepEqual(cmd2.versions, ['2023.2']);
+
+    const cmd3 = commandsMap.get('cmd3');
+    assert.deepEqual(cmd3.versions, ['2024.2']);
+});
