@jackuait/blok 0.8.3-beta.2 → 0.8.3-beta.4

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.
@@ -54,6 +54,50 @@ interface LegacyToggleListData {
54
54
  };
55
55
  }
56
56
 
57
+ /**
58
+ * Legacy callout data structure for data model transformation.
59
+ * Old format: { title?: string, body: { blocks: [] } | null, variant, emoji, isEmojiVisible }
60
+ */
61
+ interface LegacyCalloutData {
62
+ title?: string;
63
+ body?: {
64
+ blocks?: OutputBlockData[];
65
+ } | null;
66
+ variant?: string;
67
+ emoji?: string | null;
68
+ isEmojiVisible?: boolean;
69
+ }
70
+
71
+ /**
72
+ * Map legacy callout variant to backgroundColor preset name
73
+ */
74
+ const VARIANT_TO_BG_PRESET: Record<string, string | null> = {
75
+ general: null,
76
+ note: 'blue',
77
+ important: 'purple',
78
+ warning: 'orange',
79
+ additional: 'yellow',
80
+ recommendation: 'green',
81
+ caution: 'red',
82
+ };
83
+
84
+ /**
85
+ * Map backgroundColor preset name back to legacy variant
86
+ */
87
+ const BG_PRESET_TO_VARIANT: Record<string, string> = {
88
+ blue: 'note',
89
+ purple: 'important',
90
+ orange: 'warning',
91
+ yellow: 'additional',
92
+ green: 'recommendation',
93
+ red: 'caution',
94
+ };
95
+
96
+ /**
97
+ * Default emoji for callout blocks
98
+ */
99
+ const CALLOUT_DEFAULT_EMOJI = '💡';
100
+
57
101
  /**
58
102
  * Result of analyzing the input data format
59
103
  */
@@ -172,6 +216,21 @@ const isLegacyToggleListBlock = (block: OutputBlockData): block is OutputBlockDa
172
216
  return typeof data === 'object' && data !== null && 'title' in data;
173
217
  };
174
218
 
219
+ /**
220
+ * Check if a block is in legacy callout format
221
+ * Legacy format: { type: "callout", data: { body: { blocks: [...] }, variant, emoji, isEmojiVisible } }
222
+ * New format has textColor/backgroundColor instead of variant/body
223
+ */
224
+ const isLegacyCalloutBlock = (block: OutputBlockData): block is OutputBlockData<string, LegacyCalloutData> => {
225
+ if (block.type !== 'callout') {
226
+ return false;
227
+ }
228
+
229
+ const data = block.data as Record<string, unknown>;
230
+
231
+ return typeof data === 'object' && data !== null && 'body' in data;
232
+ };
233
+
175
234
  /**
176
235
  * Check if a block contains nested hierarchy in its items
177
236
  */
@@ -211,10 +270,15 @@ export const analyzeDataFormat = (blocks: OutputBlockData[]): DataFormatAnalysis
211
270
  // Check if any block uses legacy toggleList format
212
271
  const foundLegacyToggle = blocks.some(isLegacyToggleListBlock);
213
272
 
