Add SUB STORE YAML ASSEMBLER script with cleaning options for enhanced processing
This commit is contained in:
252
config-sub-converter/scripts/convert-awg-to-clash-dev.js
Normal file
252
config-sub-converter/scripts/convert-awg-to-clash-dev.js
Normal file
@@ -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}`;
|
||||
}
|
||||
Reference in New Issue
Block a user