64 lines
3.6 KiB
JavaScript
64 lines
3.6 KiB
JavaScript
// 只读提取源码中的中文候选;不扫描图片、生成物或第三方依赖。
|
|
const fs = require('node:fs');
|
|
const path = require('node:path');
|
|
const parser = require('@babel/parser');
|
|
const traverse = require('@babel/traverse').default;
|
|
const sfc = require('@vue/compiler-sfc');
|
|
const dom = require('@vue/compiler-dom');
|
|
const han = /[\p{Script=Han}]/u;
|
|
const root = path.resolve(__dirname, '../src');
|
|
const entries = [];
|
|
function scripts(code, file, offset = 0, context = 'script') {
|
|
if (context === 'expression') { code = '(' + code + ')'; offset -= 1; }
|
|
const ast = parser.parse(code, {sourceType: 'module', plugins: ['typescript'], errorRecovery: true});
|
|
traverse(ast, {
|
|
StringLiteral(p) { add(p, p.node.value, 'string'); },
|
|
TemplateLiteral(p) {
|
|
if (!p.node.quasis.some(q => han.test(q.value.cooked || q.value.raw))) return;
|
|
const text = p.node.quasis.map((q, i) => (q.value.cooked ?? q.value.raw) + (i < p.node.expressions.length ? `{p${i}}` : '')).join('');
|
|
add(p, text, 'template');
|
|
},
|
|
});
|
|
function add(p, text, kind) {
|
|
if (!han.test(text) || /^https?:/.test(text)) return;
|
|
const ancestors = [];
|
|
let q = p;
|
|
for (let i = 0; q && i < 5; i++, q = q.parentPath) {
|
|
const n = q.node;
|
|
ancestors.push(n.type + (n.key?.name ? ':' + n.key.name : '') + (n.type === 'CallExpression' ? ':' + code.slice(n.callee.start, n.callee.end) : ''));
|
|
}
|
|
entries.push({file, start: offset + p.node.start, end: offset + p.node.end, kind, text, context, ancestors: ancestors.join(' > '), snippet: code.slice(Math.max(0, p.node.start - 65), Math.min(code.length, p.node.end + 65))});
|
|
}
|
|
}
|
|
function inspect(file) {
|
|
const code = fs.readFileSync(path.join(root, file), 'utf8');
|
|
if (file.endsWith('.js')) return scripts(code, file);
|
|
const {descriptor} = sfc.parse(code);
|
|
for (const block of [descriptor.script, descriptor.scriptSetup]) if (block) scripts(block.content, file, block.loc.start.offset);
|
|
const block = descriptor.template;
|
|
if (!block) return;
|
|
const ast = dom.baseParse(block.content);
|
|
function walk(n) {
|
|
if (n.type === 2 && han.test(n.content)) entries.push({file, start: block.loc.start.offset + n.loc.start.offset, end: block.loc.start.offset + n.loc.end.offset, kind:'text', text:n.content, context:'template'});
|
|
if (n.type === 5 && han.test(n.content.content)) scripts(n.content.content, file, block.loc.start.offset + n.content.loc.start.offset, 'expression');
|
|
for (const prop of n.props || []) {
|
|
if (prop.type === 6 && prop.value && han.test(prop.value.content) && !/^(src|class|id|style|key|name|value)$/.test(prop.name)) entries.push({file, start:block.loc.start.offset + prop.loc.start.offset, end:block.loc.start.offset + prop.loc.end.offset, kind:'attribute', text:prop.value.content, attribute:prop.name, context:'template'});
|
|
if (prop.type === 7 && prop.exp && han.test(prop.exp.content)) {
|
|
try { scripts(prop.exp.content, file, block.loc.start.offset + prop.exp.loc.start.offset, 'expression'); } catch (e) { /* v-for 等非独立表达式保留人工检查 */ }
|
|
}
|
|
}
|
|
for (const child of n.children || []) walk(child);
|
|
}
|
|
walk(ast);
|
|
}
|
|
function walk(dir) {
|
|
for (const item of fs.readdirSync(path.join(root, dir), {withFileTypes:true})) {
|
|
const file = path.posix.join(dir, item.name);
|
|
if (item.isDirectory()) { if (item.name !== 'static' && item.name !== 'locales') walk(file); }
|
|
else if (/\.(vue|js)$/.test(file) && !/match\.min\.js|audioManager\.js/.test(file)) inspect(file);
|
|
}
|
|
}
|
|
walk('');
|
|
if (require.main === module) process.stdout.write(JSON.stringify(entries, null, 2));
|
|
module.exports = entries;
|