diff --git a/Rewrite-Parser.beta.js b/Rewrite-Parser.beta.js index 5825c892..1e811448 100644 --- a/Rewrite-Parser.beta.js +++ b/Rewrite-Parser.beta.js @@ -257,6 +257,9 @@ let skipBox = [] //skip-ip let realBox = [] //real-ip let hndelBox = [] //正则剔除的主机名 let sgArg = [] //surge模块参数 +let loonSgArg = [] //转换为 Loon 时实际需要保留的参数 +let surgeRuleToggleArgs = new Map() //Surge 用行首 # 注释控制脚本启停的参数 +let argumentKeyRenameMap = new Map() //Surge 模板参数名 -> 脚本实际读取的 $argument key let hnaddMethod = '%APPEND%' let fheaddMethod = '%APPEND%' @@ -520,6 +523,12 @@ if (binaryInfo != null && binaryInfo.length > 0) { jsSuf = jsSuf + `&evalUrlmodi=${encodeURIComponent(scEvUrlmodi)}` } + const leadingTemplate = takeLeadingTemplate(x) + const leadingTemplateIsNameOnly = !!leadingTemplate && /^\s*=/.test(leadingTemplate.rest) + if (leadingTemplate) { + x = leadingTemplate.rest + } + //模块信息 if (/^#!.+?=\s*$/.test(x)) { } else if (isLooniOS && /^#!(?:select|input)\s*=\s*.+/.test(x)) { @@ -534,7 +543,7 @@ if (binaryInfo != null && binaryInfo.length > 0) { } //hostname - if (/^hostname\s*=.+/.test(x)) hnaddMethod = getHn(x, hnBox, hnaddMethod) + if (/^hostname\s*=.+/.test(x)) hnaddMethod = getHn(x, hnBox, hnaddMethod, leadingTemplate?.key) if (hn2 == true && x.match(hn2name)) hnaddMethod = getHn(x, hnBox, hnaddMethod) @@ -800,6 +809,7 @@ if (binaryInfo != null && binaryInfo.length > 0) { style, scriptname, updatetime, + toggleKey: !leadingTemplateIsNameOnly ? leadingTemplate?.key || '' : '', ori: x, num: y, }) @@ -810,11 +820,13 @@ if (binaryInfo != null && binaryInfo.length > 0) { mark = getMark(y, body) noteK = isNoteK(x) jsurl = getJsInfo(x, /script-path\s*=\s*/) - jsname = /[=,]\s*type\s*=\s*/.test(x) + jsname = leadingTemplateIsNameOnly + ? leadingTemplate.key + : /[=,]\s*type\s*=\s*/.test(x) ? x.split(/\s*=/)[0].replace(/^#/, '') : /,\s*tag\s*=\s*/.test(x) - ? getJsInfo(x, /,\s*tag\s*=\s*/) - : jsurl.substring(jsurl.lastIndexOf('/') + 1, jsurl.lastIndexOf('.')) + ? getJsInfo(x, /,\s*tag\s*=\s*/) + : jsurl.substring(jsurl.lastIndexOf('/') + 1, jsurl.lastIndexOf('.')) img = getJsInfo(x, /[,\s]\s*img-url\s*=\s*/) jsfrom = 'surge' jsurl = toJsc(jsurl, jscStatus, jsc2Status, jsfrom) @@ -832,13 +844,15 @@ if (binaryInfo != null && binaryInfo.length > 0) { cronexp = /cronexpr?\s*=\s*/.test(x) ? getJsInfo(x, /[=,\s]\s*cronexpr?\s*=\s*/) : /cron\s+"/.test(x) - ? x.split('"')[1] - : /cron\s+[^\s]+?\s+/ - ? x.split(/\s/)[1] - : '' + ? x.split('"')[1] + : /cron\s+[^\s]+?\s+/ + ? x.split(/\s/)[1] + : '' ability = getJsInfo(x, /[=,\s]\s*ability\s*=\s*/) engine = getJsInfo(x, /[=,\s]\s*engine\s*=\s*/) - jsenable = getJsInfo(x, /[=,\s]\s*enable\s*=\s*/) + jsenable = getJsInfo(x, /[=,\s]\s*enabled?\s*=\s*/) + jsenable = jsenable || (!leadingTemplateIsNameOnly && leadingTemplate?.key ? `{${leadingTemplate.key}}` : '') + getTemplateKeys(jsenable).forEach(key => surgeRuleToggleArgs.set(key, true)) updatetime = getJsInfo(x, /[=,\s]\s*script-update-interval\s*=\s*/) timeout = getJsInfo(x, /[=,\s]\s*timeout\s*=\s*/) tilesicon = jstype == 'generic' && /icon=/.test(x) ? x.split('icon=')[1].split('&')[0] : '' @@ -892,6 +906,7 @@ if (binaryInfo != null && binaryInfo.length > 0) { eventname, engine, jsenable, + namePrefix: !leadingTemplateIsNameOnly ? leadingTemplate?.key || '' : '', ori: x, num: y, }) @@ -1071,12 +1086,12 @@ if (binaryInfo != null && binaryInfo.length > 0) { shNotify(outBox) //mitm删除主机名 - if (hnDel != null && hnBox.length > 0) hnBox = hnBox.filter(item => hnDel.indexOf(item) == -1) + if (hnDel != null && hnBox.length > 0) hnBox = hnBox.filter(item => hnDel.indexOf(getHnValue(item)) == -1) //mitm正则删除主机名 if (hnRegDel != null) { - hndelBox = hnBox.filter(item => hnRegDel.test(item)) - hnBox = hnBox.filter(item => !hnRegDel.test(item)) + hndelBox = hnBox.filter(item => hnRegDel.test(getHnValue(item))) + hnBox = hnBox.filter(item => !hnRegDel.test(getHnValue(item))) } hndelBox.length > 0 && noNtf == false && $.msg(JS_NAME, notifyName + ' 已根据正则剔除主机名', `${hndelBox}`) @@ -1090,11 +1105,17 @@ if (binaryInfo != null && binaryInfo.length > 0) { let sgargArr = [] for (let i = 0; i < sgArg.length; i++) { let key = sgArg[i].key - let value = sgArg[i].value.split(',')[0].trim() + let value = getSurgeArgumentDefault(sgArg[i], surgeRuleToggleArgs.has(key)) let a = key + ':' + value sgargArr.push(a) } modInfoObj['arguments'] = (sgargArr[0] || '') && `${sgargArr.join(',')}` + modInfoObj['arguments-desc'] = modInfoObj['arguments-desc'] || buildSurgeArgumentsDesc(sgArg) + } + + if (isLooniOS) { + collectArgumentKeyRenameMap(jsBox) + loonSgArg = filterLoonArguments(sgArg, jsBox, hnBox) } //模块信息输出 @@ -1121,8 +1142,12 @@ if (binaryInfo != null && binaryInfo.length > 0) { value = value.includes('mac') ? (value.includes('ios') ? ((delsystem = true), 'mac') : 'mac') : 'ios' } else if (isLooniOS && key == 'category') { key = 'tag' + } else if (!isLooniOS && key == 'tag') { + key = 'category' } else if (!isLooniOS && key == 'keyword') { key = 'category' + } else if (isLooniOS && key == 'arguments-desc') { + continue } let info = !isStashiOS ? '#!' + key + '=' + value : key + ': |-\n ' + value !delsystem && modInfo.push(info) @@ -1137,13 +1162,14 @@ if (binaryInfo != null && binaryInfo.length > 0) { } //模块信息输出结束 //surge模块参数转[Argument]输出 - if (isLooniOS && sgArg.length > 0) { - for (let i = 0; i < sgArg.length; i++) { - let key = sgArg[i].key - let type = sgArg[i].type - let value = sgArg[i].value - if (type == 'switch') value = /^true/.test(value) ? '"true","false"' : '"false","true"' - let tag = sgArg[i].tag + if (isLooniOS && loonSgArg.length > 0) { + applySurgeArgumentsDesc(loonSgArg, modInfoObj['arguments-desc']) + for (let i = 0; i < loonSgArg.length; i++) { + let key = argumentKeyRenameMap.get(loonSgArg[i].key) || loonSgArg[i].key + let isRuleToggle = surgeRuleToggleArgs.has(loonSgArg[i].key) || surgeRuleToggleArgs.has(key) + let type = isRuleToggle ? 'switch' : loonSgArg[i].type + let value = formatLoonArgumentValue(loonSgArg[i], type, isRuleToggle) + let tag = loonSgArg[i].tag loonArg.push(key + '=' + type + ',' + value + ',' + tag) } } @@ -1274,8 +1300,8 @@ if (binaryInfo != null && binaryInfo.length > 0) { isShadowrocket && /-video/.test(rwtype) ? 'reject-img' : isLooniOS && /-tinygif/.test(rwtype) - ? 'reject-img' - : rwtype + ? 'reject-img' + : rwtype URLRewrite.push(mark + noteK + rwptn + ' ' + rwvalue + ' ' + rwtype) break @@ -1412,8 +1438,8 @@ if (binaryInfo != null && binaryInfo.length > 0) { mockurl = mockBox[i].mockurl ? ' data="' + mockBox[i].mockurl + '"' : mocktype == ' data-type=text' - ? ' data=""' - : '' + ? ' data=""' + : '' mockstatus = mockBox[i].mockstatus ? ' status-code=' + mockBox[i].mockstatus : '' switch (targetApp) { @@ -1443,10 +1469,10 @@ if (binaryInfo != null && binaryInfo.length > 0) { (mockBox[i].datapath ? ` data-path=${mockBox[i].datapath}` : mockBox[i].data - ? ` data="${mockBox[i].data}"` - : mockBox[i].mockurl - ? ` data-path=${mockBox[i].mockurl}` - : '') + + ? ` data="${mockBox[i].data}"` + : mockBox[i].mockurl + ? ` data-path=${mockBox[i].mockurl}` + : '') + mockstatus + (mockBox[i].mockbase64 ? ' mock-data-is-base64=true' : '') ) @@ -1488,8 +1514,8 @@ if (binaryInfo != null && binaryInfo.length > 0) { isLooniOS && /event/.test(jstype) ? 'network-changed' : !isLooniOS && /network-changed/.test(jstype) - ? 'event' - : jstype + ? 'event' + : jstype jsurl = jsBox[i].jsurl rebody = jsBox[i].rebody ? istrue(jsBox[i].rebody) : '' proto = jsBox[i].proto ? istrue(jsBox[i].proto) : '' @@ -1503,20 +1529,28 @@ if (binaryInfo != null && binaryInfo.length > 0) { timeout = jsBox[i].timeout ? jsBox[i].timeout : '' jsarg = jsBox[i].jsarg ? jsBox[i].jsarg : '' ori = jsBox[i].ori + let scriptPrefix = '' jsarg = reJsValue(nArgTarget || 'null', nArg, jsname, ori, jsarg) .replace(/t;amp;/g, '&') .replace(/t;add;/g, '+') + jsarg = normalizeScriptArgument(jsarg, targetApp) cronexp = reJsValue(nCron || 'null', ncronexp, jsname, ori, cronexp) + cronexp = normalizeTemplateValue(cronexp, targetApp) - cronexp = /,/.test(cronexp) ? '"' + cronexp + '"' : cronexp + cronexp = formatCronexp(cronexp, targetApp) jsname = reJsValue(njsnametarget || 'null', njsname, jsname, ori, jsname) + if (isLooniOS && jsBox[i].namePrefix && !jsname.startsWith(jsBox[i].namePrefix)) { + jsname = jsBox[i].namePrefix + jsname + } timeout = reJsValue(timeoutt || 'null', timeoutv, jsname, ori, timeout) engine = reJsValue(enginet || 'null', enginev, jsname, ori, engine) + jsenable = normalizeTemplateValue(jsenable, targetApp) + scriptPrefix = isSurgeiOS || isShadowrocket ? getSurgeRuleTogglePrefix(jsenable) : '' switch (targetApp) { case 'surge-module': @@ -1528,8 +1562,10 @@ if (binaryInfo != null && binaryInfo.length > 0) { timeout = timeout ? ', timeout=' + timeout : '' engine = engine && isSurgeiOS ? ', engine=' + engine : '' jsenable = jsenable && isLooniOS ? ', enable=' + jsenable : '' - if (jsarg != '' && /,/.test(jsarg) && !/^".+"$/.test(jsarg)) jsarg = ', argument="' + jsarg + '"' - if (jsarg != '' && (!/,/.test(jsarg) || /^".+"$/.test(jsarg))) jsarg = ', argument=' + jsarg + if (jsarg != '' && /,/.test(jsarg) && !/^".+"$/.test(jsarg) && !isLoonArgumentContainer(jsarg)) + jsarg = ', argument="' + jsarg + '"' + if (jsarg != '' && (!/,/.test(jsarg) || /^".+"$/.test(jsarg) || isLoonArgumentContainer(jsarg))) + jsarg = ', argument=' + jsarg if (/generic/.test(jstype) && isShadowrocket) { otherRule.push(ori) @@ -1558,6 +1594,7 @@ if (binaryInfo != null && binaryInfo.length > 0) { : script.push( mark + noteK + + scriptPrefix + jsname + ' = type=' + jstype + @@ -1577,6 +1614,7 @@ if (binaryInfo != null && binaryInfo.length > 0) { script.push( mark + noteK + + scriptPrefix + jsname + ' = type=' + jstype + @@ -1593,6 +1631,7 @@ if (binaryInfo != null && binaryInfo.length > 0) { script.push( mark + noteK + + scriptPrefix + jsname + ' = type=' + jstype + @@ -1607,12 +1646,15 @@ if (binaryInfo != null && binaryInfo.length > 0) { jsarg ) } else if (jstype == 'cron' && isLooniOS) { + const loonCronexp = /^\{[^{}]+\}$/.test(`${cronexp ?? ''}`.trim()) + ? `${cronexp ?? ''}`.trim() + : `"${`${cronexp ?? ''}`.replace(/"/g, '')}"` script.push( mark + noteK + jstype + ' ' + - `"${cronexp.replace(/"/g, '')}"` + + loonCronexp + ' script-path=' + jsurl + timeout + @@ -1626,6 +1668,7 @@ if (binaryInfo != null && binaryInfo.length > 0) { script.push( mark + noteK + + scriptPrefix + jsname + ' = type=' + jstype + @@ -1706,15 +1749,15 @@ if (binaryInfo != null && binaryInfo.length > 0) { jsarg && jstype == 'generic' ? noteKn4 + 'argument: |-' + noteKn6 + jsarg : jsarg && jstype != 'generic' - ? noteKn6 + 'argument: |-' + noteKn8 + jsarg - : '' + ? noteKn6 + 'argument: |-' + noteKn8 + jsarg + : '' timeout = timeout && jstype == 'generic' ? noteKn4 + 'timeout: ' + timeout : timeout && jstype != 'generic' - ? noteKn6 + 'timeout: ' + timeout - : '' + ? noteKn6 + 'timeout: ' + timeout + : '' if (/request|response/.test(jstype)) { script.push( @@ -1994,6 +2037,366 @@ function getArgArr(str) { return arr.map(item => item.replace(/➕/g, '+')) } +function stripWrapQuote(str) { + str = `${str ?? ''}`.trim() + return /^".*"$/.test(str) || /^'.*'$/.test(str) ? str.slice(1, -1) : str +} + +function splitTopLevel(str, sep = ',') { + let arr = [] + let current = '' + let quote = '' + let braceDepth = 0 + let bracketDepth = 0 + let parenDepth = 0 + for (let i = 0; i < str.length; i++) { + const char = str[i] + const prev = str[i - 1] + if (quote) { + current += char + if (char === quote && prev !== '\\') quote = '' + continue + } + if (char === '"' || char === "'") { + quote = char + current += char + continue + } + if (char === '{') braceDepth++ + if (char === '}') braceDepth = Math.max(0, braceDepth - 1) + if (char === '[') bracketDepth++ + if (char === ']') bracketDepth = Math.max(0, bracketDepth - 1) + if (char === '(') parenDepth++ + if (char === ')') parenDepth = Math.max(0, parenDepth - 1) + if (char === sep && braceDepth === 0 && bracketDepth === 0 && parenDepth === 0) { + arr.push(current.trim()) + current = '' + } else { + current += char + } + } + arr.push(current.trim()) + return arr +} + +function splitFirstTopLevel(str, sep) { + const parts = splitTopLevel(str, sep) + return [parts[0] || '', parts.slice(1).join(sep).trim()] +} + +function quoteIfNeeded(str) { + str = `${str ?? ''}`.trim() + if (/^'.*'$/.test(str)) str = `"${str.slice(1, -1)}"` + return /[\s,]/.test(str) && !/^".*"$/.test(str) ? `"${str}"` : str +} + +function quoteLoonInputValue(str) { + str = stripWrapQuote(`${str ?? ''}`.trim()) + return `"${str}"` +} + +function getSwitchDefault(value) { + value = stripWrapQuote(`${value ?? ''}`.trim()) + .split(',')[0] + .trim() + return /^true$/i.test(value) ? 'true' : 'false' +} + +function getToggleSwitchDefault(value) { + value = stripWrapQuote(`${value ?? ''}`.trim()) + .split(',')[0] + .trim() + return /^(false|0|off|no|#)?$/i.test(value) ? 'false' : 'true' +} + +function getSurgeArgumentDefault(item, isRuleToggle = false) { + const value = `${item.value ?? ''}`.trim() + if (isRuleToggle) return getSwitchDefault(value) == 'true' ? '' : '#' + if (item.type == 'switch') return getSwitchDefault(value) + return quoteIfNeeded(splitTopLevel(value, ',')[0] || value) +} + +function formatLoonArgumentValue(item, type = item.type, isRuleToggle = false) { + const value = `${item.value ?? ''}`.trim() + if (type == 'switch') { + const switchDefault = isRuleToggle ? getToggleSwitchDefault(value) : getSwitchDefault(value) + return switchDefault == 'true' ? 'true,false' : 'false,true' + } + return quoteLoonInputValue(value) +} + +function isLoonArgumentList(str) { + return /^\s*\[(?:\s*\{[^{}]+\}\s*,?)+\]\s*$/.test(stripWrapQuote(str)) +} + +function isLoonArgumentContainer(str) { + return /^\s*\[[\s\S]*\]\s*$/.test(stripWrapQuote(str)) +} + +function unwrapJsonArgument(str, targetApp = 'loon-plugin') { + let value = normalizeTemplateValue(str, targetApp) + value = stripWrapQuote(value).trim() + const unescaped = value.replace(/\\"/g, '"').replace(/\\'/g, "'").replace(/\\\\/g, '\\').trim() + if (/^\s*\{[\s\S]*\}\s*$/.test(unescaped) && /:/.test(unescaped)) return unescaped + if (/^\s*\{[\s\S]*\}\s*$/.test(value) && /:/.test(value)) return value + return /^\s*\{[\s\S]*\}\s*$/.test(unescaped) && /:/.test(unescaped) ? unescaped : '' +} + +function isJsonObjectArgument(str) { + return !!unwrapJsonArgument(str) +} + +function formatLoonJsonArgument(str, renameMap = argumentKeyRenameMap) { + const objectBody = unwrapJsonArgument(str, 'loon-plugin') + .replace(/["']([^"']+)["']\s*:/g, '$1:') + .replace(/:\s*["']?\{\{\{([^{}]+)\}\}\}["']?/g, ':$1') + .replace(/:\s*["']?\{\{([^{}]+)\}\}["']?/g, ':$1') + .replace(/:\s*["']?\{([^{}]+)\}["']?/g, ':$1') + .replace(/^\s*\{|\}\s*$/g, '') + return splitTopLevel(objectBody, ',') + .filter(Boolean) + .map(item => { + const [key, value] = splitFirstTopLevel(item, ':') + const scriptKey = stripWrapQuote(key).replace(/^\\+|\\+$/g, '') + const templateKey = stripWrapQuote(value).replace(/^\{+|\}+$/g, '').replace(/^\\+|\\+$/g, '') + if (templateKey && scriptKey) renameMap.set(templateKey, scriptKey) + return `{${scriptKey}}` + }) + .join(',') +} + +function collectArgumentKeyRenameMap(box) { + for (let i = 0; i < box.length; i++) { + if (isJsonObjectArgument(box[i].jsarg)) { + formatLoonJsonArgument(box[i].jsarg) + continue + } + parseSurgeTemplateArgumentPairs(box[i].jsarg) + } +} + +function getTemplateKeys(str) { + str = stripWrapQuote(str) + return [ + ...[...str.matchAll(/\{\{\{([^{}]+)\}\}\}/g)].map(item => item[1].trim()), + ...[...str.replace(/\{\{\{[^{}]+\}\}\}/g, '').matchAll(/\{([^{}]+)\}/g)].map(item => item[1].trim()), + ].filter(Boolean) +} + +function getArgumentDefaultValue(item) { + const value = `${item?.value ?? ''}`.trim() + return stripWrapQuote(splitTopLevel(value, ',')[0] || value).trim() +} + +function collectUsedArgumentKeys(jsBox, hnBox = []) { + const keys = new Set() + for (let i = 0; i < jsBox.length; i++) { + ;['jsarg', 'jsenable', 'cronexp'].forEach(field => { + getTemplateKeys(jsBox[i][field] || '').forEach(key => keys.add(key)) + }) + } + for (let i = 0; i < hnBox.length; i++) { + const key = getHnToggleKey(hnBox[i]) + key && keys.add(key) + } + return keys +} + +function filterLoonArguments(args, jsBox, hnBox = []) { + const usedKeys = collectUsedArgumentKeys(jsBox, hnBox) + return args.filter(item => { + const sourceKey = item.key + const targetKey = argumentKeyRenameMap.get(sourceKey) || sourceKey + const defaultValue = getArgumentDefaultValue(item) + if (!usedKeys.has(sourceKey) && !usedKeys.has(targetKey)) return false + if (defaultValue === '--' && sourceKey === targetKey) return false + return true + }) +} + +function getLoonArgumentKeys(str) { + str = stripWrapQuote(str) + if (!isLoonArgumentList(str)) return [] + return [...str.matchAll(/\{([^{}]+)\}/g)].map(item => item[1].trim()).filter(Boolean) +} + +function getSurgeTemplateArgumentKeys(str) { + str = stripWrapQuote(str) + if (!/\{\{\{[^{}]+\}\}\}/.test(str)) return [] + return splitTopLevel(str, '&') + .map(item => { + const matched = item.match(/^\s*([^=\s]+)\s*=\s*"?\{\{\{([^{}]+)\}\}\}"?\s*$/) + return matched && matched[1].trim() === matched[2].trim() ? matched[1].trim() : '' + }) + .filter(Boolean) +} + +function parseSurgeTemplateArgumentPairs(str, renameMap = argumentKeyRenameMap) { + str = stripWrapQuote(str) + if (!/(\{\{\{[^{}]+\}\}\}|\{[^{}]+\})/.test(str) || !/&/.test(str) && !/^\s*[^=\s]+\s*=/.test(str)) return [] + const items = splitTopLevel(str, '&') + const pairs = [] + for (let i = 0; i < items.length; i++) { + const matched = items[i].match(/^\s*([^=\s]+)\s*=\s*["']?(?:\{\{\{([^{}]+)\}\}\}|\{([^{}]+)\})["']?\s*$/) + if (!matched) return [] + const scriptKey = matched[1].trim() + const templateKey = (matched[2] || matched[3] || '').trim() + if (scriptKey && templateKey) renameMap.set(templateKey, scriptKey) + pairs.push({ scriptKey, templateKey }) + } + return pairs +} + +function getSurgeTemplateArgumentListKeys(str) { + return parseSurgeTemplateArgumentPairs(str).map(item => item.scriptKey) +} + +function normalizeScriptArgument(jsarg, targetApp) { + if (!jsarg) return jsarg + const loonKeys = getLoonArgumentKeys(jsarg) + if ((targetApp == 'surge-module' || targetApp == 'shadowrocket-module') && loonKeys.length > 0) { + return loonKeys.map(key => `${key}="{{{${key}}}}"`).join('&') + } + if (targetApp == 'loon-plugin') { + if (isJsonObjectArgument(jsarg)) return `[${formatLoonJsonArgument(jsarg)}]` + const surgeListKeys = getSurgeTemplateArgumentListKeys(jsarg) + if (surgeListKeys.length > 0) return `[${surgeListKeys.map(key => `{${key}}`).join(',')}]` + const surgeKeys = getSurgeTemplateArgumentKeys(jsarg) + if (surgeKeys.length > 0) return `[${surgeKeys.map(key => `{${key}}`).join(',')}]` + } + if (targetApp == 'loon-plugin') return normalizeTemplateValue(jsarg, targetApp) + return jsarg +} + +function takeLeadingTemplate(str) { + const matched = `${str ?? ''}`.match(/^(\s*)\{\{\{([^{}]+)\}\}\}\s*(.*)$/) + return matched ? { key: matched[2].trim(), rest: matched[1] + matched[3] } : null +} + +function getHnValue(item) { + return typeof item == 'object' && item !== null ? item.value : item +} + +function getHnToggleKey(item) { + return typeof item == 'object' && item !== null ? item.toggleKey : '' +} + +function normalizeTemplateValue(value, targetApp) { + if (!value) return value + value = stripWrapQuote(value) + if (targetApp == 'surge-module' || targetApp == 'shadowrocket-module') { + return value.replace(/(? 0) return `{{{${keys[0]}}}}` + return getSwitchDefault(value) == 'false' ? '#' : '' +} + +function parseArgumentTagFields(str) { + str = `${str ?? ''}`.trim() + const tag = str.match(/(?:^|,\s*)tag\s*=\s*([\s\S]*?)(?=,\s*desc\s*=|$)/)?.[1]?.trim() + const desc = str.match(/(?:^|,\s*)desc\s*=\s*([\s\S]*)/)?.[1]?.trim() + return { tag, desc } +} + +function escapeArgumentDesc(str) { + return `${str ?? ''}`.replace(/\r?\n/g, '\\n').trim() +} + +function buildSurgeArgumentsDesc(args) { + return args + .map(item => { + const { tag, desc } = parseArgumentTagFields(item.tag) + const title = tag && tag !== item.key ? tag : item.key + const detail = desc && desc !== item.key ? desc : '' + return `${item.key}: ${escapeArgumentDesc([title, detail].filter(Boolean).join('\n'))}` + }) + .filter(Boolean) + .join('\\n\\n') +} + +function escapeRegExp(str) { + return `${str}`.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') +} + +function normalizeArgumentsDescLine(str) { + return `${str ?? ''}` + .trim() + .replace(/^[\-*•]+\s*/, '') + .replace(/^\d+\s*[\.\)、]\s*/, '') + .replace(/^[①②③④⑤⑥⑦⑧⑨⑩⓵⓶⓷⓸⓹⓺⓻⓼⓽⓾]\s*/, '') +} + +function parseSurgeArgumentsDescLine(str, keys = []) { + const line = normalizeArgumentsDescLine(str) + if (!line) return null + const keyPattern = keys.length > 0 ? keys.map(escapeRegExp).join('|') : '[^::\\n]+' + const matched = line.match(new RegExp(`^(${keyPattern})\\s*[::]\\s*([\\s\\S]+)$`)) + if (!matched) return null + return { + key: matched[1].trim(), + desc: matched[2].trim(), + } +} + +function parseSurgeArgumentsDesc(str, keys = []) { + str = `${str ?? ''}`.replace(/\\n/g, '\n') + const map = {} + const keyPattern = keys.length > 0 ? keys.map(escapeRegExp).join('|') : '[^:\\n]+' + const regex = new RegExp(`(?:^|\\n)(${keyPattern}):\\s*`, 'g') + const matches = [...str.matchAll(regex)] + for (let i = 0; i < matches.length; i++) { + const key = matches[i][1].trim() + const start = matches[i].index + matches[i][0].length + const end = i + 1 < matches.length ? matches[i + 1].index : str.length + const value = str.slice(start, end).trim() + if (key && value) map[key] = value + } + str.split(/\r?\n/).forEach(line => { + const parsed = parseSurgeArgumentsDescLine(line, keys) + if (parsed?.key && parsed?.desc && !map[parsed.key]) { + map[parsed.key] = parsed.desc + } + }) + return map +} + +function applySurgeArgumentsDesc(args, rawDesc) { + const descMap = parseSurgeArgumentsDesc( + rawDesc, + args.map(item => item.key) + ) + args.forEach(item => { + const desc = descMap[item.key] + if (!desc) return + const { tag } = parseArgumentTagFields(item.tag) + const firstLine = desc.split(/\r?\n/)[0].trim() + item.tag = `tag=${tag && tag !== item.key ? tag : firstLine || item.key}, desc=${escapeArgumentDesc(desc)}` + }) +} + +function rewriteArgumentTagKey(tag, oldKey, newKey) { + if (!tag || oldKey === newKey) return tag + return tag.replace(new RegExp(`(tag|desc)=${escapeRegExp(oldKey)}(?=,|$)`, 'g'), `$1=${newKey}`) +} + //loon的input select互动按钮解析 function getInputInfo(x, box) { x = x.replace(/\s*=\s*/, '=') @@ -2076,12 +2479,12 @@ function getJsInfo(x, regex, parserRegex) { typeof parserRegex != 'undefined' ? parserRegex : /script-name\s*=/.test(x) - ? panelRegex - : /script-path\s*=/.test(x) - ? jsRegex - : /\s(data-type|data|data-path)\s*=/.test(x) - ? mockRegex - : '' + ? panelRegex + : /script-path\s*=/.test(x) + ? jsRegex + : /\s(data-type|data|data-path)\s*=/.test(x) + ? mockRegex + : '' if (regex.test(x)) { return x.split(regex)[1].split(parserRegex)[0] } else { @@ -2122,19 +2525,20 @@ function getQxReInfo(x, y, mark) { jsBox.push({ mark, noteK, jsname, jstype, jsptn, jsurl, rebody, size, timeout: '30', jsarg, ori: x, num: y }) } -function getHn(x, arr, addMethod) { +function getHn(x, arr, addMethod, toggleKey = '') { let hnBox2 = x .replace(/\s|%.+%/g, '') .split('=')[1] .split(/,/) for (let i = 0; i < hnBox2.length; i++) { - hnBox2[i].length > 0 && arr.push(hnBox2[i]) + hnBox2[i].length > 0 && arr.push(toggleKey ? { value: hnBox2[i], toggleKey } : hnBox2[i]) } //for if (/%INSERT%/i.test(x)) return '%INSERT%' else return addMethod } function pieceHn(arr) { + arr = arr.map(item => getHnValue(item)) if (!isStashiOS && arr.length > 0) return arr.join(', ') else if (isStashiOS && arr.length > 0) return arr.join(`"\n - "`) else return [] @@ -2304,8 +2708,8 @@ function getMockInfo(x, mark, y) { mockheader != '' && !/&contentType=/.test(mockheader) ? '&header=' + encodeURIComponent(mockheader) : mockheader != '' && /&contentType=/.test(mockheader) - ? mockheader - : '' + ? mockheader + : '' if (keepHeader == false) mockheader = '' mockurl = `http://script.hub/convert/_start_/${mockurl}/_end_/${mfile}?type=mock&target-app=${targetApp}&headers=${encodeURIComponent( @@ -2394,32 +2798,40 @@ function getPolicy(str) { function parseArguments(str) { if (/#!arguments/.test(str)) { const queryString = str.split(/#!arguments\s*=\s*/)[1] //获取查询字符串部分 - const regex = /([^:,]+):(\s*".+?"|[^,]*)/g //匹配键值对的正则表达式 - let match - - while ((match = regex.exec(queryString))) { - const key = match[1].trim().replace(/^"(.+)"$/, '$1') //去除头尾空白符和引号 - const value = match[2].trim().replace(/^"(.+)"$/, '$1') //去除头尾空白符和引号 - const type = /^(true|false)$/.test(value) ? 'switch' : 'input' + const items = splitTopLevel(queryString, ',') + + for (let i = 0; i < items.length; i++) { + const [rawKey, rawValue] = splitFirstTopLevel(items[i], ':') + if (!rawKey || !rawValue) continue + const key = stripWrapQuote(rawKey) + const value = rawValue.trim() + const type = /^(true|false)$/i.test(stripWrapQuote(value)) ? 'switch' : 'input' const tag = `tag=${key}, desc=${key}` sgArg.push({ key, value, type, tag }) //将键值对添加到对象中 - if (value == 'hostname') { + if (stripWrapQuote(value) == 'hostname') { hn2 = true hn2name = '{{{' + key + '}}}' } } } else { - const regex = /(^.*?)\s*=\s*(.*?)\s*,(.*?),\s*([^,]*\s*=.+)/ //获取信息 - const key = str.match(regex)[1] - const type = str.match(regex)[2] - const value = str.match(regex)[3] - const tag = str.match(regex)[4] + const matched = str.match(/^([^=]+?)\s*=\s*(.+)$/) + if (!matched) return + const rawKey = matched[1] + const rawRest = matched[2] + const parts = splitTopLevel(rawRest, ',') + const key = rawKey.trim() + const type = parts.shift() + const tagIndex = parts.findIndex(item => /^\s*(?:tag|desc)\s*=/.test(item)) + const valueParts = tagIndex === -1 ? parts : parts.slice(0, tagIndex) + const tagParts = tagIndex === -1 ? [] : parts.slice(tagIndex) + const value = type == 'select' ? valueParts[0] : valueParts.join(',') + const tag = tagParts.join(', ') || `tag=${key}, desc=${key}` sgArg.push({ key, value, type, tag }) - if (value == 'hostname') { + if (stripWrapQuote(value) == 'hostname') { hn2 = true hn2name = '{{{' + key + '}}}' } diff --git a/modules/script-hub.beta.egern.yaml b/modules/script-hub.beta.egern.yaml index 875b43e7..23afe72a 100644 --- a/modules/script-hub.beta.egern.yaml +++ b/modules/script-hub.beta.egern.yaml @@ -1,5 +1,6 @@ name: 'Script Hub(β): 重写 & 规则集转换' description: https://script.hub +icon: https://raw.githubusercontent.com/Script-Hub-Org/Script-Hub/main/assets/icon-dark144x144.png compat_arguments: Notify: 开启通知 compat_arguments_desc: Notify:\nScriptHub通知设置, 可选 开启通知, 关闭通知, 跟随链接 diff --git a/modules/script-hub.egern.yaml b/modules/script-hub.egern.yaml index 44109bfe..df57d434 100644 --- a/modules/script-hub.egern.yaml +++ b/modules/script-hub.egern.yaml @@ -1,5 +1,6 @@ name: 'Script Hub: 重写 & 规则集转换' description: https://script.hub +icon: https://raw.githubusercontent.com/Script-Hub-Org/Script-Hub/main/assets/icon-dark144x144.png compat_arguments: Notify: 开启通知 compat_arguments_desc: Notify:\nScriptHub通知设置, 可选 开启通知, 关闭通知, 跟随链接 diff --git a/script-converter.beta.js b/script-converter.beta.js index e23b7536..95560853 100644 --- a/script-converter.beta.js +++ b/script-converter.beta.js @@ -56,7 +56,8 @@ let url const evUrlori = queryObject.evalUrlori ?? '' const evUrlmodi = queryObject.evalUrlmodi ?? '' const wrap_response = queryObject.wrap_response - const compatibilityOnly = queryObject.compatibilityOnly + const compatibilityOnly = istrue(queryObject.compatibilityOnly) + const isScriptConversion = type.endsWith('-script') const subconverter = queryObject.subconverter @@ -361,8 +362,7 @@ global.$done = _scriptSonverterDone } if (type === 'qx-script' || compatibilityOnly) { const content = `${prefix}\n${compatibilityOnly ? body : body.replace(/\$done\(/g, '_scriptSonverterDone(')}` - body = `${prepend || ''} -const _scriptSonverterCompatibilityType = typeof $response !== 'undefined' ? 'response' : typeof $request !== 'undefined' ? 'request' : '' + body = `const _scriptSonverterCompatibilityType = typeof $response !== 'undefined' ? 'response' : typeof $request !== 'undefined' ? 'request' : '' const _scriptSonverterCompatibilityDone = $done try { ${content} @@ -379,7 +379,12 @@ try { } else { throw e } -}` + }` + } + + if (prepend && isScriptConversion) { + body = `${prepend} +${body}` } status = status ?? 200 diff --git a/script-converter.js b/script-converter.js index e23b7536..95560853 100644 --- a/script-converter.js +++ b/script-converter.js @@ -56,7 +56,8 @@ let url const evUrlori = queryObject.evalUrlori ?? '' const evUrlmodi = queryObject.evalUrlmodi ?? '' const wrap_response = queryObject.wrap_response - const compatibilityOnly = queryObject.compatibilityOnly + const compatibilityOnly = istrue(queryObject.compatibilityOnly) + const isScriptConversion = type.endsWith('-script') const subconverter = queryObject.subconverter @@ -361,8 +362,7 @@ global.$done = _scriptSonverterDone } if (type === 'qx-script' || compatibilityOnly) { const content = `${prefix}\n${compatibilityOnly ? body : body.replace(/\$done\(/g, '_scriptSonverterDone(')}` - body = `${prepend || ''} -const _scriptSonverterCompatibilityType = typeof $response !== 'undefined' ? 'response' : typeof $request !== 'undefined' ? 'request' : '' + body = `const _scriptSonverterCompatibilityType = typeof $response !== 'undefined' ? 'response' : typeof $request !== 'undefined' ? 'request' : '' const _scriptSonverterCompatibilityDone = $done try { ${content} @@ -379,7 +379,12 @@ try { } else { throw e } -}` + }` + } + + if (prepend && isScriptConversion) { + body = `${prepend} +${body}` } status = status ?? 200