fy-convertor
Version:
Convert excel/xml/json/bin/protocl/ts/as ...
975 lines • 43.9 kB
JavaScript
import fg from 'fast-glob';
import fs from 'fs-extra';
import path from 'path';
import md5 from 'md5';
import xmljs from 'xml-js';
import { copyFiles, 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 { RepoHolder, adjustSVN } from './RepoHolder.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';
import { decode, encode } from 'msgpack-lite';
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 RepoHolder.Instance.checkSVN(!toolchain.options.debug, this.structPath, adjustSVN(toolchain.projCfg.Svn.DesignerSvn) + 'xml');
await RepoHolder.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 RepoHolder.Instance.commit('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 copyFiles(this.jsonPath, sp, '*.json');
await fs.copyFile(path.join(this.errorIdPath, 'ErrorId.ts'), path.join(ep, 'ErrorId.ts'));
await RepoHolder.Instance.commit('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 RepoHolder.Instance.checkSVN(true, jsonPath, adjustSVN(repository) + (toolchain.projCfg.xml2json?.jsonRoot ?? 'project/bin/res/data'));
await RepoHolder.Instance.checkSVN(true, errorIdPath, adjustSVN(repository) + (toolchain.projCfg.xml2protocol?.protocolRoot ?? 'project/src/automatic/protocol'));
}
else {
await RepoHolder.Instance.checkSVN(true, jsonPath, adjustSVN(repository) + (toolchain.projCfg.xml2json?.jsonRoot ?? 'project/Assets/AssetSources/data'));
await RepoHolder.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.err) {
if (!this.errorMap[this.crtXml])
this.errorMap[this.crtXml] = [];
this.errorMap[this.crtXml].push(out.err);
}
if (!out.isNotUsed && this.crtWeight > 0) {
cfgs.push(out.o);
}
}
if (cfgs.length === rawCfgs.length) {
// 那就把第一个当成表头
cfgs.shift();
}
// 检查refer字段
await this.checkReferEquality(structDef, cfgs);
// 检查表格合法性
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 && cfgs.length > 1) {
if (dvRule.serial_keys) {
const nk = dvRule.serial_keys.find(x => !(x in cfgs[0]));
if (nk != null) {
await Alert.Instance.sendBuildFailureAlert(`diff rule错误:${structName}:${nk}不存在!`);
process.exit(1);
}
}
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 = Object.keys(this.errorMap);
if (errorFiles.length > 0) {
let totalErrorCount = 0;
for (const file of errorFiles) {
console.error(file);
const arr = this.errorMap[file];
totalErrorCount += arr.length;
arr.forEach((v) => console.error(v.msg));
console.log('---------------------');
}
await Alert.Instance.sendBuildFailureAlert(`${totalErrorCount}项错误\n: ${this.errorMap[errorFiles[0]][0].msg}: ${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 checkReferEquality(structDef, cfgs) {
// 检查refer字段
for (const o of cfgs) {
for (let p of structDef.entries) {
if (p._attributes.refer) {
const referEntry = structDef.entries.find(e => e._attributes.name == p._attributes.refer);
if (!referEntry || referEntry.isNoJson) {
continue;
}
const f = o[p._attributes.name];
if (f != null && f instanceof Array) {
const referField = o[p._attributes.refer] ?? 0;
if (f.length != referField) {
const ci = JsonStructParser.Instance.getConvInfo(this.crtStruct);
const err = { msg: `${ci.sheet}@${ci.xlsx} 引用字段 ${p._attributes.refer} 数值(${referField})与实际数组长度(${f.length})不匹配: ${this.tryGetIndetifier(o, structDef)}...`, type: 2 /* EErrorType.ReferNotMatch */ };
if (!this.errorMap[this.crtXml])
this.errorMap[this.crtXml] = [];
this.errorMap[this.crtXml].push(err);
}
}
}
}
}
}
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);
}
// 生成msgpacker版本
const buffer = encode(bundle);
await fs.writeFile(path.join(this.jsonPath, 'bundle.bytes'), buffer);
decode(buffer);
}
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 });
let err = undefined;
return { o, isNotUsed: isNotUsed, err };
}
tryGetIndetifier(o, structDef) {
if (structDef._attributes.sortkey) {
return `${structDef._attributes.sortkey}: ${o[structDef._attributes.sortkey]}`;
}
const keys = Object.keys(o).sort((a, b) => a.length - b.length);
const key = keys.find(k => k.match(/m_\w*ID/));
return key ? `${key}: ${o[key]}` : JSON.stringify(o).substring(0, 20);
}
/**
* 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 = await 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 };
}
}
async 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({ msg: `富文本格式错误:${out}`, type: 1 /* EErrorType.RichText */ });
}
return out;
}
// 将字符串转成数值
let num = Number(raw);
if (num == -2147483648) {
await Alert.Instance.sendBuildFailureAlert(`[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