214
- if (foundLegacyList || foundLegacyToggle) {
273
+ // Check if any block uses legacy callout format (has body field)
274
+ const foundLegacyCallout = blocks.some(isLegacyCalloutBlock);
275
+
276
+ if (foundLegacyList || foundLegacyToggle || foundLegacyCallout) {
215
277
  // Check if there's actual nesting for the hasHierarchy flag
216
278
  const hasNesting = blocks.some(hasNestedItems) || blocks.some(block =>
217
279
  isLegacyToggleListBlock(block) && block.data.body?.blocks !== undefined && block.data.body.blocks.length > 0
280
+ ) || blocks.some(block =>
281
+ isLegacyCalloutBlock(block) && block.data.body?.blocks !== undefined && block.data.body.blocks.length > 0
218
282
  );
219
283
 
220
284
  return { format: 'legacy', hasHierarchy: hasNesting };
@@ -363,6 +427,57 @@ const expandToggleListToHierarchical = (
363
427
  return blocks;
364
428
  };
365
429
 
430
+ /**
431
+ * Expand a legacy callout block into flat callout block + child blocks
432
+ */
433
+ const expandCalloutToHierarchical = (
434
+ block: OutputBlockData<string, LegacyCalloutData>
435
+ ): OutputBlockData[] => {
436
+ const blocks: OutputBlockData[] = [];
437
+ const calloutId = block.id ?? generateBlockId();
438
+ const bodyBlocks = block.data.body?.blocks ?? [];
439
+
440
+ // Collect child IDs, ensuring each child has an ID
441
+ const childIds: BlockId[] = [];
442
+ const childBlocks: OutputBlockData[] = [];
443
+
444
+ for (const childBlock of bodyBlocks) {
445
+ const childId = childBlock.id ?? generateBlockId();
446
+
447
+ childIds.push(childId);
448
+ childBlocks.push({
449
+ ...childBlock,
450
+ id: childId,
451
+ parent: calloutId,
452
+ });
453
+ }
454
+
455
+ // Map variant → backgroundColor preset
456
+ const variant = block.data.variant ?? 'general';
457
+ const backgroundColor = variant in VARIANT_TO_BG_PRESET ? VARIANT_TO_BG_PRESET[variant] : null;
458
+
459
+ // Map emoji + isEmojiVisible → emoji string
460
+ const emoji: string = block.data.isEmojiVisible === false
461
+ ? ''
462
+ : (block.data.emoji ?? CALLOUT_DEFAULT_EMOJI);
463
+
464
+ blocks.push({
465
+ id: calloutId,
466
+ type: 'callout',
467
+ data: {
468
+ emoji,
469
+ textColor: null,
470
+ backgroundColor,
471
+ },
472
+ ...(block.tunes !== undefined ? { tunes: block.tunes } : {}),
473
+ ...(childIds.length > 0 ? { content: childIds } : {}),
474
+ });
475
+
476
+ blocks.push(...childBlocks);
477
+
478
+ return blocks;
479
+ };
480
+
366
481
  /**
367
482
  * Expand legacy nested format to hierarchical flat-with-references format
368
483
  * @param blocks - array of blocks potentially containing nested structures
@@ -382,6 +497,11 @@ export const expandToHierarchical = (blocks: OutputBlockData[]): OutputBlockData
382
497
  // Expand toggleList to flat toggle + child blocks
383
498
  const expanded = expandToggleListToHierarchical(block);
384
499
 
500
+ expandedBlocks.push(...expanded);
501
+ } else if (isLegacyCalloutBlock(block)) {
502
+ // Expand legacy callout to flat callout + child blocks
503
+ const expanded = expandCalloutToHierarchical(block);
504
+
385
505
  expandedBlocks.push(...expanded);
386
506
  } else {
387
507
  // Non-list blocks pass through unchanged (with guaranteed ID)
@@ -639,6 +759,71 @@ const isFlatModelListBlock = (block: OutputBlockData): boolean => {
639
759
  return hasText && !hasItems;
640
760
  };
641
761
 
762
+ /**
763
+ * Check if a block is a flat-model callout block (has no 'body' field)
764
+ */
765
+ const isFlatModelCalloutBlock = (block: OutputBlockData): boolean => {
766
+ if (block.type !== 'callout') {
767
+ return false;
768
+ }
769
+
770
+ const data = block.data as Record<string, unknown>;
771
+
772
+ return typeof data === 'object' && data !== null && !('body' in data);
773
+ };
774
+
775
+ /**
776
+ * Process a root callout block (flat model) and convert to a legacy callout block
777
+ */
778
+ const processRootCalloutItem = (
779
+ block: OutputBlockData,
780
+ blockMap: Map<BlockId, OutputBlockData>,
781
+ processedIds: Set<BlockId>
782
+ ): OutputBlockData => {
783
+ markBlockAsProcessed(block.id, processedIds);
784
+
785
+ const data = block.data as Record<string, unknown>;
786
+
787
+ // Map backgroundColor preset → variant
788
+ const backgroundColor = data.backgroundColor as string | null | undefined;
789
+ const variant = backgroundColor !== null && backgroundColor !== undefined
790
+ ? (BG_PRESET_TO_VARIANT[backgroundColor] ?? 'general')
791
+ : 'general';
792
+
793
+ // Map emoji string → isEmojiVisible + emoji
794
+ const emojiValue = data.emoji as string | undefined;
795
+ const isEmojiVisible = typeof emojiValue === 'string' && emojiValue.length > 0;
796
+ const emoji = isEmojiVisible ? emojiValue : null;
797
+
798
+ // Collect child blocks
799
+ const childBlocks: OutputBlockData[] = [];
800
+ const contentIds = block.content ?? [];
801
+
802
+ for (const childId of contentIds) {
803
+ const childBlock = blockMap.get(childId);
804
+
805
+ if (childBlock) {
806
+ markBlockAsProcessed(childId, processedIds);
807
+ childBlocks.push(stripHierarchyFields(childBlock));
808
+ }
809
+ }
810
+
811
+ const legacyBlock: OutputBlockData = {
812
+ id: block.id,
813
+ type: 'callout',
814
+ data: {
815
+ title: '',
816
+ variant,
817
+ emoji,
818
+ isEmojiVisible,
819
+ ...(childBlocks.length > 0 ? { body: { blocks: childBlocks } } : {}),
820
+ },
821
+ ...(block.tunes !== undefined ? { tunes: block.tunes } : {}),
822
+ };
823
+
824
+ return legacyBlock;
825
+ };
826
+
642
827
  /**
643
828
  * Collapse hierarchical flat-with-references format back to legacy nested format
644
829
  * @param blocks - array of flat blocks with parent/content references
@@ -654,12 +839,13 @@ export const collapseToLegacy = (blocks: OutputBlockData[]): OutputBlockData[] =
654
839
  }
655
840
  }
656
841
 
657
- // If no flat-model list or toggle blocks, just strip hierarchy fields and return
842
+ // If no flat-model list, toggle, or callout blocks, just strip hierarchy fields and return
658
843
  const hasFlatListBlocks = blocks.some(isFlatModelListBlock);
659
844
  const hasFlatToggleBlocks = blocks.some(isFlatModelToggleBlock);
660
845
  const hasFlatToggleableHeaders = blocks.some(b => isToggleableHeaderBlock(b) && !b.parent);
846
+ const hasFlatCalloutBlocks = blocks.some(isFlatModelCalloutBlock);
661
847
 
662
- if (!hasFlatListBlocks && !hasFlatToggleBlocks && !hasFlatToggleableHeaders) {
848
+ if (!hasFlatListBlocks && !hasFlatToggleBlocks && !hasFlatToggleableHeaders && !hasFlatCalloutBlocks) {
663
849
  return blocks.map(stripHierarchyFields);
664
850
  }
665
851
 
@@ -680,7 +866,9 @@ export const collapseToLegacy = (blocks: OutputBlockData[]): OutputBlockData[] =
680
866
  const isRootToggleItem = isFlatToggleBlock && !block.parent;
681
867
  const isToggleableHeader = isToggleableHeaderBlock(block);
682
868
  const isRootToggleableHeader = isToggleableHeader && !block.parent;
683
- const isNonListItem = !isFlatListBlock && !isFlatToggleBlock && !isToggleableHeader;
869
+ const isFlatCallout = isFlatModelCalloutBlock(block);
870
+ const isRootCallout = isFlatCallout && !block.parent;
871
+ const isNonListItem = !isFlatListBlock && !isFlatToggleBlock && !isToggleableHeader && !isFlatCallout;
684
872
 
685
873
  if (isRootListItem) {
686
874
  const listBlock = processRootListItem(block, blockMap, processedIds);
@@ -700,6 +888,12 @@ export const collapseToLegacy = (blocks: OutputBlockData[]): OutputBlockData[] =
700
888
  result.push(legacyBlock);
701
889
  }
702
890
 
891
+ if (isRootCallout) {
892
+ const calloutBlock = processRootCalloutItem(block, blockMap, processedIds);
893
+
894
+ result.push(calloutBlock);
895
+ }
896
+
703
897
  if (isNonListItem) {
704
898
  result.push(stripHierarchyFields(block));
705
899
  markBlockAsProcessed(block.id, processedIds);
@@ -27,6 +27,35 @@ import {
27
27
  DEFAULT_EMOJI,
28
28
  } from './constants';
29
29
 
30
+ /**
31
+ * Resolve emoji from legacy callout data fields
32
+ */
33
+ function resolveLegacyEmoji(data: Record<string, unknown>): string {
34
+ if (data.isEmojiVisible === false) {
35
+ return '';
36
+ }
37
+
38
+ if (typeof data.emoji === 'string' && data.emoji.length > 0) {
39
+ return data.emoji;
40
+ }
41
+
42
+ return DEFAULT_EMOJI;
43
+ }
44
+
45
+ /**
46
+ * Map legacy callout variant to backgroundColor preset name.
47
+ * Used when receiving data from older format that has variant instead of backgroundColor.
48
+ */
49
+ const VARIANT_TO_BG_PRESET: Record<string, string | null> = {
50
+ general: null,
51
+ note: 'blue',
52
+ important: 'purple',
53
+ warning: 'orange',
54
+ additional: 'yellow',
55
+ recommendation: 'green',
56
+ caution: 'red',
57
+ };
58
+
30
59
  export class CalloutTool implements BlockTool {
31
60
  private readonly api: API;
32
61
  private readonly readOnly: boolean;
@@ -47,6 +76,13 @@ export class CalloutTool implements BlockTool {
47
76
  }
48
77
 
49
78
  private normalizeData(data: Partial<CalloutData>): CalloutData {
79
+ const legacyData = data as Record<string, unknown>;
80
+ const hasLegacyFields = 'variant' in legacyData || 'isEmojiVisible' in legacyData;
81
+
82
+ if (hasLegacyFields) {
83
+ return this.normalizeLegacyData(legacyData);
84
+ }
85
+
50
86
  return {
51
87
  emoji: typeof data.emoji === 'string' ? data.emoji : DEFAULT_EMOJI,
52
88
  textColor: typeof data.textColor === 'string' ? data.textColor : null,
@@ -54,6 +90,21 @@ export class CalloutTool implements BlockTool {
54
90
  };
55
91
  }
56
92
 
93
+ private normalizeLegacyData(data: Record<string, unknown>): CalloutData {
94
+ // Map variant to backgroundColor
95
+ const variant = typeof data.variant === 'string' ? data.variant : 'general';
96
+ const backgroundColor = variant in VARIANT_TO_BG_PRESET ? VARIANT_TO_BG_PRESET[variant] : null;
97
+
98
+ // Map isEmojiVisible + emoji to emoji string
99
+ const emoji = resolveLegacyEmoji(data);
100
+
101
+ return {
102
+ emoji,
103
+ textColor: null,
104
+ backgroundColor: backgroundColor ?? null,
105
+ };
106
+ }
107
+
57
108
  public render(): HTMLElement {
58
109
  const dom = buildCalloutDOM({
59
110
  emoji: this._data.emoji,
@@ -32,6 +32,8 @@ export const Italic: InlineToolConstructable;
32
32
  export const Link: InlineToolConstructable;
33
33
  export const Convert: InlineToolConstructable;
34
34
  export const Marker: InlineToolConstructable;
35
+ export const Strikethrough: InlineToolConstructable;
36
+ export const Underline: InlineToolConstructable;
35
37
 
36
38
  // Block tunes
37
39
  export const Delete: BlockTuneConstructable;
@@ -54,4 +56,6 @@ export const defaultInlineTools: {
54
56
  readonly italic: {};
55
57
  readonly link: {};
56
58
  readonly marker: {};
59
+ readonly strikethrough: {};
60
+ readonly underline: {};
57
61
  };