UNPKG

mixone

Version:

MixOne is a Node scaffolding tool implemented based on Vite, used for compiling HTML5, JavasCript, Vue, React and other codes. It supports packaging Web applications with multiple HTML entry points (BS architecture) and desktop installation packages (CS a

381 lines (368 loc) 17.4 kB
/** - 在代码字符串中,从指定位置的左括号 ( 开始进行括号配平,找到与之匹配的右括号 ) ,并返回括号内的参数文本与结束位置。 - 解决正则无法处理多行参数、嵌套括号、字符串/模板字面量中的括号等复杂场景的问题。} code */ /** * 初始行,文件名,生成callid,生成替换的callMain,支持nodeJS、PJS * @param {*} code * @param {*} parenStart * @returns */ function findBalancedArgs(code, parenStart) { // 从指定位置的 '(' 开始向右扫描,使用深度计数配平括号 // 同时跟踪单/双/反引号字符串与模板字面量,避免误判括号 if (!code || parenStart == null || code[parenStart] !== '(') return null; let depth = 0; let inS = false, inD = false, inT = false, esc = false; for (let i = parenStart; i < code.length; i++) { const ch = code[i]; if (inS) { if (esc) { esc = false; } else if (ch === '\\') { esc = true; } else if (ch === '\'') { inS = false; } continue; } if (inD) { if (esc) { esc = false; } else if (ch === '\\') { esc = true; } else if (ch === '"') { inD = false; } continue; } if (inT) { if (esc) { esc = false; } else if (ch === '\\') { esc = true; } else if (ch === '`') { inT = false; } continue; } if (ch === '\'') { inS = true; continue; } if (ch === '"') { inD = true; continue; } if (ch === '`') { inT = true; continue; } if (ch === '(') depth++; else if (ch === ')') { depth--; if (depth === 0) return { args: code.slice(parenStart + 1, i), endIndex: i + 1 }; } } return null; } function indexToLineCol(code, idx) { // 将字符串位置索引转换为 1 基础的行列坐标 let line = 1, col = 1; for (let i = 0; i < idx && i < code.length; i++) { if (code[i] === '\n') { line++; col = 1; } else col++; } return { line, col }; } function splitTopLevelArgs(text) { // 仅在顶层逗号处切分参数,内部的括号/数组/对象与字符串中的逗号会被跳过 const parts = []; let start = 0; let inS = false, inD = false, inT = false, esc = false; let p = 0, b = 0, c = 0; for (let i = 0; i < text.length; i++) { const ch = text[i]; if (inS) { if (esc) { esc = false; } else if (ch === '\\') { esc = true; } else if (ch === '\'') { inS = false; } continue; } if (inD) { if (esc) { esc = false; } else if (ch === '\\') { esc = true; } else if (ch === '"') { inD = false; } continue; } if (inT) { if (esc) { esc = false; } else if (ch === '\\') { esc = true; } else if (ch === '`') { inT = false; } continue; } if (ch === '\'') { inS = true; continue; } if (ch === '"') { inD = true; continue; } if (ch === '`') { inT = true; continue; } if (ch === '(') p++; else if (ch === ')') p--; else if (ch === '[') b++; else if (ch === ']') b--; else if (ch === '{') c++; else if (ch === '}') c--; else if (ch === ',' && p === 0 && b === 0 && c === 0) { parts.push(text.slice(start, i).trim()); start = i + 1; } } const last = text.slice(start).trim(); if (last.length) parts.push(last); return parts; } function buildArgsObject(argsText) { // 将参数文本转为对象,键名为 arg1..argN(位置从 1 开始) if (!argsText || !argsText.trim()) return {}; const arr = splitTopLevelArgs(argsText); const obj = {}; for (let i = 0; i < arr.length; i++) obj['arg' + (i + 1)] = arr[i]; return obj; } function buildArgsObjectStr(obj) { // 将参数对象转为稳定的字符串表示,按数字顺序输出键 const keys = Object.keys(obj); if (!keys.length) return ''; const sorted = keys.slice().sort((a, b) => { const na = parseInt(a.replace(/^\D+/, '')) || 0; const nb = parseInt(b.replace(/^\D+/, '')) || 0; return na - nb; }); const parts = sorted.map(k => k + ': ' + obj[k]); return '{ ' + parts.join(', ') + ' }'; } function buildFnBody(n) { const argNumArr = Object.keys(n.argsObject); const callModuleName = n.module; const argsPart = n.isFunctionCall ? `(${argNumArr.join(', ')})` : ''; let requireLine = `const { ${callModuleName} } = require('electron');`; if (n.keyword === 'NodeJS') { requireLine = `const ${callModuleName} = require('${callModuleName}');`; } else if (n.keyword === 'PJS') { requireLine = `const ${callModuleName} = require('./${callModuleName}.fn');`; } return `${requireLine}\n return ${n.path}${argsPart};`; } function extractMainSugar(code) { //单引号和双引号内的像语法糖语法的字符属于字符串而不是代码,不能被当做语法糖。而在``之中,并且处于${}括号中语法糖则需要处理。 function maskStringsKeepTplExpr(text) { let res = ''; let inS = false, inD = false, inT = false, esc = false; let tpl = 0; for (let i = 0; i < text.length; i++) { const ch = text[i]; const next = i + 1 < text.length ? text[i + 1] : ''; if (inS) { if (esc) { esc = false; res += ' '; continue; } if (ch === '\\') { esc = true; res += ' '; continue; } if (ch === '\'') { inS = false; res += ' '; continue; } res += (ch === '\n' ? '\n' : ' '); continue; } if (inD) { if (esc) { esc = false; res += ' '; continue; } if (ch === '\\') { esc = true; res += ' '; continue; } if (ch === '"') { inD = false; res += ' '; continue; } res += (ch === '\n' ? '\n' : ' '); continue; } if (inT) { if (esc) { esc = false; res += (tpl ? ch : (ch === '\n' ? '\n' : ' ')); continue; } if (ch === '\\') { esc = true; res += (tpl ? ch : ' '); continue; } if (tpl === 0) { if (ch === '$' && next === '{') { res += '${'; i++; tpl = 1; continue; } if (ch === '`') { inT = false; res += '`'; continue; } res += (ch === '\n' ? '\n' : ' '); continue; } else { if (ch === '{') { tpl++; res += ch; continue; } if (ch === '}') { tpl--; res += ch; continue; } res += ch; continue; } } if (ch === '\'') { inS = true; res += ' '; continue; } if (ch === '"') { inD = true; res += ' '; continue; } if (ch === '`') { inT = true; tpl = 0; res += '`'; continue; } res += ch; } return res; } // 注释屏蔽:保留行结构,替换注释区域为空格 // 特别处理块注释的“宽松闭合”:允许 '*' 后跟空白再 '/' function maskCommentsOnly(text) { let res = ''; let inLine = false, inBlock = false; for (let i = 0; i < text.length; i++) { const ch = text[i]; const next = i + 1 < text.length ? text[i + 1] : ''; if (inLine) { if (ch === '\n') { inLine = false; res += ch; } else { res += ' '; } continue; } if (inBlock) { if (ch === '*') { let j = i + 1; while (j < text.length && (text[j] === ' ' || text[j] === '\t')) j++; if (j < text.length && text[j] === '/') { const len = (j - i + 1); res += ' '.repeat(len); inBlock = false; i = j; continue; } } res += (ch === '\n' ? '\n' : ' '); continue; } if (ch === '/' && next === '/') { inLine = true; res += ' '; i++; res += ' '; continue; } if (ch === '/' && next === '*') { inBlock = true; res += ' '; i++; res += ' '; continue; } res += ch; } return res; } function computeZones(text) { const len = text.length; const s = new Array(len).fill(false); const d = new Array(len).fill(false); const t = new Array(len).fill(false); const tExpr = new Array(len).fill(false); let inS = false, inD = false, inT = false, esc = false; let tplDepth = 0; for (let i = 0; i < len; i++) { const ch = text[i]; const next = i + 1 < len ? text[i + 1] : ''; if (inS) { s[i] = true; if (esc) { esc = false; continue; } if (ch === '\\') { esc = true; continue; } if (ch === '\'') { inS = false; } continue; } if (inD) { d[i] = true; if (esc) { esc = false; continue; } if (ch === '\\') { esc = true; continue; } if (ch === '"') { inD = false; } continue; } if (inT) { t[i] = true; if (esc) { esc = false; } else if (ch === '\\') { esc = true; } else if (tplDepth === 0 && ch === '$' && next === '{') { tplDepth = 1; tExpr[i] = true; i++; tExpr[i] = true; continue; } else if (tplDepth > 0 && ch === '{') { tplDepth++; tExpr[i] = true; } else if (tplDepth > 0 && ch === '}') { tExpr[i] = true; tplDepth--; } else { if (tplDepth > 0) tExpr[i] = true; } if (ch === '`') { inT = false; } continue; } if (ch === '\'') { inS = true; s[i] = true; continue; } if (ch === '"') { inD = true; d[i] = true; continue; } if (ch === '`') { inT = true; t[i] = true; tplDepth = 0; continue; } } return { s, d, t, tExpr }; } const results = []; const pattern = /\b(Main|NodeJS|PJS)\s*((?:\s*(?:\[\s*(?:['"][^'"]+['"]|[\w]+)\s*\]|\.\s*[\w]+))+)/g; let m; // 仅屏蔽注释以保持行结构;字符串与模板表达式不在掩码阶段处理, // 而是在后续通过位置与片段分析来决定是否命中与替换,避免误杀正常代码。 const masked = maskCommentsOnly(code); /** * “匹配语法糖时,单引号和双引号内的像语法糖语法的字符属于字符串而不是代码,不能被当做语法糖。而在``之中,并且处于${}括号中语法糖则需要处理。” */ const zones = computeZones(code); while ((m = pattern.exec(masked)) !== null) { const keyword = m[1] || 'Main'; const pathRaw = m[2] || ''; if (zones.s[m.index] || zones.d[m.index] || (zones.t[m.index] && !zones.tExpr[m.index])) { continue; } const path = pathRaw .replace(/\s*\.\s*/g, '.') .replace(/\[\s*(?:['"]([^'"]+)['"]|([\w]+))\s*\]/g, (_, a, b) => '.' + (a || b)) .replace(/^\./, ''); let i = m.index + m[0].length; while (i < masked.length && /\s/.test(masked[i])) i++; let isFunctionCall = false; let argsText = ''; let end = i; let parenStartIndex = null; if (masked[i] === '(') { // 识别函数调用并提取完整参数片段 isFunctionCall = true; parenStartIndex = i; const b = findBalancedArgs(code, i); if (b) { argsText = b.args; end = b.endIndex; } } const startPos = indexToLineCol(code, m.index); const endPos = indexToLineCol(code, end); const preContext = masked.slice(Math.max(0, m.index - 100), m.index).trimEnd(); // 判断是否被 await 修饰(考虑 await( ) 的写法) const awaited = /\bawait\b(?:\s*\()?\s*$/i.test(preContext); const moduleName = path.split('.')[0] || ''; // 保留原始访问路径及其占用行数,便于替换与定位 const originAccessPath = code.slice(m.index, isFunctionCall ? parenStartIndex : (m.index + m[0].length)); const originAccessPathLineCount = originAccessPath.split(/\n/).length; // 参数对象与其字符串表示 const argsObject = isFunctionCall ? buildArgsObject(argsText) : {}; const argsObjectStr = isFunctionCall ? buildArgsObjectStr(argsObject) : ''; results.push({ keyword, path, module: moduleName, isFunctionCall, argsText, argsObject, argsObjectStr, awaited, originAccessPath, originAccessPathLineCount, originSugarCode: code.slice(m.index, end), startIndex: m.index, endIndex: end, startLine: startPos.line, startColumn: startPos.col, endLine: endPos.line, endColumn: endPos.col }); pattern.lastIndex = end; } return results; } function extractMainSugarTree(code, depth = 1, idState, idPrefix = '', ancestorPoints = []) { // 为语法糖节点构建树结构:标注层级 depth 与全局唯一 id // idState 支持传入数字或 { value:number },并支持可选前缀 idPrefix if (idState == null) { idState = { value: 1 }; } else if (typeof idState === 'number') { idState = { value: idState }; } else if (typeof idState !== 'object' || typeof idState.value !== 'number') { idState = { value: 1 }; } const nodes = extractMainSugar(code); return nodes.map(n => { // 按遍历顺序分配自增 id(跨层级共享计数器) const id = idPrefix ? idPrefix + idState.value : String(idState.value); idState.value++; const point = `D${depth}L${n.startLine}C${n.startColumn}`; const lineTrace = ancestorPoints.concat(point); const fnId = id + lineTrace.join('_'); const rendererCallFn = `${n.awaited ? '' : 'await '}window.electron.callMainFunction('${fnId}'${n.argsObjectStr? ', ' + n.argsObjectStr : ', {}'})`; const mainFunctionBody = buildFnBody(n); let children = []; if (n.isFunctionCall && n.argsText) { children = extractMainSugarTree(n.argsText, depth + 1, idState, idPrefix, lineTrace); } return Object.assign({}, n, { id, depth, lineTrace, fnId, rendererCallFn, mainFunctionBody, children }); }); } function replaceSugarInText(text, idState, idPrefix) { const nodes = extractMainSugar(text); const ranges = []; for (const n of nodes) { let replacedArgs = n.argsText || ''; if (n.isFunctionCall && n.argsText) { replacedArgs = replaceSugarInText(n.argsText, idState, idPrefix); } const argsObject = n.isFunctionCall ? buildArgsObject(replacedArgs) : {}; const argsObjectStr = n.isFunctionCall ? buildArgsObjectStr(argsObject) : ''; const fnIdLocal = `${n.fnId}`; const call = `${n.awaited ? '' : 'await '}window.electron.callMainFunction('${fnIdLocal}'${argsObjectStr ? ', ' + argsObjectStr : ', {}'})`; ranges.push({ start: n.startIndex, end: n.endIndex, rep: call }); } if (!ranges.length) return text; ranges.sort((a, b) => b.start - a.start); let out = text; for (const r of ranges) { out = out.slice(0, r.start) + r.rep + out.slice(r.end); } return out; } function renderSugarBottomUp(code, idPrefix = '', idStart = 1) { const idState = typeof idStart === 'number' ? { value: idStart } : (idStart && typeof idStart.value === 'number' ? idStart : { value: 1 }); return replaceSugarInText(code, idState, idPrefix); } function renderSugarFromTree(code, nodes) { // 根据源代码的换行风格(CRLF/LF)选择补行时使用的换行符,确保行号对齐一致 const nl = /\r\n/.test(code) ? '\r\n' : '\n'; function renderNode(node) { let argsTextRendered = node.argsText || ''; // 若参数中存在嵌套语法糖,先自内向外渲染其子节点,再将渲染结果拼入参数文本 if (node.isFunctionCall && node.children && node.children.length) { const ranges = node.children.map(ch => ({ start: ch.startIndex, end: ch.endIndex, rep: renderNode(ch) })); ranges.sort((a, b) => b.start - a.start); for (const r of ranges) { argsTextRendered = argsTextRendered.slice(0, r.start) + r.rep + argsTextRendered.slice(r.end); } } // 以子节点渲染后的参数文本生成稳定的参数对象与字符串表示 const argsObject = node.isFunctionCall ? buildArgsObject(argsTextRendered) : {}; const argsObjectStr = node.isFunctionCall ? buildArgsObjectStr(argsObject) : ''; // 构造替换调用文本(可能因对象/数组参数而占用多行) const call = `${node.awaited ? '' : 'await '}window.electron.callMainFunction('${node.fnId}'${argsObjectStr ? ', ' + argsObjectStr : ',{}'})`; // 计算原始语法糖片段的行数与替换文本行数,按“原始行数 - 替换行数”补齐顶层的空行 const originLineCount = (node.originSugarCode || '').split(/\r?\n/).length || 1; const callLineCount = call.split(/\r?\n/).length || 1; const padNewlines = node.depth === 1 ? Math.max(0, originLineCount - callLineCount) : 0; return padNewlines ? (call + nl.repeat(padNewlines)) : call; } const ranges = nodes.map(n => ({ start: n.startIndex, end: n.endIndex, rep: renderNode(n) })); ranges.sort((a, b) => b.start - a.start); let out = code; for (const r of ranges) { out = out.slice(0, r.start) + r.rep + out.slice(r.end); } return out; } if (require.main === module) { // 演示:从示例代码中提取语法糖树,带深度与前缀 id const fs = require('fs'); const path = require('path'); const code = fs.readFileSync(path.join(__dirname, 'sugar_codeV3.js'), 'utf8'); const results = extractMainSugarTree(code,1,1,'demo-file-'); console.log('=== Syntax Sugar Tree ==='); console.log(JSON.stringify(results, null, 2)); const renderedViaExtractTree = renderSugarFromTree(code, results); console.log('=== Bottom-up Rendered Code (via extractMainSugarTree) ==='); console.log(renderedViaExtractTree); } module.exports = { extractMainSugar, extractMainSugarTree, renderSugarBottomUp, renderSugarFromTree };