@sendbird/actionbook-core 0.10.1 → 0.10.2

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.
@@ -426,4 +426,10 @@ declare function toProseMirrorJSON(doc: DocumentNode): JSONContent;
426
426
  */
427
427
  declare function astNodesToJSONContent(nodes: (InlineNode | BlockNode)[]): JSONContent[];
428
428
 
429
- export { type ActionbookPlugin, ActionbookRenderer, type ActionbookRendererProps, DocumentTreeView, type DocumentTreeViewProps, EditorShell, type EditorShellProps, type EditorViewConfig, type EditorViewHandle, FloatingMenu, type FloatingMenuProps, type InlineSuggestContext, type InlineSuggestProvider, type InlineSuggestState, JUMP_POINT_ADJACENT_SPEC, JinjaTreeView, type JinjaTreeViewProps, type NodeViewFactory, type ReactNodeViewOptions, type ReactNodeViewProps, type SlashCommandItem, SlashCommandMenu, type SlashCommandMenuProps, type SlashCommandState, actionbookSchema, astNodesToJSONContent, convertBlock, convertInline, createDragHandlePlugin, createHistoryPlugin, createInlineSuggestPlugin, createInlineToolTagNodeViewPlugin, createInputRulesPlugin, createJinjaDecorationPlugin, createJinjaIfBlockPlugin, createJumpPointAdjacentPlugin, createJumpPointNodeViewPlugin, createJumpPointValidationPlugin, createKeymapPlugin, createLinkPlugin, createMarkdownClipboardPlugin, createNoteBlockPlugin, createPlaceholderPlugin, createPluginArray, createReactNodeView, createSlashCommandPlugin, createTodoNodeViewPlugin, hasBrokenAnchorRefs, hasDuplicateJumpPoints, inlineSuggestKey, slashCommandKey, toProseMirrorJSON, useEditorView };
429
+ /**
430
+ * Check if any jinjaIfBranch in the editor has an invalid (non-empty) condition.
431
+ * Used to block save when conditions have syntax errors.
432
+ */
433
+ declare function hasInvalidJinjaConditions(state: EditorState): boolean;
434
+
435
+ export { type ActionbookPlugin, ActionbookRenderer, type ActionbookRendererProps, DocumentTreeView, type DocumentTreeViewProps, EditorShell, type EditorShellProps, type EditorViewConfig, type EditorViewHandle, FloatingMenu, type FloatingMenuProps, type InlineSuggestContext, type InlineSuggestProvider, type InlineSuggestState, JUMP_POINT_ADJACENT_SPEC, JinjaTreeView, type JinjaTreeViewProps, type NodeViewFactory, type ReactNodeViewOptions, type ReactNodeViewProps, type SlashCommandItem, SlashCommandMenu, type SlashCommandMenuProps, type SlashCommandState, actionbookSchema, astNodesToJSONContent, convertBlock, convertInline, createDragHandlePlugin, createHistoryPlugin, createInlineSuggestPlugin, createInlineToolTagNodeViewPlugin, createInputRulesPlugin, createJinjaDecorationPlugin, createJinjaIfBlockPlugin, createJumpPointAdjacentPlugin, createJumpPointNodeViewPlugin, createJumpPointValidationPlugin, createKeymapPlugin, createLinkPlugin, createMarkdownClipboardPlugin, createNoteBlockPlugin, createPlaceholderPlugin, createPluginArray, createReactNodeView, createSlashCommandPlugin, createTodoNodeViewPlugin, hasBrokenAnchorRefs, hasDuplicateJumpPoints, hasInvalidJinjaConditions, inlineSuggestKey, slashCommandKey, toProseMirrorJSON, useEditorView };
package/dist/ui/index.js CHANGED
@@ -5149,6 +5149,320 @@ function analyzeJinjaBlocks(doc2) {
5149
5149
  });
5150
5150
  }
5151
5151
 
