@wuchale/svelte
Version:
Protobuf-like i18n from normal code
395 lines • 14.5 kB
JavaScript
import MagicString from "magic-string";
import { parse } from "svelte/compiler";
import { NestText } from 'wuchale/adapters';
import { Transformer, parseScript, runtimeConst } from 'wuchale/adapter-vanilla';
const nodesWithChildren = ['RegularElement', 'Component'];
const rtComponent = 'WuchaleTrans';
const snipPrefix = 'wuchaleSnippet';
const rtFuncCtx = `${runtimeConst}.cx`;
const rtFuncCtxTrans = `${runtimeConst}.tx`;
export class SvelteTransformer extends Transformer {
// state
currentElement;
inCompoundText = false;
commentDirectivesStack = [];
lastVisitIsComment = false;
currentSnippet = 0;
constructor(content, filename, index, heuristic, pluralsFunc, initInsideFuncExpr) {
super(content, filename, index, heuristic, pluralsFunc, initInsideFuncExpr);
}
visitExpressionTag = (node) => this.visit(node.expression);
nonWhitespaceText = (node) => {
let trimmedS = node.data.trimStart();
const startWh = node.data.length - trimmedS.length;
let trimmed = trimmedS.trimEnd();
const endWh = trimmedS.length - trimmed.length;
return [startWh, trimmed, endWh];
};
separatelyVisitChildren = (node) => {
let hasTextChild = false;
let hasNonTextChild = false;
let heurTxt = '';
let hasCommentDirectives = false;
for (const child of node.nodes) {
if (child.type === 'Text') {
const txt = child.data.trim();
if (!txt) {
continue;
}
hasTextChild = true;
heurTxt += child.data + ' ';
}
else if (child.type === 'Comment') {
if (child.data.trim().startsWith('@wc-')) {
hasCommentDirectives = true;
}
}
else {
hasNonTextChild = true;
heurTxt += `# `;
}
}
heurTxt = heurTxt.trimEnd();
const [passHeuristic] = this.checkHeuristic(heurTxt, { scope: 'markup', element: this.currentElement });
let hasCompoundText = hasTextChild && hasNonTextChild;
const visitAsOne = passHeuristic && !hasCommentDirectives;
if (this.inCompoundText || hasCompoundText && visitAsOne) {
return [false, hasTextChild, hasCompoundText, []];
}
const txts = [];
// can't be extracted as one; visitSv each separately
for (const child of node.nodes) {
txts.push(...this.visitSv(child));
}
return [true, false, false, txts];
};
visitFragment = (node) => {
if (node.nodes.length === 0) {
return [];
}
const [visitedSeparately, hasTextChild, hasCompoundText, separateTxts] = this.separatelyVisitChildren(node);
if (visitedSeparately) {
return separateTxts;
}
let txt = '';
let iArg = 0;
let iTag = 0;
const lastChildEnd = node.nodes.slice(-1)[0].end;
const childrenForSnippets = [];
let hasTextDescendants = false;
const txts = [];
for (const child of node.nodes) {
if (child.type === 'Comment') {
continue;
}
if (child.type === 'Text') {
const [startWh, trimmed, endWh] = this.nonWhitespaceText(child);
const nTxt = new NestText(trimmed, 'markup', this.commentDirectives.context);
if (startWh && !txt.endsWith(' ')) {
txt += ' ';
}
if (!trimmed) { // whitespace
continue;
}
txt += nTxt.text;
if (endWh) {
txt += ' ';
}
this.mstr.remove(child.start, child.end);
continue;
}
if (child.type === 'ExpressionTag') {
txts.push(...this.visitExpressionTag(child));
if (!hasCompoundText) {
continue;
}
txt += `{${iArg}}`;
let moveStart = child.start;
if (iArg > 0) {
this.mstr.update(child.start, child.start + 1, ', ');
}
else {
moveStart++;
this.mstr.remove(child.start, child.start + 1);
}
this.mstr.move(moveStart, child.end - 1, lastChildEnd);
this.mstr.remove(child.end - 1, child.end);
iArg++;
continue;
}
// elements, components and other things as well
const nestedTextSupported = nodesWithChildren.includes(child.type);
const inCompoundTextPrev = this.inCompoundText;
this.inCompoundText = nestedTextSupported;
const childTxts = this.visitSv(child);
this.inCompoundText = inCompoundTextPrev; // restore
let snippNeedsCtx = false;
let chTxt = '';
for (const txt of childTxts) {
if (nodesWithChildren.includes(child.type) && txt.scope === 'markup') {
chTxt += txt.text[0];
hasTextDescendants = true;
snippNeedsCtx = true;
}
else { // attributes, blocks
txts.push(txt);
}
}
childrenForSnippets.push([child.start, child.end, snippNeedsCtx]);
if (nodesWithChildren.includes(child.type) && chTxt) {
chTxt = `<${iTag}>${chTxt}</${iTag}>`;
}
else {
// childless elements and everything else
chTxt = `<${iTag}/>`;
}
iTag++;
txt += chTxt;
}
txt = txt.trim();
if (!txt) {
return txts;
}
const nTxt = new NestText(txt, 'markup', this.commentDirectives.context);
if (hasTextChild || hasTextDescendants) {
txts.push(nTxt);
}
else {
return txts;
}
if (childrenForSnippets.length) {
const snippets = [];
// create and reference snippets
for (const [childStart, childEnd, haveCtx] of childrenForSnippets) {
const snippetName = `${snipPrefix}${this.currentSnippet}`;
snippets.push(snippetName);
this.currentSnippet++;
const snippetBegin = `\n{#snippet ${snippetName}(${haveCtx ? 'ctx' : ''})}\n`;
this.mstr.appendRight(childStart, snippetBegin);
this.mstr.prependLeft(childEnd, '\n{/snippet}');
}
let begin = `\n<${rtComponent} tags={[${snippets.join(', ')}]} ctx=`;
if (this.inCompoundText) {
begin += `{ctx} nest`;
}
else {
const index = this.index.get(nTxt.toKey());
begin += `{${rtFuncCtx}(${index})}`;
}
let end = ' />\n';
if (iArg > 0) {
begin += ' args={[';
end = ']}' + end;
}
this.mstr.appendLeft(lastChildEnd, begin);
this.mstr.appendRight(lastChildEnd, end);
}
else if (hasTextChild) {
// no need for component use
let begin = '{';
let end = ')}';
if (this.inCompoundText) {
begin += `${rtFuncCtxTrans}(ctx`;
}
else {
begin += `${this.rtFunc}(${this.index.get(nTxt.toKey())}`;
}
if (iArg) {
begin += ', [';
end = ']' + end;
}
this.mstr.appendLeft(lastChildEnd, begin);
this.mstr.appendRight(lastChildEnd, end);
}
return txts;
};
visitRegularElement = (node) => {
const currentElement = this.currentElement;
this.currentElement = node.name;
const txts = [];
for (const attrib of node.attributes) {
txts.push(...this.visitSv(attrib));
}
txts.push(...this.visitFragment(node.fragment));
this.currentElement = currentElement;
return txts;
};
visitComponent = this.visitRegularElement;
visitText = (node) => {
const [startWh, trimmed, endWh] = this.nonWhitespaceText(node);
const [pass, txt] = this.checkHeuristic(trimmed, {
scope: 'markup',
element: this.currentElement,
});
if (!pass) {
return [];
}
this.mstr.update(node.start + startWh, node.end - endWh, `{${this.rtFunc}(${this.index.get(txt.toKey())})}`);
return [txt];
};
visitSpreadAttribute = (node) => this.visit(node.expression);
visitAttribute = (node) => {
if (node.value === true) {
return [];
}
const txts = [];
let values;
if (Array.isArray(node.value)) {
values = node.value;
}
else {
values = [node.value];
}
for (const value of values) {
if (value.type !== 'Text') { // ExpressionTag
txts.push(...this.visitSv(value));
continue;
}
// Text
const { start, end } = value;
const [pass, txt] = this.checkHeuristic(value.data, {
scope: 'attribute',
element: this.currentElement,
attribute: node.name,
});
if (!pass) {
continue;
}
txts.push(txt);
this.mstr.update(value.start, value.end, `{${this.rtFunc}(${this.index.get(txt.toKey())})}`);
if (!`'"`.includes(this.content[start - 1])) {
continue;
}
this.mstr.remove(start - 1, start);
this.mstr.remove(end, end + 1);
}
return txts;
};
visitSnippetBlock = (node) => this.visitFragment(node.body);
visitIfBlock = (node) => {
const txts = this.visit(node.test);
txts.push(...this.visitSv(node.consequent));
if (node.alternate) {
txts.push(...this.visitSv(node.alternate));
}
return txts;
};
visitEachBlock = (node) => {
const txts = [
...this.visit(node.expression),
...this.visitSv(node.body),
];
if (node.key) {
txts.push(...this.visit(node.key));
}
if (node.fallback) {
txts.push(...this.visitSv(node.fallback));
}
return txts;
};
visitKeyBlock = (node) => {
return [
...this.visit(node.expression),
...this.visitSv(node.fragment),
];
};
visitAwaitBlock = (node) => {
const txts = [
...this.visit(node.expression),
...this.visitFragment(node.then),
];
if (node.pending) {
txts.push(...this.visitFragment(node.pending));
}
if (node.catch) {
txts.push(...this.visitFragment(node.catch));
}
return txts;
};
visitSvelteBody = (node) => node.attributes.map(this.visitSv).flat();
visitSvelteDocument = (node) => node.attributes.map(this.visitSv).flat();
visitSvelteElement = (node) => node.attributes.map(this.visitSv).flat();
visitSvelteBoundary = (node) => [
...node.attributes.map(this.visitSv).flat(),
...this.visitSv(node.fragment),
];
visitSvelteHead = (node) => this.visitSv(node.fragment);
visitSvelteWindow = (node) => node.attributes.map(this.visitSv).flat();
visitRoot = (node) => {
const txts = this.visitFragment(node.fragment);
if (node.instance) {
this.commentDirectives = {}; // reset
txts.push(...this.visitProgram(node.instance.content));
}
// @ts-ignore: module is a reserved keyword, not sure how to specify the type
if (node.module) {
this.commentDirectives = {}; // reset
// @ts-ignore
txts.push(...this.visitProgram(node.module.content));
}
return txts;
};
visitSv = (node) => {
if (node.type === 'Comment') {
const directives = this.processCommentDirectives(node.data.trim());
if (this.lastVisitIsComment) {
this.commentDirectivesStack[this.commentDirectivesStack.length - 1] = directives;
}
else {
this.commentDirectivesStack.push(directives);
}
this.lastVisitIsComment = true;
return [];
}
let txts = [];
const commentDirectivesPrev = this.commentDirectives;
if (this.lastVisitIsComment) {
this.commentDirectives = this.commentDirectivesStack.pop();
}
if (this.commentDirectives.forceInclude !== false) {
txts = this.visit(node);
}
this.commentDirectives = commentDirectivesPrev;
this.lastVisitIsComment = false;
return txts;
};
transformSv = (header) => {
const isComponent = this.filename.endsWith('.svelte');
let ast;
if (isComponent) {
ast = parse(this.content, { modern: true });
}
else {
const [pAst, comments] = parseScript(this.content);
ast = pAst;
this.comments = comments;
}
this.mstr = new MagicString(this.content);
const txts = this.visitSv(ast);
if (!txts.length) {
return this.finalize(txts);
}
const headerFin = [
`\nimport ${rtComponent} from "@wuchale/svelte/runtime.svelte"`,
header.head,
`const ${runtimeConst} = $derived(${header.expr})\n`,
].join('\n');
if (ast.type === 'Program') {
this.mstr.appendRight(0, headerFin + '\n');
return this.finalize(txts);
}
if (ast.module) {
// @ts-ignore
this.mstr.appendRight(ast.module.content.start, headerFin);
}
else if (ast.instance) {
// @ts-ignore
this.mstr.appendRight(ast.instance.content.start, headerFin);
}
else {
this.mstr.prepend(`<script>${headerFin}</script>\n`);
}
return this.finalize(txts);
};
}
//# sourceMappingURL=transformer.js.map