UNPKG

fy-convertor

Version:

Convert excel/xml/json/bin/protocl/ts/as ...

922 lines 40.9 kB
import fg from 'fast-glob'; import fs from 'fs-extra'; import path from 'path'; import md5 from 'md5'; import xmljs from 'xml-js'; import { count, findFiles, getValues, joinValues } from "../tools/vendor.js"; import { isArrayEntry, isComplexType, isLiteralStringType, name_ClientUsed, NumberLike, StringLike } from './def.js'; import { JsonStructParser } from './JsonStructParser.js'; import { SvnHolder, adjustSVN } from './SvnHolder.js'; import sizeof from 'object-sizeof'; import convertSize from 'convert-size'; import { Alert } from '../tools/alert.js'; import _ from 'lodash'; import { toolchain } from '../tools/toolchain.js'; export class Xml2Json { static ins; static get Instance() { if (!Xml2Json.ins) Xml2Json.ins = new Xml2Json(); return Xml2Json.ins; } HanPattern = /[\u4e00-\u9fa5]+/; structPath; xmlPath; jsonPath; errorIdPath; jsonPathsToSync = []; errorIdPathsToSync = []; defMd5Map = {}; oldMd5Map; newMd5Map = {}; nameMap; strings = ['']; allLangs = []; crtXml; crtStruct; crtWeight = 0; toDeletes = []; errorMap = {}; async execute() { if (toolchain.projCfg.xml2json?.nameMap) { this.nameMap = JSON.parse(toolchain.projCfg.xml2json.nameMap); } const tmpFolder = toolchain.options.publish ? toolchain.options.project + '_publish' : toolchain.options.project; const temp = path.join(toolchain.options.workspace, tmpFolder); this.structPath = path.join(temp, 'struct'); this.xmlPath = path.join(temp, 'xml'); this.jsonPath = path.join(temp, 'json'); this.errorIdPath = path.join(temp, 'ts/protocol'); const defMd5Path = path.join(temp, 'defMd5.json'); const md5Path = path.join(temp, 'md5.json'); // 更新svn await SvnHolder.Instance.checkSVN(!toolchain.options.debug, this.structPath, adjustSVN(toolchain.projCfg.Svn.DesignerSvn) + 'xml'); await SvnHolder.Instance.checkSVN(!toolchain.options.debug, this.xmlPath, adjustSVN(toolchain.projCfg.Svn.DesignerSvn) + 'bin/Client'); await this.initClientSVNs(toolchain.projCfg.Svn.ClientSvn, this.jsonPath, this.errorIdPath); if (toolchain.projCfg.Sync) { for (let i = 0, len = toolchain.projCfg.Sync.length; i < len; i++) { const sp = path.join(temp, `jsonToSync${i}`); const ep = path.join(temp, `ts/protocolToSync${i}`); this.jsonPathsToSync.push(sp); this.errorIdPathsToSync.push(ep); await this.initClientSVNs(toolchain.projCfg.Sync[i], sp, ep); } } // 读入md5信息 if (fs.existsSync(defMd5Path)) { this.defMd5Map = await fs.readJson(defMd5Path); } else { console.log("No def md5 record found."); } if (fs.existsSync(md5Path)) { this.oldMd5Map = await fs.readJson(md5Path); } else { console.log("No old md5 record found."); } await JsonStructParser.Instance.readStructs(this.structPath, true); // 调试模式下先删除就的所有json文件 if (toolchain.options.force) { const files = await fs.readdir(this.jsonPath); for (const f of files) { if (f.endsWith('.json')) { await fs.remove(path.join(this.jsonPath, f)); } } } // 转json await this.convert(); // 生成ErrorId.ts await this.makeErrorId(); // 写md5信息 await fs.writeJSON(defMd5Path, this.defMd5Map, { spaces: 2 }); await fs.writeJSON(md5Path, this.newMd5Map, { spaces: 2 }); // 提交svn if (!toolchain.options.debug) { console.log("Committing..."); await SvnHolder.Instance.commitSVN('xml2json@fy-convertor~', this.jsonPath, this.errorIdPath); // 同步 if (toolchain.projCfg.Sync) { for (let i = 0, len = toolchain.projCfg.Sync.length; i < len; i++) { console.log(`Syncing to ${toolchain.projCfg.Sync[i]}...`); const sp = this.jsonPathsToSync[i]; const ep = this.errorIdPathsToSync[i]; await fs.copy(this.jsonPath, sp, { filter: (s) => s.endsWith('.json') }); await fs.copyFile(path.join(this.errorIdPath, 'ErrorId.ts'), path.join(ep, 'ErrorId.ts')); await SvnHolder.Instance.commitSVN('xml2json@fy-convertor~', sp, ep); } } } else { console.log("Debug mode, committing skipped."); } // 统计json占用内存 const files = await fs.readdir(this.jsonPath); const sizeDetails = []; let totalSize = 0; for (const f of files) { if (f.endsWith('.json')) { const obj = await fs.readJSON(path.join(this.jsonPath, f)); const s = sizeof(obj); sizeDetails.push({ name: f, size: s }); totalSize += s; } } sizeDetails.sort((a, b) => b.size - a.size); console.log('total json size:', convertSize(totalSize, { accuracy: 1 })); console.log('-----------------'); for (const sd of sizeDetails) { console.log(sd.name + ':', convertSize(sd.size, { accuracy: 1 })); } } async initClientSVNs(repository, jsonPath, errorIdPath) { if (toolchain.projCfg.Project.ProjectType === 'H5ts') { await SvnHolder.Instance.checkSVN(true, jsonPath, adjustSVN(repository) + (toolchain.projCfg.xml2json?.jsonRoot ?? 'project/bin/res/data')); await SvnHolder.Instance.checkSVN(true, errorIdPath, adjustSVN(repository) + (toolchain.projCfg.xml2protocol?.protocolRoot ?? 'project/src/automatic/protocol')); } else { await SvnHolder.Instance.checkSVN(true, jsonPath, adjustSVN(repository) + (toolchain.projCfg.xml2json?.jsonRoot ?? 'project/Assets/AssetSources/data')); await SvnHolder.Instance.checkSVN(true, errorIdPath, adjustSVN(repository) + (toolchain.projCfg.xml2protocol?.protocolRoot ?? 'project/TsScripts/System/protocol')); } } async convert() { // 选择配置 const selectRuleMap = {}; const selectRules = toolchain.projCfg.xml2json?.selectRules; if (selectRules) { for (const rule of selectRules) { selectRuleMap[rule.name] = rule; } } // 获取drop字段配置 const dfRuleMap = {}; // 解析diff规则 const dfRules = toolchain.projCfg.xml2json?.dropFieldRules; if (dfRules) { for (const rule of dfRules) { dfRuleMap[rule.name] = rule; } } // 获取diff配置 const dvRuleMap = {}; // 解析diff规则 const dvRules = toolchain.projCfg.xml2json?.diffValueRules; if (dvRules) { for (const rule of dvRules) { dvRuleMap[rule.name] = rule; } } const files = findFiles(this.xmlPath, ['.xml'], '+'); const structJsonMap = {}; const structXmlMap = {}; for (let f of files) { this.crtXml = f; // if(!f.endsWith('PinstanceDiffBonus.xml')) continue; if (f.endsWith('language.xml') || f.endsWith('ErrnoLang.config.xml')) continue; let fileContent = await fs.readFile(f, 'utf-8'); const xml = xmljs.xml2json(fileContent, { compact: true, spaces: 0 }); const doc = JSON.parse(xml); const structNameKey = this.identifyStructNameKey(doc); if (!structNameKey) { console.error(`[ERROR]Cannot identify struct name: ${f}`); continue; } const fb = path.basename(f); const rawStructName = structNameKey.substring(0, structNameKey.length - 4); const structName = rawStructName.replace('_Flash', 'M'); this.crtStruct = structName; // if (structName != 'PinGuanQiaM') continue; let xmls = structXmlMap[structName]; if (!xmls) { structXmlMap[structName] = xmls = []; } xmls.push(fb); const structDef = JsonStructParser.Instance.getStructDef(structName); if (!structDef) { continue; } const dvRule = dvRuleMap[structName]; // 计算定义md5 const defMd5 = md5(this.getStructFingerprint(structDef) + (dvRule ? JSON.stringify(dvRule) : '')); const oldDefMd5 = this.defMd5Map[structName]; this.defMd5Map[structName] = defMd5; const fileMd5 = md5(fileContent); this.newMd5Map[fb] = fileMd5; if (oldDefMd5 === defMd5 && this.oldMd5Map && this.oldMd5Map[fb] == fileMd5 && !toolchain.options.force) { console.log(`MD5 no change, skip: ${fb}`); continue; } console.log(`converting json: ${fb}`); const onlyForLang = toolchain.projCfg.xml2json?.onlyForLang?.includes(fb); const cfgObjs = doc[structNameKey][rawStructName]; let cfgs = []; let rawCfgs; if (cfgObjs instanceof Array) { rawCfgs = cfgObjs; } else { rawCfgs = [cfgObjs]; } for (let i = 0, len = rawCfgs.length; i < len; i++) { this.crtWeight = 0; const c = rawCfgs[i]; const cu = c[name_ClientUsed]; if (cu != null) { if (cu._text === '0 ') { continue; } } let out = await this.structToJson(c, structDef, onlyForLang ?? false); if (!out.isNotUsed && this.crtWeight > 0) { cfgs.push(out.o); } } if (cfgs.length === rawCfgs.length) { // 那就把第一个当成表头 cfgs.shift(); } // 检查表格合法性 await this.checkValidity(structDef, cfgs); if (!onlyForLang) { // 增强版排序,需要先排序,再删除optional字段 if (structDef._attributes.sortPlusKeys) { const rawSkeys = structDef._attributes.sortPlusKeys.split(','); const skeys = rawSkeys.map((v) => v.replace(/[\+-]+$/, '')); const orders = rawSkeys.map((v) => v.endsWith('-') ? 'desc' : 'asc'); cfgs = _.orderBy(cfgs, skeys, orders); } // 选择 const selectRule = selectRuleMap[structName]; if (selectRule) this.select(selectRule, cfgs, structDef); // drop字段 const dfRule = dfRuleMap[structName]; if (dfRule) this.dropFields(dfRule, cfgs, structDef); // diff数据 const dvRule = dvRuleMap[structName]; if (dvRule) this.diffValues(dvRule, cfgs, structDef); // 最后删除optional字段,因上述各种处理可能涉及到排序,而排序可能使用到optional字段 for (const td of this.toDeletes) { for (const k of td.deleteKeys) { delete td.o[k]; } } this.toDeletes.length = 0; const info = { cfgs, fb }; let jinfos = structJsonMap[structName]; if (jinfos == null) { structJsonMap[structName] = jinfos = [info]; } else { jinfos.push(info); } } } // 检查是否有错误 const errorFiles = []; for (const file in this.errorMap) errorFiles.push(file); if (errorFiles.length > 0) { for (const file of errorFiles) { console.error(file); const arr = this.errorMap[file]; arr.forEach((v) => console.error(v)); console.log('---------------------'); } await Alert.Instance.sendBuildFailureAlert(`富文本格式错误: ${errorFiles[0].replaceAll('\\', '/')}`); await Alert.Instance.alertErrorFile(errorFiles[0]); process.exit(1); } for (const structName in structJsonMap) { const jinfos = structJsonMap[structName]; for (const info of jinfos) { let saveName = this.nameMap?.[info.fb]; if (saveName == null) saveName = structName + '.json'; if (structXmlMap[structName].length > 1) { const mch = info.fb.match(/\w+\.(\w+)\.xml/); if (mch != null) { saveName = saveName.replace('.json', `.${mch[1]}.json`); } else { console.error(`Different xmls contain same struct! Cannot decide the json name! ${jinfos.map((value) => value.fb).join(', ')}`); process.exit(1); } } await fs.writeJson(path.join(this.jsonPath, saveName), info.cfgs, { spaces: 2 }); } } if (this.strings.length > 0) { // 生成strings.json await fs.writeJSON(path.join(this.jsonPath, 'strings.json'), this.strings, { spaces: 2 }); } if (this.allLangs.length > 0) { this.allLangs.sort(); await fs.writeJSON(path.join(this.jsonPath, 'lang.json'), this.allLangs, { spaces: 2 }); } // 清洗数据 await this.shakeTree(); // 生成bundle.json if (toolchain.projCfg.xml2json?.bundle == 'YES' || toolchain.projCfg.xml2json?.bundle == 'YES_JS') { // 由于旧项目可能会对所有json达成一个ab包,故增加一个新控制开关 await this.makeBundle(); } // 提取字符串 if (toolchain.projCfg.xml2json?.extractStrings == 'YES') { await this.extractStrings(); } } getStructFingerprint(struct) { let s = JSON.stringify(struct); for (const e of struct.entries) { if (isComplexType(e._attributes.type) && !e.isAdditional) { const et = JsonStructParser.Instance.getStructDef(e._attributes.type); s += this.getStructFingerprint(et); } } return s; } async checkValidity(structDef, cfgs) { // 检查unique字段,目前只支持非数组字段,要求不同配置的字段值不同 const entries = structDef.entries.filter(v => v.isUnique); for (const entry of entries) { if (isArrayEntry(entry)) { console.warn(`[WARN]Unique field ${entry._attributes.name} is array entry, cannot check!`); } else { const valueRecorder = {}; for (const c of cfgs) { const v = c[entry._attributes.name]; if (v == null) continue; if (valueRecorder[v] > 0) { const ci = JsonStructParser.Instance.getConvInfo(this.crtStruct); await Alert.Instance.sendBuildFailureAlert(`${ci.sheet}@${ci.xlsx}中的列${entry._attributes.name}(${entry._attributes.cname ?? entry._attributes.desc})的值不允许重复!`); process.exit(1); } else { valueRecorder[v] = (valueRecorder[v] ?? 0) + 1; } } } } // 检查validate规则 const ventryies = structDef.entries.filter(v => v.validates != null); for (const entry of ventryies) { for (const v of entry.validates) { if (v.type == 'LE') { for (const c of cfgs) { const cv = c[entry._attributes.name]; if (cv == null) continue; if (cv > v.value) { const ci = JsonStructParser.Instance.getConvInfo(this.crtStruct); await Alert.Instance.sendBuildFailureAlert(`${ci.sheet}@${ci.xlsx}中的列${entry._attributes.name}(${entry._attributes.cname ?? entry._attributes.desc})的值(${cv})校验失败!(${v.description})`); process.exit(1); } } } } } } async shakeTree() { // 解析清洗规则 const tsrRules = toolchain.projCfg.xml2json?.treeShakeRules; if (!tsrRules) { console.log('no treeshake rules, skip!'); return; } console.log('treeshake rules:', tsrRules); // 清洗数据 for (const rule of tsrRules) { let equalHitValues = [], notEqualHitValues = []; for (const test of rule.tests) { const [equalName, equalKeyStr] = test.select.split('::'); const equalKeys = equalKeyStr.split('.'); const equalJsonFile = path.join(this.jsonPath, `${equalName}.json`); const equalJson = await fs.readJSON(equalJsonFile); for (const c of equalJson) { const values = getValues(c, equalKeys).filter((v) => Boolean(v)); if (test.rule == 'if_equal') { equalHitValues.push(...values); } else { notEqualHitValues.push(...values); } } } equalHitValues = _.uniq(equalHitValues); notEqualHitValues = _.uniq(notEqualHitValues); if (equalHitValues.length > 0) { console.log(`tree shake ${rule.name}, ${equalHitValues.length} item off`); const keepKeys = rule.then_keep.split(','); const jsonFile = path.join(this.jsonPath, `${rule.name}.json`); const json = await fs.readJson(jsonFile); const newCfgs = []; for (const c of json) { if (equalHitValues.includes(c[rule.if_key]) && !notEqualHitValues.includes(c[rule.if_key])) { newCfgs.push(_.pick(c, keepKeys)); } else { newCfgs.push(c); } } await fs.writeJson(jsonFile, newCfgs, { spaces: 2 }); } } } dropFields(rule, cfgs, structDef) { for (const c of cfgs) { for (const dk of rule.dropKeys) { const entryDef = structDef.entries.find((v) => v._attributes.name == dk.name); const f = c[dk.name]; if (f instanceof Array) { let reduceCnt = 0; for (let i = f.length - 1; i >= 0; i--) { const elem = f[i]; if (this.testDropable(elem, dk.tests)) { f.splice(i, 1); reduceCnt++; } } if (reduceCnt > 0) { if (entryDef._attributes.refer) { c[entryDef._attributes.refer] = c[entryDef._attributes.refer] - reduceCnt; // 检查optional字段 if (c[entryDef._attributes.refer] == 0) { const referEntry = structDef.entries.find((v) => v._attributes.name == entryDef?._attributes.refer); if (referEntry.isOptional) delete c[entryDef._attributes.refer]; } } if (f.length == 0 && entryDef?.isOptional) { delete c[dk.name]; } } } else { if (this.testDropable(f, dk.tests)) { delete c[dk.name]; } } } } } select(rule, cfgs, structDef) { for (let i = cfgs.length - 1; i >= 0; i--) { const c = cfgs[i]; if (!this.testDropable(c, rule.tests)) { cfgs.splice(i, 1); } } } testDropable(f, tests) { if (typeof (f) == 'object') { // 复杂字段 if (tests) { for (const test of tests) { if (test.hitValue != null) { if (f[test.key] == test.hitValue) { return true; } } else { if (!f[test.key]) { return true; } } } } else { // 没有tests,默认所有字段都是空字段则命中 let used = false; for (const key in f) { if (f[key]) { used = true; break; } } return !used; } } else { if (tests) { // 简单字段可以通过hitValue指定命中值 for (const test of tests) { if (f == test.hitValue) { return true; } } } else { // 没有tests,默认空字段即命中 return !f; } } return false; } diffValues(rule, cfgs, structDef) { // 先按照系列进行分类 const defaultSerial = '_serial'; const serialMap = {}; if (rule.serial_keys) { for (const c of cfgs) { const svk = joinValues(c, rule.serial_keys); let m = serialMap[svk]; if (!m) serialMap[svk] = m = []; m.push(c); } } else { // 否则视为同一系列 serialMap[defaultSerial] = cfgs.slice(); } // 再进行排序 for (const serial in serialMap) { serialMap[serial] = _.sortBy(serialMap[serial], rule.serial_sort_keys); } // 开始diff for (const svk in serialMap) { const m = serialMap[svk]; for (let i = m.length - 1; i > 0; i--) { const prev = m[i - 1]; const elem = m[i]; for (const f of rule.diff_fields) { if (prev[f.name] == null) continue; const entryDef = structDef.entries.find((v) => v._attributes.name == f.name); if (prev[f.name] instanceof Array) { const arr = []; for (let i = 0, len = elem[f.name].length; i < len; i++) { const p1 = prev[f.name][i]; const p2 = elem[f.name][i]; if (p1 == null) { arr.push(p2); } else { if (f.key) { if (p2[f.key] != p1[f.key]) { p2[f.key] = (p2[f.key] || 0) - (p1[f.key] || 0); arr.push(p2); } } else { if (p2 != p1) { arr.push((p2 || 0) - (p1 || 0)); } } } } if (arr.length == 0 && entryDef.isOptional) { delete elem[f.name]; } else { elem[f.name] = arr; } } else { const p1 = prev[f.name]; const p2 = elem[f.name]; if (f.key) { p2[f.key] = (p2[f.key] || 0) - (p1[f.key] || 0); if (p2[f.key] == 0 && entryDef.isOptional) { delete elem[f.name]; } } else { const newValue = (p2 || 0) - (p1 || 0); if (newValue == 0 && entryDef.isOptional) { delete elem[f.name]; } else { elem[f.name] = newValue; } } } } } } } async makeBundle() { const jsons = await fg('*.json', { cwd: this.jsonPath, ignore: ['lang.json', 'bundle.json', 'dataVer.json'] }); // 构建脚本的JsonTerser会在对json进行混淆时重新覆盖生成bundle.json,此处需注意保持文件格式统一 const bundle = {}; for (const j of jsons) { const jfile = path.join(this.jsonPath, j); const jsonContent = await fs.readFile(jfile, 'utf-8'); bundle[path.basename(j, '.json')] = JSON.parse(jsonContent); } if (toolchain.projCfg.xml2json?.bundle == 'YES_JS') { await this.json2js(path.join(this.jsonPath, 'bundle.js'), bundle); } else { await fs.writeJSON(path.join(this.jsonPath, 'bundle.json'), bundle); } } async structToJson(obj, structDef, onlyForLang) { let isNotUsed = true; let o = {}, deleteKeys = []; for (let p of structDef.entries) { if (p._attributes.name === name_ClientUsed) continue; const noJson = onlyForLang || p.isNoJson, lang = onlyForLang || p.isLang, optional = !onlyForLang && p.isOptional; let so = obj[p._attributes.name]; if (!so) { // 表格里没填,则xml不会转出相应字段 if (!p.isAdditional && !noJson) { o[p._attributes.name] = this.getDefaultValue(p, false); if (optional) { deleteKeys.push(p._attributes.name); } } } else { if (isArrayEntry(p)) { // Array let pArr = []; let emptyAllowed = false; if (!(so instanceof Array)) { // 数值型数组会转成以空格隔开的单个字段 if (!isComplexType(p._attributes.type)) { console.assert(typeof (so._text) == 'string'); // 逻辑上说,只有p._attributes.sparse_array == '1'的情况下emptyAllowed才为true // trimArrayEnd讲自动裁剪掉无效的数组,为了兼容老项目,需显示启用本功能 emptyAllowed = toolchain.projCfg.xml2json?.trimArrayEnd == 'YES' ? p._attributes.sparse_array == '1' : true; // todo: 是否应该trim(),为了维持和perl版结果一致,NPCRandomStoreCfgM:m_dtRefreshTimes会多转出一个空字符串 let textArr = so._text.trimEnd().split(/\s+/); let newSoArr = []; for (let textElem of textArr) { newSoArr.push({ _text: textElem }); } so = newSoArr; } } let entryNotUsed = false; if (so instanceof Array) { let soArr = so; if (!soArr) { console.error(`array undefined!`); } let emptyElems = [], usedCnt = 0; for (let soElem of soArr) { let sout = await this.propertyToJson(soElem, p, true, onlyForLang); if (!sout.isNotUsed) usedCnt++; if (!sout.isNotUsed || emptyAllowed) { // 先将先前的空结构压入 for (let ee of emptyElems) { pArr.push(ee); } emptyElems.length = 0; pArr.push(sout.o); } else { if (toolchain.projCfg.xml2json?.trimArrayEnd == 'YES' || p._attributes.sparse_array == '1') { // 逻辑上说无论如何都需要缓存起来 // 为了兼容老项目(老项目需p._attributes.sparse_array == '1'),有了这个if // 先将空结构缓存起来 emptyElems.push(sout.o); } } } entryNotUsed = usedCnt == 0; } else { // Although defined as array, but only one element got const sout = await this.propertyToJson(so, p, true, onlyForLang); entryNotUsed = sout.isNotUsed; if (!sout.isNotUsed) { pArr.push(sout.o); } } if (!noJson) { o[p._attributes.name] = pArr; if (entryNotUsed && optional) { deleteKeys.push(p._attributes.name); } } if (!entryNotUsed) { isNotUsed = false; } } else { // Not array console.assert(!(so instanceof Array), `Single node expected: ${p._attributes.name}`); const sout = await this.propertyToJson(so, p, false, onlyForLang); if (!noJson) { o[p._attributes.name] = sout.o; if (sout.isNotUsed && optional) { deleteKeys.push(p._attributes.name); } if (!sout.isNotUsed) { isNotUsed = false; } } if (isLiteralStringType(p._attributes.type) && typeof (sout.o) === 'string') { if (sout.o && lang && !this.allLangs.includes(sout.o) && sout.o.search(this.HanPattern) >= 0) this.allLangs.push(sout.o); if (p.isAtoiArr) { if (sout.isNotUsed) { if (!optional) { o[p._attributes.name] = []; } } else { o[p._attributes.name] = sout.o.split(/[,,]/).map((v) => parseInt(v)); } } else if (p.isAtoi && !sout.isNotUsed) { const idx = this.strings.indexOf(sout.o); if (idx >= 0) { o[p._attributes.name] = idx; } else { this.strings.push(sout.o); o[p._attributes.name] = this.strings.length - 1; } } } } } } this.toDeletes.push({ o, deleteKeys }); return { o, isNotUsed: isNotUsed }; } /** * Convert a property into json, a 'true' value will be returned when the value is significant. * @param obj * @param out * @param p * @returns */ async propertyToJson(obj, p, isArrayItem, onlyForLang) { if (isComplexType(p._attributes.type)) { // Complex attr let complexObj = obj; complexObj._attributes.type = complexObj._attributes.type.replace('_Flash', 'M'); if (p._attributes.type != complexObj._attributes.type) { // 可能结构定义已改,但是未转表,导致字段类型不匹配 console.error(`[ERROR]Property type not match, ${p._attributes.type}(bin) vs ${complexObj._attributes.type}(definition): ${this.crtXml}`); const convInfo = JsonStructParser.Instance.getConvInfo(this.crtStruct); await Alert.Instance.sendBuildFailureAlert(`表格未转表导致转json失败: ${convInfo.name}(${convInfo.sheet}@${convInfo.xlsx})`); process.exit(1); } let pStructDef = JsonStructParser.Instance.getStructDef(p._attributes.type); if (!pStructDef) { console.error(`[ERROR]Cannot find struct definition: ${p._attributes.type}`); process.exit(1); } return await this.structToJson(complexObj, pStructDef, onlyForLang); } // Simple attr if (obj._text) { let o = this.convertValue(obj._text, p); if (isLiteralStringType(p._attributes.type) && typeof (o) === 'string' && o.startsWith('0')) { return { o, isNotUsed: true }; } if (Boolean(o)) { this.crtWeight++; } return { o, isNotUsed: !Boolean(o) }; } else { let o = this.getDefaultValue(p, isArrayItem); return { o, isNotUsed: true }; } } convertValue(raw, p) { if (StringLike.includes(p._attributes.type)) { // 换行符换成\n let out = raw.replace(/(?:\\r)?\\n/g, '\n').replace(/\r?\n/g, '\n'); // 检查富文本格式 if (count(out.replaceAll('#N', '').replaceAll(/<color=#\w.+>/g, '').replaceAll(/<font color=('|")#\w.+\1>/g, ''), '#') % 2 != 0) { let arr = this.errorMap[this.crtXml]; if (arr == null) this.errorMap[this.crtXml] = arr = []; arr.push(out); } return out; } // 将字符串转成数值 let num = Number(raw); if (num == -2147483648) { console.error(`[ERROR]Invalid value ${raw} found in ${this.crtXml}`); process.exit(1); } return num; } getDefaultValue(p, isArrayItem) { if (!isArrayItem && isArrayEntry(p)) { return []; } if (NumberLike.includes(p._attributes.type)) { return 0; } if (StringLike.includes(p._attributes.type)) { return ''; } return {}; } identifyStructNameKey(doc) { for (let key in doc) { if (key.endsWith('_Flash_Tab')) { return key; } } return null; } extractStrDirty = false; async extractStrings() { const strJson = path.join(this.jsonPath, 'strings.json'); const jsons = await fg('*.json', { cwd: this.jsonPath, ignore: ['LangCfgM.json', 'langs.json', 'strings.json'] }); const strings = [], strMap = {}; for (const js of jsons) { const jsf = path.join(this.jsonPath, js); const cfgs = await fs.readJson(jsf); this.extractStrDirty = false; this.collectStrings(cfgs, strings, strMap); if (this.extractStrDirty) { await fs.writeJson(jsf, cfgs, { spaces: 2 }); } } await fs.writeJSON(strJson, strings, { spaces: 2 }); } collectStrings(o, strings, strMap) { for (const key in o) { const value = o[key]; const t = typeof (value); if (t == 'string') { let strId = strMap[value]; if (!strId) { strings.push(value); strId = strings.length; strMap[value] = strId; } o[key] = strId; this.extractStrDirty = true; } else if (t == 'object') { this.collectStrings(value, strings, strMap); } } } async json2js(jsf, o) { const strings = []; this.collectConsts(o, strings); const cfgsStr = JSON.stringify(o, undefined, 2).replaceAll(/(?<=: )"\$s(\d+)"/g, (substring, ...args) => `_ss[${args[0]}]`).replaceAll(/"(\w+)"(?=: )/g, (stubString, ...args) => args[0]); let content = `const _ss = ${JSON.stringify(strings, undefined, 2)}; window._cfgs = ${cfgsStr}; `; await fs.writeFile(jsf, content, 'utf-8'); } collectConsts(o, strings) { for (const key in o) { const value = o[key]; const t = typeof (value); if (t == 'string') { let idx = strings.indexOf(value); if (idx < 0) { strings.push(value); idx = strings.length - 1; } o[key] = `$s${idx}`; } else if (t == 'object') { this.collectConsts(value, strings); } } } async makeErrorId() { let fileContent = await fs.readFile(path.join(this.xmlPath, 'Errno.config.xml'), 'utf-8'); console.log('making ErrorId.ts...'); let xml = xmljs.xml2json(fileContent, { compact: true, spaces: 0 }); let doc = JSON.parse(xml); let items = doc['ErrnoConfig_Flash_Tab']['ErrnoConfig_Flash']; let itemArr; if (items instanceof Array) { itemArr = items; } else { itemArr = [items]; } let content = '/**错误码集合(由xml2json自动生成)*/\n'; content += `export const enum ErrorId {\n`; for (let item of itemArr) { let name = item['m_szName']; if (name._text.startsWith('0')) continue; let desc = item['m_szDescriptionZH']; let value = item['m_uiValue']; content += ` /**${desc._text}*/\n`; content += ` ${name._text} = ${value._text},\n`; } content += '}'; await fs.writeFile(path.join(this.errorIdPath, 'ErrorId.ts'), content, 'utf-8'); } } //# sourceMappingURL=Xml2Json.js.map