@hankchen/canvas-editor 0.3.0 → 0.4.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.js CHANGED
@@ -501,6 +501,7 @@ function normalizeSchemaType(value) {
501
501
  const primary = value.find((entry) => entry !== "null" && entry !== "");
502
502
  return primary;
503
503
  }
504
+ var FORMAT_ANNOTATIONS = /* @__PURE__ */ new Set(["date", "date-time"]);
504
505
  function annotationsOf(raw) {
505
506
  const node = {};
506
507
  const rawType = raw["type"];
@@ -515,6 +516,10 @@ function annotationsOf(raw) {
515
516
  if (typeof rawDescription === "string") node.description = rawDescription;
516
517
  const rawTitle = raw["title"];
517
518
  if (typeof rawTitle === "string") node.title = rawTitle;
519
+ const rawFormat = raw["format"];
520
+ if (node.type === "string" && typeof rawFormat === "string" && FORMAT_ANNOTATIONS.has(rawFormat)) {
521
+ node.format = rawFormat;
522
+ }
518
523
  return node;
519
524
  }
520
525
  function pointerEscape(key) {
@@ -692,15 +697,170 @@ function parseExpressionPath(expr) {
692
697
  return { ok: true, head: { kind: "bare", name: head }, rest };
693
698
  }
694
699
 
700
+ // src/shared/expressionCall.ts
701
+ var TRIM_CHARS = " \n\r\0\v";
702
+ var WS_CHARS = " \n\r";
703
+ var HEAD_STOP_CHARS = `()',.${TRIM_CHARS}`;
704
+ function phpTrim(value) {
705
+ let start = 0;
706
+ let end = value.length;
707
+ while (start < end && TRIM_CHARS.includes(value[start])) start += 1;
708
+ while (end > start && TRIM_CHARS.includes(value[end - 1])) end -= 1;
709
+ return value.slice(start, end);
710
+ }
711
+ function skipWs(expr, pos) {
712
+ while (pos < expr.length && WS_CHARS.includes(expr[pos])) pos += 1;
713
+ return pos;
714
+ }
715
+ function isCallFragment(expr) {
716
+ let head = 0;
717
+ while (head < expr.length && !HEAD_STOP_CHARS.includes(expr[head])) head += 1;
718
+ if (head === 0) return false;
719
+ let pos = head;
720
+ while (pos < expr.length && TRIM_CHARS.includes(expr[pos])) pos += 1;
721
+ return expr[pos] === "(";
722
+ }
723
+ function parseExpressionCall(expr) {
724
+ let head = 0;
725
+ while (head < expr.length && !HEAD_STOP_CHARS.includes(expr[head])) head += 1;
726
+ if (head === 0) return { ok: false, error: "expression_syntax_error" };
727
+ const name = expr.slice(0, head);
728
+ let pos = skipWs(expr, head);
729
+ if (pos >= expr.length || expr[pos] !== "(") return { ok: false, error: "expression_syntax_error" };
730
+ pos = skipWs(expr, pos + 1);
731
+ const args = [];
732
+ if (pos < expr.length && expr[pos] === ")") {
733
+ pos += 1;
734
+ } else {
735
+ for (; ; ) {
736
+ if (pos >= expr.length) return { ok: false, error: "expression_syntax_error" };
737
+ const arg = parseCallArg(expr, pos);
738
+ if (!arg.ok) return arg;
739
+ args.push(arg.value);
740
+ pos = skipWs(expr, arg.end);
741
+ if (pos >= expr.length) return { ok: false, error: "expression_syntax_error" };
742
+ if (expr[pos] === ",") {
743
+ pos = skipWs(expr, pos + 1);
744
+ if (pos < expr.length && expr[pos] === ")") {
745
+ return { ok: false, error: "expression_syntax_error" };
746
+ }
747
+ continue;
748
+ }
749
+ if (expr[pos] === ")") {
750
+ pos += 1;
751
+ break;
752
+ }
753
+ return { ok: false, error: "expression_syntax_error" };
754
+ }
755
+ }
756
+ if (skipWs(expr, pos) !== expr.length) return { ok: false, error: "expression_syntax_error" };
757
+ if (args[0] !== void 0 && args[0].type !== "path") return { ok: false, error: "expression_syntax_error" };
758
+ for (const arg of args) {
759
+ if (arg.type === "path" && !parseExpressionPath(arg.value).ok) {
760
+ return { ok: false, error: "expression_syntax_error" };
761
+ }
762
+ }
763
+ return { ok: true, name, args };
764
+ }
765
+ function parseCallArg(expr, pos) {
766
+ const len = expr.length;
767
+ const ch = expr[pos];
768
+ if (ch === "'") {
769
+ let cursor = pos + 1;
770
+ let buf = "";
771
+ for (; ; ) {
772
+ if (cursor >= len) return { ok: false, error: "expression_syntax_error" };
773
+ if (expr[cursor] === "'") {
774
+ if (cursor + 1 < len && expr[cursor + 1] === "'") {
775
+ buf += "'";
776
+ cursor += 2;
777
+ continue;
778
+ }
779
+ cursor += 1;
780
+ break;
781
+ }
782
+ buf += expr[cursor];
783
+ cursor += 1;
784
+ }
785
+ return { ok: true, value: { type: "string", value: buf }, end: cursor };
786
+ }
787
+ if (ch >= "0" && ch <= "9") {
788
+ const start2 = pos;
789
+ const isDigitAt = (index) => {
790
+ const c = expr[index];
791
+ return c !== void 0 && c >= "0" && c <= "9";
792
+ };
793
+ while (isDigitAt(pos)) pos += 1;
794
+ if (pos < len && expr[pos] === "." && isDigitAt(pos + 1)) {
795
+ pos += 1;
796
+ while (isDigitAt(pos)) pos += 1;
797
+ }
798
+ return { ok: true, value: { type: "number", value: expr.slice(start2, pos) }, end: pos };
799
+ }
800
+ const start = pos;
801
+ while (pos < len && expr[pos] !== "," && expr[pos] !== ")") pos += 1;
802
+ const raw = phpTrim(expr.slice(start, pos));
803
+ if (raw === "") return { ok: false, error: "expression_syntax_error" };
804
+ return { ok: true, value: { type: "path", value: raw }, end: pos };
805
+ }
806
+ function expressionCallSlotAtEnd(expr) {
807
+ let head = 0;
808
+ while (head < expr.length && !HEAD_STOP_CHARS.includes(expr[head])) head += 1;
809
+ if (head === 0) return null;
810
+ let pos = skipWs(expr, head);
811
+ if (pos >= expr.length || expr[pos] !== "(") return null;
812
+ pos = skipWs(expr, pos + 1);
813
+ let index = 0;
814
+ for (; ; ) {
815
+ if (pos >= expr.length) return { name: expr.slice(0, head), index, text: "", stringSlot: false };
816
+ const ch = expr[pos];
817
+ if (ch === "'") {
818
+ let cursor = pos + 1;
819
+ for (; ; ) {
820
+ if (cursor >= expr.length) {
821
+ return { name: expr.slice(0, head), index, text: expr.slice(pos + 1), stringSlot: true };
822
+ }
823
+ if (expr[cursor] === "'") {
824
+ if (cursor + 1 < expr.length && expr[cursor + 1] === "'") {
825
+ cursor += 2;
826
+ continue;
827
+ }
828
+ cursor += 1;
829
+ break;
830
+ }
831
+ cursor += 1;
832
+ }
833
+ pos = cursor;
834
+ } else if (ch !== "," && ch !== ")") {
835
+ const argStart = pos;
836
+ while (pos < expr.length && expr[pos] !== "," && expr[pos] !== ")") pos += 1;
837
+ if (pos >= expr.length) {
838
+ return { name: expr.slice(0, head), index, text: phpTrim(expr.slice(argStart)), stringSlot: false };
839
+ }
840
+ }
841
+ if (pos >= expr.length) return { name: expr.slice(0, head), index: index + 1, text: "", stringSlot: false };
842
+ if (expr[pos] === ")") return null;
843
+ index += 1;
844
+ pos = skipWs(expr, pos + 1);
845
+ }
846
+ }
847
+
695
848
  // src/shared/expressionScan.ts
696
849
  var PHP_TRIM_CHARS = " \n\r\0\v";
697
- function phpTrim(value) {
850
+ function phpTrim2(value) {
698
851
  let start = 0;
699
852
  let end = value.length;
700
853
  while (start < end && PHP_TRIM_CHARS.includes(value[start])) start += 1;
701
854
  while (end > start && PHP_TRIM_CHARS.includes(value[end - 1])) end -= 1;
702
855
  return value.slice(start, end);
703
856
  }
857
+ function fragmentSyntaxError(expr) {
858
+ if (expr === "") return "expression_syntax_error";
859
+ if (isCallFragment(expr)) {
860
+ return parseExpressionCall(expr).ok ? null : "expression_syntax_error";
861
+ }
862
+ return parseExpressionPath(expr).ok ? null : "expression_syntax_error";
863
+ }
704
864
  function scanExpressionFragments(template) {
705
865
  const parts = [];
706
866
  let pos = 0;
@@ -712,15 +872,15 @@ function scanExpressionFragments(template) {
712
872
  parts.push({ kind: "literal", start: pos, end: open, text: template.slice(pos, open) });
713
873
  }
714
874
  const text = template.slice(open);
715
- parts.push({ kind: "open", start: open, end: template.length, text, expr: phpTrim(text.slice(2)) });
875
+ parts.push({ kind: "open", start: open, end: template.length, text, expr: phpTrim2(text.slice(2)) });
716
876
  return { template, parts };
717
877
  }
718
878
  if (open > pos) {
719
879
  parts.push({ kind: "literal", start: pos, end: open, text: template.slice(pos, open) });
720
880
  }
721
881
  const raw = template.slice(open + 2, close);
722
- const expr = phpTrim(raw);
723
- const error = parseExpressionPath(expr).ok ? null : "expression_syntax_error";
882
+ const expr = phpTrim2(raw);
883
+ const error = fragmentSyntaxError(expr);
724
884
  parts.push({ kind: "fragment", start: open, end: close + 2, raw, expr, error });
725
885
  pos = close + 2;
726
886
  open = template.indexOf("{{", pos);
@@ -4081,6 +4241,22 @@ function classifyWheel(input) {
4081
4241
  return "pan";
4082
4242
  }
4083
4243
 
4244
+ // src/shared/expressionFunctions.ts
4245
+ var EXPRESSION_FUNCTIONS = [
4246
+ { name: "date", signature: "date(path[, format])", summary: "\u65E5\u671F\u683C\u5F0F\u5316\uFF08moment \u98CE\u683C token\uFF0C\u9884\u8BBE date/datetime\uFF09" },
4247
+ { name: "number", signature: "number(path[, n])", summary: "\u5343\u5206\u4F4D\u5206\u7EC4" },
4248
+ { name: "fixed", signature: "fixed(path[, n])", summary: "\u5B9A\u70B9\u8865\u96F6\uFF08\u7F3A\u7701 2 \u4F4D\uFF09" },
4249
+ { name: "percent", signature: "percent(path[, n])", summary: "\u767E\u5206\u6BD4\uFF08\u4E58 100\uFF09" }
4250
+ ];
4251
+ var NUMBER_FUNCTION_SUGGESTIONS = EXPRESSION_FUNCTIONS.filter(
4252
+ (fn) => fn.name !== "date"
4253
+ );
4254
+ var DATE_FORMAT_SUGGESTIONS = [
4255
+ { segment: "date", description: "\u9884\u8BBE \u2192 YYYY-MM-DD" },
4256
+ { segment: "datetime", description: "\u9884\u8BBE \u2192 YYYY-MM-DD HH:mm:ss" },
4257
+ { segment: "YYYY\u5E74MM\u6708DD\u65E5", description: "\u4E2D\u6587\u6A21\u677F\uFF08\u5199\u5373\u6240\u5F97\uFF09" }
4258
+ ];
4259
+
4084
4260
  // src/shared/expressionCandidates.ts
4085
4261
  function headName(head) {
4086
4262
  if (head.kind === "row") return "row";
@@ -4098,9 +4274,57 @@ function childCandidate(entry, prefix) {
4098
4274
  const path = prefix === "" ? entry.key : `${prefix}.${entry.key}`;
4099
4275
  return { path, segment: entry.key, ...candidateMeta(entry.node) };
4100
4276
  }
4101
- function headCandidates(schema, context, rowSchema) {
4277
+ function callCandidatesFor(entry, path, exprLen) {
4278
+ const extras = [];
4279
+ if (entry.node.format === "date" || entry.node.format === "date-time") {
4280
+ extras.push({
4281
+ path: `date(${path}, '')`,
4282
+ segment: `date(${path}, '')`,
4283
+ cursorBack: 2,
4284
+ replaceLen: exprLen,
4285
+ filterKey: entry.key,
4286
+ description: "\u65E5\u671F\u683C\u5F0F\u5316",
4287
+ type: "\u51FD\u6570"
4288
+ });
4289
+ } else if (entry.node.type === "number") {
4290
+ for (const fn of NUMBER_FUNCTION_SUGGESTIONS) {
4291
+ extras.push({
4292
+ path: `${fn.name}(${path})`,
4293
+ segment: `${fn.name}(${path})`,
4294
+ replaceLen: exprLen,
4295
+ filterKey: entry.key,
4296
+ description: fn.summary,
4297
+ type: "\u51FD\u6570"
4298
+ });
4299
+ }
4300
+ }
4301
+ return extras;
4302
+ }
4303
+ function childCandidates(entry, prefix, exprLen, withBuckets) {
4304
+ const base = childCandidate(entry, prefix);
4305
+ if (!withBuckets) return [base];
4306
+ return [base, ...callCandidatesFor(entry, base.path, exprLen)];
4307
+ }
4308
+ function functionCandidates() {
4309
+ return EXPRESSION_FUNCTIONS.map((fn) => ({
4310
+ path: fn.signature,
4311
+ segment: `${fn.name}()`,
4312
+ cursorBack: 1,
4313
+ description: fn.summary,
4314
+ type: "\u51FD\u6570"
4315
+ }));
4316
+ }
4317
+ function dateFormatCandidates() {
4318
+ return DATE_FORMAT_SUGGESTIONS.map((suggestion) => ({
4319
+ path: suggestion.segment,
4320
+ segment: suggestion.segment,
4321
+ description: suggestion.description,
4322
+ type: "\u683C\u5F0F"
4323
+ }));
4324
+ }
4325
+ function headCandidates(schema, context, rowSchema, exprLen, withBuckets) {
4102
4326
  const view = schemaChildEntries(schema);
4103
- const candidates = view.entries.map((entry) => childCandidate(entry, ""));
4327
+ const candidates = view.entries.flatMap((entry) => childCandidates(entry, "", exprLen, withBuckets));
4104
4328
  candidates.push({ path: "$root", segment: "$root" });
4105
4329
  if (context === "row") {
4106
4330
  candidates.push(rowSchema ? { path: "row", segment: "row", ...candidateMeta(rowSchema) } : { path: "row", segment: "row" });
@@ -4108,15 +4332,35 @@ function headCandidates(schema, context, rowSchema) {
4108
4332
  }
4109
4333
  return view.open === true ? { candidates, open: true } : { candidates };
4110
4334
  }
4111
- function enumerateExpressionCandidates(schema, query) {
4335
+ function callSlotResult(schema, query) {
4336
+ const slot = expressionCallSlotAtEnd(query.expr);
4337
+ if (slot === null) {
4338
+ return { ok: true, prefix: "", partial: "", candidates: [] };
4339
+ }
4340
+ if (slot.stringSlot && slot.index >= 1 && slot.name === "date") {
4341
+ return { ok: true, prefix: "", partial: slot.text, candidates: dateFormatCandidates() };
4342
+ }
4343
+ if (slot.index === 0 && !isCallFragment(slot.text)) {
4344
+ const sub = enumeratePathCandidates(schema, query, slot.text, false);
4345
+ if (!sub.ok) return { ok: true, prefix: "", partial: "", candidates: [] };
4346
+ return sub;
4347
+ }
4348
+ return { ok: true, prefix: "", partial: "", candidates: [] };
4349
+ }
4350
+ function enumeratePathCandidates(schema, query, expr, allowCallSuggestions) {
4112
4351
  if (schema === null) {
4113
4352
  return { ok: true, prefix: "", partial: "", candidates: [] };
4114
4353
  }
4115
- const hadTrailingDot = query.expr.endsWith(".");
4116
- const stripped = hadTrailingDot ? query.expr.slice(0, -1) : query.expr;
4354
+ const hadTrailingDot = expr.endsWith(".");
4355
+ const stripped = hadTrailingDot ? expr.slice(0, -1) : expr;
4117
4356
  if (stripped === "") {
4118
- if (query.expr === "") {
4119
- return { ok: true, prefix: "", partial: "", ...headCandidates(schema, query.context, query.rowSchema) };
4357
+ if (expr === "") {
4358
+ return {
4359
+ ok: true,
4360
+ prefix: "",
4361
+ partial: "",
4362
+ ...headCandidates(schema, query.context, query.rowSchema, expr.length, false)
4363
+ };
4120
4364
  }
4121
4365
  return { ok: false, reason: "expression_syntax_error" };
4122
4366
  }
@@ -4137,8 +4381,17 @@ function enumerateExpressionCandidates(schema, query) {
4137
4381
  prefixPath = [];
4138
4382
  partial = headName(head);
4139
4383
  }
4384
+ const withBuckets = allowCallSuggestions && partial !== "";
4140
4385
  if (prefixPath.length === 0) {
4141
- return { ok: true, prefix: "", partial, ...headCandidates(schema, query.context, query.rowSchema) };
4386
+ const view2 = headCandidates(schema, query.context, query.rowSchema, expr.length, withBuckets);
4387
+ const fns = allowCallSuggestions && expr !== "" ? functionCandidates() : [];
4388
+ return {
4389
+ ok: true,
4390
+ prefix: "",
4391
+ partial,
4392
+ candidates: [...view2.candidates, ...fns],
4393
+ ...view2.open === true ? { open: true } : {}
4394
+ };
4142
4395
  }
4143
4396
  let base;
4144
4397
  let segments;
@@ -4167,10 +4420,19 @@ function enumerateExpressionCandidates(schema, query) {
4167
4420
  ok: true,
4168
4421
  prefix,
4169
4422
  partial,
4170
- candidates: view.entries.map((entry) => childCandidate(entry, prefix)),
4423
+ candidates: view.entries.flatMap((entry) => childCandidates(entry, prefix, expr.length, withBuckets)),
4171
4424
  ...view.open === true ? { open: true } : {}
4172
4425
  };
4173
4426
  }
4427
+ function enumerateExpressionCandidates(schema, query) {
4428
+ if (schema === null) {
4429
+ return { ok: true, prefix: "", partial: "", candidates: [] };
4430
+ }
4431
+ if (isCallFragment(query.expr)) {
4432
+ return callSlotResult(schema, query);
4433
+ }
4434
+ return enumeratePathCandidates(schema, query, query.expr, true);
4435
+ }
4174
4436
 
4175
4437
  // src/shared/rowsPathCandidates.ts
4176
4438
  function isArrayNode(node) {
@@ -4320,15 +4582,18 @@ export {
4320
4582
  ANCHORS,
4321
4583
  ARM_CREATE_LAYER_TYPES,
4322
4584
  CREATE_DEAD_ZONE_SCREEN_PX,
4585
+ DATE_FORMAT_SUGGESTIONS,
4323
4586
  DEFAULT_EDITOR_SHORTCUTS,
4324
4587
  DEFAULT_ZOOM_BOUNDS,
4325
4588
  EDITOR_STATE_SCHEMA_VERSION,
4589
+ EXPRESSION_FUNCTIONS,
4326
4590
  EditorSession,
4327
4591
  EditorStore,
4328
4592
  FontCatalog,
4329
4593
  HORIZONTAL_ALIGNS,
4330
4594
  LAYER_TYPES,
4331
4595
  MAX_HISTORY_STEPS,
4596
+ NUMBER_FUNCTION_SUGGESTIONS,
4332
4597
  PACKAGE_NAME,
4333
4598
  PAN_TO_BOX_MARGIN_PX,
4334
4599
  PASTE_OFFSET_PX,
@@ -4366,6 +4631,7 @@ export {
4366
4631
  deleteLayerInDraft,
4367
4632
  enumerateExpressionCandidates,
4368
4633
  enumerateRowsPathCandidates,
4634
+ expressionCallSlotAtEnd,
4369
4635
  expressionFragmentAtCursor,
4370
4636
  fitRect,
4371
4637
  fitViewport,
@@ -4375,6 +4641,7 @@ export {
4375
4641
  hitTest,
4376
4642
  insertRootLayerAdjacentInDraft,
4377
4643
  insertRootLayerInDraft,
4644
+ isCallFragment,
4378
4645
  isImageContentUndrawn,
4379
4646
  isLayerPath,
4380
4647
  isLockedPath,
@@ -4393,11 +4660,13 @@ export {
4393
4660
  normalizeExpressionSchemaSource,
4394
4661
  panBy,
4395
4662
  panToBox,
4663
+ parseExpressionCall,
4396
4664
  parseExpressionPath,
4397
4665
  parseExpressionSchema,
4398
4666
  pastePosition,
4399
4667
  pathStartsWith,
4400
4668
  pathsEqual,
4669
+ phpTrim2 as phpTrim,
4401
4670
  pickNextTextStylePatch,
4402
4671
  prepareRootPaste,
4403
4672
  readSpecField,