mirror of
https://github.com/eliasstepanik/strudel.git
synced 2026-01-11 13:48:40 +00:00
266 lines
9.6 KiB
JavaScript
266 lines
9.6 KiB
JavaScript
/* 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 }),
|
|
],
|
|
}),
|
|
];
|
|
}
|