Files
clash-rules/config-sub-converter/scripts/assemble-mihomo-config.js

239 lines
7.4 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* SUB STORE YAML ASSEMBLER (Content-Tag Aware)
* * Требование: Каждый файл должен начинаться с комментария:
* # @file: filename.yaml
*/
// --- UTILS ---
// Функция нормализации теперь парсит содержимое, чтобы найти имя файла
function normalizeFiles(rawFiles) {
const map = new Map();
if (!rawFiles || !Array.isArray(rawFiles)) return map;
rawFiles.forEach((content, index) => {
if (typeof content !== 'string') return;
// Ищем магический тег: # @file: filename.yaml
const match = content.match(/^#\s*@file:\s*(.+?)(\s|$)/m);
if (match) {
const filename = match[1].trim();
map.set(filename, content);
} else {
// Если тега нет, файл остается "анонимным" и недоступным через манифест,
// но мы можем логировать это.
// console.log(`File at index ${index} has no @file tag`);
}
});
return map;
}
function cleanText(text) {
return String(text || "").replace(/\r\n/g, "\n");
}
// Извлечение name из элемента списка
function extractName(block) {
// Ищем name: value с учетом отступов (4 пробела)
const match = block.match(/^ {4}name:\s*(?:["']?)(.*?)(?:["']?)\s*(?:#.*)?$/m);
return match ? match[1].trim() : null;
}
// Извлечение ключа из map (0 или 2 пробела)
function extractKey(block, indentLevel) {
const spaceStr = " ".repeat(indentLevel);
const re = new RegExp(`^${spaceStr}([^#\\s][^:]+):`);
const match = block.match(re);
return match ? match[1].trim().replace(/['"]/g, "") : null;
}
// Разбивка текста на логические блоки
function splitBlocks(text, type) {
const lines = cleanText(text).split("\n");
const blocks = [];
let currentBuf = [];
// Детекторы начала блока
const isListStart = (l) => l.match(/^ {2}-\s/); // " - "
const isMapStart2 = (l) => l.match(/^ {2}[^ \-#][^:]*:/); // " key:"
const isMapStart0 = (l) => l.match(/^[^ \-#][^:]*:/); // "key:" (root)
const isStart = (l) => {
if (type === 'list') return isListStart(l);
if (type === 'map2') return isMapStart2(l);
if (type === 'map0') return isMapStart0(l);
return false;
};
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
// Если строка начинается с # @file:, пропускаем ее, чтобы не мусорить в конфиге
if (line.match(/^#\s*@file:/)) continue;
if (isStart(line)) {
if (currentBuf.length > 0) {
blocks.push(currentBuf.join("\n"));
currentBuf = [];
}
}
// Пропуск пустых строк в начале, если буфер пуст
if (currentBuf.length === 0 && !line.trim()) continue;
currentBuf.push(line);
}
if (currentBuf.length > 0) {
blocks.push(currentBuf.join("\n"));
}
return blocks;
}
// --- MERGE LOGIC ---
function processSection(sectionName, manifestEntries, fileMap) {
let sectionOutput = [];
const seenKeys = new Set();
// Добавляем заголовок секции (кроме root, x-substore и rules)
if (!['root', 'x-substore', 'rules'].includes(sectionName)) {
sectionOutput.push(`${sectionName}:`);
}
if (sectionName === 'x-substore') {
sectionOutput.push(`x-substore:`);
}
for (const entry of manifestEntries) {
const content = fileMap.get(entry.file);
if (!content) {
// FAIL-FAST: Если файл из манифеста не найден (нет тега или не загружен)
throw new Error(`CRITICAL: File "${entry.file}" not found inside input bundle. Did you add '# @file: ${entry.file}' header?`);
}
const mode = entry.mode || "concat";
sectionOutput.push(`\n# --- source: ${entry.file} | mode: ${mode} ---`);
if (mode === 'concat') {
// Просто чистим от тега @file при вставке
const lines = cleanText(content).split('\n').filter(l => !l.match(/^#\s*@file:/));
sectionOutput.push(lines.join('\n'));
continue;
}
if (mode === 'first_wins') {
let blockType = 'map2';
if (sectionName === 'root') blockType = 'map0';
if (['proxies', 'proxy-groups'].includes(sectionName)) blockType = 'list';
const blocks = splitBlocks(content, blockType);
if (blocks.length === 0) {
sectionOutput.push(`# WARNING: No valid blocks found in ${entry.file}`);
}
for (const block of blocks) {
let id = null;
if (blockType === 'list') {
id = extractName(block);
} else if (blockType === 'map0') {
id = extractKey(block, 0);
} else {
id = extractKey(block, 2);
}
if (id) {
if (seenKeys.has(id)) {
sectionOutput.push(`# [SKIP] Duplicate "${id}" ignored`);
continue;
}
seenKeys.add(id);
}
sectionOutput.push(block);
}
}
}
return sectionOutput.join("\n");
}
// --- MAIN EXECUTION ---
try {
// 1. Создаем карту файлов на основе тегов # @file:
const fileMap = normalizeFiles($files);
// 2. Ищем Манифест
let manifestKey = null;
// Ищем файл, чье имя (из тега) начинается с 00-manifest
for (const k of fileMap.keys()) {
if (k.startsWith("00-manifest")) {
manifestKey = k;
break;
}
}
if (!manifestKey) {
const foundFiles = Array.from(fileMap.keys()).join(", ");
throw new Error(`Manifest file (00-manifest-...) not found in headers. Found files: [${foundFiles}]`);
}
// 3. Парсим Манифест
const manifestRaw = fileMap.get(manifestKey);
const manifestObj = ProxyUtils.yaml.safeLoad(manifestRaw);
if (!manifestObj?.['x-substore']?.manifest) {
throw new Error("Invalid Manifest: missing x-substore.manifest structure.");
}
const manifestList = manifestObj['x-substore'].manifest;
const replacements = manifestObj['x-substore'].replacements || [];
// 4. Планирование порядка секций
const sectionOrder = [
"x-substore", "root", "hosts", "sniffer", "tun", "dns",
"proxies", "proxy-providers", "proxy-groups", "rule-providers", "rules"
];
const plan = {};
sectionOrder.forEach(s => plan[s] = []);
manifestList.forEach(entry => {
// Если секция в манифесте есть, а в нашем плане нет - добавляем динамически (на всякий случай)
if (!plan[entry.section]) plan[entry.section] = [];
plan[entry.section].push(entry);
});
// 5. Сборка
const finalChunks = [];
for (const sec of sectionOrder) {
if (!plan[sec] || plan[sec].length === 0) continue;
// Обработка секции
const secStr = processSection(sec, plan[sec], fileMap);
finalChunks.push(secStr);
}
let result = finalChunks.join("\n\n");
// 6. Replacements (Literal)
if (Array.isArray(replacements)) {
replacements.forEach(rep => {
if (rep.from && rep.to) {
result = result.split(rep.from).join(rep.to);
}
});
}
$content = result;
} catch (err) {
$content = `# CRITICAL ERROR in Assembler:\n# ${err.message}\n\n# Stack:\n${err.stack}`;
}