tiptap-model-language 1.0.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/LICENSE +21 -0
- package/README.md +147 -0
- package/dist/index.cjs +1725 -0
- package/dist/index.d.cts +207 -0
- package/dist/index.d.ts +207 -0
- package/dist/index.js +1687 -0
- package/package.json +96 -0
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,1725 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var modelLanguage = require('model-language');
|
|
4
|
+
var dateFns = require('date-fns');
|
|
5
|
+
var core = require('@tiptap/core');
|
|
6
|
+
var state = require('@tiptap/pm/state');
|
|
7
|
+
var view = require('@tiptap/pm/view');
|
|
8
|
+
var react$1 = require('@tiptap/react');
|
|
9
|
+
var suggestion = require('@tiptap/suggestion');
|
|
10
|
+
var tippy = require('tippy.js');
|
|
11
|
+
var react = require('react');
|
|
12
|
+
var clsx = require('clsx');
|
|
13
|
+
var tailwindMerge = require('tailwind-merge');
|
|
14
|
+
var jsxRuntime = require('react/jsx-runtime');
|
|
15
|
+
|
|
16
|
+
function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
|
|
17
|
+
|
|
18
|
+
var tippy__default = /*#__PURE__*/_interopDefault(tippy);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
|
|
22
|
+
// src/schema/namespaces.ts
|
|
23
|
+
var CONTACT_NAMESPACE_KEY = "contact";
|
|
24
|
+
var FLOW_NAMESPACE_KEY = "flow";
|
|
25
|
+
var STATIC_NAMESPACES = [
|
|
26
|
+
{
|
|
27
|
+
key: "system",
|
|
28
|
+
label: "System",
|
|
29
|
+
fields: [
|
|
30
|
+
{ key: "now", type: "datetime", label: "Current date/time" },
|
|
31
|
+
{ key: "timezone", type: "string", label: "Timezone" }
|
|
32
|
+
]
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
key: "conversation",
|
|
36
|
+
label: "Conversation",
|
|
37
|
+
fields: [
|
|
38
|
+
{ key: "id", type: "string", label: "Conversation ID" },
|
|
39
|
+
{ key: "integrationId", type: "string", label: "Integration ID" },
|
|
40
|
+
{ key: "title", type: "string", label: "Title" },
|
|
41
|
+
{ key: "unreadCount", type: "number", label: "Unread count" }
|
|
42
|
+
]
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
key: "organisation",
|
|
46
|
+
label: "Organisation",
|
|
47
|
+
fields: [
|
|
48
|
+
{ key: "id", type: "string", label: "Org ID" },
|
|
49
|
+
{ key: "name", type: "string", label: "Name" },
|
|
50
|
+
{ key: "website", type: "string", label: "Website" },
|
|
51
|
+
{ key: "phone", type: "string", label: "Phone" },
|
|
52
|
+
{ key: "email", type: "string", label: "Email" },
|
|
53
|
+
{ key: "description", type: "string", label: "Description" }
|
|
54
|
+
]
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
key: "lastMessage",
|
|
58
|
+
label: "Last message",
|
|
59
|
+
fields: [
|
|
60
|
+
{ key: "text", type: "string", label: "Text" },
|
|
61
|
+
{ key: "deliveryStatus", type: "string", label: "Delivery status" }
|
|
62
|
+
]
|
|
63
|
+
}
|
|
64
|
+
];
|
|
65
|
+
var DATE_FORMATS = [
|
|
66
|
+
"yyyy-MM-dd",
|
|
67
|
+
"MMM d, yyyy",
|
|
68
|
+
"HH:mm",
|
|
69
|
+
"h:mm A",
|
|
70
|
+
"EEEE"
|
|
71
|
+
];
|
|
72
|
+
var ML_FILTERS = [
|
|
73
|
+
// text (string / enum)
|
|
74
|
+
{
|
|
75
|
+
name: "default",
|
|
76
|
+
hint: "Fallback when empty",
|
|
77
|
+
argTemplate: ': "there"',
|
|
78
|
+
inputs: "any"
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
name: "upper",
|
|
82
|
+
hint: "UPPERCASE",
|
|
83
|
+
inputs: ["string", "enum"],
|
|
84
|
+
output: "string"
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
name: "lower",
|
|
88
|
+
hint: "lowercase",
|
|
89
|
+
inputs: ["string", "enum"],
|
|
90
|
+
output: "string"
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
name: "capitalize",
|
|
94
|
+
hint: "Capitalise first letter",
|
|
95
|
+
inputs: ["string", "enum"],
|
|
96
|
+
output: "string"
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
name: "trim",
|
|
100
|
+
hint: "Trim whitespace",
|
|
101
|
+
inputs: ["string"],
|
|
102
|
+
output: "string"
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
name: "truncate",
|
|
106
|
+
hint: "Shorten to N chars",
|
|
107
|
+
argTemplate: ": 40",
|
|
108
|
+
inputs: ["string"],
|
|
109
|
+
output: "string"
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
name: "replace",
|
|
113
|
+
hint: "Replace text",
|
|
114
|
+
argTemplate: ': "a", "b"',
|
|
115
|
+
inputs: ["string"],
|
|
116
|
+
output: "string"
|
|
117
|
+
},
|
|
118
|
+
// number
|
|
119
|
+
{
|
|
120
|
+
name: "round",
|
|
121
|
+
hint: "Round to N decimals",
|
|
122
|
+
argTemplate: ": 2",
|
|
123
|
+
inputs: ["number"],
|
|
124
|
+
output: "number"
|
|
125
|
+
},
|
|
126
|
+
{ name: "floor", hint: "Round down", inputs: ["number"], output: "number" },
|
|
127
|
+
{ name: "ceil", hint: "Round up", inputs: ["number"], output: "number" },
|
|
128
|
+
{ name: "abs", hint: "Absolute value", inputs: ["number"], output: "number" },
|
|
129
|
+
{
|
|
130
|
+
name: "percent",
|
|
131
|
+
hint: "Format as a percentage",
|
|
132
|
+
inputs: ["number"],
|
|
133
|
+
output: "string"
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
name: "currency",
|
|
137
|
+
hint: "Format as currency",
|
|
138
|
+
argTemplate: ': "USD"',
|
|
139
|
+
inputs: ["number"],
|
|
140
|
+
output: "string"
|
|
141
|
+
},
|
|
142
|
+
// datetime
|
|
143
|
+
{
|
|
144
|
+
name: "date",
|
|
145
|
+
hint: 'Format a date, e.g. "HH:mm"',
|
|
146
|
+
argTemplate: ': "MMM d, yyyy"',
|
|
147
|
+
inputs: ["datetime"],
|
|
148
|
+
output: "string"
|
|
149
|
+
},
|
|
150
|
+
{
|
|
151
|
+
name: "days_ago",
|
|
152
|
+
hint: "Whole days since (number)",
|
|
153
|
+
inputs: ["datetime"],
|
|
154
|
+
output: "number"
|
|
155
|
+
},
|
|
156
|
+
{
|
|
157
|
+
name: "days_until",
|
|
158
|
+
hint: "Whole days until (number)",
|
|
159
|
+
inputs: ["datetime"],
|
|
160
|
+
output: "number"
|
|
161
|
+
},
|
|
162
|
+
{
|
|
163
|
+
name: "is_past",
|
|
164
|
+
hint: "True if in the past",
|
|
165
|
+
inputs: ["datetime"],
|
|
166
|
+
output: "boolean"
|
|
167
|
+
},
|
|
168
|
+
{
|
|
169
|
+
name: "is_future",
|
|
170
|
+
hint: "True if in the future",
|
|
171
|
+
inputs: ["datetime"],
|
|
172
|
+
output: "boolean"
|
|
173
|
+
},
|
|
174
|
+
// array
|
|
175
|
+
{
|
|
176
|
+
name: "count",
|
|
177
|
+
hint: "Number of items",
|
|
178
|
+
inputs: ["array"],
|
|
179
|
+
output: "number"
|
|
180
|
+
},
|
|
181
|
+
{
|
|
182
|
+
name: "join",
|
|
183
|
+
hint: "Join a list",
|
|
184
|
+
argTemplate: ': ", "',
|
|
185
|
+
inputs: ["array"],
|
|
186
|
+
output: "string"
|
|
187
|
+
},
|
|
188
|
+
{ name: "first", hint: "First item", inputs: ["array"] },
|
|
189
|
+
{ name: "last", hint: "Last item", inputs: ["array"] },
|
|
190
|
+
{
|
|
191
|
+
name: "limit",
|
|
192
|
+
hint: "Keep the first N",
|
|
193
|
+
argTemplate: ": 3",
|
|
194
|
+
inputs: ["array"],
|
|
195
|
+
output: "array"
|
|
196
|
+
},
|
|
197
|
+
{
|
|
198
|
+
name: "pluck",
|
|
199
|
+
hint: "Pull one field from each",
|
|
200
|
+
argTemplate: ': "name"',
|
|
201
|
+
inputs: ["array"],
|
|
202
|
+
output: "array"
|
|
203
|
+
},
|
|
204
|
+
{
|
|
205
|
+
name: "where",
|
|
206
|
+
hint: "Filter a list",
|
|
207
|
+
argTemplate: ': "status", "==", "open"',
|
|
208
|
+
inputs: ["array"],
|
|
209
|
+
output: "array"
|
|
210
|
+
},
|
|
211
|
+
{
|
|
212
|
+
name: "sort",
|
|
213
|
+
hint: "Sort a list",
|
|
214
|
+
argTemplate: ': "field"',
|
|
215
|
+
inputs: ["array"],
|
|
216
|
+
output: "array"
|
|
217
|
+
},
|
|
218
|
+
{ name: "sum", hint: "Sum a list", inputs: ["array"], output: "number" },
|
|
219
|
+
{ name: "max", hint: "Largest", inputs: ["array"], output: "number" },
|
|
220
|
+
{ name: "min", hint: "Smallest", inputs: ["array"], output: "number" }
|
|
221
|
+
];
|
|
222
|
+
var DIRECTIVE_RE = /^(#|\/[a-z]|(?:if|elseif|else|for|end|include)\b)/i;
|
|
223
|
+
function splitPipes(inner) {
|
|
224
|
+
const out = [];
|
|
225
|
+
let cur = "";
|
|
226
|
+
let quote = null;
|
|
227
|
+
for (const ch of inner) {
|
|
228
|
+
if (quote) {
|
|
229
|
+
cur += ch;
|
|
230
|
+
if (ch === quote) quote = null;
|
|
231
|
+
} else if (ch === '"' || ch === "'") {
|
|
232
|
+
quote = ch;
|
|
233
|
+
cur += ch;
|
|
234
|
+
} else if (ch === "|") {
|
|
235
|
+
out.push(cur);
|
|
236
|
+
cur = "";
|
|
237
|
+
} else {
|
|
238
|
+
cur += ch;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
out.push(cur);
|
|
242
|
+
return out;
|
|
243
|
+
}
|
|
244
|
+
function parseModelToken(inner) {
|
|
245
|
+
const trimmed = inner.trim();
|
|
246
|
+
const directive = DIRECTIVE_RE.test(trimmed);
|
|
247
|
+
const parts = splitPipes(inner);
|
|
248
|
+
const path = (parts[0] ?? "").trim();
|
|
249
|
+
const dot = path.indexOf(".");
|
|
250
|
+
const filters = parts.slice(1).map((p) => {
|
|
251
|
+
const seg = p.trim();
|
|
252
|
+
const colon = seg.indexOf(":");
|
|
253
|
+
return colon < 0 ? { name: seg, args: "" } : { name: seg.slice(0, colon).trim(), args: seg.slice(colon + 1).trim() };
|
|
254
|
+
});
|
|
255
|
+
return {
|
|
256
|
+
path,
|
|
257
|
+
namespace: dot < 0 ? path : path.slice(0, dot),
|
|
258
|
+
field: dot < 0 ? "" : path.slice(dot + 1),
|
|
259
|
+
filters,
|
|
260
|
+
directive
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// src/autocomplete/options.ts
|
|
265
|
+
var COMMON_TZ = [
|
|
266
|
+
"UTC",
|
|
267
|
+
"Europe/Kyiv",
|
|
268
|
+
"Europe/London",
|
|
269
|
+
"Europe/Berlin",
|
|
270
|
+
"Europe/Paris",
|
|
271
|
+
"America/New_York",
|
|
272
|
+
"America/Los_Angeles",
|
|
273
|
+
"Asia/Dubai",
|
|
274
|
+
"Asia/Tokyo"
|
|
275
|
+
];
|
|
276
|
+
function previewDateFormat(token) {
|
|
277
|
+
const t = token.replace(/\[([^\]]*)\]/g, "'$1'").replace(/YYYY/g, "yyyy").replace(/YY/g, "yy").replace(/DD/g, "dd").replace(/dddd/g, "EEEE").replace(/ddd/g, "EEE").replace(/A/g, "a");
|
|
278
|
+
try {
|
|
279
|
+
return dateFns.format(/* @__PURE__ */ new Date(), t);
|
|
280
|
+
} catch {
|
|
281
|
+
return "";
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
function timezoneOptions(frag) {
|
|
285
|
+
let zones = [];
|
|
286
|
+
try {
|
|
287
|
+
zones = Intl.supportedValuesOf?.("timeZone") ?? [];
|
|
288
|
+
} catch {
|
|
289
|
+
zones = [];
|
|
290
|
+
}
|
|
291
|
+
if (!zones.length) zones = COMMON_TZ;
|
|
292
|
+
const q = frag.toLowerCase();
|
|
293
|
+
return zones.filter((z) => !q || z.toLowerCase().includes(q));
|
|
294
|
+
}
|
|
295
|
+
function resolveType(namespaces, expr) {
|
|
296
|
+
const parts = expr.split("|").map((s) => s.trim()).filter(Boolean);
|
|
297
|
+
if (!parts.length) return "any";
|
|
298
|
+
let type = "any";
|
|
299
|
+
const f = findField(namespaces, parts[0]);
|
|
300
|
+
if (f) type = f.type;
|
|
301
|
+
for (let i = 1; i < parts.length; i++) {
|
|
302
|
+
const meta = ML_FILTERS.find(
|
|
303
|
+
(x) => x.name === parts[i].split(":")[0].trim()
|
|
304
|
+
);
|
|
305
|
+
if (meta?.output) type = meta.output;
|
|
306
|
+
}
|
|
307
|
+
return type;
|
|
308
|
+
}
|
|
309
|
+
function filterApplies(f, type) {
|
|
310
|
+
if (f.inputs === "any" || type === "any") return true;
|
|
311
|
+
const t = type === "enum" ? "string" : type;
|
|
312
|
+
return f.inputs.includes(t);
|
|
313
|
+
}
|
|
314
|
+
function findField(namespaces, path) {
|
|
315
|
+
const dot = path.indexOf(".");
|
|
316
|
+
if (dot < 0) return void 0;
|
|
317
|
+
const ns = namespaces.find((n) => n.key === path.slice(0, dot));
|
|
318
|
+
return ns?.fields.find((f) => f.key === path.slice(dot + 1));
|
|
319
|
+
}
|
|
320
|
+
function fieldMatches(namespaces, frag) {
|
|
321
|
+
const out = [];
|
|
322
|
+
const q = frag.toLowerCase();
|
|
323
|
+
for (const ns of namespaces) {
|
|
324
|
+
for (const f of ns.fields) {
|
|
325
|
+
const path = `${ns.key}.${f.key}`;
|
|
326
|
+
if (q && !path.toLowerCase().includes(q) && !f.label.toLowerCase().includes(q))
|
|
327
|
+
continue;
|
|
328
|
+
out.push({ path, f });
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
return out;
|
|
332
|
+
}
|
|
333
|
+
function operatorOptions(lead, path, f) {
|
|
334
|
+
const g = { group: "Operator", kind: "block", hint: `${f.type}` };
|
|
335
|
+
const valOp = (op) => ({
|
|
336
|
+
...g,
|
|
337
|
+
insert: f.values?.length ? `${lead}${path} ${op} ` : `${lead}${path} ${op} ""`,
|
|
338
|
+
label: f.values?.length ? `${op} \u2026` : `${op} "\u2026"`,
|
|
339
|
+
close: false
|
|
340
|
+
});
|
|
341
|
+
const notValOp = (op) => ({
|
|
342
|
+
...g,
|
|
343
|
+
insert: `${lead}not ${path} ${op} ""`,
|
|
344
|
+
label: `not ${op} "\u2026"`,
|
|
345
|
+
close: false
|
|
346
|
+
});
|
|
347
|
+
const listOp = (op) => ({
|
|
348
|
+
...g,
|
|
349
|
+
insert: `${lead}${path} ${op} [""]`,
|
|
350
|
+
label: `${op} [\u2026]`,
|
|
351
|
+
close: false
|
|
352
|
+
});
|
|
353
|
+
const arrValOp = (op) => ({
|
|
354
|
+
...g,
|
|
355
|
+
insert: `${lead}${path} ${op} [`,
|
|
356
|
+
label: `${op} [\u2026]`,
|
|
357
|
+
close: false
|
|
358
|
+
});
|
|
359
|
+
const numOp = (op) => ({
|
|
360
|
+
...g,
|
|
361
|
+
insert: `${lead}${path} ${op} `,
|
|
362
|
+
label: `${op} \u2026`,
|
|
363
|
+
close: false
|
|
364
|
+
});
|
|
365
|
+
const bare = (op) => ({
|
|
366
|
+
...g,
|
|
367
|
+
insert: `${lead}${path} ${op} `,
|
|
368
|
+
label: op,
|
|
369
|
+
close: false
|
|
370
|
+
});
|
|
371
|
+
const notBare = (op) => ({
|
|
372
|
+
...g,
|
|
373
|
+
insert: `${lead}not ${path} ${op} `,
|
|
374
|
+
label: `not ${op}`,
|
|
375
|
+
close: false
|
|
376
|
+
});
|
|
377
|
+
const dateFilter = (name, tail = "") => ({
|
|
378
|
+
...g,
|
|
379
|
+
insert: `${lead}${path} | ${name}${tail}`,
|
|
380
|
+
label: `| ${name}${tail}`,
|
|
381
|
+
close: false
|
|
382
|
+
});
|
|
383
|
+
switch (f.type) {
|
|
384
|
+
case "boolean":
|
|
385
|
+
return [
|
|
386
|
+
{ ...g, insert: `${lead}${path}`, label: "is true", close: true },
|
|
387
|
+
{ ...g, insert: `${lead}not ${path}`, label: "is false", close: true }
|
|
388
|
+
];
|
|
389
|
+
case "number":
|
|
390
|
+
return [
|
|
391
|
+
numOp("=="),
|
|
392
|
+
numOp("!="),
|
|
393
|
+
numOp(">"),
|
|
394
|
+
numOp("<"),
|
|
395
|
+
numOp(">="),
|
|
396
|
+
numOp("<="),
|
|
397
|
+
listOp("in"),
|
|
398
|
+
bare("exists"),
|
|
399
|
+
notBare("exists")
|
|
400
|
+
];
|
|
401
|
+
case "datetime":
|
|
402
|
+
return [
|
|
403
|
+
dateFilter("is_past"),
|
|
404
|
+
dateFilter("is_future"),
|
|
405
|
+
dateFilter("days_ago", " > 30"),
|
|
406
|
+
dateFilter("days_until", " < 30"),
|
|
407
|
+
bare("exists"),
|
|
408
|
+
notBare("exists")
|
|
409
|
+
];
|
|
410
|
+
case "array":
|
|
411
|
+
return [
|
|
412
|
+
valOp("contains"),
|
|
413
|
+
notValOp("contains"),
|
|
414
|
+
arrValOp("contains_any"),
|
|
415
|
+
arrValOp("contains_all"),
|
|
416
|
+
bare("is_empty"),
|
|
417
|
+
notBare("is_empty"),
|
|
418
|
+
bare("exists")
|
|
419
|
+
];
|
|
420
|
+
case "enum":
|
|
421
|
+
return [
|
|
422
|
+
valOp("=="),
|
|
423
|
+
valOp("!="),
|
|
424
|
+
arrValOp("in"),
|
|
425
|
+
bare("exists"),
|
|
426
|
+
notBare("exists")
|
|
427
|
+
];
|
|
428
|
+
default:
|
|
429
|
+
return [
|
|
430
|
+
valOp("=="),
|
|
431
|
+
valOp("!="),
|
|
432
|
+
valOp("contains"),
|
|
433
|
+
notValOp("contains"),
|
|
434
|
+
valOp("startsWith"),
|
|
435
|
+
valOp("endsWith"),
|
|
436
|
+
valOp("matches"),
|
|
437
|
+
listOp("in"),
|
|
438
|
+
bare("is_empty"),
|
|
439
|
+
bare("exists"),
|
|
440
|
+
notBare("exists")
|
|
441
|
+
];
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
var BLOCK_SNIPPETS = [
|
|
445
|
+
{ insert: "if ", label: "if \u2026", hint: "condition block", close: false },
|
|
446
|
+
{
|
|
447
|
+
insert: "elseif ",
|
|
448
|
+
label: "elseif \u2026",
|
|
449
|
+
hint: "another branch",
|
|
450
|
+
close: false
|
|
451
|
+
},
|
|
452
|
+
{ insert: "else", label: "else", hint: "fallback branch", close: true },
|
|
453
|
+
{ insert: "/if", label: "/if", hint: "close the if block", close: true },
|
|
454
|
+
{
|
|
455
|
+
insert: "for item in ",
|
|
456
|
+
label: "for \u2026 in \u2026",
|
|
457
|
+
hint: "loop over a list",
|
|
458
|
+
close: false
|
|
459
|
+
},
|
|
460
|
+
{ insert: "/for", label: "/for", hint: "close the for loop", close: true },
|
|
461
|
+
{
|
|
462
|
+
insert: "#priority ",
|
|
463
|
+
label: "#priority \u2026",
|
|
464
|
+
hint: "set priority level",
|
|
465
|
+
close: false
|
|
466
|
+
},
|
|
467
|
+
{
|
|
468
|
+
insert: "/priority",
|
|
469
|
+
label: "/priority",
|
|
470
|
+
hint: "close the priority block",
|
|
471
|
+
close: true
|
|
472
|
+
},
|
|
473
|
+
{
|
|
474
|
+
insert: 'include "signature"',
|
|
475
|
+
label: "include \u2026",
|
|
476
|
+
hint: "reusable snippet",
|
|
477
|
+
close: true
|
|
478
|
+
}
|
|
479
|
+
];
|
|
480
|
+
function buildModelOptions(namespaces, query) {
|
|
481
|
+
const pipe = query.lastIndexOf("|");
|
|
482
|
+
if (pipe >= 0) {
|
|
483
|
+
const left = query.slice(0, pipe).trim();
|
|
484
|
+
const seg = query.slice(pipe + 1);
|
|
485
|
+
const dateTz = seg.match(/^\s*date\s*:\s*"([^"]*)"\s*,\s*"?([^"]*)$/i);
|
|
486
|
+
if (dateTz) {
|
|
487
|
+
const [, fmt, tzFrag] = dateTz;
|
|
488
|
+
return timezoneOptions(tzFrag).map((z) => ({
|
|
489
|
+
insert: `${left} | date: "${fmt}", "${z}"`,
|
|
490
|
+
label: z,
|
|
491
|
+
hint: "timezone",
|
|
492
|
+
group: "Timezone",
|
|
493
|
+
kind: "filter",
|
|
494
|
+
close: true
|
|
495
|
+
}));
|
|
496
|
+
}
|
|
497
|
+
const dateArg = seg.match(/^\s*date\s*:\s*"?([^"]*)$/i);
|
|
498
|
+
if (dateArg) {
|
|
499
|
+
const dq = dateArg[1].toLowerCase();
|
|
500
|
+
return DATE_FORMATS.filter(
|
|
501
|
+
(d) => !dq || d.toLowerCase().includes(dq)
|
|
502
|
+
).map((d) => {
|
|
503
|
+
const eg = previewDateFormat(d);
|
|
504
|
+
return {
|
|
505
|
+
insert: `${left} | date: "${d}"`,
|
|
506
|
+
label: `"${d}"`,
|
|
507
|
+
// Show what the token renders as right now, e.g. "Jul 6, 2026".
|
|
508
|
+
hint: eg ? `e.g. ${eg}` : "date/time format",
|
|
509
|
+
group: "Date format",
|
|
510
|
+
kind: "filter",
|
|
511
|
+
close: true
|
|
512
|
+
};
|
|
513
|
+
});
|
|
514
|
+
}
|
|
515
|
+
const defArg = seg.match(/^\s*default\s*:\s*(.*)$/i);
|
|
516
|
+
if (defArg) {
|
|
517
|
+
const field = findField(namespaces, left.split("|")[0].trim());
|
|
518
|
+
const dtype = resolveType(namespaces, left);
|
|
519
|
+
const frag = defArg[1].trim().replace(/^"|"$/g, "").toLowerCase();
|
|
520
|
+
const mk = (val, label) => ({
|
|
521
|
+
insert: `${left} | default: ${val}`,
|
|
522
|
+
label,
|
|
523
|
+
hint: "default value",
|
|
524
|
+
group: "Default",
|
|
525
|
+
kind: "filter",
|
|
526
|
+
close: true
|
|
527
|
+
});
|
|
528
|
+
if (field?.values?.length)
|
|
529
|
+
return field.values.filter((v) => !frag || v.toLowerCase().includes(frag)).map((v) => mk(`"${v}"`, `"${v}"`));
|
|
530
|
+
if (dtype === "boolean")
|
|
531
|
+
return [mk("true", "true"), mk("false", "false")].filter(
|
|
532
|
+
(o) => !frag || o.label.toLowerCase().includes(frag)
|
|
533
|
+
);
|
|
534
|
+
if (dtype === "number") return [mk("0", "0")];
|
|
535
|
+
return [mk('""', '"\u2026"')];
|
|
536
|
+
}
|
|
537
|
+
const fq = seg.trim().toLowerCase();
|
|
538
|
+
const type = resolveType(namespaces, left);
|
|
539
|
+
const inCondition = /^(if|elseif)\s/i.test(query);
|
|
540
|
+
return ML_FILTERS.filter(
|
|
541
|
+
(f) => (!fq || f.name.includes(fq)) && filterApplies(f, type) && (!inCondition || f.output === "boolean" || f.output === "number")
|
|
542
|
+
).map((f) => ({
|
|
543
|
+
insert: f.name === "date" ? `${left} | date: ` : f.name === "default" ? `${left} | default: ` : `${left} | ${f.name}${f.argTemplate ?? ""}`,
|
|
544
|
+
label: `| ${f.name}`,
|
|
545
|
+
hint: f.hint,
|
|
546
|
+
group: "Filters",
|
|
547
|
+
kind: "filter",
|
|
548
|
+
close: f.name !== "date" && f.name !== "default"
|
|
549
|
+
}));
|
|
550
|
+
}
|
|
551
|
+
const prm = query.match(/^#priority\s+(\w*)$/i);
|
|
552
|
+
if (prm) {
|
|
553
|
+
const pq = prm[1].toLowerCase();
|
|
554
|
+
return ["high", "medium", "low"].filter((l) => !pq || l.startsWith(pq)).map((l) => ({
|
|
555
|
+
insert: `#priority ${l}`,
|
|
556
|
+
label: l,
|
|
557
|
+
hint: "priority level",
|
|
558
|
+
group: "Priority",
|
|
559
|
+
kind: "block",
|
|
560
|
+
close: true
|
|
561
|
+
}));
|
|
562
|
+
}
|
|
563
|
+
const cm = query.match(/^(if|elseif)\s+(.*)$/i);
|
|
564
|
+
if (cm) {
|
|
565
|
+
const kw = cm[1].toLowerCase();
|
|
566
|
+
const rest = cm[2];
|
|
567
|
+
const conn = rest.match(/^(.*\b(?:and|or)\s+)(.*)$/i);
|
|
568
|
+
const prefix = conn ? conn[1] : "";
|
|
569
|
+
let clause = conn ? conn[2] : rest;
|
|
570
|
+
let lead = `${kw} ${prefix}`;
|
|
571
|
+
const negate = clause.match(/^(not\s+)(.*)$/i);
|
|
572
|
+
if (negate) {
|
|
573
|
+
lead += negate[1];
|
|
574
|
+
clause = negate[2];
|
|
575
|
+
}
|
|
576
|
+
if (/(?:"[^"]*"|\]|\b(?:exists|is_empty|is_past|is_future)\b|>\s*\d|\d)\s+$/i.test(
|
|
577
|
+
clause
|
|
578
|
+
)) {
|
|
579
|
+
const cur = `${kw} ${rest.trim()}`;
|
|
580
|
+
return [
|
|
581
|
+
{
|
|
582
|
+
insert: `${cur} and `,
|
|
583
|
+
label: "and",
|
|
584
|
+
hint: "both must match",
|
|
585
|
+
group: "Connector",
|
|
586
|
+
kind: "block",
|
|
587
|
+
close: false
|
|
588
|
+
},
|
|
589
|
+
{
|
|
590
|
+
insert: `${cur} or `,
|
|
591
|
+
label: "or",
|
|
592
|
+
hint: "either matches",
|
|
593
|
+
group: "Connector",
|
|
594
|
+
kind: "block",
|
|
595
|
+
close: false
|
|
596
|
+
},
|
|
597
|
+
{
|
|
598
|
+
insert: cur,
|
|
599
|
+
label: "\xB7 finish \xB7",
|
|
600
|
+
hint: "close the condition",
|
|
601
|
+
group: "Connector",
|
|
602
|
+
kind: "block",
|
|
603
|
+
close: true
|
|
604
|
+
}
|
|
605
|
+
];
|
|
606
|
+
}
|
|
607
|
+
const am = clause.match(
|
|
608
|
+
/^([\w.]+)\s+(in|contains_any|contains_all)\s+\[([^\]]*)$/
|
|
609
|
+
);
|
|
610
|
+
if (am) {
|
|
611
|
+
const f = findField(namespaces, am[1]);
|
|
612
|
+
if (!f?.values?.length) return [];
|
|
613
|
+
const body = am[3];
|
|
614
|
+
const picked = [...body.matchAll(/"([^"]*)"/g)].map((x) => x[1]);
|
|
615
|
+
const fragMatch = body.match(/(?:^|,)\s*"?([^",\]]*)$/);
|
|
616
|
+
const frag = (fragMatch ? fragMatch[1] : "").toLowerCase();
|
|
617
|
+
const prefix2 = picked.map((v) => `"${v}"`).join(", ");
|
|
618
|
+
const open = `${lead}${am[1]} ${am[2]} [${prefix2}${prefix2 ? ", " : ""}`;
|
|
619
|
+
const opts2 = f.values.filter(
|
|
620
|
+
(v) => !picked.includes(v) && (!frag || v.toLowerCase().includes(frag))
|
|
621
|
+
).map((v) => ({
|
|
622
|
+
insert: `${open}"${v}", `,
|
|
623
|
+
label: `"${v}"`,
|
|
624
|
+
hint: f.label,
|
|
625
|
+
group: "Value",
|
|
626
|
+
kind: "block",
|
|
627
|
+
close: false
|
|
628
|
+
}));
|
|
629
|
+
if (picked.length)
|
|
630
|
+
opts2.push({
|
|
631
|
+
insert: `${lead}${am[1]} ${am[2]} [${prefix2}] `,
|
|
632
|
+
label: "\xB7 done \xB7",
|
|
633
|
+
hint: "close the list",
|
|
634
|
+
group: "Value",
|
|
635
|
+
kind: "block",
|
|
636
|
+
close: false
|
|
637
|
+
});
|
|
638
|
+
return opts2;
|
|
639
|
+
}
|
|
640
|
+
const vm = clause.match(/^([\w.]+)\s+(==|!=|contains)\s+"?([^"]*)$/);
|
|
641
|
+
if (vm) {
|
|
642
|
+
const f = findField(namespaces, vm[1]);
|
|
643
|
+
if (f?.values?.length) {
|
|
644
|
+
const vq = vm[3].toLowerCase();
|
|
645
|
+
return f.values.filter((v) => !vq || v.toLowerCase().includes(vq)).map((v) => ({
|
|
646
|
+
insert: `${lead}${vm[1]} ${vm[2]} "${v}" `,
|
|
647
|
+
label: `"${v}"`,
|
|
648
|
+
hint: f.label,
|
|
649
|
+
group: "Value",
|
|
650
|
+
kind: "block",
|
|
651
|
+
close: false
|
|
652
|
+
}));
|
|
653
|
+
}
|
|
654
|
+
return [];
|
|
655
|
+
}
|
|
656
|
+
const om = clause.match(/^([\w.]+)\s+([^\s]*)$/);
|
|
657
|
+
if (om) {
|
|
658
|
+
const f = findField(namespaces, om[1]);
|
|
659
|
+
if (!f) return [];
|
|
660
|
+
const of = om[2].toLowerCase();
|
|
661
|
+
return operatorOptions(lead, om[1], f).filter(
|
|
662
|
+
(o) => !of || o.label.toLowerCase().includes(of)
|
|
663
|
+
);
|
|
664
|
+
}
|
|
665
|
+
const pm = clause.match(/^([\w.]*)$/);
|
|
666
|
+
if (pm) {
|
|
667
|
+
return fieldMatches(namespaces, pm[1]).map(({ path, f }) => ({
|
|
668
|
+
insert: `${lead}${path} `,
|
|
669
|
+
label: path,
|
|
670
|
+
hint: `${f.label} \xB7 ${f.type}`,
|
|
671
|
+
group: "Field",
|
|
672
|
+
kind: "path",
|
|
673
|
+
close: false
|
|
674
|
+
}));
|
|
675
|
+
}
|
|
676
|
+
return [];
|
|
677
|
+
}
|
|
678
|
+
const fm = query.match(/^for\s+\w+\s+in\s+([\w.]*)$/i);
|
|
679
|
+
if (fm) {
|
|
680
|
+
return fieldMatches(namespaces, fm[1]).map(({ path, f }) => ({
|
|
681
|
+
insert: `for item in ${path}`,
|
|
682
|
+
label: path,
|
|
683
|
+
hint: `${f.label} \xB7 ${f.type}`,
|
|
684
|
+
group: "Loop over",
|
|
685
|
+
kind: "path",
|
|
686
|
+
close: true
|
|
687
|
+
}));
|
|
688
|
+
}
|
|
689
|
+
const bv = query.match(/^([\w.]+)\s+$/);
|
|
690
|
+
if (bv) {
|
|
691
|
+
const f = findField(namespaces, bv[1]);
|
|
692
|
+
if (f) {
|
|
693
|
+
const out = [
|
|
694
|
+
{
|
|
695
|
+
insert: bv[1],
|
|
696
|
+
label: "\xB7 insert \xB7",
|
|
697
|
+
hint: "use as-is",
|
|
698
|
+
group: "Finish",
|
|
699
|
+
kind: "block",
|
|
700
|
+
close: true
|
|
701
|
+
}
|
|
702
|
+
];
|
|
703
|
+
for (const flt of ML_FILTERS.filter((x) => filterApplies(x, f.type))) {
|
|
704
|
+
out.push({
|
|
705
|
+
insert: flt.name === "date" ? `${bv[1]} | date: ` : flt.name === "default" ? `${bv[1]} | default: ` : `${bv[1]} | ${flt.name}${flt.argTemplate ?? ""}`,
|
|
706
|
+
label: `| ${flt.name}`,
|
|
707
|
+
hint: flt.hint,
|
|
708
|
+
group: "Filters",
|
|
709
|
+
kind: "filter",
|
|
710
|
+
close: flt.name !== "date" && flt.name !== "default"
|
|
711
|
+
});
|
|
712
|
+
}
|
|
713
|
+
return out;
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
const q = query.trim().toLowerCase();
|
|
717
|
+
const opts = [];
|
|
718
|
+
for (const b of BLOCK_SNIPPETS) {
|
|
719
|
+
if (q && !b.label.toLowerCase().includes(q) && !b.insert.toLowerCase().includes(q))
|
|
720
|
+
continue;
|
|
721
|
+
opts.push({ ...b, group: "Blocks", kind: "block" });
|
|
722
|
+
}
|
|
723
|
+
for (const { path, f } of fieldMatches(namespaces, query.trim())) {
|
|
724
|
+
opts.push({
|
|
725
|
+
insert: `${path} `,
|
|
726
|
+
label: path,
|
|
727
|
+
hint: f.values?.length ? `${f.label}: ${f.values.join(", ")}` : f.label,
|
|
728
|
+
group: "Variables",
|
|
729
|
+
kind: "path",
|
|
730
|
+
close: false
|
|
731
|
+
});
|
|
732
|
+
}
|
|
733
|
+
return opts;
|
|
734
|
+
}
|
|
735
|
+
function optionGroups(options) {
|
|
736
|
+
const seen = [];
|
|
737
|
+
for (const o of options) if (!seen.includes(o.group)) seen.push(o.group);
|
|
738
|
+
return seen;
|
|
739
|
+
}
|
|
740
|
+
function capOptions(options, perGroup = 8) {
|
|
741
|
+
const counts = /* @__PURE__ */ new Map();
|
|
742
|
+
return options.filter((o) => {
|
|
743
|
+
const n = counts.get(o.group) ?? 0;
|
|
744
|
+
if (n >= perGroup) return false;
|
|
745
|
+
counts.set(o.group, n + 1);
|
|
746
|
+
return true;
|
|
747
|
+
});
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
// src/core/types.ts
|
|
751
|
+
var DEFAULT_LABELS = {
|
|
752
|
+
expectedOperator: "Expected an operator (e.g. ==, contains)",
|
|
753
|
+
unclosedBlock: (name) => `Unclosed, needs {{/${name}}}`,
|
|
754
|
+
noOpenBlock: (name) => `No open {{${name}}}`,
|
|
755
|
+
addDefault: "Add default",
|
|
756
|
+
changeTo: (operator) => `Change to ${operator}`,
|
|
757
|
+
addCloseTag: (tag) => `Add ${tag}`,
|
|
758
|
+
quickFix: "Quick fix",
|
|
759
|
+
noMatches: "No variables match.",
|
|
760
|
+
severity: { error: "error", warning: "warning", info: "info" }
|
|
761
|
+
};
|
|
762
|
+
function resolveLabels(overrides) {
|
|
763
|
+
return overrides ? { ...DEFAULT_LABELS, ...overrides } : DEFAULT_LABELS;
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
// src/display.ts
|
|
767
|
+
function modelTokenDisplay(inner, namespaces) {
|
|
768
|
+
const p = parseModelToken(inner);
|
|
769
|
+
if (p.directive) return inner.trim();
|
|
770
|
+
const ns = namespaces.find((n) => n.key === p.namespace);
|
|
771
|
+
const field = ns?.fields.find((f) => f.key === p.field);
|
|
772
|
+
const base = field?.label ?? p.field ?? p.path;
|
|
773
|
+
const label = ns ? base : p.path;
|
|
774
|
+
return p.filters.length ? `${label} \xB7 ${p.filters.map((f) => f.name).join(" \xB7 ")}` : label;
|
|
775
|
+
}
|
|
776
|
+
function modelTokenTone(inner, namespaces) {
|
|
777
|
+
const p = parseModelToken(inner);
|
|
778
|
+
if (p.directive) return "directive";
|
|
779
|
+
if (p.namespace === "flow" || p.namespace === "contact") return "value";
|
|
780
|
+
const ns = namespaces.find((n) => n.key === p.namespace);
|
|
781
|
+
if (!ns) return "unknown";
|
|
782
|
+
return ns.dynamic || ns.fields.some((f) => f.key === p.field) ? "value" : "unknown";
|
|
783
|
+
}
|
|
784
|
+
var key = new state.PluginKey("modelSyntax");
|
|
785
|
+
function cn(...inputs) {
|
|
786
|
+
return tailwindMerge.twMerge(clsx.clsx(inputs));
|
|
787
|
+
}
|
|
788
|
+
var ModelTokenList = react.forwardRef(function ModelTokenList2({ items, command, emptyLabel }, ref) {
|
|
789
|
+
const [selected, setSelected] = react.useState(0);
|
|
790
|
+
react.useEffect(() => setSelected(0), [items]);
|
|
791
|
+
react.useImperativeHandle(
|
|
792
|
+
ref,
|
|
793
|
+
() => ({
|
|
794
|
+
onKeyDown: (event) => {
|
|
795
|
+
if (items.length === 0) return false;
|
|
796
|
+
if (event.key === "ArrowDown") {
|
|
797
|
+
setSelected((s) => (s + 1) % items.length);
|
|
798
|
+
return true;
|
|
799
|
+
}
|
|
800
|
+
if (event.key === "ArrowUp") {
|
|
801
|
+
setSelected((s) => (s - 1 + items.length) % items.length);
|
|
802
|
+
return true;
|
|
803
|
+
}
|
|
804
|
+
if (event.key === "Enter" || event.key === "Tab") {
|
|
805
|
+
if (items[selected]) {
|
|
806
|
+
command(items[selected]);
|
|
807
|
+
return true;
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
return false;
|
|
811
|
+
}
|
|
812
|
+
}),
|
|
813
|
+
[items, selected, command]
|
|
814
|
+
);
|
|
815
|
+
if (items.length === 0) {
|
|
816
|
+
return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "bg-popover text-muted-foreground w-72 rounded-md border p-3 text-center text-xs shadow-md", children: emptyLabel ?? "No variables match." });
|
|
817
|
+
}
|
|
818
|
+
const grouped = optionGroups(items).map((g) => ({
|
|
819
|
+
group: g,
|
|
820
|
+
items: items.filter((o) => o.group === g)
|
|
821
|
+
}));
|
|
822
|
+
return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "bg-popover text-popover-foreground max-h-72 w-80 overflow-y-auto rounded-md border p-1 shadow-md", children: grouped.map((section) => /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "mb-1 last:mb-0", children: [
|
|
823
|
+
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-muted-foreground px-2 pt-1.5 pb-0.5 text-[10px] font-semibold tracking-wide uppercase", children: section.group }),
|
|
824
|
+
section.items.map((o) => {
|
|
825
|
+
const idx = items.indexOf(o);
|
|
826
|
+
const active = idx === selected;
|
|
827
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
828
|
+
"button",
|
|
829
|
+
{
|
|
830
|
+
type: "button",
|
|
831
|
+
onMouseDown: (e) => {
|
|
832
|
+
e.preventDefault();
|
|
833
|
+
command(o);
|
|
834
|
+
},
|
|
835
|
+
onMouseEnter: () => setSelected(idx),
|
|
836
|
+
className: cn(
|
|
837
|
+
"flex w-full items-start gap-2 rounded-sm px-2 py-1.5 text-left",
|
|
838
|
+
active && "bg-accent"
|
|
839
|
+
),
|
|
840
|
+
children: [
|
|
841
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
842
|
+
"span",
|
|
843
|
+
{
|
|
844
|
+
className: cn(
|
|
845
|
+
"mt-1 size-1.5 shrink-0 rounded-full",
|
|
846
|
+
o.kind === "block" ? "bg-violet-500" : o.kind === "filter" ? "bg-sky-500" : "bg-muted-foreground/50"
|
|
847
|
+
)
|
|
848
|
+
}
|
|
849
|
+
),
|
|
850
|
+
/* @__PURE__ */ jsxRuntime.jsxs("span", { className: "min-w-0 flex-1", children: [
|
|
851
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "block truncate font-mono text-xs", children: o.label }),
|
|
852
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-muted-foreground block truncate text-[11px]", children: o.hint })
|
|
853
|
+
] })
|
|
854
|
+
]
|
|
855
|
+
},
|
|
856
|
+
o.insert
|
|
857
|
+
);
|
|
858
|
+
})
|
|
859
|
+
] }, section.group)) });
|
|
860
|
+
});
|
|
861
|
+
|
|
862
|
+
// src/autocomplete/suggestion.ts
|
|
863
|
+
function createSuggestionPlugin(deps) {
|
|
864
|
+
const { editor, ctx, onOpen, labels } = deps;
|
|
865
|
+
return suggestion.Suggestion({
|
|
866
|
+
editor,
|
|
867
|
+
char: "{{",
|
|
868
|
+
pluginKey: new state.PluginKey("modelSyntaxSuggestion"),
|
|
869
|
+
allowSpaces: true,
|
|
870
|
+
startOfLine: false,
|
|
871
|
+
decorationClass: "model-lang-suggestion",
|
|
872
|
+
findSuggestionMatch: ({ $position }) => {
|
|
873
|
+
const before = $position.doc.textBetween(
|
|
874
|
+
Math.max(0, $position.pos - 200),
|
|
875
|
+
$position.pos,
|
|
876
|
+
"\n",
|
|
877
|
+
"\0"
|
|
878
|
+
);
|
|
879
|
+
const mm = before.match(/\{\{([^}\n]*)$/);
|
|
880
|
+
if (!mm) return null;
|
|
881
|
+
if (((mm[1].match(/"/g) || []).length & 1) === 1) return null;
|
|
882
|
+
const start = $position.pos - (before.length - (mm.index ?? 0));
|
|
883
|
+
const docSize = $position.doc.content.size;
|
|
884
|
+
const after = $position.doc.textBetween(
|
|
885
|
+
$position.pos,
|
|
886
|
+
Math.min(docSize, $position.pos + 200),
|
|
887
|
+
"\n",
|
|
888
|
+
"\0"
|
|
889
|
+
);
|
|
890
|
+
const closedToken = after.match(/^[^{}\n]*\}\}/);
|
|
891
|
+
if (closedToken && !ctx.lastTrDocChanged && (ctx.lastTrSelectionMoved || !ctx.suggestionActive))
|
|
892
|
+
return null;
|
|
893
|
+
const wordTail = after.match(/^[\w.]*/);
|
|
894
|
+
const to = $position.pos + (wordTail ? wordTail[0].length : 0);
|
|
895
|
+
return { range: { from: start, to }, query: mm[1] ?? "", text: mm[0] };
|
|
896
|
+
},
|
|
897
|
+
items: ({ query }) => {
|
|
898
|
+
const st = key.getState(editor.state);
|
|
899
|
+
return capOptions(buildModelOptions(st?.namespaces ?? [], query));
|
|
900
|
+
},
|
|
901
|
+
command: ({ editor: editor2, range, props }) => {
|
|
902
|
+
const docSize = editor2.state.doc.content.size;
|
|
903
|
+
const tail = editor2.state.doc.textBetween(
|
|
904
|
+
range.to,
|
|
905
|
+
Math.min(docSize, range.to + 200),
|
|
906
|
+
"\n",
|
|
907
|
+
"\0"
|
|
908
|
+
);
|
|
909
|
+
const alreadyClosed = /^[^{}\n]*\}\}/.test(tail);
|
|
910
|
+
const needBraces = props.close && !alreadyClosed;
|
|
911
|
+
let content = needBraces ? `{{${props.insert}}}` : `{{${props.insert}`;
|
|
912
|
+
if (/^\s*("|\[|-?\d|true\b|false\b)/.test(tail))
|
|
913
|
+
content = content.replace(/\s*(?:""|\[""\])$/, "");
|
|
914
|
+
if (content.endsWith(" ") && tail.startsWith(" "))
|
|
915
|
+
content = content.slice(0, -1);
|
|
916
|
+
editor2.chain().focus().deleteRange(range).insertContent({ type: "text", text: content }).run();
|
|
917
|
+
const q = content.indexOf('""');
|
|
918
|
+
if (needBraces) {
|
|
919
|
+
if (q >= 0) editor2.commands.setTextSelection(range.from + q + 1);
|
|
920
|
+
} else {
|
|
921
|
+
editor2.commands.setTextSelection(
|
|
922
|
+
q >= 0 ? range.from + q + 1 : range.from + content.length
|
|
923
|
+
);
|
|
924
|
+
}
|
|
925
|
+
},
|
|
926
|
+
render: () => {
|
|
927
|
+
let component;
|
|
928
|
+
let popup;
|
|
929
|
+
const onScroll = (e) => {
|
|
930
|
+
const el = popup?.[0]?.popper;
|
|
931
|
+
if (el && e.target instanceof Node && el.contains(e.target)) return;
|
|
932
|
+
popup?.[0]?.hide();
|
|
933
|
+
};
|
|
934
|
+
return {
|
|
935
|
+
onStart: (props) => {
|
|
936
|
+
ctx.suggestionActive = true;
|
|
937
|
+
onOpen();
|
|
938
|
+
window.addEventListener("scroll", onScroll, true);
|
|
939
|
+
component = new react$1.ReactRenderer(ModelTokenList, {
|
|
940
|
+
props: {
|
|
941
|
+
items: props.items,
|
|
942
|
+
command: props.command,
|
|
943
|
+
emptyLabel: labels.noMatches
|
|
944
|
+
},
|
|
945
|
+
editor: props.editor
|
|
946
|
+
});
|
|
947
|
+
if (!props.clientRect) return;
|
|
948
|
+
popup = tippy__default.default("body", {
|
|
949
|
+
getReferenceClientRect: props.clientRect,
|
|
950
|
+
appendTo: () => document.body,
|
|
951
|
+
content: component.element,
|
|
952
|
+
showOnCreate: props.items.length > 0,
|
|
953
|
+
interactive: true,
|
|
954
|
+
trigger: "manual",
|
|
955
|
+
placement: "bottom-start",
|
|
956
|
+
popperOptions: {
|
|
957
|
+
modifiers: [
|
|
958
|
+
{
|
|
959
|
+
name: "flip",
|
|
960
|
+
options: {
|
|
961
|
+
boundary: "viewport",
|
|
962
|
+
fallbackPlacements: ["top-start", "bottom-start"],
|
|
963
|
+
padding: 8
|
|
964
|
+
}
|
|
965
|
+
},
|
|
966
|
+
{
|
|
967
|
+
name: "preventOverflow",
|
|
968
|
+
options: { boundary: "viewport", padding: 8 }
|
|
969
|
+
}
|
|
970
|
+
]
|
|
971
|
+
}
|
|
972
|
+
});
|
|
973
|
+
},
|
|
974
|
+
onUpdate: (props) => {
|
|
975
|
+
component.updateProps({
|
|
976
|
+
items: props.items,
|
|
977
|
+
command: props.command,
|
|
978
|
+
emptyLabel: labels.noMatches
|
|
979
|
+
});
|
|
980
|
+
if (props.items.length === 0) popup?.[0]?.hide();
|
|
981
|
+
else popup?.[0]?.show();
|
|
982
|
+
if (props.clientRect) {
|
|
983
|
+
popup?.[0]?.setProps({
|
|
984
|
+
getReferenceClientRect: props.clientRect
|
|
985
|
+
});
|
|
986
|
+
}
|
|
987
|
+
},
|
|
988
|
+
onKeyDown: (props) => {
|
|
989
|
+
if (props.event.key === "Escape") {
|
|
990
|
+
popup?.[0]?.hide();
|
|
991
|
+
return true;
|
|
992
|
+
}
|
|
993
|
+
return component.ref?.onKeyDown(props.event) ?? false;
|
|
994
|
+
},
|
|
995
|
+
onExit: () => {
|
|
996
|
+
ctx.suggestionActive = false;
|
|
997
|
+
window.removeEventListener("scroll", onScroll, true);
|
|
998
|
+
popup?.[0]?.destroy();
|
|
999
|
+
component?.destroy();
|
|
1000
|
+
}
|
|
1001
|
+
};
|
|
1002
|
+
}
|
|
1003
|
+
});
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
// src/core/constants.ts
|
|
1007
|
+
var BRACE_CLASS = "text-muted-foreground/60";
|
|
1008
|
+
var DIAG_CLASS = {
|
|
1009
|
+
error: "underline decoration-wavy decoration-red-500 underline-offset-2",
|
|
1010
|
+
warning: "underline decoration-wavy decoration-amber-500 underline-offset-2",
|
|
1011
|
+
info: "underline decoration-dotted decoration-sky-500 underline-offset-2"
|
|
1012
|
+
};
|
|
1013
|
+
var SEV_COLOR = {
|
|
1014
|
+
error: "#ef4444",
|
|
1015
|
+
warning: "#f59e0b",
|
|
1016
|
+
info: "#0ea5e9"
|
|
1017
|
+
};
|
|
1018
|
+
var SEV_RANK = {
|
|
1019
|
+
error: 3,
|
|
1020
|
+
warning: 2,
|
|
1021
|
+
info: 1
|
|
1022
|
+
};
|
|
1023
|
+
|
|
1024
|
+
// src/validation/diagnostics.ts
|
|
1025
|
+
function diagnosticsByPath(diags) {
|
|
1026
|
+
const m = /* @__PURE__ */ new Map();
|
|
1027
|
+
for (const d of diags) {
|
|
1028
|
+
if (!d.fieldPath) continue;
|
|
1029
|
+
const prev = m.get(d.fieldPath);
|
|
1030
|
+
if (!prev || SEV_RANK[d.severity] > SEV_RANK[prev.severity]) {
|
|
1031
|
+
m.set(d.fieldPath, {
|
|
1032
|
+
severity: d.severity,
|
|
1033
|
+
message: d.message,
|
|
1034
|
+
code: d.code
|
|
1035
|
+
});
|
|
1036
|
+
}
|
|
1037
|
+
}
|
|
1038
|
+
return m;
|
|
1039
|
+
}
|
|
1040
|
+
function addsDefault(d) {
|
|
1041
|
+
return d.code === "ML210" || /\bdefault\b/i.test(d.message);
|
|
1042
|
+
}
|
|
1043
|
+
function defaultFilterFor(field) {
|
|
1044
|
+
if (field?.values?.length) return ` | default: "${field.values[0]}"`;
|
|
1045
|
+
switch (field?.type) {
|
|
1046
|
+
case "number":
|
|
1047
|
+
return " | default: 0";
|
|
1048
|
+
case "boolean":
|
|
1049
|
+
return " | default: false";
|
|
1050
|
+
default:
|
|
1051
|
+
return ' | default: ""';
|
|
1052
|
+
}
|
|
1053
|
+
}
|
|
1054
|
+
var OPERATOR_WORDS = [
|
|
1055
|
+
"contains",
|
|
1056
|
+
"contains_any",
|
|
1057
|
+
"contains_all",
|
|
1058
|
+
"in",
|
|
1059
|
+
"is_empty",
|
|
1060
|
+
"exists",
|
|
1061
|
+
"startsWith",
|
|
1062
|
+
"endsWith",
|
|
1063
|
+
"matches"
|
|
1064
|
+
];
|
|
1065
|
+
function suggestOperator(word) {
|
|
1066
|
+
const w = word.toLowerCase();
|
|
1067
|
+
if (!w) return null;
|
|
1068
|
+
const hits = OPERATOR_WORDS.filter((o) => o.toLowerCase().startsWith(w));
|
|
1069
|
+
if (!hits.length) return null;
|
|
1070
|
+
return hits.sort((a, b) => a.length - b.length)[0];
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
// src/highlight/highlight.ts
|
|
1074
|
+
var HL_CLASS = {
|
|
1075
|
+
keyword: "text-violet-600 dark:text-violet-400 font-medium",
|
|
1076
|
+
// if / for / else
|
|
1077
|
+
operator: "text-pink-600 dark:text-pink-400",
|
|
1078
|
+
// == contains exists and or
|
|
1079
|
+
math: "text-orange-600 dark:text-orange-400",
|
|
1080
|
+
// + - * / ( )
|
|
1081
|
+
string: "text-emerald-600 dark:text-emerald-400",
|
|
1082
|
+
// "..."
|
|
1083
|
+
number: "text-orange-500 dark:text-orange-300",
|
|
1084
|
+
// 123 true false
|
|
1085
|
+
filter: "text-teal-600 dark:text-teal-400",
|
|
1086
|
+
// | default
|
|
1087
|
+
func: "text-cyan-600 dark:text-cyan-400",
|
|
1088
|
+
// calculate(
|
|
1089
|
+
path: "text-sky-600 dark:text-sky-400",
|
|
1090
|
+
// contact.first_name
|
|
1091
|
+
punct: "text-muted-foreground"
|
|
1092
|
+
// , : |
|
|
1093
|
+
};
|
|
1094
|
+
var KEYWORDS = /* @__PURE__ */ new Set(["if", "elseif", "else", "for", "end", "include"]);
|
|
1095
|
+
var WORD_OPS = /* @__PURE__ */ new Set([
|
|
1096
|
+
"and",
|
|
1097
|
+
"or",
|
|
1098
|
+
"not",
|
|
1099
|
+
"in",
|
|
1100
|
+
"contains",
|
|
1101
|
+
"contains_any",
|
|
1102
|
+
"contains_all",
|
|
1103
|
+
"is_empty",
|
|
1104
|
+
"exists",
|
|
1105
|
+
"startswith",
|
|
1106
|
+
"endswith",
|
|
1107
|
+
"matches"
|
|
1108
|
+
]);
|
|
1109
|
+
var LITERALS = /* @__PURE__ */ new Set(["true", "false", "null"]);
|
|
1110
|
+
var SCAN = /(\s+)|("(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*')|(\d+(?:\.\d+)?)|([#/]?[A-Za-z_][\w.]*)|(==|!=|<=|>=|[<>=])|(\|\||\||[-+*/()])|([,:])|(.)/y;
|
|
1111
|
+
function tokenizeExpression(s) {
|
|
1112
|
+
const out = [];
|
|
1113
|
+
SCAN.lastIndex = 0;
|
|
1114
|
+
let afterPipe = false;
|
|
1115
|
+
let m;
|
|
1116
|
+
while (m = SCAN.exec(s)) {
|
|
1117
|
+
const start = m.index;
|
|
1118
|
+
const end = start + m[0].length;
|
|
1119
|
+
if (end === start) {
|
|
1120
|
+
SCAN.lastIndex++;
|
|
1121
|
+
continue;
|
|
1122
|
+
}
|
|
1123
|
+
if (m[1]) continue;
|
|
1124
|
+
let kind = "punct";
|
|
1125
|
+
if (m[2]) kind = "string";
|
|
1126
|
+
else if (m[3]) kind = "number";
|
|
1127
|
+
else if (m[4]) {
|
|
1128
|
+
const raw = m[4];
|
|
1129
|
+
const lower = raw.toLowerCase();
|
|
1130
|
+
if (raw[0] === "#" || raw[0] === "/") kind = "keyword";
|
|
1131
|
+
else if (KEYWORDS.has(lower)) kind = "keyword";
|
|
1132
|
+
else if (WORD_OPS.has(lower)) kind = "operator";
|
|
1133
|
+
else if (LITERALS.has(lower)) kind = "number";
|
|
1134
|
+
else if (afterPipe) kind = "filter";
|
|
1135
|
+
else kind = /^\s*\(/.test(s.slice(end)) ? "func" : "path";
|
|
1136
|
+
} else if (m[5]) kind = "operator";
|
|
1137
|
+
else if (m[6]) {
|
|
1138
|
+
if (m[0] === "|") {
|
|
1139
|
+
out.push({ start, end, kind: "punct" });
|
|
1140
|
+
afterPipe = true;
|
|
1141
|
+
continue;
|
|
1142
|
+
}
|
|
1143
|
+
kind = m[0] === "||" ? "operator" : "math";
|
|
1144
|
+
} else if (m[7]) kind = "punct";
|
|
1145
|
+
out.push({ start, end, kind });
|
|
1146
|
+
afterPipe = false;
|
|
1147
|
+
}
|
|
1148
|
+
return out;
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1151
|
+
// src/highlight/decorations.ts
|
|
1152
|
+
var TOKEN_RE = /\{\{[^{}]*\}\}/g;
|
|
1153
|
+
function buildDecorations(doc, st, opts) {
|
|
1154
|
+
const { labels } = opts;
|
|
1155
|
+
const show = (sev) => opts.severities.includes(sev);
|
|
1156
|
+
const showErr = show("error");
|
|
1157
|
+
const decos = [];
|
|
1158
|
+
let flat = "";
|
|
1159
|
+
const posAt = [];
|
|
1160
|
+
let prevEnd = -1;
|
|
1161
|
+
doc.descendants((node, pos) => {
|
|
1162
|
+
if (!node.isText || !node.text) return;
|
|
1163
|
+
if (prevEnd >= 0 && pos !== prevEnd) {
|
|
1164
|
+
flat += "\n";
|
|
1165
|
+
posAt.push(-1);
|
|
1166
|
+
}
|
|
1167
|
+
const text = node.text;
|
|
1168
|
+
for (let k = 0; k < text.length; k++) {
|
|
1169
|
+
flat += text[k];
|
|
1170
|
+
posAt.push(pos + k);
|
|
1171
|
+
}
|
|
1172
|
+
prevEnd = pos + text.length;
|
|
1173
|
+
});
|
|
1174
|
+
const painter = {
|
|
1175
|
+
paint(a, b, attrs) {
|
|
1176
|
+
let from = -1;
|
|
1177
|
+
let to = -1;
|
|
1178
|
+
for (let i = a; i < b; i++) {
|
|
1179
|
+
const p = posAt[i];
|
|
1180
|
+
if (p < 0) {
|
|
1181
|
+
if (from >= 0) decos.push(view.Decoration.inline(from, to, attrs));
|
|
1182
|
+
from = -1;
|
|
1183
|
+
continue;
|
|
1184
|
+
}
|
|
1185
|
+
if (from < 0) {
|
|
1186
|
+
from = p;
|
|
1187
|
+
to = p + 1;
|
|
1188
|
+
} else if (p === to) {
|
|
1189
|
+
to = p + 1;
|
|
1190
|
+
} else {
|
|
1191
|
+
decos.push(view.Decoration.inline(from, to, attrs));
|
|
1192
|
+
from = p;
|
|
1193
|
+
to = p + 1;
|
|
1194
|
+
}
|
|
1195
|
+
}
|
|
1196
|
+
if (from >= 0) decos.push(view.Decoration.inline(from, to, attrs));
|
|
1197
|
+
},
|
|
1198
|
+
pos(i) {
|
|
1199
|
+
for (let k = i; k < posAt.length; k++) if (posAt[k] >= 0) return posAt[k];
|
|
1200
|
+
return doc.content.size;
|
|
1201
|
+
}
|
|
1202
|
+
};
|
|
1203
|
+
const blocks = [];
|
|
1204
|
+
TOKEN_RE.lastIndex = 0;
|
|
1205
|
+
let m;
|
|
1206
|
+
while (m = TOKEN_RE.exec(flat)) {
|
|
1207
|
+
const start = m.index;
|
|
1208
|
+
const end = start + m[0].length;
|
|
1209
|
+
const rawInner = m[0].slice(2, -2);
|
|
1210
|
+
const inner = rawInner.trim();
|
|
1211
|
+
const parsed = parseModelToken(inner);
|
|
1212
|
+
const innerStart = start + 2;
|
|
1213
|
+
blocks.push({ start, end, inner });
|
|
1214
|
+
if (parsed.namespace === "flow" && parsed.field) {
|
|
1215
|
+
painter.paint(start, end, {
|
|
1216
|
+
"data-card-id": parsed.field.split(".")[0]
|
|
1217
|
+
});
|
|
1218
|
+
}
|
|
1219
|
+
painter.paint(start, start + 2, { class: BRACE_CLASS });
|
|
1220
|
+
painter.paint(end - 2, end, { class: BRACE_CLASS });
|
|
1221
|
+
for (const tok of tokenizeExpression(rawInner)) {
|
|
1222
|
+
painter.paint(innerStart + tok.start, innerStart + tok.end, {
|
|
1223
|
+
class: HL_CLASS[tok.kind]
|
|
1224
|
+
});
|
|
1225
|
+
}
|
|
1226
|
+
const hasDefaultFilter = parsed.filters.some((f) => f.name === "default");
|
|
1227
|
+
const fieldFor = (path) => {
|
|
1228
|
+
const dot = path.indexOf(".");
|
|
1229
|
+
if (dot < 0) return void 0;
|
|
1230
|
+
const ns = st.namespaces.find((n) => n.key === path.slice(0, dot));
|
|
1231
|
+
return ns?.fields.find((f) => f.key === path.slice(dot + 1));
|
|
1232
|
+
};
|
|
1233
|
+
for (const [fp, d] of st.byPath) {
|
|
1234
|
+
if (!fp || !show(d.severity)) continue;
|
|
1235
|
+
if ((parsed.directive || hasDefaultFilter) && addsDefault(d)) continue;
|
|
1236
|
+
let idx = rawInner.indexOf(fp);
|
|
1237
|
+
while (idx >= 0) {
|
|
1238
|
+
const prev = rawInner[idx - 1];
|
|
1239
|
+
const next = rawInner[idx + fp.length];
|
|
1240
|
+
const bounded = (prev === void 0 || !/[\w.]/.test(prev)) && (next === void 0 || !/[\w.]/.test(next));
|
|
1241
|
+
if (bounded) {
|
|
1242
|
+
const attrs = {
|
|
1243
|
+
class: DIAG_CLASS[d.severity],
|
|
1244
|
+
"data-ml-error": d.message,
|
|
1245
|
+
"data-ml-sev": d.severity,
|
|
1246
|
+
"data-ml-code": d.code
|
|
1247
|
+
};
|
|
1248
|
+
if (addsDefault(d)) {
|
|
1249
|
+
attrs["data-ml-fix-kind"] = "insert";
|
|
1250
|
+
attrs["data-ml-fix-pos"] = String(
|
|
1251
|
+
painter.pos(innerStart + idx + fp.length)
|
|
1252
|
+
);
|
|
1253
|
+
attrs["data-ml-fix-text"] = defaultFilterFor(fieldFor(fp));
|
|
1254
|
+
attrs["data-ml-fix-label"] = labels.addDefault;
|
|
1255
|
+
}
|
|
1256
|
+
painter.paint(innerStart + idx, innerStart + idx + fp.length, attrs);
|
|
1257
|
+
}
|
|
1258
|
+
idx = rawInner.indexOf(fp, idx + fp.length);
|
|
1259
|
+
}
|
|
1260
|
+
}
|
|
1261
|
+
if (showErr && /^(if|elseif)\b/i.test(inner)) {
|
|
1262
|
+
const toks = tokenizeExpression(rawInner);
|
|
1263
|
+
for (let i = 0; i + 2 < toks.length; i++) {
|
|
1264
|
+
if (toks[i].kind === "path" && toks[i + 1].kind === "path") {
|
|
1265
|
+
const bad = toks[i + 1];
|
|
1266
|
+
const word = rawInner.slice(bad.start, bad.end);
|
|
1267
|
+
const attrs = {
|
|
1268
|
+
class: DIAG_CLASS.error,
|
|
1269
|
+
"data-ml-error": labels.expectedOperator,
|
|
1270
|
+
"data-ml-sev": "error",
|
|
1271
|
+
"data-ml-code": "ML001"
|
|
1272
|
+
};
|
|
1273
|
+
const suggest = suggestOperator(word);
|
|
1274
|
+
if (suggest) {
|
|
1275
|
+
attrs["data-ml-fix-kind"] = "replace";
|
|
1276
|
+
attrs["data-ml-fix-pos"] = String(
|
|
1277
|
+
painter.pos(innerStart + bad.start)
|
|
1278
|
+
);
|
|
1279
|
+
attrs["data-ml-fix-end"] = String(
|
|
1280
|
+
painter.pos(innerStart + bad.end)
|
|
1281
|
+
);
|
|
1282
|
+
attrs["data-ml-fix-text"] = suggest;
|
|
1283
|
+
attrs["data-ml-fix-label"] = labels.changeTo(suggest);
|
|
1284
|
+
}
|
|
1285
|
+
painter.paint(innerStart + bad.start, innerStart + bad.end, attrs);
|
|
1286
|
+
}
|
|
1287
|
+
}
|
|
1288
|
+
}
|
|
1289
|
+
}
|
|
1290
|
+
if (showErr) addBlockBalance(blocks, painter, labels);
|
|
1291
|
+
return view.DecorationSet.create(doc, decos);
|
|
1292
|
+
}
|
|
1293
|
+
function addBlockBalance(blocks, painter, labels) {
|
|
1294
|
+
const errAttrs = (msg, fix) => {
|
|
1295
|
+
const a = {
|
|
1296
|
+
class: DIAG_CLASS.error,
|
|
1297
|
+
"data-ml-error": msg,
|
|
1298
|
+
"data-ml-sev": "error",
|
|
1299
|
+
"data-ml-code": "ML001"
|
|
1300
|
+
};
|
|
1301
|
+
return a;
|
|
1302
|
+
};
|
|
1303
|
+
const flagUnclosed = (s, boundary) => {
|
|
1304
|
+
const tag = `{{/${s.close}}}`;
|
|
1305
|
+
const a = {
|
|
1306
|
+
class: DIAG_CLASS.error,
|
|
1307
|
+
"data-ml-error": labels.unclosedBlock(s.close),
|
|
1308
|
+
"data-ml-sev": "error",
|
|
1309
|
+
"data-ml-code": "ML001",
|
|
1310
|
+
"data-ml-fix-text": tag,
|
|
1311
|
+
"data-ml-fix-label": labels.addCloseTag(tag)
|
|
1312
|
+
};
|
|
1313
|
+
if (boundary != null) {
|
|
1314
|
+
a["data-ml-fix-kind"] = "insertLine";
|
|
1315
|
+
a["data-ml-fix-pos"] = String(painter.pos(boundary));
|
|
1316
|
+
} else {
|
|
1317
|
+
a["data-ml-fix-kind"] = "append";
|
|
1318
|
+
}
|
|
1319
|
+
painter.paint(s.start, s.end, a);
|
|
1320
|
+
};
|
|
1321
|
+
const stack = [];
|
|
1322
|
+
for (const b of blocks) {
|
|
1323
|
+
let mm;
|
|
1324
|
+
if (/^if\b/i.test(b.inner))
|
|
1325
|
+
stack.push({ close: "if", start: b.start, end: b.start + 4 });
|
|
1326
|
+
else if (/^for\b/i.test(b.inner))
|
|
1327
|
+
stack.push({ close: "for", start: b.start, end: b.start + 5 });
|
|
1328
|
+
else if (mm = b.inner.match(/^#(\w+)/))
|
|
1329
|
+
stack.push({
|
|
1330
|
+
close: mm[1].toLowerCase(),
|
|
1331
|
+
start: b.start,
|
|
1332
|
+
end: b.start + 2 + mm[0].length
|
|
1333
|
+
});
|
|
1334
|
+
else if (mm = b.inner.match(/^(elseif|else)\b/i)) {
|
|
1335
|
+
const branchesFor = mm[1].toLowerCase() === "else";
|
|
1336
|
+
let matched = false;
|
|
1337
|
+
while (stack.length) {
|
|
1338
|
+
const top = stack[stack.length - 1];
|
|
1339
|
+
if (top.close === "if" || branchesFor && top.close === "for") {
|
|
1340
|
+
matched = true;
|
|
1341
|
+
break;
|
|
1342
|
+
}
|
|
1343
|
+
flagUnclosed(top, b.start);
|
|
1344
|
+
stack.pop();
|
|
1345
|
+
}
|
|
1346
|
+
if (!matched)
|
|
1347
|
+
painter.paint(b.start, b.end, errAttrs(labels.noOpenBlock("if")));
|
|
1348
|
+
} else if (mm = b.inner.match(/^\/(\w+)/)) {
|
|
1349
|
+
const name = mm[1].toLowerCase();
|
|
1350
|
+
let idx = -1;
|
|
1351
|
+
for (let k = stack.length - 1; k >= 0; k--)
|
|
1352
|
+
if (stack[k].close === name) {
|
|
1353
|
+
idx = k;
|
|
1354
|
+
break;
|
|
1355
|
+
}
|
|
1356
|
+
if (idx === -1)
|
|
1357
|
+
painter.paint(b.start, b.end, errAttrs(labels.noOpenBlock(name)));
|
|
1358
|
+
else {
|
|
1359
|
+
for (let k = stack.length - 1; k > idx; k--)
|
|
1360
|
+
flagUnclosed(stack[k], b.start);
|
|
1361
|
+
stack.length = idx;
|
|
1362
|
+
}
|
|
1363
|
+
}
|
|
1364
|
+
}
|
|
1365
|
+
for (const s of stack) flagUnclosed(s, null);
|
|
1366
|
+
}
|
|
1367
|
+
|
|
1368
|
+
// src/schema/build-schema.ts
|
|
1369
|
+
function buildValidateSchema(namespaces, orgSchema) {
|
|
1370
|
+
const byPath = /* @__PURE__ */ new Map();
|
|
1371
|
+
for (const ns of namespaces)
|
|
1372
|
+
for (const f of ns.fields)
|
|
1373
|
+
byPath.set(`${ns.key}.${f.key}`, {
|
|
1374
|
+
path: `${ns.key}.${f.key}`,
|
|
1375
|
+
type: f.type,
|
|
1376
|
+
values: f.values,
|
|
1377
|
+
// Dynamic (flow) values are runtime — don't nag for a default on them.
|
|
1378
|
+
nullable: ns.dynamic ? false : void 0,
|
|
1379
|
+
name: f.label
|
|
1380
|
+
});
|
|
1381
|
+
for (const d of orgSchema) byPath.set(d.path, d);
|
|
1382
|
+
return [...byPath.values()];
|
|
1383
|
+
}
|
|
1384
|
+
function createDiagnosticTooltip(deps) {
|
|
1385
|
+
const { editor, ctx, labels } = deps;
|
|
1386
|
+
let errTip;
|
|
1387
|
+
let errEl = null;
|
|
1388
|
+
let errTipInteractive = false;
|
|
1389
|
+
let hoverTimer;
|
|
1390
|
+
let hideTimer;
|
|
1391
|
+
let ptrX = 0;
|
|
1392
|
+
let ptrY = 0;
|
|
1393
|
+
const setPointer = (x, y) => {
|
|
1394
|
+
ptrX = x;
|
|
1395
|
+
ptrY = y;
|
|
1396
|
+
};
|
|
1397
|
+
const cancelHide = () => {
|
|
1398
|
+
if (hideTimer) clearTimeout(hideTimer);
|
|
1399
|
+
hideTimer = void 0;
|
|
1400
|
+
};
|
|
1401
|
+
const hideError = () => {
|
|
1402
|
+
cancelHide();
|
|
1403
|
+
if (hoverTimer) clearTimeout(hoverTimer);
|
|
1404
|
+
hoverTimer = void 0;
|
|
1405
|
+
errTip?.destroy();
|
|
1406
|
+
errTip = void 0;
|
|
1407
|
+
errEl = null;
|
|
1408
|
+
errTipInteractive = false;
|
|
1409
|
+
};
|
|
1410
|
+
const hideErrorSoon = () => {
|
|
1411
|
+
cancelHide();
|
|
1412
|
+
hideTimer = setTimeout(hideError, 500);
|
|
1413
|
+
};
|
|
1414
|
+
const applyFix = (kind, fixText, pos, end) => {
|
|
1415
|
+
if (kind === "replace" && pos != null && end != null) {
|
|
1416
|
+
editor.chain().focus().insertContentAt({ from: pos, to: end }, fixText).run();
|
|
1417
|
+
return;
|
|
1418
|
+
}
|
|
1419
|
+
if (kind === "insertLine" && pos != null) {
|
|
1420
|
+
const hasBreak2 = !!editor.schema.nodes.hardBreak;
|
|
1421
|
+
const content2 = hasBreak2 ? [{ type: "text", text: fixText }, { type: "hardBreak" }] : `${fixText}
|
|
1422
|
+
`;
|
|
1423
|
+
editor.chain().focus().insertContentAt(pos, content2).run();
|
|
1424
|
+
return;
|
|
1425
|
+
}
|
|
1426
|
+
if (kind === "insert" && pos != null) {
|
|
1427
|
+
editor.chain().focus().insertContentAt(pos, fixText).run();
|
|
1428
|
+
const q = fixText.indexOf('""');
|
|
1429
|
+
if (q >= 0) editor.commands.setTextSelection(pos + q + 1);
|
|
1430
|
+
return;
|
|
1431
|
+
}
|
|
1432
|
+
const docEnd = editor.state.doc.content.size;
|
|
1433
|
+
const hasBreak = !!editor.schema.nodes.hardBreak;
|
|
1434
|
+
const content = hasBreak ? [{ type: "hardBreak" }, { type: "text", text: fixText }] : `
|
|
1435
|
+
${fixText}`;
|
|
1436
|
+
editor.chain().focus("end").insertContentAt(docEnd, content).run();
|
|
1437
|
+
};
|
|
1438
|
+
const renderError = (el) => {
|
|
1439
|
+
const x = ptrX;
|
|
1440
|
+
const y = ptrY;
|
|
1441
|
+
const msg = el.getAttribute("data-ml-error");
|
|
1442
|
+
if (!msg) return;
|
|
1443
|
+
const sev = el.getAttribute("data-ml-sev") ?? "error";
|
|
1444
|
+
const code = el.getAttribute("data-ml-code") ?? "";
|
|
1445
|
+
const fixKind = el.getAttribute("data-ml-fix-kind");
|
|
1446
|
+
const fixText = el.getAttribute("data-ml-fix-text");
|
|
1447
|
+
const fixLabel = el.getAttribute("data-ml-fix-label");
|
|
1448
|
+
const fixPosAttr = el.getAttribute("data-ml-fix-pos");
|
|
1449
|
+
const fixEndAttr = el.getAttribute("data-ml-fix-end");
|
|
1450
|
+
const fix = !!fixKind && !!fixText;
|
|
1451
|
+
const color = SEV_COLOR[sev] ?? SEV_COLOR.error;
|
|
1452
|
+
const box = document.createElement("div");
|
|
1453
|
+
box.style.cssText = "background:#18181b;color:#e4e4e7;border:1px solid #3f3f46;border-radius:8px;padding:7px 10px;box-shadow:0 8px 24px rgba(0,0,0,.5);font-size:12px;line-height:1.4;max-width:300px";
|
|
1454
|
+
const head = document.createElement("div");
|
|
1455
|
+
head.style.cssText = "display:flex;gap:7px;align-items:center;margin-bottom:2px";
|
|
1456
|
+
const sevEl = document.createElement("span");
|
|
1457
|
+
sevEl.textContent = labels.severity[sev] ?? sev;
|
|
1458
|
+
sevEl.style.cssText = `font-size:10px;font-weight:700;text-transform:uppercase;letter-spacing:.03em;color:${color}`;
|
|
1459
|
+
head.appendChild(sevEl);
|
|
1460
|
+
if (code) {
|
|
1461
|
+
const codeEl = document.createElement("span");
|
|
1462
|
+
codeEl.textContent = code;
|
|
1463
|
+
codeEl.style.cssText = "font-family:ui-monospace,monospace;font-size:10.5px;color:#71717a";
|
|
1464
|
+
head.appendChild(codeEl);
|
|
1465
|
+
}
|
|
1466
|
+
const bodyEl = document.createElement("div");
|
|
1467
|
+
bodyEl.textContent = msg;
|
|
1468
|
+
box.appendChild(head);
|
|
1469
|
+
box.appendChild(bodyEl);
|
|
1470
|
+
if (fix) {
|
|
1471
|
+
const btn = document.createElement("button");
|
|
1472
|
+
btn.type = "button";
|
|
1473
|
+
btn.textContent = fixLabel ?? labels.quickFix;
|
|
1474
|
+
btn.style.cssText = "margin-top:7px;padding:3px 9px;border-radius:6px;border:1px solid #3f3f46;background:#27272a;color:#93c5fd;font-size:11px;font-weight:500;cursor:pointer";
|
|
1475
|
+
btn.addEventListener("mousedown", (e) => {
|
|
1476
|
+
e.preventDefault();
|
|
1477
|
+
applyFix(
|
|
1478
|
+
fixKind,
|
|
1479
|
+
fixText,
|
|
1480
|
+
fixPosAttr != null ? Number(fixPosAttr) : void 0,
|
|
1481
|
+
fixEndAttr != null ? Number(fixEndAttr) : void 0
|
|
1482
|
+
);
|
|
1483
|
+
hideError();
|
|
1484
|
+
});
|
|
1485
|
+
box.appendChild(btn);
|
|
1486
|
+
}
|
|
1487
|
+
errTip?.destroy();
|
|
1488
|
+
errTipInteractive = true;
|
|
1489
|
+
errTip = tippy__default.default(document.body, {
|
|
1490
|
+
getReferenceClientRect: () => ({
|
|
1491
|
+
width: 0,
|
|
1492
|
+
height: 0,
|
|
1493
|
+
x,
|
|
1494
|
+
y,
|
|
1495
|
+
top: y,
|
|
1496
|
+
bottom: y,
|
|
1497
|
+
left: x,
|
|
1498
|
+
right: x
|
|
1499
|
+
}),
|
|
1500
|
+
content: box,
|
|
1501
|
+
showOnCreate: true,
|
|
1502
|
+
trigger: "manual",
|
|
1503
|
+
placement: "bottom-start",
|
|
1504
|
+
offset: [0, 8],
|
|
1505
|
+
maxWidth: "none",
|
|
1506
|
+
appendTo: () => document.body,
|
|
1507
|
+
interactive: true,
|
|
1508
|
+
interactiveBorder: 32
|
|
1509
|
+
});
|
|
1510
|
+
errTip.popper.addEventListener("mouseenter", cancelHide);
|
|
1511
|
+
errTip.popper.addEventListener("mouseleave", hideError);
|
|
1512
|
+
};
|
|
1513
|
+
const scheduleError = (target, x, y) => {
|
|
1514
|
+
ptrX = x;
|
|
1515
|
+
ptrY = y;
|
|
1516
|
+
const el = target?.closest?.("[data-ml-error]") ?? null;
|
|
1517
|
+
if (el === errEl) {
|
|
1518
|
+
cancelHide();
|
|
1519
|
+
return;
|
|
1520
|
+
}
|
|
1521
|
+
if (!el) {
|
|
1522
|
+
if (errTip && errTipInteractive) hideErrorSoon();
|
|
1523
|
+
else hideError();
|
|
1524
|
+
return;
|
|
1525
|
+
}
|
|
1526
|
+
hideError();
|
|
1527
|
+
errEl = el;
|
|
1528
|
+
if (ctx.suggestionActive) return;
|
|
1529
|
+
hoverTimer = setTimeout(() => renderError(el), 900);
|
|
1530
|
+
};
|
|
1531
|
+
const handleEditorMouseLeave = () => {
|
|
1532
|
+
if (errTip && errTipInteractive) hideErrorSoon();
|
|
1533
|
+
else hideError();
|
|
1534
|
+
};
|
|
1535
|
+
return { scheduleError, setPointer, hideError, handleEditorMouseLeave };
|
|
1536
|
+
}
|
|
1537
|
+
function runValidation(editor, options, storage) {
|
|
1538
|
+
const { skipValidation, debounceMs, onResult, translateDiagnostic } = options;
|
|
1539
|
+
if (storage.timer) clearTimeout(storage.timer);
|
|
1540
|
+
if (skipValidation) return;
|
|
1541
|
+
storage.timer = setTimeout(() => {
|
|
1542
|
+
if (!editor || editor.isDestroyed) return;
|
|
1543
|
+
const template = editor.getText({ blockSeparator: "\n" });
|
|
1544
|
+
const push = (byPath) => {
|
|
1545
|
+
if (editor.isDestroyed) return;
|
|
1546
|
+
editor.view.dispatch(editor.state.tr.setMeta(key, { byPath }));
|
|
1547
|
+
};
|
|
1548
|
+
const schema = key.getState(editor.state)?.schema ?? [];
|
|
1549
|
+
if (!schema.length || !template.includes("{{")) {
|
|
1550
|
+
push(/* @__PURE__ */ new Map());
|
|
1551
|
+
onResult?.({ diagnostics: [], maxTokenEstimate: null });
|
|
1552
|
+
return;
|
|
1553
|
+
}
|
|
1554
|
+
let result;
|
|
1555
|
+
try {
|
|
1556
|
+
result = modelLanguage.validate(template, schema);
|
|
1557
|
+
} catch {
|
|
1558
|
+
return;
|
|
1559
|
+
}
|
|
1560
|
+
const diagnostics = result.diagnostics.map((d) => {
|
|
1561
|
+
const base = {
|
|
1562
|
+
code: d.code,
|
|
1563
|
+
severity: d.severity,
|
|
1564
|
+
message: d.message,
|
|
1565
|
+
fieldPath: d.fieldPath ?? null
|
|
1566
|
+
};
|
|
1567
|
+
return { ...base, message: translateDiagnostic?.(base) ?? base.message };
|
|
1568
|
+
});
|
|
1569
|
+
push(diagnosticsByPath(diagnostics));
|
|
1570
|
+
onResult?.({ diagnostics, maxTokenEstimate: result.maxTokenEstimate });
|
|
1571
|
+
}, debounceMs);
|
|
1572
|
+
}
|
|
1573
|
+
|
|
1574
|
+
// src/extension.ts
|
|
1575
|
+
var ModelSyntax = core.Extension.create({
|
|
1576
|
+
name: "modelSyntax",
|
|
1577
|
+
addOptions() {
|
|
1578
|
+
return {
|
|
1579
|
+
namespaces: [],
|
|
1580
|
+
schema: [],
|
|
1581
|
+
skipValidation: false,
|
|
1582
|
+
debounceMs: 300,
|
|
1583
|
+
severities: ["error", "warning", "info"],
|
|
1584
|
+
labels: {},
|
|
1585
|
+
translateDiagnostic: void 0,
|
|
1586
|
+
onResult: void 0
|
|
1587
|
+
};
|
|
1588
|
+
},
|
|
1589
|
+
addStorage() {
|
|
1590
|
+
return { timer: null };
|
|
1591
|
+
},
|
|
1592
|
+
addCommands() {
|
|
1593
|
+
return {
|
|
1594
|
+
setModelData: ({ namespaces, schema }) => ({ tr, dispatch, editor }) => {
|
|
1595
|
+
if (dispatch) {
|
|
1596
|
+
dispatch(
|
|
1597
|
+
tr.setMeta(key, {
|
|
1598
|
+
namespaces,
|
|
1599
|
+
schema: buildValidateSchema(namespaces, schema ?? [])
|
|
1600
|
+
})
|
|
1601
|
+
);
|
|
1602
|
+
runValidation(editor, this.options, this.storage);
|
|
1603
|
+
}
|
|
1604
|
+
return true;
|
|
1605
|
+
}
|
|
1606
|
+
};
|
|
1607
|
+
},
|
|
1608
|
+
onCreate() {
|
|
1609
|
+
runValidation(this.editor, this.options, this.storage);
|
|
1610
|
+
},
|
|
1611
|
+
onUpdate({ transaction }) {
|
|
1612
|
+
if (!transaction.docChanged) return;
|
|
1613
|
+
runValidation(this.editor, this.options, this.storage);
|
|
1614
|
+
},
|
|
1615
|
+
onDestroy() {
|
|
1616
|
+
if (this.storage.timer) clearTimeout(this.storage.timer);
|
|
1617
|
+
},
|
|
1618
|
+
addProseMirrorPlugins() {
|
|
1619
|
+
const labels = resolveLabels(this.options.labels);
|
|
1620
|
+
const severities = this.options.severities;
|
|
1621
|
+
const initialNamespaces = this.options.namespaces;
|
|
1622
|
+
const initialSchema = buildValidateSchema(
|
|
1623
|
+
this.options.namespaces,
|
|
1624
|
+
this.options.schema
|
|
1625
|
+
);
|
|
1626
|
+
const ctx = {
|
|
1627
|
+
lastTrDocChanged: false,
|
|
1628
|
+
lastTrSelectionMoved: false,
|
|
1629
|
+
suggestionActive: false
|
|
1630
|
+
};
|
|
1631
|
+
const tooltip = createDiagnosticTooltip({
|
|
1632
|
+
editor: this.editor,
|
|
1633
|
+
ctx,
|
|
1634
|
+
labels
|
|
1635
|
+
});
|
|
1636
|
+
const main = new state.Plugin({
|
|
1637
|
+
key,
|
|
1638
|
+
state: {
|
|
1639
|
+
init: () => ({
|
|
1640
|
+
namespaces: initialNamespaces,
|
|
1641
|
+
schema: initialSchema,
|
|
1642
|
+
byPath: /* @__PURE__ */ new Map()
|
|
1643
|
+
}),
|
|
1644
|
+
apply(tr, value, oldState, newState) {
|
|
1645
|
+
ctx.lastTrDocChanged = tr.docChanged;
|
|
1646
|
+
ctx.lastTrSelectionMoved = !oldState.selection.eq(newState.selection);
|
|
1647
|
+
const meta = tr.getMeta(key);
|
|
1648
|
+
return meta ? { ...value, ...meta } : value;
|
|
1649
|
+
}
|
|
1650
|
+
},
|
|
1651
|
+
props: {
|
|
1652
|
+
decorations(state) {
|
|
1653
|
+
const st = key.getState(state);
|
|
1654
|
+
return st ? buildDecorations(state.doc, st, { severities, labels }) : view.DecorationSet.empty;
|
|
1655
|
+
},
|
|
1656
|
+
handleDOMEvents: {
|
|
1657
|
+
mousemove: (_view, event) => {
|
|
1658
|
+
tooltip.setPointer(event.clientX, event.clientY);
|
|
1659
|
+
return false;
|
|
1660
|
+
},
|
|
1661
|
+
mouseover: (_view, event) => {
|
|
1662
|
+
const target = event.target;
|
|
1663
|
+
tooltip.scheduleError(target, event.clientX, event.clientY);
|
|
1664
|
+
const id = target?.closest?.("[data-card-id]")?.getAttribute("data-card-id");
|
|
1665
|
+
if (id)
|
|
1666
|
+
document.querySelector(`[data-id="${id}"]`)?.classList.add("variable-hover-highlight");
|
|
1667
|
+
return false;
|
|
1668
|
+
},
|
|
1669
|
+
mouseout: (_view, event) => {
|
|
1670
|
+
const id = event.target?.closest?.("[data-card-id]")?.getAttribute("data-card-id");
|
|
1671
|
+
if (id)
|
|
1672
|
+
document.querySelector(`[data-id="${id}"]`)?.classList.remove("variable-hover-highlight");
|
|
1673
|
+
return false;
|
|
1674
|
+
},
|
|
1675
|
+
mouseleave: () => {
|
|
1676
|
+
tooltip.handleEditorMouseLeave();
|
|
1677
|
+
return false;
|
|
1678
|
+
}
|
|
1679
|
+
}
|
|
1680
|
+
}
|
|
1681
|
+
});
|
|
1682
|
+
const suggestion = createSuggestionPlugin({
|
|
1683
|
+
editor: this.editor,
|
|
1684
|
+
ctx,
|
|
1685
|
+
onOpen: tooltip.hideError,
|
|
1686
|
+
labels
|
|
1687
|
+
});
|
|
1688
|
+
return [main, suggestion];
|
|
1689
|
+
}
|
|
1690
|
+
});
|
|
1691
|
+
|
|
1692
|
+
Object.defineProperty(exports, "parse", {
|
|
1693
|
+
enumerable: true,
|
|
1694
|
+
get: function () { return modelLanguage.parse; }
|
|
1695
|
+
});
|
|
1696
|
+
Object.defineProperty(exports, "render", {
|
|
1697
|
+
enumerable: true,
|
|
1698
|
+
get: function () { return modelLanguage.render; }
|
|
1699
|
+
});
|
|
1700
|
+
Object.defineProperty(exports, "serialize", {
|
|
1701
|
+
enumerable: true,
|
|
1702
|
+
get: function () { return modelLanguage.serialize; }
|
|
1703
|
+
});
|
|
1704
|
+
Object.defineProperty(exports, "validate", {
|
|
1705
|
+
enumerable: true,
|
|
1706
|
+
get: function () { return modelLanguage.validate; }
|
|
1707
|
+
});
|
|
1708
|
+
exports.CONTACT_NAMESPACE_KEY = CONTACT_NAMESPACE_KEY;
|
|
1709
|
+
exports.DATE_FORMATS = DATE_FORMATS;
|
|
1710
|
+
exports.DEFAULT_LABELS = DEFAULT_LABELS;
|
|
1711
|
+
exports.FLOW_NAMESPACE_KEY = FLOW_NAMESPACE_KEY;
|
|
1712
|
+
exports.HL_CLASS = HL_CLASS;
|
|
1713
|
+
exports.ML_FILTERS = ML_FILTERS;
|
|
1714
|
+
exports.ModelSyntax = ModelSyntax;
|
|
1715
|
+
exports.STATIC_NAMESPACES = STATIC_NAMESPACES;
|
|
1716
|
+
exports.buildModelOptions = buildModelOptions;
|
|
1717
|
+
exports.buildValidateSchema = buildValidateSchema;
|
|
1718
|
+
exports.defaultFilterFor = defaultFilterFor;
|
|
1719
|
+
exports.diagnosticsByPath = diagnosticsByPath;
|
|
1720
|
+
exports.modelTokenDisplay = modelTokenDisplay;
|
|
1721
|
+
exports.modelTokenTone = modelTokenTone;
|
|
1722
|
+
exports.parseModelToken = parseModelToken;
|
|
1723
|
+
exports.resolveLabels = resolveLabels;
|
|
1724
|
+
exports.suggestOperator = suggestOperator;
|
|
1725
|
+
exports.tokenizeExpression = tokenizeExpression;
|