jevstrudel.git / packages / transpiler / plugin-widgets.mjs

plugin-widgets.mjs - <short description TODO> Copyright (C) 2022 Strudel contributors - see https://codeberg.org/uzu/strudel/src/branch/main/packages/superdough/superdough.mjs This 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/.

7import { registerTranspilerPlugin } from './transpiler.mjs';
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];

these functions are connected to @strudel/codemirror -> slider.mjs 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}
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;

Use range-based ID for stability during block evaluation

82  const id = `${from}:${to}`;

add loc as identifier to first argument 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}
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);