/* import { parseScriptWithLocation } from './shift-parser/index.js'; // npm module does not work in the browser import traverser from './shift-traverser/index.js'; // npm module does not work in the browser */ import { parseScriptWithLocation } from 'shift-parser'; import traverser from './shift-traverser/index.js'; const { replace } = traverser; import { LiteralStringExpression, IdentifierExpression, CallExpression, StaticMemberExpression, ReturnStatement, ArrayExpression, LiteralNumericExpression, } from 'shift-ast'; import shiftCodegen from 'shift-codegen'; const codegen = shiftCodegen.default || shiftCodegen; // parcel module resolution fuckup import * as strudel from '@strudel/core/strudel.mjs'; const { Pattern } = strudel; const isNote = (name) => /^[a-gC-G][bs]?[0-9]$/.test(name); const addLocations = true; export const addMiniLocations = true; export default (_code) => { const { code, addReturn } = wrapAsync(_code); const ast = parseScriptWithLocation(code); const artificialNodes = []; const parents = []; const shifted = replace(ast.tree, { enter(node, parent) { parents.push(parent); const isSynthetic = parents.some((p) => artificialNodes.includes(p)); if (isSynthetic) { return node; } // replace template string `xxx` with mini(`xxx`) if (isBackTickString(node)) { return minifyWithLocation(node, node, ast.locations, artificialNodes); } // allows to use top level strings, which are normally directives... but we don't need directives if (node.directives?.length === 1 && !node.statements?.length) { const str = new LiteralStringExpression({ value: node.directives[0].rawValue }); const wrapped = minifyWithLocation(str, node.directives[0], ast.locations, artificialNodes); return { ...node, directives: [], statements: [wrapped] }; } // replace double quote string "xxx" with mini('xxx') if (isStringWithDoubleQuotes(node, ast.locations, code)) { return minifyWithLocation(node, node, ast.locations, artificialNodes); } // operator overloading => still not done const operators = { '*': 'fast', '/': 'slow', '&': 'stack', '&&': 'append', }; if ( node.type === 'BinaryExpression' && operators[node.operator] && ['LiteralNumericExpression', 'LiteralStringExpression', 'IdentifierExpression'].includes(node.right?.type) && canBeOverloaded(node.left) ) { let arg = node.left; if (node.left.type === 'IdentifierExpression') { arg = wrapFunction('reify', node.left); } return new CallExpression({ callee: new StaticMemberExpression({ property: operators[node.operator], object: wrapFunction('reify', arg), }), arguments: [node.right], }); } const isMarkable = isPatternArg(parents) || hasModifierCall(parent); // add to location to pure(x) calls if (node.type === 'CallExpression' && node.callee.name === 'pure') { const literal = node.arguments[0]; // const value = literal[{ LiteralNumericExpression: 'value', LiteralStringExpression: 'name' }[literal.type]]; return reifyWithLocation(literal, node.arguments[0], ast.locations, artificialNodes); } // replace pseudo note variables if (node.type === 'IdentifierExpression') { if (isNote(node.name)) { const value = node.name[1] === 's' ? node.name.replace('s', '#') : node.name; if (addLocations && isMarkable) { return reifyWithLocation(new LiteralStringExpression({ value }), node, ast.locations, artificialNodes); } return new LiteralStringExpression({ value }); } if (node.name === 'r') { return new IdentifierExpression({ name: 'silence' }); } } if ( addLocations && ['LiteralStringExpression' /* , 'LiteralNumericExpression' */].includes(node.type) && isMarkable ) { // TODO: to make LiteralNumericExpression work, we need to make sure we're not inside timeCat... return reifyWithLocation(node, node, ast.locations, artificialNodes); } if (addMiniLocations) { return addMiniNotationLocations(node, ast.locations, artificialNodes); } return node; }, leave() { parents.pop(); }, }); // add return to last statement (because it's wrapped in an async function artificially) addReturn(shifted); const generated = codegen(shifted); return generated; }; function wrapAsync(code) { // wrap code in async to make await work on top level => this will create 1 line offset to locations // this is why line offset is -1 in getLocationObject calls below code = `(async () => { ${code} })()`; const addReturn = (ast) => { const body = ast.statements[0].expression.callee.body; // actual code ast inside async function body body.statements = body.statements .slice(0, -1) .concat([new ReturnStatement({ expression: body.statements.slice(-1)[0] })]); }; return { code, addReturn, }; } function addMiniNotationLocations(node, locations, artificialNodes) { const miniFunctions = ['mini', 'm']; // const isAlreadyWrapped = parent?.type === 'CallExpression' && parent.callee.name === 'withLocationOffset'; if (node.type === 'CallExpression' && miniFunctions.includes(node.callee.name)) { // mini('c3') if (node.arguments.length > 1) { // TODO: transform mini(...args) to cat(...args.map(mini)) ? console.warn('multi arg mini locations not supported yet...'); return node; } const str = node.arguments[0]; return minifyWithLocation(str, str, locations, artificialNodes); } if (node.type === 'StaticMemberExpression' && miniFunctions.includes(node.property)) { // 'c3'.mini or 'c3'.m return minifyWithLocation(node.object, node, locations, artificialNodes); } return node; } function wrapFunction(name, ...args) { return new CallExpression({ callee: new IdentifierExpression({ name }), arguments: args, }); } function isBackTickString(node) { return node.type === 'TemplateExpression' && node.elements.length === 1; } function isStringWithDoubleQuotes(node, locations, code) { if (node.type !== 'LiteralStringExpression') { return false; } const loc = locations.get(node); const snippet = code.slice(loc.start.offset, loc.end.offset); return snippet[0] === '"'; // we can trust the end is also ", as the parsing did not fail } // returns true if the given parents belong to a pattern argument node // this is used to check if a node should receive a location for highlighting function isPatternArg(parents) { if (!parents.length) { return false; } const ancestors = parents.slice(0, -1); const parent = parents[parents.length - 1]; if (isPatternFactory(parent)) { return true; } if (parent?.type === 'ArrayExpression') { return isPatternArg(ancestors); } return false; } function hasModifierCall(parent) { // TODO: modifiers are more than composables, for example every is not composable but should be seen as modifier.. // need all prototypes of Pattern return ( parent?.type === 'StaticMemberExpression' && Object.keys(Pattern.prototype.composable).includes(parent.property) ); } function isPatternFactory(node) { return node?.type === 'CallExpression' && Object.keys(Pattern.prototype.factories).includes(node.callee.name); } function canBeOverloaded(node) { return (node.type === 'IdentifierExpression' && isNote(node.name)) || isPatternFactory(node); // TODO: support sequence(c3).transpose(3).x.y.z } // turns node in reify(value).withLocation(location), where location is the node's location in the source code // with this, the reified pattern can pass its location to the event, to know where to highlight when it's active function reifyWithLocation(literalNode, node, locations, artificialNodes) { const args = getLocationArguments(node, locations); const withLocation = new CallExpression({ callee: new StaticMemberExpression({ object: wrapFunction('reify', literalNode), property: 'withLocation', }), arguments: args, }); artificialNodes.push(withLocation); return withLocation; } // turns node in reify(value).withLocation(location), where location is the node's location in the source code // with this, the reified pattern can pass its location to the event, to know where to highlight when it's active function minifyWithLocation(literalNode, node, locations, artificialNodes) { const args = getLocationArguments(node, locations); const withLocation = new CallExpression({ callee: new StaticMemberExpression({ object: wrapFunction('mini', literalNode), property: 'withMiniLocation', }), arguments: args, }); artificialNodes.push(withLocation); return withLocation; } function getLocationArguments(node, locations) { const loc = locations.get(node); return [ new ArrayExpression({ elements: [ new LiteralNumericExpression({ value: loc.start.line - 1 }), // the minus 1 assumes the code has been wrapped in async iife new LiteralNumericExpression({ value: loc.start.column }), new LiteralNumericExpression({ value: loc.start.offset }), ], }), new ArrayExpression({ elements: [ new LiteralNumericExpression({ value: loc.end.line - 1 }), // the minus 1 assumes the code has been wrapped in async iife new LiteralNumericExpression({ value: loc.end.column }), new LiteralNumericExpression({ value: loc.end.offset }), ], }), ]; }