5152
+ // src/jinja/evaluator.ts
5153
+ var KEYWORDS2 = {
5154
+ and: "AND",
5155
+ or: "OR",
5156
+ not: "NOT",
5157
+ in: "IN",
5158
+ is: "IS",
5159
+ True: "BOOL",
5160
+ False: "BOOL",
5161
+ true: "BOOL",
5162
+ false: "BOOL",
5163
+ None: "NONE",
5164
+ null: "NONE"
5165
+ };
5166
+ function tokenize(input) {
5167
+ const tokens = [];
5168
+ let i = 0;
5169
+ while (i < input.length) {
5170
+ if (/\s/.test(input[i])) {
5171
+ i++;
5172
+ continue;
5173
+ }
5174
+ if (input[i] === '"' || input[i] === "'") {
5175
+ const quote = input[i];
5176
+ let str = "";
5177
+ i++;
5178
+ while (i < input.length && input[i] !== quote) {
5179
+ if (input[i] === "\\" && i + 1 < input.length) {
5180
+ str += input[i + 1];
5181
+ i += 2;
5182
+ } else {
5183
+ str += input[i];
5184
+ i++;
5185
+ }
5186
+ }
5187
+ if (i >= input.length) return { tokens: [], error: "Unterminated string literal" };
5188
+ i++;
5189
+ tokens.push({ type: "STRING", value: str });
5190
+ continue;
5191
+ }
5192
+ if (/[0-9]/.test(input[i]) || input[i] === "-" && i + 1 < input.length && /[0-9]/.test(input[i + 1]) && (tokens.length === 0 || ["AND", "OR", "NOT", "EQ", "NEQ", "LT", "GT", "LTE", "GTE", "LPAREN", "IN", "IS"].includes(tokens[tokens.length - 1].type))) {
5193
+ let num = "";
5194
+ if (input[i] === "-") {
5195
+ num = "-";
5196
+ i++;
5197
+ }
5198
+ while (i < input.length && /[0-9]/.test(input[i])) {
5199
+ num += input[i];
5200
+ i++;
5201
+ }
5202
+ if (i < input.length && input[i] === ".") {
5203
+ num += ".";
5204
+ i++;
5205
+ while (i < input.length && /[0-9]/.test(input[i])) {
5206
+ num += input[i];
5207
+ i++;
5208
+ }
5209
+ }
5210
+ tokens.push({ type: "NUMBER", value: num });
5211
+ continue;
5212
+ }
5213
+ if (/[a-zA-Z_]/.test(input[i])) {
5214
+ let ident = "";
5215
+ while (i < input.length && /[a-zA-Z0-9_.]/.test(input[i])) {
5216
+ ident += input[i];
5217
+ i++;
5218
+ }
5219
+ const kwType = KEYWORDS2[ident];
5220
+ if (kwType) {
5221
+ tokens.push({ type: kwType, value: ident });
5222
+ } else {
5223
+ tokens.push({ type: "IDENT", value: ident });
5224
+ }
5225
+ continue;
5226
+ }
5227
+ if (i + 1 < input.length) {
5228
+ const two = input[i] + input[i + 1];
5229
+ if (two === "==") {
5230
+ tokens.push({ type: "EQ", value: "==" });
5231
+ i += 2;
5232
+ continue;
5233
+ }
5234
+ if (two === "!=") {
5235
+ tokens.push({ type: "NEQ", value: "!=" });
5236
+ i += 2;
5237
+ continue;
5238
+ }
5239
+ if (two === "<=") {
5240
+ tokens.push({ type: "LTE", value: "<=" });
5241
+ i += 2;
5242
+ continue;
5243
+ }
5244
+ if (two === ">=") {
5245
+ tokens.push({ type: "GTE", value: ">=" });
5246
+ i += 2;
5247
+ continue;
5248
+ }
5249
+ }
5250
+ if (input[i] === "<") {
5251
+ tokens.push({ type: "LT", value: "<" });
5252
+ i++;
5253
+ continue;
5254
+ }
5255
+ if (input[i] === ">") {
5256
+ tokens.push({ type: "GT", value: ">" });
5257
+ i++;
5258
+ continue;
5259
+ }
5260
+ if (input[i] === "(") {
5261
+ tokens.push({ type: "LPAREN", value: "(" });
5262
+ i++;
5263
+ continue;
5264
+ }
5265
+ if (input[i] === ")") {
5266
+ tokens.push({ type: "RPAREN", value: ")" });
5267
+ i++;
5268
+ continue;
5269
+ }
5270
+ return { tokens: [], error: `Unexpected character: ${input[i]}` };
5271
+ }
5272
+ tokens.push({ type: "EOF", value: "" });
5273
+ return { tokens };
5274
+ }
5275
+ var Parser = class {
5276
+ pos = 0;
5277
+ tokens;
5278
+ variables;
5279
+ constructor(tokens, variables) {
5280
+ this.tokens = tokens;
5281
+ this.variables = variables;
5282
+ }
5283
+ peek() {
5284
+ return this.tokens[this.pos] ?? { type: "EOF", value: "" };
5285
+ }
5286
+ advance() {
5287
+ const t = this.tokens[this.pos];
5288
+ this.pos++;
5289
+ return t;
5290
+ }
5291
+ expect(type) {
5292
+ const t = this.peek();
5293
+ if (t.type !== type) {
5294
+ throw new Error(`Expected ${type}, got ${t.type}`);
5295
+ }
5296
+ return this.advance();
5297
+ }
5298
+ // Grammar (precedence low→high):
5299
+ // expr → or_expr
5300
+ // or_expr → and_expr ('or' and_expr)*
5301
+ // and_expr → not_expr ('and' not_expr)*
5302
+ // not_expr → 'not' not_expr | cmp_expr
5303
+ // cmp_expr → primary (('=='|'!='|'<'|'>'|'<='|'>='|'in'|'is'|'is not') primary)?
5304
+ // primary → STRING | NUMBER | BOOL | NONE | IDENT | '(' expr ')'
5305
+ evaluate() {
5306
+ const result = this.orExpr();
5307
+ if (this.peek().type !== "EOF") {
5308
+ throw new Error(`Unexpected token: ${this.peek().value}`);
5309
+ }
5310
+ return result;
5311
+ }
5312
+ orExpr() {
5313
+ let left = this.andExpr();
5314
+ while (this.peek().type === "OR") {
5315
+ this.advance();
5316
+ const right = this.andExpr();
5317
+ left = isTruthy(left) || isTruthy(right);
5318
+ }
5319
+ return left;
5320
+ }
5321
+ andExpr() {
5322
+ let left = this.notExpr();
5323
+ while (this.peek().type === "AND") {
5324
+ this.advance();
5325
+ const right = this.notExpr();
5326
+ left = isTruthy(left) && isTruthy(right);
5327
+ }
5328
+ return left;
5329
+ }
5330
+ notExpr() {
5331
+ if (this.peek().type === "NOT") {
5332
+ this.advance();
5333
+ const val = this.notExpr();
5334
+ return !isTruthy(val);
5335
+ }
5336
+ return this.cmpExpr();
5337
+ }
5338
+ cmpExpr() {
5339
+ const left = this.primary();
5340
+ const op = this.peek().type;
5341
+ switch (op) {
5342
+ case "EQ":
5343
+ this.advance();
5344
+ return looseEqual(left, this.primary());
5345
+ case "NEQ":
5346
+ this.advance();
5347
+ return !looseEqual(left, this.primary());
5348
+ case "LT":
5349
+ this.advance();
5350
+ return toNumber(left) < toNumber(this.primary());
5351
+ case "GT":
5352
+ this.advance();
5353
+ return toNumber(left) > toNumber(this.primary());
5354
+ case "LTE":
5355
+ this.advance();
5356
+ return toNumber(left) <= toNumber(this.primary());
5357
+ case "GTE":
5358
+ this.advance();
5359
+ return toNumber(left) >= toNumber(this.primary());
5360
+ case "IN": {
5361
+ this.advance();
5362
+ const collection = this.primary();
5363
+ if (Array.isArray(collection)) {
5364
+ return collection.some((item) => looseEqual(item, left));
5365
+ }
5366
+ if (typeof collection === "string" && typeof left === "string") {
5367
+ return collection.includes(left);
5368
+ }
5369
+ return false;
5370
+ }
5371
+ case "IS": {
5372
+ this.advance();
5373
+ if (this.peek().type === "NOT") {
5374
+ this.advance();
5375
+ return !looseEqual(left, this.primary());
5376
+ }
5377
+ return looseEqual(left, this.primary());
5378
+ }
5379
+ default:
5380
+ return left;
5381
+ }
5382
+ }
5383
+ primary() {
5384
+ const t = this.peek();
5385
+ switch (t.type) {
5386
+ case "STRING":
5387
+ this.advance();
5388
+ return t.value;
5389
+ case "NUMBER":
5390
+ this.advance();
5391
+ return parseFloat(t.value);
5392
+ case "BOOL":
5393
+ this.advance();
5394
+ return t.value === "True" || t.value === "true";
5395
+ case "NONE":
5396
+ this.advance();
5397
+ return null;
5398
+ case "IDENT": {
5399
+ this.advance();
5400
+ return this.resolveVariable(t.value);
5401
+ }
5402
+ case "LPAREN": {
5403
+ this.advance();
5404
+ const val = this.orExpr();
5405
+ this.expect("RPAREN");
5406
+ return val;
5407
+ }
5408
+ default:
5409
+ throw new Error(`Unexpected token in primary: ${t.type} "${t.value}"`);
5410
+ }
5411
+ }
5412
+ resolveVariable(name) {
5413
+ const entry = this.variables.get(name);
5414
+ if (entry === void 0) {
5415
+ return null;
5416
+ }
5417
+ switch (entry.type) {
5418
+ case "string":
5419
+ return String(entry.value);
5420
+ case "number":
5421
+ return Number(entry.value);
5422
+ case "boolean":
5423
+ return entry.value === true || entry.value === "true";
5424
+ default:
5425
+ return String(entry.value);
5426
+ }
5427
+ }
5428
+ };
5429
+ function isTruthy(v) {
5430
+ if (v === null || v === void 0) return false;
5431
+ if (typeof v === "boolean") return v;
5432
+ if (typeof v === "number") return v !== 0;
5433
+ if (typeof v === "string") return v.length > 0;
5434
+ if (Array.isArray(v)) return v.length > 0;
5435
+ return true;
5436
+ }
5437
+ function looseEqual(a, b) {
5438
+ if (a === b) return true;
5439
+ if (a === null || b === null) return a === b;
5440
+ if (typeof a === "number" && typeof b === "string") return a === parseFloat(b);
5441
+ if (typeof a === "string" && typeof b === "number") return parseFloat(a) === b;
5442
+ return false;
5443
+ }
5444
+ function toNumber(v) {
5445
+ if (typeof v === "number") return v;
5446
+ if (typeof v === "string") return parseFloat(v) || 0;
5447
+ if (typeof v === "boolean") return v ? 1 : 0;
5448
+ return 0;
5449
+ }
5450
+ function validateCondition(condition) {
5451
+ const trimmed = condition.trim();
5452
+ if (trimmed === "") return { valid: true };
5453
+ const { tokens, error: tokenError } = tokenize(trimmed);
5454
+ if (tokenError) return { valid: false, error: tokenError };
5455
+ if (tokens.length === 0) return { valid: false, error: "Empty expression" };
5456
+ try {
5457
+ const parser = new Parser(tokens, /* @__PURE__ */ new Map());
5458
+ parser.evaluate();
5459
+ return { valid: true };
5460
+ } catch (e) {
5461
+ const msg = e instanceof Error ? e.message : "Invalid expression";
5462
+ return { valid: false, error: msg };
5463
+ }
5464
+ }
5465
+
5152
5466
  // src/tree/documentTree.ts
