fast-cocos 1.0.8 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/copy-assets.json +8 -0
- package/dist/audio/src/audio.js +7 -0
- package/dist/audio/src/index.js +4 -0
- package/dist/bmfont/src/BMFont.js +9 -0
- package/dist/bmfont/src/index.js +4 -0
- package/dist/index.js +24 -1
- package/dist/json/src/JsonUtils.js +50 -0
- package/dist/json/src/index.js +10 -7
- package/dist/logger.js +21 -0
- package/dist/robot/src/Robot.js +9 -3
- package/dist/robot/src/index.js +4 -0
- package/dist/ttf/jar/font.jar +0 -0
- package/dist/ttf/jar/sfnttool.jar +0 -0
- package/dist/ttf/py/modify_fonts.py +251 -0
- package/dist/ttf/py/modify_fonts1.py +284 -0
- package/dist/ttf/src/TTF.js +83 -0
- package/dist/ttf/src/index.js +34 -0
- package/dist/utils.js +4 -0
- package/dist/xlsx/src/XlsxParser.js +26 -4
- package/dist/xlsx/src/index.js +4 -0
- package/dist/xxteakey/src/index.js +4 -0
- package/package.json +1 -1
- package/dist/json/src/JSON.js +0 -111
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
from fontTools.ttLib import TTFont
|
|
3
|
+
import argparse
|
|
4
|
+
import os
|
|
5
|
+
import re
|
|
6
|
+
import shutil
|
|
7
|
+
import sys
|
|
8
|
+
|
|
9
|
+
# 字体选择标志常量
|
|
10
|
+
FS_SELECTION_BOLD = 0x20
|
|
11
|
+
FS_SELECTION_REGULAR = 0x40
|
|
12
|
+
|
|
13
|
+
# macStyle标志常量
|
|
14
|
+
MAC_STYLE_BOLD = 0x01
|
|
15
|
+
|
|
16
|
+
def get_font_info(font_path):
|
|
17
|
+
"""获取字体的原始信息"""
|
|
18
|
+
try:
|
|
19
|
+
font = TTFont(font_path)
|
|
20
|
+
name_table = font['name']
|
|
21
|
+
|
|
22
|
+
info = {
|
|
23
|
+
'family': None,
|
|
24
|
+
'subfamily': None,
|
|
25
|
+
'fullname': None,
|
|
26
|
+
'postscript': None,
|
|
27
|
+
'weight': 400
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
# 获取字体信息
|
|
31
|
+
for record in name_table.names:
|
|
32
|
+
if record.nameID == 1: # Family name
|
|
33
|
+
info['family'] = record.toUnicode()
|
|
34
|
+
elif record.nameID == 2: # Subfamily
|
|
35
|
+
info['subfamily'] = record.toUnicode()
|
|
36
|
+
elif record.nameID == 4: # Full name
|
|
37
|
+
info['fullname'] = record.toUnicode()
|
|
38
|
+
elif record.nameID == 6: # PostScript name
|
|
39
|
+
info['postscript'] = record.toUnicode()
|
|
40
|
+
|
|
41
|
+
# 获取字重
|
|
42
|
+
if 'OS/2' in font:
|
|
43
|
+
info['weight'] = font['OS/2'].usWeightClass
|
|
44
|
+
|
|
45
|
+
font.close()
|
|
46
|
+
return info
|
|
47
|
+
except Exception as e:
|
|
48
|
+
print(f"获取字体信息失败: {e}")
|
|
49
|
+
return {
|
|
50
|
+
'family': 'Unknown',
|
|
51
|
+
'subfamily': 'Regular',
|
|
52
|
+
'fullname': 'Unknown Regular',
|
|
53
|
+
'postscript': 'Unknown-Regular',
|
|
54
|
+
'weight': 400
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
def detect_weight_from_font(font_path):
|
|
58
|
+
"""从字体文件的OS/2表获取实际字重"""
|
|
59
|
+
try:
|
|
60
|
+
font = TTFont(font_path)
|
|
61
|
+
weight = 400 # 默认
|
|
62
|
+
|
|
63
|
+
if 'OS/2' in font:
|
|
64
|
+
weight = font['OS/2'].usWeightClass
|
|
65
|
+
|
|
66
|
+
font.close()
|
|
67
|
+
except Exception as e:
|
|
68
|
+
print(f"读取字体文件失败: {e}")
|
|
69
|
+
weight = 400
|
|
70
|
+
raise
|
|
71
|
+
|
|
72
|
+
# 根据字重值返回对应的样式名
|
|
73
|
+
if weight <= 100:
|
|
74
|
+
return weight, 'Thin'
|
|
75
|
+
elif weight <= 200:
|
|
76
|
+
return weight, 'ExtraLight'
|
|
77
|
+
elif weight <= 300:
|
|
78
|
+
return weight, 'Light'
|
|
79
|
+
elif weight <= 450:
|
|
80
|
+
return weight, 'Regular'
|
|
81
|
+
elif weight <= 550:
|
|
82
|
+
return weight, 'Medium'
|
|
83
|
+
elif weight <= 650:
|
|
84
|
+
return weight, 'SemiBold'
|
|
85
|
+
elif weight <= 750:
|
|
86
|
+
return weight, 'Bold'
|
|
87
|
+
elif weight <= 850:
|
|
88
|
+
return weight, 'ExtraBold'
|
|
89
|
+
else:
|
|
90
|
+
return weight, 'Black'
|
|
91
|
+
|
|
92
|
+
def generate_unique_family_name(original_family, weight_style, index):
|
|
93
|
+
"""基于索引生成唯一的字体家族名"""
|
|
94
|
+
# 移除原始家族名中的非ASCII字符和特殊字符
|
|
95
|
+
clean_name = re.sub(r'[^\w]', '', original_family)
|
|
96
|
+
clean_name = ''.join(c for c in clean_name if ord(c) < 128) # 只保留ASCII字符
|
|
97
|
+
|
|
98
|
+
# 如果清理后为空,使用默认名称
|
|
99
|
+
if not clean_name:
|
|
100
|
+
clean_name = "Font"
|
|
101
|
+
|
|
102
|
+
# 生成简短的唯一标识
|
|
103
|
+
if len(clean_name) > 8:
|
|
104
|
+
clean_name = clean_name[:8]
|
|
105
|
+
|
|
106
|
+
# 基于索引和字重生成唯一名称
|
|
107
|
+
return f"{clean_name}{weight_style}{index}"
|
|
108
|
+
|
|
109
|
+
def sanitize_postscript_name(name):
|
|
110
|
+
"""清理PostScript名称,确保符合命名规范"""
|
|
111
|
+
# PostScript名称只能包含ASCII字母、数字和连字符
|
|
112
|
+
# 不能有空格,必须以字母开头
|
|
113
|
+
sanitized = re.sub(r'[^a-zA-Z0-9-]', '', name.replace(' ', ''))
|
|
114
|
+
|
|
115
|
+
# 确保以字母开头
|
|
116
|
+
if sanitized and not sanitized[0].isalpha():
|
|
117
|
+
sanitized = 'Font' + sanitized
|
|
118
|
+
|
|
119
|
+
# 如果完全为空,返回默认值
|
|
120
|
+
if not sanitized:
|
|
121
|
+
sanitized = 'Font-Regular'
|
|
122
|
+
|
|
123
|
+
return sanitized
|
|
124
|
+
|
|
125
|
+
def modify_font_complete(font_path, new_family_name=None, weight_value=None, style_name=None, index=1):
|
|
126
|
+
"""完全修改字体的所有相关元数据"""
|
|
127
|
+
# 验证字体文件
|
|
128
|
+
if not os.path.exists(font_path):
|
|
129
|
+
raise FileNotFoundError(f"字体文件不存在: {font_path}")
|
|
130
|
+
|
|
131
|
+
if not font_path.lower().endswith(('.ttf')):
|
|
132
|
+
raise ValueError(f"不支持的字体格式: {font_path}")
|
|
133
|
+
|
|
134
|
+
# 如果没有提供参数,自动检测
|
|
135
|
+
if not all([new_family_name, weight_value, style_name]):
|
|
136
|
+
font_info = get_font_info(font_path)
|
|
137
|
+
|
|
138
|
+
# 从字体文件获取实际字重
|
|
139
|
+
detected_weight, detected_style = detect_weight_from_font(font_path)
|
|
140
|
+
|
|
141
|
+
# 如果没有提供参数,使用检测到的值
|
|
142
|
+
if not new_family_name:
|
|
143
|
+
new_family_name = generate_unique_family_name(font_info['family'], detected_style, index)
|
|
144
|
+
if not weight_value:
|
|
145
|
+
weight_value = detected_weight
|
|
146
|
+
if not style_name:
|
|
147
|
+
style_name = "Regular" # iOS推荐所有独立字体都使用Regular
|
|
148
|
+
|
|
149
|
+
# 验证字重值
|
|
150
|
+
if not isinstance(weight_value, int) or weight_value < 1 or weight_value > 1000:
|
|
151
|
+
raise ValueError(f"无效的字重值: {weight_value},必须在1-1000之间")
|
|
152
|
+
|
|
153
|
+
print(f"\n处理字体: {os.path.basename(font_path)}")
|
|
154
|
+
print(f"新家族名: {new_family_name}")
|
|
155
|
+
print(f"字重: {weight_value}")
|
|
156
|
+
print(f"样式: {style_name}")
|
|
157
|
+
|
|
158
|
+
try:
|
|
159
|
+
font = TTFont(font_path)
|
|
160
|
+
except Exception as e:
|
|
161
|
+
raise ValueError(f"无法打开字体文件: {e}")
|
|
162
|
+
|
|
163
|
+
# 获取name表
|
|
164
|
+
name_table = font['name']
|
|
165
|
+
|
|
166
|
+
# 需要修改的所有nameID
|
|
167
|
+
# 1 = Family name
|
|
168
|
+
# 2 = Subfamily name
|
|
169
|
+
# 3 = Unique identifier
|
|
170
|
+
# 4 = Full name
|
|
171
|
+
# 6 = PostScript name
|
|
172
|
+
# 16 = Typographic Family name (Preferred family)
|
|
173
|
+
# 17 = Typographic Subfamily name (Preferred subfamily)
|
|
174
|
+
|
|
175
|
+
# 使用清理函数生成符合规范的PostScript名称
|
|
176
|
+
postscript_name = sanitize_postscript_name(f"{new_family_name}-{style_name}")
|
|
177
|
+
full_name = f"{new_family_name} {style_name}"
|
|
178
|
+
|
|
179
|
+
# 创建新的name记录
|
|
180
|
+
name_updates = {
|
|
181
|
+
1: new_family_name, # Family
|
|
182
|
+
2: style_name, # Subfamily
|
|
183
|
+
3: f"{new_family_name} {style_name}", # Unique ID
|
|
184
|
+
4: full_name, # Full name
|
|
185
|
+
6: postscript_name, # PostScript name
|
|
186
|
+
16: new_family_name, # Preferred Family
|
|
187
|
+
17: style_name, # Preferred Subfamily
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
# 更新所有相关的name记录
|
|
191
|
+
for record in name_table.names:
|
|
192
|
+
if record.nameID in name_updates:
|
|
193
|
+
old_value = record.toUnicode()
|
|
194
|
+
new_value = name_updates[record.nameID]
|
|
195
|
+
try:
|
|
196
|
+
# 使用UTF-16-BE编码,这是字体行业的通用标准
|
|
197
|
+
record.string = new_value.encode('utf-16-be')
|
|
198
|
+
print(f"NameID {record.nameID}: {old_value} -> {new_value}")
|
|
199
|
+
except Exception as e:
|
|
200
|
+
print(f"编码失败 (NameID {record.nameID}): {e}")
|
|
201
|
+
# 保持原值不变
|
|
202
|
+
continue
|
|
203
|
+
|
|
204
|
+
# 修改OS/2表
|
|
205
|
+
if 'OS/2' in font:
|
|
206
|
+
os2_table = font['OS/2']
|
|
207
|
+
old_weight = os2_table.usWeightClass
|
|
208
|
+
os2_table.usWeightClass = weight_value
|
|
209
|
+
print(f"OS/2 Weight: {old_weight} -> {weight_value}")
|
|
210
|
+
|
|
211
|
+
# 设置字体选择标志
|
|
212
|
+
os2_table.fsSelection = 0
|
|
213
|
+
if weight_value >= 700: # Bold
|
|
214
|
+
os2_table.fsSelection |= FS_SELECTION_BOLD
|
|
215
|
+
elif weight_value == 400: # Regular
|
|
216
|
+
os2_table.fsSelection |= FS_SELECTION_REGULAR
|
|
217
|
+
|
|
218
|
+
# 修改head表中的macStyle
|
|
219
|
+
if 'head' in font:
|
|
220
|
+
head_table = font['head']
|
|
221
|
+
head_table.macStyle = 0
|
|
222
|
+
if weight_value >= 700:
|
|
223
|
+
head_table.macStyle |= MAC_STYLE_BOLD
|
|
224
|
+
|
|
225
|
+
# 保存修改后的字体
|
|
226
|
+
base_name = os.path.splitext(font_path)[0]
|
|
227
|
+
output_path = f"{base_name}.ttf"
|
|
228
|
+
|
|
229
|
+
try:
|
|
230
|
+
font.save(output_path)
|
|
231
|
+
print(f"Saved to: {output_path}\n")
|
|
232
|
+
font.close()
|
|
233
|
+
return output_path
|
|
234
|
+
except Exception as e:
|
|
235
|
+
font.close()
|
|
236
|
+
print(f"保存字体失败: {e}")
|
|
237
|
+
raise
|
|
238
|
+
|
|
239
|
+
def copy_files(src_folder, dst_folder):
|
|
240
|
+
"""
|
|
241
|
+
将源文件夹所有文件复制到目标文件夹
|
|
242
|
+
:param src_folder: 源文件夹路径
|
|
243
|
+
:param dst_folder: 目标文件夹路径
|
|
244
|
+
"""
|
|
245
|
+
try:
|
|
246
|
+
# 确保目标文件夹存在
|
|
247
|
+
os.makedirs(dst_folder, exist_ok=True)
|
|
248
|
+
|
|
249
|
+
# 遍历源文件夹
|
|
250
|
+
for item in os.listdir(src_folder):
|
|
251
|
+
src_path = os.path.join(src_folder, item)
|
|
252
|
+
dst_path = os.path.join(dst_folder, item)
|
|
253
|
+
|
|
254
|
+
# 如果是文件则复制
|
|
255
|
+
if os.path.isfile(src_path):
|
|
256
|
+
shutil.copy2(src_path, dst_path)
|
|
257
|
+
print(f"已复制: {src_path} -> {dst_path}")
|
|
258
|
+
# 如果是子文件夹则递归处理(可选)
|
|
259
|
+
# else:
|
|
260
|
+
# copy_files(src_path, dst_path)
|
|
261
|
+
|
|
262
|
+
print(f"操作完成!共复制了{len(os.listdir(src_folder))}个文件")
|
|
263
|
+
except Exception as e:
|
|
264
|
+
print(f"发生错误: {e}")
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
def main():
|
|
269
|
+
parser = argparse.ArgumentParser(description="修改 TTF 字体元数据(name / OS/2 / head)")
|
|
270
|
+
parser.add_argument(
|
|
271
|
+
"font_path",
|
|
272
|
+
help="要处理的 TTF 字体文件路径(通常为瘦身后的输出文件)",
|
|
273
|
+
)
|
|
274
|
+
args = parser.parse_args()
|
|
275
|
+
font_path = os.path.abspath(args.font_path)
|
|
276
|
+
if not os.path.isfile(font_path):
|
|
277
|
+
raise SystemExit(f"字体文件不存在: {font_path}")
|
|
278
|
+
modify_font_complete(font_path)
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
if __name__ == "__main__":
|
|
282
|
+
# main()
|
|
283
|
+
modify_font_complete(sys.argv[1])
|
|
284
|
+
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.ttf = void 0;
|
|
7
|
+
const child_process_1 = require("child_process");
|
|
8
|
+
const fs_1 = __importDefault(require("fs"));
|
|
9
|
+
const path_1 = __importDefault(require("path"));
|
|
10
|
+
const logger_1 = __importDefault(require("../../logger"));
|
|
11
|
+
const utils_1 = require("../../utils");
|
|
12
|
+
class TTF {
|
|
13
|
+
/**
|
|
14
|
+
* 字体瘦身:抽取字符、合并 JSON 与自定义字表,再用 sfnttool 生成子集 TTF。
|
|
15
|
+
* 流程对齐参考脚本 `ttf.js`。
|
|
16
|
+
* @param options 选项
|
|
17
|
+
*/
|
|
18
|
+
parse(options) {
|
|
19
|
+
// 获取所有指定目录下所有后缀文件
|
|
20
|
+
const outFiles = utils_1.utils.findFiles(options.output, [".ttf"]);
|
|
21
|
+
const srcFiles = utils_1.utils.findFiles(options.source, [".ttf"]);
|
|
22
|
+
// 需要处理的文件名
|
|
23
|
+
const needProcessFileNames = outFiles.map((file) => path_1.default.basename(file));
|
|
24
|
+
// 需要处理的文件信息
|
|
25
|
+
const processFiles = [];
|
|
26
|
+
for (const file of srcFiles) {
|
|
27
|
+
const fileName = path_1.default.basename(file);
|
|
28
|
+
if (needProcessFileNames.includes(fileName)) {
|
|
29
|
+
processFiles.push({ src: file, dst: path_1.default.join(options.output, fileName) });
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
// 处理
|
|
33
|
+
const pyPath = path_1.default.join(__dirname, "..", "py", "modify_fonts.py");
|
|
34
|
+
// 处理字符集
|
|
35
|
+
const fontPath = this.handleTxt(options);
|
|
36
|
+
// 构建后 __dirname 为 dist/ttf/src,与 copy-assets 中 dist/ttf/jar 对齐
|
|
37
|
+
const jarDir = path_1.default.join(__dirname, "..", "jar");
|
|
38
|
+
const sfnttoolJar = path_1.default.join(jarDir, "sfnttool.jar");
|
|
39
|
+
logger_1.default.info(`[${new Date().toLocaleString()}] 开始字体瘦身`);
|
|
40
|
+
for (const { src, dst } of processFiles) {
|
|
41
|
+
(0, child_process_1.execFileSync)("java", ["-jar", sfnttoolJar, "-c", fontPath, src, dst], { stdio: "inherit" });
|
|
42
|
+
logger_1.default.info(`字体瘦身完成: ${dst}`);
|
|
43
|
+
if (fs_1.default.existsSync(pyPath)) {
|
|
44
|
+
const python = process.platform === "win32" ? "python" : "python3";
|
|
45
|
+
(0, child_process_1.execFileSync)(python, [pyPath, dst], { stdio: "inherit" });
|
|
46
|
+
}
|
|
47
|
+
else {
|
|
48
|
+
logger_1.default.warn(`未找到脚本,跳过元数据修改: ${pyPath}`);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
// 删除临时文件
|
|
52
|
+
utils_1.utils.rmSync(path_1.default.join(options.output, "font.txt"));
|
|
53
|
+
// utils.rmSync(fontPath);
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* 处理字符集
|
|
57
|
+
* @param options
|
|
58
|
+
* @returns 字符集文本路径
|
|
59
|
+
*/
|
|
60
|
+
handleTxt(options) {
|
|
61
|
+
// 抽取输出目录中的字符集
|
|
62
|
+
const jarDir = path_1.default.join(__dirname, "..", "jar");
|
|
63
|
+
const fontJar = path_1.default.join(jarDir, "font.jar");
|
|
64
|
+
(0, child_process_1.execFileSync)("java", ["-jar", fontJar, options.output], { stdio: "inherit" });
|
|
65
|
+
let fontStr = fs_1.default.readFileSync(path_1.default.join(options.output, "font.txt"), "utf-8");
|
|
66
|
+
const fontSet = new Set([...fontStr]);
|
|
67
|
+
// 自定义字符集
|
|
68
|
+
const txtFiles = utils_1.utils.findFiles(options.source, [".txt"]);
|
|
69
|
+
for (const txtFile of txtFiles) {
|
|
70
|
+
const str = fs_1.default.readFileSync(txtFile, "utf-8");
|
|
71
|
+
[...str].forEach((s) => fontSet.add(s));
|
|
72
|
+
}
|
|
73
|
+
// 将新的字符集写入临时文件
|
|
74
|
+
const tmpTxtPath = path_1.default.join(options.source, "tmp.txt");
|
|
75
|
+
if (fs_1.default.existsSync(tmpTxtPath)) {
|
|
76
|
+
fs_1.default.unlinkSync(tmpTxtPath);
|
|
77
|
+
}
|
|
78
|
+
fs_1.default.writeFileSync(tmpTxtPath, [...fontSet].join(""), "utf-8");
|
|
79
|
+
console.info("\x1b[32m%s\x1b[0m", `临时字符集文件路径: ${tmpTxtPath}`);
|
|
80
|
+
return tmpTxtPath;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
exports.ttf = new TTF();
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
3
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
4
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
5
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
6
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
7
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
8
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
|
+
});
|
|
10
|
+
};
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
const commander_1 = require("commander");
|
|
13
|
+
const TTF_1 = require("./TTF");
|
|
14
|
+
/**
|
|
15
|
+
* 解析机器人消息命令参数,并按类型发送到对应平台。
|
|
16
|
+
*/
|
|
17
|
+
function main() {
|
|
18
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
19
|
+
commander_1.program
|
|
20
|
+
.name("ttf")
|
|
21
|
+
.description("ttf parser")
|
|
22
|
+
.version("1.0.0")
|
|
23
|
+
.requiredOption("-s, --source <directory>", "source directory")
|
|
24
|
+
.requiredOption("-o, --output <directory>", "output directory")
|
|
25
|
+
.parse(process.argv);
|
|
26
|
+
const options = commander_1.program.opts();
|
|
27
|
+
TTF_1.ttf.parse(options);
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
// 捕获入口异步异常并以非零状态码退出。
|
|
31
|
+
void main().catch((err) => {
|
|
32
|
+
console.error(err);
|
|
33
|
+
process.exit(1);
|
|
34
|
+
});
|
package/dist/utils.js
CHANGED
|
@@ -36,6 +36,7 @@ var utils;
|
|
|
36
36
|
function getSubDirsSync(dir) {
|
|
37
37
|
const subDirs = [];
|
|
38
38
|
const files = fs_1.default.readdirSync(dir);
|
|
39
|
+
// 将目录项过滤成完整路径形式的子目录列表。
|
|
39
40
|
files.forEach((file) => {
|
|
40
41
|
const filePath = path_1.default.join(dir, file);
|
|
41
42
|
const fileStat = fs_1.default.statSync(filePath);
|
|
@@ -55,6 +56,7 @@ var utils;
|
|
|
55
56
|
if (!fs_1.default.existsSync(dir)) {
|
|
56
57
|
return false;
|
|
57
58
|
}
|
|
59
|
+
// 只要存在一个目标扩展名文件,即认为目录满足条件。
|
|
58
60
|
return fs_1.default.readdirSync(dir).some((file) => path_1.default.extname(file) === ext);
|
|
59
61
|
}
|
|
60
62
|
utils.containsFileExt = containsFileExt;
|
|
@@ -96,6 +98,7 @@ var utils;
|
|
|
96
98
|
exts = [exts];
|
|
97
99
|
}
|
|
98
100
|
const fileList = fs_1.default.readdirSync(dir);
|
|
101
|
+
// 递归收集匹配扩展名的文件路径。
|
|
99
102
|
fileList.forEach((file) => {
|
|
100
103
|
const filePath = path_1.default.join(dir, file);
|
|
101
104
|
const fileStat = fs_1.default.statSync(filePath);
|
|
@@ -202,6 +205,7 @@ var utils;
|
|
|
202
205
|
return;
|
|
203
206
|
}
|
|
204
207
|
const files = fs_1.default.readdirSync(dir); // 同步读取目录内容
|
|
208
|
+
// 仅删除当前目录中符合扩展名且未被排除的文件。
|
|
205
209
|
files.forEach((file) => {
|
|
206
210
|
const filePath = path_1.default.join(dir, file);
|
|
207
211
|
// 获取文件的状态
|
|
@@ -20,6 +20,10 @@ var SheetType;
|
|
|
20
20
|
* Xlsx解析器
|
|
21
21
|
*/
|
|
22
22
|
class XlsxParser {
|
|
23
|
+
/**
|
|
24
|
+
* 初始化解析器配置,并清理上一次导出的临时目录。
|
|
25
|
+
* @param options xlsx 解析与输出路径配置
|
|
26
|
+
*/
|
|
23
27
|
constructor(options) {
|
|
24
28
|
/** 数组分隔符类 */
|
|
25
29
|
this.separators = ["|", ";", ",", "-", "_"];
|
|
@@ -40,10 +44,12 @@ class XlsxParser {
|
|
|
40
44
|
*/
|
|
41
45
|
parse() {
|
|
42
46
|
const xlsxFiles = this.findFilesWithExtensions(this.options.xlsxDir, [".xlsx", ".xls"]);
|
|
47
|
+
// 逐个解析源目录下的 Excel 文件。
|
|
43
48
|
xlsxFiles.forEach((filePath) => {
|
|
44
49
|
console.info("\x1b[35m%s\x1b[0m", "解析文件: " + filePath);
|
|
45
50
|
const xlsxName = path_1.default.basename(filePath, path_1.default.extname(filePath));
|
|
46
51
|
const workSheetsFromFile = node_xlsx_1.default.parse(filePath);
|
|
52
|
+
// 将同一个工作簿中的每个工作表分别按表类型导出。
|
|
47
53
|
workSheetsFromFile.forEach((workSheet) => {
|
|
48
54
|
this.parseWorkSheet(xlsxName, workSheet);
|
|
49
55
|
});
|
|
@@ -63,6 +69,7 @@ class XlsxParser {
|
|
|
63
69
|
// 解析表名、类型、版本
|
|
64
70
|
const [basename, sheetType, version] = arr;
|
|
65
71
|
// 去除空数组
|
|
72
|
+
// 丢弃完全为空的行,避免表头或数据解析被空行干扰。
|
|
66
73
|
data = data.filter((item) => item.length > 0);
|
|
67
74
|
/** 介绍(第一行) */
|
|
68
75
|
let intros = data[0];
|
|
@@ -82,7 +89,11 @@ class XlsxParser {
|
|
|
82
89
|
data.shift();
|
|
83
90
|
data.shift();
|
|
84
91
|
}
|
|
85
|
-
|
|
92
|
+
// 统一清洗每行中的单元格文本。
|
|
93
|
+
data.forEach((row) => {
|
|
94
|
+
// 将单元格转成字符串并去掉首尾空白。
|
|
95
|
+
row.forEach((v, i) => (row[i] = String(v || "").trim()));
|
|
96
|
+
});
|
|
86
97
|
let fileName = `${basename}${sheetType}`;
|
|
87
98
|
let typeName = fileName;
|
|
88
99
|
// 带版本号
|
|
@@ -143,6 +154,7 @@ class XlsxParser {
|
|
|
143
154
|
for (let i = 0; i < row.length; ++i) {
|
|
144
155
|
const field = fields[i];
|
|
145
156
|
if (field) {
|
|
157
|
+
// 提供整列值,供数组类型自动推断真实分隔符。
|
|
146
158
|
const columns = data.map((v) => v[i]);
|
|
147
159
|
rowInfo[field] = this.parseValue(types[i], row[i], columns);
|
|
148
160
|
}
|
|
@@ -172,6 +184,7 @@ class XlsxParser {
|
|
|
172
184
|
for (let j = 0; j < col.length; ++j) {
|
|
173
185
|
const field = fields[j];
|
|
174
186
|
if (field) {
|
|
187
|
+
// 提供整列值,供数组类型自动推断真实分隔符。
|
|
175
188
|
const columns = data.map((v) => v[j]);
|
|
176
189
|
info[field] = this.parseValue(types[j], col[j], columns);
|
|
177
190
|
}
|
|
@@ -197,6 +210,7 @@ class XlsxParser {
|
|
|
197
210
|
for (let i = 0; i < data.length; ++i) {
|
|
198
211
|
const col = data[i];
|
|
199
212
|
if (col[0]) {
|
|
213
|
+
// 新的首列值表示一个新的合并行分组。
|
|
200
214
|
const columns = data.map((v) => v[0]);
|
|
201
215
|
group = {
|
|
202
216
|
[fields[0]]: this.parseValue(types[0], col[0], columns),
|
|
@@ -223,6 +237,7 @@ class XlsxParser {
|
|
|
223
237
|
for (let i = 0; i < row.length; ++i) {
|
|
224
238
|
const field = fields[i];
|
|
225
239
|
if (field) {
|
|
240
|
+
// 提供整列值,供数组类型自动推断真实分隔符。
|
|
226
241
|
const columns = data.map((v) => v[i]);
|
|
227
242
|
info[field] = this.parseValue(types[i], row[i], columns);
|
|
228
243
|
}
|
|
@@ -237,6 +252,7 @@ class XlsxParser {
|
|
|
237
252
|
if (data.length <= 0 || !data[0][0]) {
|
|
238
253
|
return false;
|
|
239
254
|
}
|
|
255
|
+
// 首列后续行为空但同一行仍有值时,视为首列合并行数组。
|
|
240
256
|
return data.some((row, index) => index > 0 && !row[0] && row.some((value) => value));
|
|
241
257
|
}
|
|
242
258
|
/**
|
|
@@ -260,7 +276,7 @@ class XlsxParser {
|
|
|
260
276
|
this.writeSheetSync(sheetInfo.outPath, content);
|
|
261
277
|
}
|
|
262
278
|
/**
|
|
263
|
-
*
|
|
279
|
+
* 解析三列表结构的变量映射表
|
|
264
280
|
* @param sheetInfo 工作表信息
|
|
265
281
|
*/
|
|
266
282
|
parseVMap(sheetInfo) {
|
|
@@ -353,6 +369,7 @@ class XlsxParser {
|
|
|
353
369
|
// 分隔符数量
|
|
354
370
|
let separatorCount = separators.length;
|
|
355
371
|
// 逆序分隔符
|
|
372
|
+
// 去掉空分隔符声明,并反向应用以匹配递归拆分顺序。
|
|
356
373
|
let realSeparators = separators.filter((item) => item !== "").reverse();
|
|
357
374
|
// 从列中抽取分隔符
|
|
358
375
|
if (realSeparators.length <= 0 && separatorCount > 0) {
|
|
@@ -397,6 +414,7 @@ class XlsxParser {
|
|
|
397
414
|
return [this.splitStringToNestedArray(baseType, valueStr, remainingDelimiters)];
|
|
398
415
|
}
|
|
399
416
|
// 分割当前层级并递归处理每个部分
|
|
417
|
+
// 当前层拆分后的每一段继续使用剩余分隔符递归解析。
|
|
400
418
|
return valueStr.split(currentDelimiter).map((part) => this.splitStringToNestedArray(baseType, part, remainingDelimiters));
|
|
401
419
|
}
|
|
402
420
|
/**
|
|
@@ -411,6 +429,7 @@ class XlsxParser {
|
|
|
411
429
|
extensions = [extensions];
|
|
412
430
|
}
|
|
413
431
|
const fileList = fs_1.default.readdirSync(dir);
|
|
432
|
+
// 递归收集匹配扩展名的文件路径。
|
|
414
433
|
fileList.forEach((file) => {
|
|
415
434
|
const filePath = path_1.default.join(dir, file);
|
|
416
435
|
const fileStat = fs_1.default.statSync(filePath);
|
|
@@ -531,7 +550,7 @@ class XlsxParser {
|
|
|
531
550
|
}
|
|
532
551
|
/**
|
|
533
552
|
* 去除多余类型字符
|
|
534
|
-
* @param str
|
|
553
|
+
* @param str 原始类型字符串
|
|
535
554
|
*/
|
|
536
555
|
removeExtraType(str) {
|
|
537
556
|
// 使用正则表达式匹配所有方括号中的内容
|
|
@@ -552,7 +571,7 @@ class XlsxParser {
|
|
|
552
571
|
}
|
|
553
572
|
/**
|
|
554
573
|
* 处理字符串,确保它不以数字开头,并且移除所有空格、制表符和换行符
|
|
555
|
-
* @param input
|
|
574
|
+
* @param input 原始字符串
|
|
556
575
|
*/
|
|
557
576
|
processString(input) {
|
|
558
577
|
// Remove leading digits
|
|
@@ -569,6 +588,7 @@ class XlsxParser {
|
|
|
569
588
|
const jsonDir = path_1.default.join(this.options.tmpDir);
|
|
570
589
|
const files = this.findFilesWithExtensions(jsonDir, [".json"]);
|
|
571
590
|
const json = {};
|
|
591
|
+
// 将每个临时表 JSON 合并到最终 xlsx.json 的表名键下。
|
|
572
592
|
files.forEach((filePath) => {
|
|
573
593
|
const basename = path_1.default.basename(filePath);
|
|
574
594
|
const arr = this.processString(basename).split(".");
|
|
@@ -618,10 +638,12 @@ class XlsxParser {
|
|
|
618
638
|
declareStr += `\n /** 整数 */`;
|
|
619
639
|
declareStr += `\n type int = number;`;
|
|
620
640
|
// 遍历声明表
|
|
641
|
+
// 拼接每张表解析过程中收集到的 TypeScript 类型声明。
|
|
621
642
|
this.declareMap.forEach((value) => {
|
|
622
643
|
if (value.length <= 0) {
|
|
623
644
|
return;
|
|
624
645
|
}
|
|
646
|
+
// 为 namespace 内部声明增加统一缩进。
|
|
625
647
|
value = value.map((v) => " ".repeat(4) + v);
|
|
626
648
|
declareStr += "\n\n";
|
|
627
649
|
declareStr += value.join("\n");
|
package/dist/xlsx/src/index.js
CHANGED
|
@@ -15,6 +15,9 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
15
15
|
const commander_1 = require("commander");
|
|
16
16
|
const path_1 = __importDefault(require("path"));
|
|
17
17
|
const XlsxParser_1 = __importDefault(require("./XlsxParser"));
|
|
18
|
+
/**
|
|
19
|
+
* 解析 xlsx 导出命令参数,并初始化表格解析器。
|
|
20
|
+
*/
|
|
18
21
|
function main() {
|
|
19
22
|
return __awaiter(this, void 0, void 0, function* () {
|
|
20
23
|
commander_1.program
|
|
@@ -43,6 +46,7 @@ function main() {
|
|
|
43
46
|
xlsxParser.parse();
|
|
44
47
|
});
|
|
45
48
|
}
|
|
49
|
+
// 捕获入口异步异常并以非零状态码退出。
|
|
46
50
|
void main().catch((err) => {
|
|
47
51
|
console.error(err);
|
|
48
52
|
process.exit(1);
|
|
@@ -27,6 +27,9 @@ function generateKey() {
|
|
|
27
27
|
// 组合成 8-4-2 格式
|
|
28
28
|
return `${part1}-${part2}-${part3}`;
|
|
29
29
|
}
|
|
30
|
+
/**
|
|
31
|
+
* 读取指定 JSON 文件,生成并写入新的 xxteaKey 字段。
|
|
32
|
+
*/
|
|
30
33
|
function main() {
|
|
31
34
|
return __awaiter(this, void 0, void 0, function* () {
|
|
32
35
|
commander_1.program
|
|
@@ -50,6 +53,7 @@ function main() {
|
|
|
50
53
|
console.log(`xxteaKey: ${xxteaKey}`);
|
|
51
54
|
});
|
|
52
55
|
}
|
|
56
|
+
// 捕获入口异步异常并以非零状态码退出。
|
|
53
57
|
void main().catch((err) => {
|
|
54
58
|
console.error(err);
|
|
55
59
|
process.exit(1);
|