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/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;