1/* 2plugin-widgets.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 { registerTranspilerPlugin } from './transpiler.mjs'; 8 9let widgetMethods = []; 10export function registerWidgetType(type) { 11 widgetMethods.push(type); 12} 13 14const widgetTranspilerPlugin = { 15 walk: (context) => ({ 16 enter: function (node, parent, prop, index) { 17 if (!isWidgetMethod(node)) return; 18 context.widgets ??= []; 19 const { widgets, options } = context; 20 const { emitWidgets } = options; 21 const type = node.callee.property.name; 22 const idx = widgets.filter((w) => w.type === type).length; 23 const widgetConfig = { 24 from: node.start, 25 to: node.end, 26 index: idx, 27 type, 28 id: options.id, 29 }; 30 emitWidgets && widgets.push(widgetConfig); 31 return this.replace(widgetWithLocation(node, widgetConfig)); 32 }, 33 }), 34}; 35 36const sliderTranspilerPlugin = { 37 walk: (context) => ({ 38 enter: function (node, parent, prop, index) { 39 if (!isSliderFunction(node)) return; 40 context.widgets ??= []; 41 context.sliders ??= []; 42 const { options, widgets, sliders, nodeOffset } = context; 43 const { emitWidgets } = options; 44 const from = node.arguments[0].start + nodeOffset; 45 const to = node.arguments[0].end + nodeOffset; 46 const id = `${from}:${to}`; // Range-based ID for stability 47 const sliderConfig = { 48 from, 49 to, 50 id, 51 value: node.arguments[0].raw, // don't use value! 52 min: node.arguments[1]?.value ?? 0, 53 max: node.arguments[2]?.value ?? 1, 54 step: node.arguments[3]?.value, 55 type: 'slider', 56 }; 57 emitWidgets && widgets.push(sliderConfig); 58 sliders.push(sliderConfig); 59 return this.replace(sliderWithLocation(node, nodeOffset)); 60 }, 61 }), 62}; 63 64export const widgetTranspilerPlugins = [sliderTranspilerPlugin, widgetTranspilerPlugin]; 65 66// these functions are connected to @strudel/codemirror -> slider.mjs 67// maybe someday there will be pluggable transpiler functions, then move this there 68function isSliderFunction(node) { 69 return node.type === 'CallExpression' && node.callee.name === 'slider'; 70} 71 72function isWidgetMethod(node) { 73 return node.type === 'CallExpression' && widgetMethods.includes(node.callee.property?.name); 74} 75 76function sliderWithLocation(node, nodeOffset = 0) { 77 // Apply nodeOffset for block-based evaluation to generate correct range 78 const from = node.arguments[0].start + nodeOffset; 79 const to = node.arguments[0].end + nodeOffset; 80 81 // Use range-based ID for stability during block evaluation 82 const id = `${from}:${to}`; 83 84 // add loc as identifier to first argument 85 // the sliderWithID function is assumed to be sliderWithID(id, value, min?, max?) 86 node.arguments.unshift({ 87 type: 'Literal', 88 value: id, 89 raw: id, 90 }); 91 node.callee.name = 'sliderWithID'; 92 return node; 93} 94 95export function getWidgetID(widgetConfig) { 96 // the widget id is used as id for the dom element + as key for eventual resources 97 // for example, for each scope widget, a new analyser + buffer (large) is created 98 // Update: use range-based ID generation for better stability during block evaluation 99 // When we have both from and to, use them together for stability 100 // Otherwise fall back to position-based ID for backward compatibility 101 let uniqueIdentifier; 102 if (widgetConfig.from !== undefined && widgetConfig.to !== undefined) { 103 // Use range for more stable identification 104 uniqueIdentifier = `${widgetConfig.from}-${widgetConfig.to}`; 105 } else { 106 // Fallback to single position (for backward compatibility) 107 uniqueIdentifier = widgetConfig.to || widgetConfig.from || 0; 108 } 109 const baseId = `${widgetConfig.id || ''}_widget_${widgetConfig.type}`; 110 return `${baseId}_${widgetConfig.index}_${uniqueIdentifier}`; 111} 112 113function widgetWithLocation(node, widgetConfig) { 114 const id = getWidgetID(widgetConfig); 115 // Store the unique ID back into the config so it's available for widget management 116 // This is critical for block-based evaluation to match existing widgets with new ones 117 widgetConfig.id = id; 118 // add loc as identifier to first argument 119 // the sliderWithID function is assumed to be sliderWithID(id, value, min?, max?) 120 node.arguments.unshift({ 121 type: 'Literal', 122 value: id, 123 raw: id, 124 }); 125 return node; 126} 127 128registerTranspilerPlugin(widgetTranspilerPlugins);