5153
5467
  var MAX_DEPTH6 = 128;
5154
5468
  var END_ACTION_RESOURCE_IDS = /* @__PURE__ */ new Set([
@@ -6116,6 +6430,18 @@ var JINJA_STYLES = `
6116
6430
  letter-spacing: -0.3px;
6117
6431
  }
6118
6432
 
6433
+ .jinja-branch-condition-invalid {
6434
+ text-decoration: wavy underline #D9352C;
6435
+ text-underline-offset: 3px;
6436
+ }
6437
+
6438
+ .jinja-condition-error-icon {
6439
+ color: #D9352C;
6440
+ margin-left: 4px;
6441
+ font-size: 12px;
6442
+ flex-shrink: 0;
6443
+ }
6444
+
6119
6445
  .jinja-token-variable {
6120
6446
  color: #4141B2;
6121
6447
  }
@@ -6564,11 +6890,14 @@ function ConditionDisplay({
6564
6890
  }
6565
6891
  );
6566
6892
  }
6893
+ const validation = condition.length > 0 ? validateCondition(condition) : null;
6894
+ const isInvalid = validation != null && !validation.valid;
6567
6895
  return /* @__PURE__ */ jsx6(
6568
6896
  "button",
6569
6897
  {
6570
6898
  type: "button",
6571
- className: "jinja-branch-condition",
6899
+ className: `jinja-branch-condition${isInvalid ? " jinja-branch-condition-invalid" : ""}`,
6900
+ title: isInvalid ? validation.error : void 0,
6572
6901
  onMouseDown: (event) => event.preventDefault(),
6573
6902
  onClick: () => {
6574
6903
  if (editable) {
@@ -6576,7 +6905,10 @@ function ConditionDisplay({
6576
6905
  setIsEditing(true);
6577
6906
  }
6578
6907
  },
6579
- children: condition.length > 0 ? renderCondition(condition) : /* @__PURE__ */ jsx6("span", { className: "jinja-condition-placeholder", children: CONDITION_PLACEHOLDER })
6908
+ children: condition.length > 0 ? /* @__PURE__ */ jsxs5(Fragment2, { children: [
6909
+ renderCondition(condition),
6910
+ isInvalid && /* @__PURE__ */ jsx6("span", { className: "jinja-condition-error-icon", children: "\u26A0" })
6911
+ ] }) : /* @__PURE__ */ jsx6("span", { className: "jinja-condition-placeholder", children: CONDITION_PLACEHOLDER })
6580
6912
  }
6581
6913
  );
6582
6914
  }
@@ -9971,6 +10303,22 @@ function createInlineSuggestPlugin(provider, endpoint, options) {
9971
10303
  ]
9972
10304
  };
9973
10305
  }
10306
+
10307
+ // src/ui/bridge/jinjaValidation.ts
10308
+ function hasInvalidJinjaConditions(state) {
10309
+ let found = false;
10310
+ state.doc.descendants((node) => {
10311
+ if (found) return false;
10312
+ if (node.type.name === "jinjaIfBranch") {
10313
+ const condition = String(node.attrs.condition ?? "");
10314
+ if (condition && !validateCondition(condition).valid) {
10315
+ found = true;
10316
+ }
10317
+ return false;
10318
+ }
10319
+ });
10320
+ return found;
10321
+ }
9974
10322
  export {
9975
10323
  ActionbookRenderer,
9976
10324
  DocumentTreeView,
@@ -10004,6 +10352,7 @@ export {
10004
10352
  createTodoNodeViewPlugin,
10005
10353
  hasBrokenAnchorRefs,
10006
10354
  hasDuplicateJumpPoints,
10355
+ hasInvalidJinjaConditions,
10007
10356
  inlineSuggestKey,
10008
10357
  slashCommandKey,
10009
10358
  toProseMirrorJSON,