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}