wuchale
Version:
Protobuf-like i18n from normal code
411 lines • 14.5 kB
JavaScript
// $$ cd .. && npm run test
import MagicString from "magic-string";
import { Parser } from 'acorn';
import { tsPlugin } from '@sveltejs/acorn-typescript';
import { defaultHeuristicFuncOnly, NestText } from '../adapters.js';
import { runtimeVars } from "../adapter-utils/index.js";
export const scriptParseOptions = {
sourceType: 'module',
ecmaVersion: 'latest',
locations: true
};
const ScriptParser = Parser.extend(tsPlugin());
export function scriptParseOptionsWithComments() {
let accumulateComments = [];
const comments = [];
return [
{
...scriptParseOptions,
// parse comments for when they are not part of the AST
onToken: token => {
if (accumulateComments.length) {
comments[token.start] = accumulateComments;
}
accumulateComments = [];
},
onComment: (block, comment) => {
accumulateComments.push({
type: block ? 'Block' : 'Line',
value: comment,
});
}
},
comments,
];
}
export function parseScript(content) {
const [opts, comments] = scriptParseOptionsWithComments();
return [ScriptParser.parse(content, opts), comments];
}
export class Transformer {
index;
heuristic;
content;
/* for when the comments are not parsed as part of the AST */
comments = [];
filename;
mstr;
pluralFunc;
vars;
runtimeOpts;
initRuntimeExpr;
// state
commentDirectives = {};
insideProgram = false;
declaring = null;
currentFuncDef = null;
currentCall;
currentTopLevelCall;
constructor(content, filename, index, heuristic, pluralsFunc, runtimeOpts, initRuntimeExpr) {
this.index = index;
this.heuristic = heuristic;
this.pluralFunc = pluralsFunc;
this.content = content;
this.filename = filename;
this.vars = runtimeVars(runtimeOpts.wrapExpr);
this.runtimeOpts = runtimeOpts;
this.initRuntimeExpr = runtimeOpts.wrapInit(initRuntimeExpr);
}
checkHeuristic = (text, detailsBase) => {
if (!text) {
// nothing to ask
return [false, null];
}
let extract = this.commentDirectives.forceInclude;
if (extract == null) {
const details = {
file: this.filename,
call: this.currentCall,
declaring: this.declaring,
funcName: this.currentFuncDef,
topLevelCall: this.currentTopLevelCall,
...detailsBase,
};
if (details.declaring == null && this.insideProgram) {
details.declaring = 'expression';
}
extract = this.heuristic(text, details) ?? defaultHeuristicFuncOnly(text, details) ?? true;
}
return [extract, new NestText(text, detailsBase.scope, this.commentDirectives.context)];
};
visitLiteral = (node) => {
if (typeof node.value !== 'string') {
return [];
}
const { start, end } = node;
const [pass, txt] = this.checkHeuristic(node.value, { scope: 'script' });
if (!pass) {
return [];
}
this.mstr.update(start, end, `${this.vars.rtTrans}(${this.index.get(txt.toKey())})`);
return [txt];
};
visitArrayExpression = (node) => {
const txts = [];
for (const elm of node.elements) {
txts.push(...this.visit(elm));
}
return txts;
};
visitObjectExpression = (node) => {
const txts = [];
for (const prop of node.properties) {
txts.push(...this.visit(prop));
}
return txts;
};
visitProperty = (node) => [
...this.visit(node.key),
...this.visit(node.value),
];
visitSpreadElement = (node) => this.visit(node.argument);
visitMemberExpression = (node) => [
...this.visit(node.object),
...this.visit(node.property),
];
visitNewExpression = (node) => node.arguments.map(this.visit).flat();
defaultVisitCallExpression = (node) => {
const txts = [...this.visit(node.callee)];
const currentCall = this.currentCall;
this.currentCall = this.getCalleeName(node.callee);
for (const arg of node.arguments) {
txts.push(...this.visit(arg));
}
this.currentCall = currentCall; // restore
return txts;
};
visitCallExpression = (node) => {
if (node.callee.type !== 'Identifier' || node.callee.name !== this.pluralFunc) {
return this.defaultVisitCallExpression(node);
}
// plural(num, ['Form one', 'Form two'])
const secondArg = node.arguments[1];
if (secondArg === null || secondArg.type !== 'ArrayExpression') {
return this.defaultVisitCallExpression(node);
}
const candidates = [];
for (const elm of secondArg.elements) {
if (elm.type !== 'Literal' || typeof elm.value !== 'string') {
return this.defaultVisitCallExpression(node);
}
candidates.push(elm.value);
}
const nTxt = new NestText(candidates, 'script', this.commentDirectives.context);
nTxt.plural = true;
const index = this.index.get(nTxt.toKey());
const pluralUpdate = `${this.vars.rtTPlural}(${index}), ${this.vars.rtPlural}`;
// @ts-ignore
this.mstr.update(secondArg.start, node.end - 1, pluralUpdate);
return [nTxt];
};
visitBinaryExpression = (node) => [
...this.visit(node.left),
...this.visit(node.right),
];
visitUnaryExpression = (node) => this.visit(node.argument);
visitLogicalExpression = (node) => [
...this.visit(node.left),
...this.visit(node.right),
];
visitAwaitExpression = (node) => this.visit(node.argument);
visitAssignmentExpression = this.visitBinaryExpression;
visitExpressionStatement = (node) => this.visit(node.expression);
visitForOfStatement = (node) => [
...this.visit(node.left),
...this.visit(node.right),
...this.visit(node.body),
];
visitForInStatement = (node) => [
...this.visit(node.left),
...this.visit(node.right),
...this.visit(node.body),
];
visitForStatement = (node) => [
...this.visit(node.init),
...this.visit(node.test),
...this.visit(node.update),
...this.visit(node.body),
];
getMemberChainName = (node) => {
let name = '';
if (node.object.type === 'Identifier') {
name = node.object.name;
}
else if (node.object.type === 'MemberExpression') {
name = this.getMemberChainName(node.object);
}
else {
name = `[${node.type}]`;
}
name += '.';
if (node.property.type === 'Identifier') {
name += node.property.name;
}
else if (node.property.type === 'MemberExpression') {
name += this.getMemberChainName(node.property);
}
else {
name = `[${node.type}]`;
}
return name;
};
getCalleeName = (callee) => {
if (callee.type === 'Identifier') {
return callee.name;
}
if (callee.type === 'MemberExpression') {
return this.getMemberChainName(callee);
}
return `[${callee.type}]`;
};
visitVariableDeclaration = (node) => {
const txts = [];
let atTopLevelDefn = this.insideProgram && !this.declaring;
for (const dec of node.declarations) {
if (!dec.init) {
continue;
}
// store the name of the function after =
if (atTopLevelDefn) {
if (dec.init.type === 'ArrowFunctionExpression') {
this.declaring = 'function';
}
else {
this.declaring = 'variable';
if (dec.init.type === 'CallExpression') {
this.currentTopLevelCall = this.getCalleeName(dec.init.callee);
}
}
}
const decVisit = this.visit(dec.init);
if (!decVisit.length) {
continue;
}
txts.push(...decVisit);
}
if (atTopLevelDefn) {
this.currentTopLevelCall = null; // reset
this.declaring = null;
}
return txts;
};
visitExportNamedDeclaration = (node) => node.declaration ? this.visit(node.declaration) : [];
visitExportDefaultDeclaration = this.visitExportNamedDeclaration;
visitFunctionBody = (node, name) => {
const prevFuncDef = this.currentFuncDef;
this.currentFuncDef = node.type === 'BlockStatement' ? name : prevFuncDef;
const txts = this.visit(node);
let initRuntimeHere = this.runtimeOpts.initInScope({ funcName: this.currentFuncDef, parentFunc: prevFuncDef, file: this.filename });
if (txts.length > 0 && initRuntimeHere && node.type === 'BlockStatement') {
this.mstr.prependLeft(
// @ts-expect-error
node.start + 1, `\nconst ${this.vars.rtConst} = ${this.initRuntimeExpr}\n`);
}
this.currentFuncDef = prevFuncDef;
return txts;
};
visitFunctionDeclaration = (node) => {
const declaring = this.declaring;
this.declaring = 'function';
const txts = this.visitFunctionBody(node.body, node.id?.name ?? null);
this.declaring = declaring;
return txts;
};
visitArrowFunctionExpression = (node) => this.visitFunctionBody(node.body, '');
visitFunctionExpression = (node) => this.visitFunctionBody(node.body, '');
visitBlockStatement = (node) => {
const txts = [];
for (const statement of node.body) {
txts.push(...this.visit(statement));
}
return txts;
};
visitReturnStatement = (node) => {
if (node.argument) {
return this.visit(node.argument);
}
return [];
};
visitIfStatement = (node) => {
const txts = this.visit(node.test);
txts.push(...this.visit(node.consequent));
if (node.alternate) {
txts.push(...this.visit(node.alternate));
}
return txts;
};
visitTemplateLiteral = (node) => {
let heurTxt = '';
for (const quasi of node.quasis) {
heurTxt += quasi.value.cooked ?? '';
if (!quasi.tail) {
heurTxt += '#';
}
}
heurTxt = heurTxt.trim();
const [pass] = this.checkHeuristic(heurTxt, { scope: 'script' });
if (!pass) {
return node.expressions.map(this.visit).flat();
}
const txts = [];
const quasi0 = node.quasis[0];
// @ts-ignore
const { start: start0, end: end0 } = quasi0;
let txt = quasi0.value?.cooked ?? '';
for (const [i, expr] of node.expressions.entries()) {
txts.push(...this.visit(expr));
const quasi = node.quasis[i + 1];
txt += `{${i}}${quasi.value.cooked}`;
// @ts-ignore
const { start, end } = quasi;
this.mstr.remove(start - 1, end);
if (i + 1 === node.expressions.length) {
continue;
}
this.mstr.update(end, end + 2, ', ');
}
const nTxt = new NestText(txt, 'script', this.commentDirectives.context);
let begin = `${this.vars.rtTrans}(${this.index.get(nTxt.toKey())}`;
let end = ')';
if (node.expressions.length) {
begin += ', [';
end = ']' + end;
this.mstr.update(start0 - 1, end0 + 2, begin);
// @ts-ignore
this.mstr.update(node.end - 1, node.end, end);
}
else {
this.mstr.update(start0 - 1, end0 + 1, begin + end);
}
txts.push(nTxt);
return txts;
};
visitProgram = (node) => {
const txts = [];
this.insideProgram = true;
for (const child of node.body) {
txts.push(...this.visit(child));
}
this.insideProgram = false;
return txts;
};
processCommentDirectives = (data) => {
const directives = { ...this.commentDirectives };
if (data === '@wc-ignore') {
directives.forceInclude = false;
}
if (data === '@wc-include') {
directives.forceInclude = true;
}
const ctxStart = '@wc-context:';
if (data.startsWith(ctxStart)) {
directives.context = data.slice(ctxStart.length).trim();
}
return directives;
};
visit = (node) => {
// for estree
const commentDirectives = { ...this.commentDirectives };
// @ts-expect-error
const comments = this.comments[node.start];
for (const comment of node.leadingComments ?? comments ?? []) {
this.commentDirectives = this.processCommentDirectives(comment.value.trim());
}
let txts = [];
if (this.commentDirectives.forceInclude !== false) {
const methodName = `visit${node.type}`;
if (methodName in this) {
txts = this[methodName](node);
// } else {
// console.log(node)
}
}
this.commentDirectives = commentDirectives; // restore
return txts;
};
finalize = (txts) => {
const output = { txts };
if (txts.length === 0) {
return output;
}
return {
txts,
code: this.mstr.toString(),
map: this.mstr.generateMap(),
};
};
transform = (headerHead) => {
const [ast, comments] = parseScript(this.content);
this.comments = comments;
this.mstr = new MagicString(this.content);
const txts = this.visit(ast);
if (txts.length) {
if (this.runtimeOpts.initInScope({ file: this.filename })) {
headerHead += `\nconst ${this.vars.rtConst} = ${this.initRuntimeExpr}`;
}
this.mstr.appendRight(0, headerHead + '\n');
}
return this.finalize(txts);
};
}
//# sourceMappingURL=transformer.js.map