@jxsuite/studio 1.0.0 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/iframe-entry.js +622 -49
- package/dist/iframe-entry.js.map +15 -12
- package/dist/studio.css +1333 -0
- package/dist/studio.js +53069 -31746
- package/dist/studio.js.map +109 -74
- package/package.json +11 -9
- package/src/canvas/canvas-live-render.ts +21 -2
- package/src/canvas/canvas-render.ts +35 -16
- package/src/canvas/canvas-utils.ts +118 -72
- package/src/canvas/iframe-entry.ts +20 -0
- package/src/canvas/iframe-eval.ts +155 -0
- package/src/canvas/iframe-host.ts +131 -5
- package/src/canvas/iframe-inline-edit.ts +107 -1
- package/src/canvas/iframe-protocol.ts +43 -3
- package/src/canvas/iframe-render.ts +18 -0
- package/src/collab/collab-session.ts +23 -8
- package/src/collab/collab-state.ts +6 -3
- package/src/component-props.ts +104 -0
- package/src/editor/inline-edit-apply.ts +36 -1
- package/src/editor/inline-edit.ts +58 -1
- package/src/editor/shortcuts.ts +41 -12
- package/src/files/file-ops.ts +14 -0
- package/src/files/files.ts +53 -1
- package/src/grid/cell-editors.ts +288 -0
- package/src/grid/cell-popovers.ts +108 -0
- package/src/grid/csv-codec.ts +122 -0
- package/src/grid/edit-buffer.ts +490 -0
- package/src/grid/grid-controller.ts +417 -0
- package/src/grid/grid-layout.ts +83 -0
- package/src/grid/grid-open.ts +167 -0
- package/src/grid/grid-panel.ts +302 -0
- package/src/grid/grid-source.ts +210 -0
- package/src/grid/grid-view.ts +367 -0
- package/src/grid/schema-columns.ts +261 -0
- package/src/grid/sources/connector-source.ts +217 -0
- package/src/grid/sources/content-source.ts +542 -0
- package/src/grid/sources/csv-file-source.ts +247 -0
- package/src/grid/tabulator-tables.d.ts +120 -0
- package/src/panels/block-action-bar.ts +8 -3
- package/src/panels/chat-panel.ts +98 -0
- package/src/panels/data-grid.ts +9 -387
- package/src/panels/editors.ts +43 -17
- package/src/panels/events-panel.ts +137 -44
- package/src/panels/formula-workspace.ts +379 -0
- package/src/panels/frontmatter-fields.ts +231 -0
- package/src/panels/frontmatter-panel.ts +132 -0
- package/src/panels/git-panel.ts +1 -1
- package/src/panels/head-panel.ts +11 -175
- package/src/panels/properties-panel.ts +154 -144
- package/src/panels/right-panel.ts +9 -47
- package/src/panels/signals-panel.ts +115 -23
- package/src/panels/statement-editor.ts +710 -0
- package/src/panels/style-panel.ts +25 -1
- package/src/panels/stylebook-panel.ts +0 -2
- package/src/panels/tab-bar.ts +175 -6
- package/src/panels/tab-strip.ts +62 -6
- package/src/panels/toolbar.ts +37 -3
- package/src/services/ai-project-tools.ts +541 -0
- package/src/services/ai-session-store.ts +28 -0
- package/src/services/ai-system-prompt.ts +194 -24
- package/src/services/ai-tools.ts +88 -21
- package/src/services/automation.ts +16 -0
- package/src/services/document-assistant.ts +81 -11
- package/src/services/gated-registry.ts +72 -0
- package/src/services/live-preview.ts +0 -0
- package/src/services/preview-eval.ts +68 -0
- package/src/services/project-adoption.ts +31 -0
- package/src/site-context.ts +2 -0
- package/src/store.ts +4 -0
- package/src/studio.ts +83 -52
- package/src/tabs/patch-ops.ts +25 -0
- package/src/tabs/tab.ts +39 -1
- package/src/tabs/transact.ts +60 -13
- package/src/types.ts +12 -0
- package/src/ui/dynamic-slot.ts +272 -0
- package/src/ui/expression-editor.ts +423 -125
- package/src/ui/field-row.ts +5 -0
- package/src/ui/formula-catalog.ts +557 -0
- package/src/ui/formula-chips.ts +216 -0
- package/src/ui/formula-palette.ts +211 -0
- package/src/ui/layers.ts +40 -0
- package/src/ui/media-picker.ts +1 -1
- package/src/ui/panel-resize.ts +15 -1
- package/src/utils/preview-format.ts +26 -0
- package/src/view.ts +4 -4
- package/src/workspace/workspace.ts +14 -0
package/src/ui/field-row.ts
CHANGED
|
@@ -18,6 +18,7 @@ import { classMap } from "lit-html/directives/class-map.js";
|
|
|
18
18
|
* hasValue: boolean;
|
|
19
19
|
* onClear?: () => void;
|
|
20
20
|
* widget: unknown;
|
|
21
|
+
* labelExtra?: unknown;
|
|
21
22
|
* span?: number;
|
|
22
23
|
* warning?: boolean;
|
|
23
24
|
* }} opts
|
|
@@ -29,6 +30,7 @@ export function renderFieldRow({
|
|
|
29
30
|
hasValue,
|
|
30
31
|
onClear,
|
|
31
32
|
widget,
|
|
33
|
+
labelExtra,
|
|
32
34
|
span,
|
|
33
35
|
warning,
|
|
34
36
|
}: {
|
|
@@ -37,6 +39,8 @@ export function renderFieldRow({
|
|
|
37
39
|
hasValue: boolean;
|
|
38
40
|
onClear?: () => void;
|
|
39
41
|
widget: unknown;
|
|
42
|
+
/** Rendered after the label inside the label cell (e.g. the dynamic-slot mode button). */
|
|
43
|
+
labelExtra?: unknown;
|
|
40
44
|
span?: number;
|
|
41
45
|
warning?: boolean;
|
|
42
46
|
}) {
|
|
@@ -58,6 +62,7 @@ export function renderFieldRow({
|
|
|
58
62
|
></span>`
|
|
59
63
|
: nothing}
|
|
60
64
|
<sp-field-label size="s" title=${prop}>${label}</sp-field-label>
|
|
65
|
+
${labelExtra ?? nothing}
|
|
61
66
|
</div>
|
|
62
67
|
${widget}
|
|
63
68
|
</div>
|
|
@@ -0,0 +1,557 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Formula catalog — the metadata registry behind the formula palette, chip labels, and Monaco
|
|
3
|
+
* completions (spec §19). Merges three sources into one uniform entry shape:
|
|
4
|
+
*
|
|
5
|
+
* 1. Blessed operators (hand-authored metadata for every member of `BLESSED_OPERATORS`),
|
|
6
|
+
* 2. Blessed pure globals (derived programmatically from `BLESSED_GLOBALS`),
|
|
7
|
+
* 3. Named formulas (derived from document state via `isNamedFormulaDef`).
|
|
8
|
+
*
|
|
9
|
+
* Pure data + factories — no DOM. Descriptions reference genuine ECMA semantics (naming law: no
|
|
10
|
+
* token is invented).
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { BLESSED_GLOBALS, BLESSED_OPERATORS, PURE_METHOD_OPS } from "@jxsuite/runtime/expression";
|
|
14
|
+
import { catalog as packagedCatalog } from "@jxsuite/formulas";
|
|
15
|
+
import { isJsonObject, isNamedFormulaDef } from "@jxsuite/schema/guards";
|
|
16
|
+
|
|
17
|
+
import type {
|
|
18
|
+
CemParameter,
|
|
19
|
+
JxExpressionNode,
|
|
20
|
+
JxExpressionOperand,
|
|
21
|
+
JxStateDefinition,
|
|
22
|
+
} from "@jxsuite/schema/types";
|
|
23
|
+
import type { JsonValue } from "../types";
|
|
24
|
+
|
|
25
|
+
export interface FormulaParameterInfo {
|
|
26
|
+
name: string;
|
|
27
|
+
/** Display string for the parameter type (CEM `{ text }` or JSON Schema `type`). */
|
|
28
|
+
type?: string;
|
|
29
|
+
description?: string;
|
|
30
|
+
default?: JsonValue;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface FormulaCatalogEntry {
|
|
34
|
+
/** Stable identifier: operator token, global path ("Math/max"), or state key. */
|
|
35
|
+
name: string;
|
|
36
|
+
/** Display label: operator token, dotted global ("Math.max"), or state key. */
|
|
37
|
+
label: string;
|
|
38
|
+
/** Palette group heading; operator groups mirror the expression editor's. */
|
|
39
|
+
group: string;
|
|
40
|
+
kind: "operator" | "global" | "formula";
|
|
41
|
+
description: string;
|
|
42
|
+
parameters: FormulaParameterInfo[];
|
|
43
|
+
/** Factory for a fresh default expression node inserting this entry. */
|
|
44
|
+
insert: () => JxExpressionNode;
|
|
45
|
+
/**
|
|
46
|
+
* A state entry this pick must copy into the document first (packaged formulas vendor their JSON
|
|
47
|
+
* in — the project owns the copy; no package resolution at build or run time).
|
|
48
|
+
*/
|
|
49
|
+
ensure?: { name: string; def: JxStateDefinition };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Apply a palette pick: vendor the entry's state def in first when required (and not already
|
|
54
|
+
* present), then hand the fresh node to the caller. The single funnel every pick site uses.
|
|
55
|
+
*/
|
|
56
|
+
export function applyCatalogPick(
|
|
57
|
+
entry: FormulaCatalogEntry,
|
|
58
|
+
onChange: (node: JxExpressionNode) => void,
|
|
59
|
+
opts?: {
|
|
60
|
+
stateEntries?: Record<string, JxStateDefinition> | null | undefined;
|
|
61
|
+
onInsertDef?: ((name: string, def: JxStateDefinition) => void) | undefined;
|
|
62
|
+
},
|
|
63
|
+
): void {
|
|
64
|
+
if (entry.ensure && !opts?.stateEntries?.[entry.ensure.name]) {
|
|
65
|
+
opts?.onInsertDef?.(entry.ensure.name, entry.ensure.def);
|
|
66
|
+
}
|
|
67
|
+
onChange(entry.insert());
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// ─── Blessed operators (hand-authored) ──────────────────────────────────────
|
|
71
|
+
|
|
72
|
+
interface OperatorMeta {
|
|
73
|
+
group: string;
|
|
74
|
+
description: string;
|
|
75
|
+
parameters: FormulaParameterInfo[];
|
|
76
|
+
insert: () => JxExpressionNode;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const REF_TARGET = { description: "Writable pointer the operation applies to", name: "target" };
|
|
80
|
+
const LEFT = { description: "Left operand", name: "target" };
|
|
81
|
+
const RIGHT = { description: "Right operand", name: "value" };
|
|
82
|
+
|
|
83
|
+
function assignMeta(op: string, verb: string): OperatorMeta {
|
|
84
|
+
return {
|
|
85
|
+
description: `${verb} assignment operator (ECMA \`${op}\`): stores the result at the target pointer.`,
|
|
86
|
+
group: "Assignment",
|
|
87
|
+
insert: () => ({ operator: op, target: { $ref: "" }, value: null }),
|
|
88
|
+
parameters: [REF_TARGET, { description: "Value assigned to the target", name: "value" }],
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function binaryMeta(group: string, op: string, description: string): OperatorMeta {
|
|
93
|
+
return {
|
|
94
|
+
description,
|
|
95
|
+
group,
|
|
96
|
+
insert: () => ({ operator: op, target: null, value: null }),
|
|
97
|
+
parameters: [LEFT, RIGHT],
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function arrayMeta(op: string, description: string, withValue: boolean): OperatorMeta {
|
|
102
|
+
return {
|
|
103
|
+
description,
|
|
104
|
+
group: "Array methods",
|
|
105
|
+
insert: () =>
|
|
106
|
+
withValue
|
|
107
|
+
? { operator: op, target: { $ref: "" }, value: null }
|
|
108
|
+
: { operator: op, target: { $ref: "" } },
|
|
109
|
+
parameters: withValue
|
|
110
|
+
? [REF_TARGET, { description: "Element to add", name: "value" }]
|
|
111
|
+
: [REF_TARGET],
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function aggregateMeta(op: string, description: string, withInitial: boolean): OperatorMeta {
|
|
116
|
+
const node: JxExpressionNode = {
|
|
117
|
+
operator: op,
|
|
118
|
+
target: { $ref: "" },
|
|
119
|
+
value: { operator: "!", target: null },
|
|
120
|
+
};
|
|
121
|
+
return {
|
|
122
|
+
description,
|
|
123
|
+
group: "Aggregate",
|
|
124
|
+
insert: () => (withInitial ? { ...node, initial: 0 } : { ...node }),
|
|
125
|
+
parameters: [
|
|
126
|
+
{ description: "Array pointer to iterate", name: "target" },
|
|
127
|
+
{ description: "Per-item expression ($map/item, $map/index)", name: "value" },
|
|
128
|
+
...(withInitial
|
|
129
|
+
? [{ description: "Initial accumulator ($reduce/acc)", name: "initial" }]
|
|
130
|
+
: []),
|
|
131
|
+
],
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const OPERATOR_META: Record<string, OperatorMeta> = {
|
|
136
|
+
"!": {
|
|
137
|
+
description: "Logical NOT operator (ECMA `!`): coerces the operand to boolean and negates it.",
|
|
138
|
+
group: "Unary",
|
|
139
|
+
insert: () => ({ operator: "!", target: null }),
|
|
140
|
+
parameters: [{ description: "Operand to negate", name: "target" }],
|
|
141
|
+
},
|
|
142
|
+
"!==": binaryMeta(
|
|
143
|
+
"Comparison",
|
|
144
|
+
"!==",
|
|
145
|
+
"Strict inequality operator (ECMA `!==`): true when the operands differ without coercion.",
|
|
146
|
+
),
|
|
147
|
+
"%": binaryMeta(
|
|
148
|
+
"Arithmetic",
|
|
149
|
+
"%",
|
|
150
|
+
"Remainder operator (ECMA `%`): remainder of dividing the left operand by the right.",
|
|
151
|
+
),
|
|
152
|
+
"&&": binaryMeta(
|
|
153
|
+
"Logical",
|
|
154
|
+
"&&",
|
|
155
|
+
"Logical AND operator (ECMA `&&`): yields the right operand when the left is truthy.",
|
|
156
|
+
),
|
|
157
|
+
"*": binaryMeta(
|
|
158
|
+
"Arithmetic",
|
|
159
|
+
"*",
|
|
160
|
+
"Multiplication operator (ECMA `*`): numeric product of the operands.",
|
|
161
|
+
),
|
|
162
|
+
"*=": assignMeta("*=", "Multiplication"),
|
|
163
|
+
"+": binaryMeta(
|
|
164
|
+
"Arithmetic",
|
|
165
|
+
"+",
|
|
166
|
+
"Addition operator (ECMA `+`): numeric addition, or string concatenation.",
|
|
167
|
+
),
|
|
168
|
+
"+=": assignMeta("+=", "Addition"),
|
|
169
|
+
"-": binaryMeta(
|
|
170
|
+
"Arithmetic",
|
|
171
|
+
"-",
|
|
172
|
+
"Subtraction operator (ECMA `-`); with no value operand it is unary negation.",
|
|
173
|
+
),
|
|
174
|
+
"-=": assignMeta("-=", "Subtraction"),
|
|
175
|
+
"/": binaryMeta("Arithmetic", "/", "Division operator (ECMA `/`): quotient of the operands."),
|
|
176
|
+
"/=": assignMeta("/=", "Division"),
|
|
177
|
+
"<": binaryMeta(
|
|
178
|
+
"Comparison",
|
|
179
|
+
"<",
|
|
180
|
+
"Less-than operator (ECMA `<`): relational comparison of the operands.",
|
|
181
|
+
),
|
|
182
|
+
"<=": binaryMeta(
|
|
183
|
+
"Comparison",
|
|
184
|
+
"<=",
|
|
185
|
+
"Less-than-or-equal operator (ECMA `<=`): relational comparison of the operands.",
|
|
186
|
+
),
|
|
187
|
+
"=": assignMeta("=", "Simple"),
|
|
188
|
+
"===": binaryMeta(
|
|
189
|
+
"Comparison",
|
|
190
|
+
"===",
|
|
191
|
+
"Strict equality operator (ECMA `===`): true when the operands are equal without coercion.",
|
|
192
|
+
),
|
|
193
|
+
">": binaryMeta(
|
|
194
|
+
"Comparison",
|
|
195
|
+
">",
|
|
196
|
+
"Greater-than operator (ECMA `>`): relational comparison of the operands.",
|
|
197
|
+
),
|
|
198
|
+
">=": binaryMeta(
|
|
199
|
+
"Comparison",
|
|
200
|
+
">=",
|
|
201
|
+
"Greater-than-or-equal operator (ECMA `>=`): relational comparison of the operands.",
|
|
202
|
+
),
|
|
203
|
+
"??": binaryMeta(
|
|
204
|
+
"Logical",
|
|
205
|
+
"??",
|
|
206
|
+
"Nullish coalescing operator (ECMA `??`): yields the right operand when the left is null or undefined.",
|
|
207
|
+
),
|
|
208
|
+
"?:": {
|
|
209
|
+
description:
|
|
210
|
+
"Conditional operator (ECMA `?:`): evaluates the test in target, yielding value when truthy, initial otherwise.",
|
|
211
|
+
group: "Conditional",
|
|
212
|
+
insert: () => ({ initial: null, operator: "?:", target: null, value: null }),
|
|
213
|
+
parameters: [
|
|
214
|
+
{ description: "Test condition", name: "target" },
|
|
215
|
+
{ description: "Result when the test is truthy", name: "value" },
|
|
216
|
+
{ description: "Result when the test is falsy", name: "initial" },
|
|
217
|
+
],
|
|
218
|
+
},
|
|
219
|
+
"||": binaryMeta(
|
|
220
|
+
"Logical",
|
|
221
|
+
"||",
|
|
222
|
+
"Logical OR operator (ECMA `||`): yields the right operand when the left is falsy.",
|
|
223
|
+
),
|
|
224
|
+
call: {
|
|
225
|
+
description:
|
|
226
|
+
"Function.prototype.call (ECMA): invokes a named formula (#/state/…) or blessed pure global (window#/…) with positional arguments.",
|
|
227
|
+
group: "Function",
|
|
228
|
+
insert: () => ({ operator: "call", target: { $ref: "" }, value: [] }),
|
|
229
|
+
parameters: [
|
|
230
|
+
{ description: "Callee pointer: #/state/<formula> or window#/<global>", name: "target" },
|
|
231
|
+
{ description: "Positional argument list", name: "value" },
|
|
232
|
+
],
|
|
233
|
+
},
|
|
234
|
+
filter: aggregateMeta(
|
|
235
|
+
"filter",
|
|
236
|
+
"Array.prototype.filter (ECMA): keeps elements whose per-item expression is truthy.",
|
|
237
|
+
false,
|
|
238
|
+
),
|
|
239
|
+
map: aggregateMeta(
|
|
240
|
+
"map",
|
|
241
|
+
"Array.prototype.map (ECMA): transforms each element via the per-item expression.",
|
|
242
|
+
false,
|
|
243
|
+
),
|
|
244
|
+
pop: arrayMeta(
|
|
245
|
+
"pop",
|
|
246
|
+
"Array.prototype.pop (ECMA): removes and returns the last element of the target array.",
|
|
247
|
+
false,
|
|
248
|
+
),
|
|
249
|
+
push: arrayMeta(
|
|
250
|
+
"push",
|
|
251
|
+
"Array.prototype.push (ECMA): appends the value to the target array.",
|
|
252
|
+
true,
|
|
253
|
+
),
|
|
254
|
+
reduce: aggregateMeta(
|
|
255
|
+
"reduce",
|
|
256
|
+
"Array.prototype.reduce (ECMA): folds the array into an accumulator via the per-item expression.",
|
|
257
|
+
true,
|
|
258
|
+
),
|
|
259
|
+
shift: arrayMeta(
|
|
260
|
+
"shift",
|
|
261
|
+
"Array.prototype.shift (ECMA): removes and returns the first element of the target array.",
|
|
262
|
+
false,
|
|
263
|
+
),
|
|
264
|
+
splice: {
|
|
265
|
+
description:
|
|
266
|
+
"Array.prototype.splice (ECMA): removes and/or inserts elements at an index of the target array.",
|
|
267
|
+
group: "Array methods",
|
|
268
|
+
insert: () => ({ operator: "splice", target: { $ref: "" }, value: [null] }),
|
|
269
|
+
parameters: [
|
|
270
|
+
REF_TARGET,
|
|
271
|
+
{ description: "Arguments: start, deleteCount, items…", name: "value" },
|
|
272
|
+
],
|
|
273
|
+
},
|
|
274
|
+
switch: {
|
|
275
|
+
description:
|
|
276
|
+
"Switch selection (ECMA `switch` semantics): matches the target's string form against case keys, falling back to default.",
|
|
277
|
+
group: "Conditional",
|
|
278
|
+
insert: () => ({ cases: {}, default: null, operator: "switch", target: null }),
|
|
279
|
+
parameters: [
|
|
280
|
+
{ description: "Discriminant value", name: "target" },
|
|
281
|
+
{ description: "Matched value → result operand map", name: "cases" },
|
|
282
|
+
{ description: "Result when no case matches", name: "default" },
|
|
283
|
+
],
|
|
284
|
+
},
|
|
285
|
+
unshift: arrayMeta(
|
|
286
|
+
"unshift",
|
|
287
|
+
"Array.prototype.unshift (ECMA): prepends the value to the target array.",
|
|
288
|
+
true,
|
|
289
|
+
),
|
|
290
|
+
};
|
|
291
|
+
|
|
292
|
+
/** Catalog entries for every blessed operator, in `BLESSED_OPERATORS` iteration order. */
|
|
293
|
+
export function operatorEntries(): FormulaCatalogEntry[] {
|
|
294
|
+
const out: FormulaCatalogEntry[] = [];
|
|
295
|
+
for (const op of BLESSED_OPERATORS) {
|
|
296
|
+
const meta = OPERATOR_META[op];
|
|
297
|
+
if (meta) {
|
|
298
|
+
out.push({
|
|
299
|
+
description: meta.description,
|
|
300
|
+
group: meta.group,
|
|
301
|
+
insert: meta.insert,
|
|
302
|
+
kind: "operator",
|
|
303
|
+
label: op,
|
|
304
|
+
name: op,
|
|
305
|
+
parameters: meta.parameters,
|
|
306
|
+
});
|
|
307
|
+
} else if (PURE_METHOD_OPS.has(op)) {
|
|
308
|
+
out.push(pureMethodEntry(op));
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
return out;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// ─── Pure standard-library methods (derived, spec §19.4d) ───────────────────
|
|
315
|
+
|
|
316
|
+
/** Prototype owner per pure method — drives grouping and the derived description. */
|
|
317
|
+
const PURE_METHOD_PROTO: Record<string, string> = {
|
|
318
|
+
at: "Array/String",
|
|
319
|
+
charAt: "String",
|
|
320
|
+
concat: "Array/String",
|
|
321
|
+
endsWith: "String",
|
|
322
|
+
flat: "Array",
|
|
323
|
+
includes: "Array/String",
|
|
324
|
+
indexOf: "Array/String",
|
|
325
|
+
join: "Array",
|
|
326
|
+
lastIndexOf: "Array/String",
|
|
327
|
+
normalize: "String",
|
|
328
|
+
padEnd: "String",
|
|
329
|
+
padStart: "String",
|
|
330
|
+
repeat: "String",
|
|
331
|
+
replaceAll: "String",
|
|
332
|
+
slice: "Array/String",
|
|
333
|
+
split: "String",
|
|
334
|
+
startsWith: "String",
|
|
335
|
+
toFixed: "Number",
|
|
336
|
+
toLocaleLowerCase: "String",
|
|
337
|
+
toLocaleString: "Number",
|
|
338
|
+
toLocaleUpperCase: "String",
|
|
339
|
+
toLowerCase: "String",
|
|
340
|
+
toPrecision: "Number",
|
|
341
|
+
toReversed: "Array",
|
|
342
|
+
toSorted: "Array",
|
|
343
|
+
toSpliced: "Array",
|
|
344
|
+
toUpperCase: "String",
|
|
345
|
+
trim: "String",
|
|
346
|
+
trimEnd: "String",
|
|
347
|
+
trimStart: "String",
|
|
348
|
+
with: "Array",
|
|
349
|
+
};
|
|
350
|
+
|
|
351
|
+
/** Methods that take no argument — their insert seed omits `value`. */
|
|
352
|
+
export const ZERO_ARG_METHODS = new Set([
|
|
353
|
+
"flat",
|
|
354
|
+
"normalize",
|
|
355
|
+
"toLocaleLowerCase",
|
|
356
|
+
"toLocaleString",
|
|
357
|
+
"toLocaleUpperCase",
|
|
358
|
+
"toLowerCase",
|
|
359
|
+
"toReversed",
|
|
360
|
+
"toSorted",
|
|
361
|
+
"toUpperCase",
|
|
362
|
+
"trim",
|
|
363
|
+
"trimEnd",
|
|
364
|
+
"trimStart",
|
|
365
|
+
]);
|
|
366
|
+
|
|
367
|
+
/** The derived catalog entry for one pure method operator (spec §19.4d). */
|
|
368
|
+
function pureMethodEntry(op: string): FormulaCatalogEntry {
|
|
369
|
+
const proto = PURE_METHOD_PROTO[op] ?? "Standard library";
|
|
370
|
+
return {
|
|
371
|
+
description: `${proto}.prototype.${op} (ECMA) — pure: never mutates the receiver. Receiver in target; argument (or argument list) in value.`,
|
|
372
|
+
group: `${proto} methods`,
|
|
373
|
+
insert: () =>
|
|
374
|
+
ZERO_ARG_METHODS.has(op)
|
|
375
|
+
? { operator: op, target: { $ref: "" } }
|
|
376
|
+
: { operator: op, target: { $ref: "" }, value: null },
|
|
377
|
+
kind: "operator",
|
|
378
|
+
label: op,
|
|
379
|
+
name: op,
|
|
380
|
+
parameters: [],
|
|
381
|
+
};
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// ─── Blessed globals (derived) ──────────────────────────────────────────────
|
|
385
|
+
|
|
386
|
+
/**
|
|
387
|
+
* Synthetic blessed helpers wrap constructor-shaped standard-library APIs (spec §19.4c); their
|
|
388
|
+
* catalog descriptions name the wrapped API rather than claiming a literal global exists.
|
|
389
|
+
*/
|
|
390
|
+
const HELPER_DESCRIPTIONS: Record<string, string> = {
|
|
391
|
+
"Intl/formatDate":
|
|
392
|
+
"Intl.formatDate(value, locale?, options?) — blessed helper wrapping new Intl.DateTimeFormat(locale, options).format(new Date(value)).",
|
|
393
|
+
"Intl/formatNumber":
|
|
394
|
+
"Intl.formatNumber(value, locale?, options?) — blessed helper wrapping new Intl.NumberFormat(locale, options).format(value).",
|
|
395
|
+
"Intl/formatRelativeTime":
|
|
396
|
+
"Intl.formatRelativeTime(value, unit, locale?, options?) — blessed helper wrapping new Intl.RelativeTimeFormat(locale, options).format(value, unit).",
|
|
397
|
+
};
|
|
398
|
+
|
|
399
|
+
/** Catalog entries derived from `BLESSED_GLOBALS`; grouped by namespace (bare → globalThis). */
|
|
400
|
+
export function globalEntries(): FormulaCatalogEntry[] {
|
|
401
|
+
const out: FormulaCatalogEntry[] = [];
|
|
402
|
+
for (const name of BLESSED_GLOBALS) {
|
|
403
|
+
const slash = name.indexOf("/");
|
|
404
|
+
const group = slash === -1 ? "globalThis" : name.slice(0, slash);
|
|
405
|
+
const label = name.replaceAll("/", ".");
|
|
406
|
+
out.push({
|
|
407
|
+
description:
|
|
408
|
+
HELPER_DESCRIPTIONS[name] ??
|
|
409
|
+
`${label} — blessed pure global from the ECMAScript standard library, invoked via the call operator (window#/${name}).`,
|
|
410
|
+
group,
|
|
411
|
+
insert: () => ({ operator: "call", target: { $ref: `window#/${name}` }, value: [] }),
|
|
412
|
+
kind: "global",
|
|
413
|
+
label,
|
|
414
|
+
name,
|
|
415
|
+
parameters: [],
|
|
416
|
+
});
|
|
417
|
+
}
|
|
418
|
+
return out;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
// ─── Named formulas (derived from document state) ───────────────────────────
|
|
422
|
+
|
|
423
|
+
/** Normalize a `(string | CemParameter)` parameter declaration to display metadata. */
|
|
424
|
+
function toParameterInfo(p: string | CemParameter): FormulaParameterInfo | null {
|
|
425
|
+
if (typeof p === "string") {
|
|
426
|
+
return p ? { name: p } : null;
|
|
427
|
+
}
|
|
428
|
+
if (!isJsonObject(p) || typeof p.name !== "string" || !p.name) {
|
|
429
|
+
return null;
|
|
430
|
+
}
|
|
431
|
+
const info: FormulaParameterInfo = { name: p.name };
|
|
432
|
+
const { type } = p;
|
|
433
|
+
if (typeof type === "string") {
|
|
434
|
+
info.type = type;
|
|
435
|
+
} else if (isJsonObject(type)) {
|
|
436
|
+
if (typeof type.text === "string") {
|
|
437
|
+
info.type = type.text;
|
|
438
|
+
} else if (typeof type.type === "string") {
|
|
439
|
+
info.type = type.type;
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
if (typeof p.description === "string") {
|
|
443
|
+
info.description = p.description;
|
|
444
|
+
}
|
|
445
|
+
if ("default" in p && p.default !== undefined) {
|
|
446
|
+
info.default = p.default as JsonValue;
|
|
447
|
+
}
|
|
448
|
+
return info;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
/** Catalog entries for the document's named formulas (parameterized `$expression` entries). */
|
|
452
|
+
export function namedFormulaEntries(
|
|
453
|
+
state?: Record<string, JxStateDefinition> | null,
|
|
454
|
+
): FormulaCatalogEntry[] {
|
|
455
|
+
const out: FormulaCatalogEntry[] = [];
|
|
456
|
+
for (const [key, def] of Object.entries(state ?? {})) {
|
|
457
|
+
if (!isNamedFormulaDef(def)) {
|
|
458
|
+
continue;
|
|
459
|
+
}
|
|
460
|
+
const parameters = (def.parameters as (string | CemParameter)[])
|
|
461
|
+
.map((p) => toParameterInfo(p))
|
|
462
|
+
.filter((p): p is FormulaParameterInfo => p !== null);
|
|
463
|
+
const description =
|
|
464
|
+
typeof def.description === "string" && def.description
|
|
465
|
+
? def.description
|
|
466
|
+
: typeof def.$title === "string" && def.$title
|
|
467
|
+
? def.$title
|
|
468
|
+
: `Named formula "${key}" from document state.`;
|
|
469
|
+
out.push({
|
|
470
|
+
description,
|
|
471
|
+
group: "Formulas",
|
|
472
|
+
insert: () => ({
|
|
473
|
+
operator: "call",
|
|
474
|
+
target: { $ref: `#/state/${key}` },
|
|
475
|
+
value: parameters.map((p) => (p.default ?? null) as JxExpressionOperand),
|
|
476
|
+
}),
|
|
477
|
+
kind: "formula",
|
|
478
|
+
label: key,
|
|
479
|
+
name: key,
|
|
480
|
+
parameters,
|
|
481
|
+
});
|
|
482
|
+
}
|
|
483
|
+
return out;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
// ─── Merged registry ────────────────────────────────────────────────────────
|
|
487
|
+
|
|
488
|
+
// ─── Packaged formulas (@jxsuite/formulas, copy-in) ─────────────────────────
|
|
489
|
+
|
|
490
|
+
/**
|
|
491
|
+
* Catalog entries for the packaged composite formulas. Picking one vendors its JSON state entry
|
|
492
|
+
* into the document via `ensure` and inserts a call node — projects stay self-contained; entries
|
|
493
|
+
* already present in state are skipped (the named-formula entry represents them instead).
|
|
494
|
+
*/
|
|
495
|
+
export function packagedFormulaEntries(
|
|
496
|
+
state?: Record<string, JxStateDefinition> | null,
|
|
497
|
+
): FormulaCatalogEntry[] {
|
|
498
|
+
const out: FormulaCatalogEntry[] = [];
|
|
499
|
+
for (const formula of packagedCatalog) {
|
|
500
|
+
if (state?.[formula.name]) {
|
|
501
|
+
continue;
|
|
502
|
+
}
|
|
503
|
+
const parameters = formula.parameters
|
|
504
|
+
.map((p) => toParameterInfo(p))
|
|
505
|
+
.filter((p): p is FormulaParameterInfo => p !== null);
|
|
506
|
+
out.push({
|
|
507
|
+
description: formula.description,
|
|
508
|
+
ensure: {
|
|
509
|
+
def: {
|
|
510
|
+
$description: formula.description,
|
|
511
|
+
$expression: formula.expression,
|
|
512
|
+
parameters: formula.parameters,
|
|
513
|
+
},
|
|
514
|
+
name: formula.name,
|
|
515
|
+
},
|
|
516
|
+
group: "Formulas library",
|
|
517
|
+
insert: () => ({
|
|
518
|
+
operator: "call",
|
|
519
|
+
target: { $ref: `#/state/${formula.name}` },
|
|
520
|
+
value: parameters.map((p) => (p.default ?? null) as JxExpressionOperand),
|
|
521
|
+
}),
|
|
522
|
+
kind: "formula",
|
|
523
|
+
label: formula.name,
|
|
524
|
+
name: formula.name,
|
|
525
|
+
parameters,
|
|
526
|
+
});
|
|
527
|
+
}
|
|
528
|
+
return out;
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
/** The full catalog: named formulas first (most specific), then packaged, operators, globals. */
|
|
532
|
+
export function formulaCatalog(
|
|
533
|
+
state?: Record<string, JxStateDefinition> | null,
|
|
534
|
+
): FormulaCatalogEntry[] {
|
|
535
|
+
return [
|
|
536
|
+
...namedFormulaEntries(state),
|
|
537
|
+
...packagedFormulaEntries(state),
|
|
538
|
+
...operatorEntries(),
|
|
539
|
+
...globalEntries(),
|
|
540
|
+
];
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
/** Resolve a `call` callee pointer to its catalog entry, when resolvable. */
|
|
544
|
+
export function calleeEntry(
|
|
545
|
+
ref: string,
|
|
546
|
+
state?: Record<string, JxStateDefinition> | null,
|
|
547
|
+
): FormulaCatalogEntry | undefined {
|
|
548
|
+
if (ref.startsWith("window#/")) {
|
|
549
|
+
const name = ref.slice("window#/".length);
|
|
550
|
+
return globalEntries().find((e) => e.name === name);
|
|
551
|
+
}
|
|
552
|
+
if (ref.startsWith("#/state/")) {
|
|
553
|
+
const key = ref.slice("#/state/".length);
|
|
554
|
+
return namedFormulaEntries(state).find((e) => e.name === key);
|
|
555
|
+
}
|
|
556
|
+
return undefined;
|
|
557
|
+
}
|