Refactor SUB STORE YAML ASSEMBLER to fix duplicate headers and streamline options parsing

This commit is contained in:
2026-01-09 20:28:32 +03:00
parent 49caf5dc65
commit 50bce03cf8

View File

@@ -1,17 +1,15 @@
/** /**
* SUB STORE YAML ASSEMBLER (v3: With Cleaning Options) * SUB STORE YAML ASSEMBLER (v4: Fix Duplicate Headers)
* * Arguments (via URL hash or Script args): * * Arguments:
* - clear-comments=true : Remove full-line comments and debug headers. * - clear-comments=true
* - clear-manifest=true : Remove 'manifest' block from x-substore. * - clear-manifest=true
* - clear-replacements=true : Remove 'replacements' block from x-substore. * - clear-replacements=true
* * Requires: Files must have header "# @file: filename.yaml" * * Requires: Header "# @file: filename.yaml" in input files.
*/ */
// --- OPTIONS PARSING --- // --- OPTIONS PARSING ---
function normalizeOptions() { function normalizeOptions() {
const args = (typeof $arguments !== "undefined" && $arguments) ? $arguments : {}; const args = (typeof $arguments !== "undefined" && $arguments) ? $arguments : {};
const asBool = (v, def = false) => { const asBool = (v, def = false) => {
if (v === undefined || v === null || v === "") return def; if (v === undefined || v === null || v === "") return def;
if (typeof v === "boolean") return v; if (typeof v === "boolean") return v;
@@ -20,7 +18,6 @@ function normalizeOptions() {
if (["0", "false", "no", "n", "off"].includes(s)) return false; if (["0", "false", "no", "n", "off"].includes(s)) return false;
return def; return def;
}; };
return { return {
clearComments: asBool(args['clear-comments'], false), clearComments: asBool(args['clear-comments'], false),
clearManifest: asBool(args['clear-manifest'], false), clearManifest: asBool(args['clear-manifest'], false),
@@ -29,17 +26,13 @@ function normalizeOptions() {
} }
// --- UTILS --- // --- UTILS ---
function normalizeFiles(rawFiles) { function normalizeFiles(rawFiles) {
const map = new Map(); const map = new Map();
if (!rawFiles || !Array.isArray(rawFiles)) return map; if (!rawFiles || !Array.isArray(rawFiles)) return map;
rawFiles.forEach((content) => { rawFiles.forEach((content) => {
if (typeof content !== 'string') return; if (typeof content !== 'string') return;
const match = content.match(/^#\s*@file:\s*(.+?)(\s|$)/m); const match = content.match(/^#\s*@file:\s*(.+?)(\s|$)/m);
if (match) { if (match) map.set(match[1].trim(), content);
map.set(match[1].trim(), content);
}
}); });
return map; return map;
} }
@@ -60,7 +53,6 @@ function extractKey(block, indentLevel) {
return match ? match[1].trim().replace(/['"]/g, "") : null; return match ? match[1].trim().replace(/['"]/g, "") : null;
} }
// Remove lines that are purely comments
function stripFullLineComments(text) { function stripFullLineComments(text) {
return text.split('\n') return text.split('\n')
.filter(line => !line.trim().startsWith('#')) .filter(line => !line.trim().startsWith('#'))
@@ -85,7 +77,6 @@ function splitBlocks(text, type) {
for (let i = 0; i < lines.length; i++) { for (let i = 0; i < lines.length; i++) {
const line = lines[i]; const line = lines[i];
// Skip internal @file tags
if (line.match(/^#\s*@file:/)) continue; if (line.match(/^#\s*@file:/)) continue;
if (isStart(line)) { if (isStart(line)) {
@@ -97,18 +88,16 @@ function splitBlocks(text, type) {
if (currentBuf.length === 0 && !line.trim()) continue; if (currentBuf.length === 0 && !line.trim()) continue;
currentBuf.push(line); currentBuf.push(line);
} }
if (currentBuf.length > 0) blocks.push(currentBuf.join("\n")); if (currentBuf.length > 0) blocks.push(currentBuf.join("\n"));
return blocks; return blocks;
} }
// --- MERGE LOGIC --- // --- MERGE LOGIC ---
function processSection(sectionName, manifestEntries, fileMap, opts) { function processSection(sectionName, manifestEntries, fileMap, opts) {
let sectionOutput = []; let sectionOutput = [];
const seenKeys = new Set(); const seenKeys = new Set();
// Header // Добавляем заголовок секции
if (!['root', 'x-substore', 'rules'].includes(sectionName)) { if (!['root', 'x-substore', 'rules'].includes(sectionName)) {
sectionOutput.push(`${sectionName}:`); sectionOutput.push(`${sectionName}:`);
} }
@@ -120,16 +109,20 @@ function processSection(sectionName, manifestEntries, fileMap, opts) {
let content = fileMap.get(entry.file); let content = fileMap.get(entry.file);
if (!content) throw new Error(`CRITICAL: File "${entry.file}" not found.`); if (!content) throw new Error(`CRITICAL: File "${entry.file}" not found.`);
// --- APPLY OPTION: clearComments (Part 1: Skip debug headers) ---
if (!opts.clearComments) { if (!opts.clearComments) {
sectionOutput.push(`\n# --- source: ${entry.file} | mode: ${entry.mode || "concat"} ---`); sectionOutput.push(`\n# --- source: ${entry.file} | mode: ${entry.mode || "concat"} ---`);
} }
// --- APPLY OPTION: clearComments (Part 2: Strip content) ---
if (opts.clearComments) { if (opts.clearComments) {
content = stripFullLineComments(content); content = stripFullLineComments(content);
} }
// [FIX] Удаляем дублирующий заголовок x-substore из контента файла,
// но оставляем содержимое (оно уже имеет правильный отступ 2 пробела)
if (sectionName === 'x-substore') {
content = content.replace(/^x-substore:\s*(?:#.*)?$/m, '');
}
const mode = entry.mode || "concat"; const mode = entry.mode || "concat";
if (mode === 'concat') { if (mode === 'concat') {
@@ -151,8 +144,6 @@ function processSection(sectionName, manifestEntries, fileMap, opts) {
for (const block of blocks) { for (const block of blocks) {
let id = null; let id = null;
// Logic to extract ID
if (blockType === 'list') { if (blockType === 'list') {
id = extractName(block); id = extractName(block);
} else if (blockType === 'map0') { } else if (blockType === 'map0') {
@@ -161,7 +152,7 @@ function processSection(sectionName, manifestEntries, fileMap, opts) {
id = extractKey(block, 2); id = extractKey(block, 2);
} }
// --- APPLY OPTIONS: clearManifest / clearReplacements --- // Apply cleaning options for x-substore content
if (sectionName === 'x-substore' && id) { if (sectionName === 'x-substore' && id) {
if (opts.clearManifest && id === 'manifest') continue; if (opts.clearManifest && id === 'manifest') continue;
if (opts.clearReplacements && id === 'replacements') continue; if (opts.clearReplacements && id === 'replacements') continue;
@@ -174,7 +165,6 @@ function processSection(sectionName, manifestEntries, fileMap, opts) {
} }
seenKeys.add(id); seenKeys.add(id);
} }
sectionOutput.push(block); sectionOutput.push(block);
} }
} }
@@ -184,12 +174,10 @@ function processSection(sectionName, manifestEntries, fileMap, opts) {
} }
// --- MAIN EXECUTION --- // --- MAIN EXECUTION ---
try { try {
const opts = normalizeOptions(); const opts = normalizeOptions();
const fileMap = normalizeFiles($files); const fileMap = normalizeFiles($files);
// Find Manifest
let manifestKey = null; let manifestKey = null;
for (const k of fileMap.keys()) { for (const k of fileMap.keys()) {
if (k.startsWith("00-manifest")) { if (k.startsWith("00-manifest")) {
@@ -199,7 +187,6 @@ try {
} }
if (!manifestKey) throw new Error("Manifest file (00-manifest-...) not found."); if (!manifestKey) throw new Error("Manifest file (00-manifest-...) not found.");
// Parse Manifest
const manifestRaw = fileMap.get(manifestKey); const manifestRaw = fileMap.get(manifestKey);
const manifestObj = ProxyUtils.yaml.safeLoad(manifestRaw); const manifestObj = ProxyUtils.yaml.safeLoad(manifestRaw);
@@ -210,7 +197,6 @@ try {
const manifestList = manifestObj['x-substore'].manifest; const manifestList = manifestObj['x-substore'].manifest;
const replacements = manifestObj['x-substore'].replacements || []; const replacements = manifestObj['x-substore'].replacements || [];
// Plan
const sectionOrder = [ const sectionOrder = [
"x-substore", "root", "hosts", "sniffer", "tun", "dns", "x-substore", "root", "hosts", "sniffer", "tun", "dns",
"proxies", "proxy-providers", "proxy-groups", "rule-providers", "rules" "proxies", "proxy-providers", "proxy-groups", "rule-providers", "rules"
@@ -223,20 +209,15 @@ try {
plan[entry.section].push(entry); plan[entry.section].push(entry);
}); });
// Assemble
const finalChunks = []; const finalChunks = [];
for (const sec of sectionOrder) { for (const sec of sectionOrder) {
if (!plan[sec] || plan[sec].length === 0) continue; if (!plan[sec] || plan[sec].length === 0) continue;
const secStr = processSection(sec, plan[sec], fileMap, opts); const secStr = processSection(sec, plan[sec], fileMap, opts);
finalChunks.push(secStr); 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"); let result = finalChunks.join("\n\n");
// Replacements
if (Array.isArray(replacements)) { if (Array.isArray(replacements)) {
replacements.forEach(rep => { replacements.forEach(rep => {
if (rep.from && rep.to) { if (rep.from && rep.to) {