jevstrudel.git / packages / transpiler / plugin-kabelsalat.mjs
1/*
2plugin-kabelsalat.mjs - <short description TODO>
3Copyright (C) 2022 Strudel contributors - see <https://codeberg.org/uzu/strudel/src/branch/main/packages/superdough/superdough.mjs>
4This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program.  If not, see <https://www.gnu.org/licenses/>.
5*/
6
7import { walk } from 'estree-walker';
8import escodegen from 'escodegen';
9import { getLanguages, registerTranspilerPlugin } from './transpiler.mjs';
10
11export function genExprSource(expr) {
12  return escodegen.generate(expr, { format: { semicolons: false } });
13}
14
15function isStrudelPatternWrap(node) {
16  if (node.type !== 'CallExpression') {
17    return false;
18  }
19  const callee = node.callee;
20  if (callee.type === 'Identifier') {
21    return callee.name === 'S';
22  }
23  if (callee.type === 'MemberExpression' && !callee.computed) {
24    return callee.property?.name === 'S';
25  }
26  return false;
27}
28
29// Used to identify transpiled `m(...)` calls for proper conversion
30// to, say, kabelsalat placeholders
31function isMiniCall(node) {
32  if (node.type !== 'CallExpression') {
33    return false;
34  }
35  const callee = node.callee;
36  if (callee.type !== 'Identifier') {
37    return false;
38  }
39  if (callee.name !== getMinilangName()) {
40    return false;
41  }
42  const firstArg = node.arguments?.[0];
43  return firstArg?.type === 'Literal' && typeof firstArg.value === 'string';
44}
45
46function getMinilangName() {
47  const minilang = getLanguages().get('minilang');
48  return minilang?.name || 'm';
49}
50
51function replaceNode(node, replacement, parents, currentRoot) {
52  const info = parents.get(node);
53  if (!info || !info.parent) {
54    return replacement;
55  }
56
57  const { parent, prop, index } = info;
58  if (Array.isArray(parent[prop])) {
59    parent[prop][index] = replacement;
60  } else {
61    parent[prop] = replacement;
62  }
63  parents.set(replacement, { parent, prop, index });
64  return currentRoot;
65}
66
67// If `start` is available, we use it. If it's already been transpiled
68// to `m(...)`, use the provided offset
69function getPatternNodeOrder(node) {
70  if (typeof node.start === 'number') {
71    return node.start;
72  }
73  if (isMiniCall(node)) {
74    const offsetArg = node.arguments?.[1];
75    if (offsetArg?.type === 'Literal' && typeof offsetArg.value === 'number') {
76      return offsetArg.value;
77    }
78  }
79  return 0;
80}
81
82function placeholderAst(index) {
83  return {
84    type: 'MemberExpression',
85    object: { type: 'Identifier', name: 'pat' },
86    property: { type: 'Literal', value: index },
87    computed: true,
88    optional: false,
89  };
90}
91
92function getStrudelPatternExpr(node) {
93  if (isStrudelPatternWrap(node)) {
94    const arg = node.arguments?.[0];
95    if (!arg) {
96      throw new Error('S(...) requires an argument');
97    }
98    return arg;
99  }
100  if (isMiniCall(node)) {
101    return node;
102  }
103  return null;
104}
105
106function cloneNode(node) {
107  return JSON.parse(JSON.stringify(node));
108}
109
110export function extractPatternPlaceholders(expr) {
111  const templateExpr = cloneNode(expr);
112  const parents = new Map();
113  const targets = [];
114
115  walk(templateExpr, {
116    enter(node, parent, prop, index) {
117      parents.set(node, { parent, prop, index });
118      const patternExpr = getStrudelPatternExpr(node);
119      if (patternExpr) {
120        targets.push({ node, patternExpr });
121        this.skip();
122      }
123    },
124  });
125
126  if (!targets.length) {
127    return { template: genExprSource(templateExpr), patternExprs: [] };
128  }
129
130  targets.sort((a, b) => getPatternNodeOrder(a.node) - getPatternNodeOrder(b.node));
131
132  const patternExprs = targets.map(({ patternExpr }) => cloneNode(patternExpr));
133
134  let currentExpr = templateExpr;
135  targets.forEach(({ node }, index) => {
136    currentExpr = replaceNode(node, placeholderAst(index), parents, currentExpr);
137  });
138
139  const template = genExprSource(currentExpr);
140  return { template, patternExprs };
141}
142
143const transpilerPlugin = {
144  walk: (context) => ({
145    leave: function (node, parent, prop, index) {
146      if (!isKabelCall(node)) return;
147      let [expr, ...rest] = node.arguments;
148      if (!expr) throw new Error('K(...) requires an expression');
149      if (shouldCallKabelExpression(expr)) {
150        expr = {
151          type: 'CallExpression',
152          callee: expr,
153          arguments: [],
154          optional: false,
155        };
156      }
157      const language = 'kabelsalat';
158      const { template, patternExprs } = extractPatternPlaceholders(expr);
159      if (patternExprs.length) {
160        const workletArgs = [
161          /*{ type: 'Literal', value: language },*/
162          { type: 'Literal', value: template },
163          ...patternExprs,
164          ...rest,
165        ];
166        let callee = node.callee;
167        if (callee.type === 'ChainExpression') callee = callee.expression;
168        if (callee.type === 'MemberExpression') {
169          return this.replace({
170            type: 'CallExpression',
171            callee: workletMemberAst(callee.object),
172            arguments: workletArgs,
173            optional: false,
174          });
175        }
176        return this.replace({
177          type: 'CallExpression',
178          callee: { type: 'Identifier', name: 'worklet' },
179          arguments: workletArgs,
180          optional: false,
181        });
182      }
183
184      const kabelSrc = genExprSource(expr);
185      const workletArgs = [/*{ type: 'Literal', value: language },*/ { type: 'Literal', value: kabelSrc }, ...rest];
186
187      let callee = node.callee;
188      if (callee.type === 'ChainExpression') callee = callee.expression;
189      if (callee.type === 'MemberExpression') {
190        return this.replace({
191          type: 'CallExpression',
192          callee: workletMemberAst(callee.object),
193          arguments: workletArgs,
194          optional: false,
195        });
196      }
197      return this.replace({
198        type: 'CallExpression',
199        callee: { type: 'Identifier', name: 'worklet' },
200        arguments: workletArgs,
201        optional: false,
202      });
203    },
204  }),
205};
206
207registerTranspilerPlugin(transpilerPlugin);
208
209function isKabelCall(node) {
210  if (node.type !== 'CallExpression') return false;
211  let callee = node.callee;
212  if (callee.type === 'ChainExpression') callee = callee.expression;
213  if (callee.type === 'MemberExpression') return !callee.computed && callee.property?.name === 'K';
214  return callee.type === 'Identifier' && callee.name === 'K';
215}
216
217function shouldCallKabelExpression(expr) {
218  if (expr.type !== 'ArrowFunctionExpression' && expr.type !== 'FunctionExpression') {
219    return false;
220  }
221  if (expr.params.length) {
222    return false;
223  }
224  return expr.body?.type === 'BlockStatement';
225}
226
227function workletMemberAst(objectExpr) {
228  return {
229    type: 'MemberExpression',
230    object: objectExpr,
231    property: { type: 'Identifier', name: 'worklet' },
232    computed: false,
233    optional: false,
234  };
235}