/** * 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}`; }