diff --git a/config-sub-converter/scripts/convert-awg-to-clash-dev.js b/config-sub-converter/scripts/convert-awg-to-clash-dev.js new file mode 100644 index 0000000..30ba921 --- /dev/null +++ b/config-sub-converter/scripts/convert-awg-to-clash-dev.js @@ -0,0 +1,252 @@ +/** + * SUB STORE YAML ASSEMBLER (v3: With Cleaning Options) + * * Arguments (via URL hash or Script args): + * - clear-comments=true : Remove full-line comments and debug headers. + * - clear-manifest=true : Remove 'manifest' block from x-substore. + * - clear-replacements=true : Remove 'replacements' block from x-substore. + * * Requires: Files must have header "# @file: filename.yaml" + */ + +// --- OPTIONS PARSING --- + +function normalizeOptions() { + const args = (typeof $arguments !== "undefined" && $arguments) ? $arguments : {}; + + const asBool = (v, def = false) => { + if (v === undefined || v === null || v === "") return def; + if (typeof v === "boolean") return v; + const s = String(v).toLowerCase().trim(); + if (["1", "true", "yes", "y", "on"].includes(s)) return true; + if (["0", "false", "no", "n", "off"].includes(s)) return false; + return def; + }; + + return { + clearComments: asBool(args['clear-comments'], false), + clearManifest: asBool(args['clear-manifest'], false), + clearReplacements: asBool(args['clear-replacements'], false), + }; +} + +// --- UTILS --- + +function normalizeFiles(rawFiles) { + const map = new Map(); + if (!rawFiles || !Array.isArray(rawFiles)) return map; + + rawFiles.forEach((content) => { + if (typeof content !== 'string') return; + const match = content.match(/^#\s*@file:\s*(.+?)(\s|$)/m); + if (match) { + map.set(match[1].trim(), content); + } + }); + return map; +} + +function cleanText(text) { + return String(text || "").replace(/\r\n/g, "\n"); +} + +function extractName(block) { + const match = block.match(/^ {4}name:\s*(?:["']?)(.*?)(?:["']?)\s*(?:#.*)?$/m); + return match ? match[1].trim() : null; +} + +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; +} + +// Remove lines that are purely comments +function stripFullLineComments(text) { + return text.split('\n') + .filter(line => !line.trim().startsWith('#')) + .join('\n'); +} + +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}[^ \-#][^:]*:/); + const isMapStart0 = (l) => l.match(/^[^ \-#][^:]*:/); + + 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]; + // Skip internal @file tags + 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, opts) { + let sectionOutput = []; + const seenKeys = new Set(); + + // Header + if (!['root', 'x-substore', 'rules'].includes(sectionName)) { + sectionOutput.push(`${sectionName}:`); + } + if (sectionName === 'x-substore') { + sectionOutput.push(`x-substore:`); + } + + for (const entry of manifestEntries) { + let content = fileMap.get(entry.file); + if (!content) throw new Error(`CRITICAL: File "${entry.file}" not found.`); + + // --- APPLY OPTION: clearComments (Part 1: Skip debug headers) --- + if (!opts.clearComments) { + sectionOutput.push(`\n# --- source: ${entry.file} | mode: ${entry.mode || "concat"} ---`); + } + + // --- APPLY OPTION: clearComments (Part 2: Strip content) --- + if (opts.clearComments) { + content = stripFullLineComments(content); + } + + const mode = entry.mode || "concat"; + + if (mode === 'concat') { + 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 && !opts.clearComments) { + sectionOutput.push(`# WARNING: No valid blocks found in ${entry.file}`); + } + + for (const block of blocks) { + let id = null; + + // Logic to extract ID + if (blockType === 'list') { + id = extractName(block); + } else if (blockType === 'map0') { + id = extractKey(block, 0); + } else { + id = extractKey(block, 2); + } + + // --- APPLY OPTIONS: clearManifest / clearReplacements --- + if (sectionName === 'x-substore' && id) { + if (opts.clearManifest && id === 'manifest') continue; + if (opts.clearReplacements && id === 'replacements') continue; + } + + if (id) { + if (seenKeys.has(id)) { + if (!opts.clearComments) sectionOutput.push(`# [SKIP] Duplicate "${id}" ignored`); + continue; + } + seenKeys.add(id); + } + + sectionOutput.push(block); + } + } + } + + return sectionOutput.join("\n"); +} + +// --- MAIN EXECUTION --- + +try { + const opts = normalizeOptions(); + const fileMap = normalizeFiles($files); + + // Find Manifest + let manifestKey = null; + for (const k of fileMap.keys()) { + if (k.startsWith("00-manifest")) { + manifestKey = k; + break; + } + } + if (!manifestKey) throw new Error("Manifest file (00-manifest-...) not found."); + + // Parse Manifest + const manifestRaw = fileMap.get(manifestKey); + const manifestObj = ProxyUtils.yaml.safeLoad(manifestRaw); + + if (!manifestObj?.['x-substore']?.manifest) { + throw new Error("Invalid Manifest structure."); + } + + const manifestList = manifestObj['x-substore'].manifest; + const replacements = manifestObj['x-substore'].replacements || []; + + // Plan + 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); + }); + + // Assemble + const finalChunks = []; + + for (const sec of sectionOrder) { + if (!plan[sec] || plan[sec].length === 0) continue; + const secStr = processSection(sec, plan[sec], fileMap, opts); + finalChunks.push(secStr); + } + + // Join with double newline if comments allowed, else single/compact might be preferred, + // but double is safer for YAML structure readability. + let result = finalChunks.join("\n\n"); + + // Replacements + 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:\n# ${err.message}\n# Stack: ${err.stack}`; +} \ No newline at end of file