koguma 0.6.6 → 2.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.
Files changed (44) hide show
  1. package/README.md +109 -139
  2. package/cli/auth.ts +101 -0
  3. package/cli/config.ts +149 -0
  4. package/cli/constants.ts +38 -0
  5. package/cli/content.ts +503 -0
  6. package/cli/dev-sync.ts +305 -0
  7. package/cli/exec.ts +61 -0
  8. package/cli/index.ts +779 -1545
  9. package/cli/log.ts +49 -0
  10. package/cli/preflight.ts +105 -0
  11. package/cli/scaffold.ts +680 -0
  12. package/cli/typegen.ts +190 -0
  13. package/cli/ui.ts +55 -0
  14. package/cli/wrangler.ts +367 -0
  15. package/package.json +7 -4
  16. package/src/admin/_bundle.ts +1 -1
  17. package/src/api/router.integration.test.ts +63 -80
  18. package/src/api/router.ts +85 -59
  19. package/src/config/define.ts +1 -1
  20. package/src/config/field.ts +10 -9
  21. package/src/config/index.ts +1 -13
  22. package/src/config/meta.ts +7 -7
  23. package/src/config/types.ts +1 -95
  24. package/src/db/init.ts +68 -0
  25. package/src/db/queries.ts +120 -211
  26. package/src/db/sql.ts +10 -25
  27. package/src/media/index.ts +105 -47
  28. package/src/react/Markdown.test.tsx +195 -0
  29. package/src/react/Markdown.tsx +40 -0
  30. package/src/react/index.ts +6 -22
  31. package/src/react/types.ts +3 -112
  32. package/src/db/migrate.ts +0 -182
  33. package/src/db/schema.ts +0 -122
  34. package/src/react/RichText.test.tsx +0 -535
  35. package/src/react/RichText.tsx +0 -350
  36. package/src/rich-text/index.ts +0 -4
  37. package/src/rich-text/koguma-to-lexical.ts +0 -340
  38. package/src/rich-text/lexical-compat.test.ts +0 -513
  39. package/src/rich-text/lexical-to-koguma.test.ts +0 -906
  40. package/src/rich-text/lexical-to-koguma.ts +0 -400
  41. package/src/rich-text/markdown-to-koguma.ts +0 -164
  42. package/src/rich-text/plain.test.ts +0 -208
  43. package/src/rich-text/plain.ts +0 -114
  44. package/src/rich-text/snapshots.test.ts +0 -284
@@ -1,906 +0,0 @@
1
- /**
2
- * lexical-to-koguma.test.ts
3
- *
4
- * Unit tests for the Lexical → KogumaDocument converter.
5
- * Uses raw JSON fixtures — no Lexical package required.
6
- */
7
- import { describe, test, expect } from 'bun:test';
8
- import { lexicalToKoguma } from './lexical-to-koguma.ts';
9
-
10
- // ── Fixtures ──────────────────────────────────────────────────────────
11
-
12
- const emptyState = { root: { type: 'root', children: [], version: 1 } };
13
-
14
- const paragraphState = {
15
- root: {
16
- type: 'root',
17
- version: 1,
18
- children: [
19
- {
20
- type: 'paragraph',
21
- version: 1,
22
- format: 0,
23
- children: [{ type: 'text', text: 'Hello world', format: 0, version: 1 }]
24
- }
25
- ]
26
- }
27
- };
28
-
29
- // ── Null / edge cases ────────────────────────────────────────────────
30
-
31
- describe('lexicalToKoguma — edge cases', () => {
32
- test('null input returns empty document', () => {
33
- expect(lexicalToKoguma(null)).toEqual({ nodes: [] });
34
- });
35
-
36
- test('undefined input returns empty document', () => {
37
- expect(lexicalToKoguma(undefined)).toEqual({ nodes: [] });
38
- });
39
-
40
- test('empty object returns empty document', () => {
41
- expect(lexicalToKoguma({})).toEqual({ nodes: [] });
42
- });
43
-
44
- test('empty root.children returns empty document', () => {
45
- expect(lexicalToKoguma(emptyState)).toEqual({ nodes: [] });
46
- });
47
- });
48
-
49
- // ── Paragraph ────────────────────────────────────────────────────────
50
-
51
- describe('lexicalToKoguma — paragraph', () => {
52
- test('plain paragraph', () => {
53
- const doc = lexicalToKoguma(paragraphState);
54
- expect(doc.nodes).toHaveLength(1);
55
- expect(doc.nodes[0].type).toBe('paragraph');
56
- const p = doc.nodes[0] as { type: 'paragraph'; children: unknown[] };
57
- expect(p.children).toHaveLength(1);
58
- });
59
-
60
- test('paragraph alignment: center', () => {
61
- const state = {
62
- root: {
63
- type: 'root',
64
- version: 1,
65
- children: [
66
- {
67
- type: 'paragraph',
68
- version: 1,
69
- format: 2,
70
- children: [{ type: 'text', text: 'hi', format: 0, version: 1 }]
71
- }
72
- ]
73
- }
74
- };
75
- const doc = lexicalToKoguma(state);
76
- expect((doc.nodes[0] as { align?: string }).align).toBe('center');
77
- });
78
-
79
- test('paragraph alignment: right', () => {
80
- const state = {
81
- root: {
82
- type: 'root',
83
- version: 1,
84
- children: [
85
- {
86
- type: 'paragraph',
87
- version: 1,
88
- format: 3,
89
- children: [{ type: 'text', text: 'hi', format: 0, version: 1 }]
90
- }
91
- ]
92
- }
93
- };
94
- const doc = lexicalToKoguma(state);
95
- expect((doc.nodes[0] as { align?: string }).align).toBe('right');
96
- });
97
- });
98
-
99
- // ── Text formatting (bitmask) ─────────────────────────────────────────
100
-
101
- describe('lexicalToKoguma — text format bitmask', () => {
102
- function textNode(format: number) {
103
- return {
104
- root: {
105
- type: 'root',
106
- version: 1,
107
- children: [
108
- {
109
- type: 'paragraph',
110
- version: 1,
111
- format: 0,
112
- children: [{ type: 'text', text: 'X', format, version: 1 }]
113
- }
114
- ]
115
- }
116
- };
117
- }
118
-
119
- function getTextNode(state: unknown) {
120
- const doc = lexicalToKoguma(state);
121
- const p = doc.nodes[0] as {
122
- children: Array<{
123
- bold?: boolean;
124
- italic?: boolean;
125
- underline?: boolean;
126
- strikethrough?: boolean;
127
- code?: boolean;
128
- subscript?: boolean;
129
- superscript?: boolean;
130
- }>;
131
- };
132
- return p.children[0];
133
- }
134
-
135
- test('bold (format=1)', () =>
136
- expect(getTextNode(textNode(1)).bold).toBe(true));
137
- test('italic (format=2)', () =>
138
- expect(getTextNode(textNode(2)).italic).toBe(true));
139
- test('strikethrough (format=4)', () =>
140
- expect(getTextNode(textNode(4)).strikethrough).toBe(true));
141
- test('underline (format=8)', () =>
142
- expect(getTextNode(textNode(8)).underline).toBe(true));
143
- test('code (format=16)', () =>
144
- expect(getTextNode(textNode(16)).code).toBe(true));
145
- test('subscript (format=32)', () =>
146
- expect(getTextNode(textNode(32)).subscript).toBe(true));
147
- test('superscript (format=64)', () =>
148
- expect(getTextNode(textNode(64)).superscript).toBe(true));
149
-
150
- test('bold+italic combined (format=3)', () => {
151
- const t = getTextNode(textNode(3));
152
- expect(t.bold).toBe(true);
153
- expect(t.italic).toBe(true);
154
- });
155
-
156
- test('no flags when format=0', () => {
157
- const t = getTextNode(textNode(0));
158
- expect(t.bold).toBeUndefined();
159
- expect(t.italic).toBeUndefined();
160
- });
161
- });
162
-
163
- // ── Headings ──────────────────────────────────────────────────────────
164
-
165
- describe('lexicalToKoguma — heading', () => {
166
- [1, 2, 3, 4, 5, 6].forEach(level => {
167
- test(`h${level}`, () => {
168
- const state = {
169
- root: {
170
- type: 'root',
171
- version: 1,
172
- children: [
173
- {
174
- type: 'heading',
175
- version: 1,
176
- tag: `h${level}`,
177
- children: [
178
- {
179
- type: 'text',
180
- text: `Heading ${level}`,
181
- format: 0,
182
- version: 1
183
- }
184
- ]
185
- }
186
- ]
187
- }
188
- };
189
- const doc = lexicalToKoguma(state);
190
- const h = doc.nodes[0] as { type: string; level: number };
191
- expect(h.type).toBe('heading');
192
- expect(h.level).toBe(level);
193
- });
194
- });
195
- });
196
-
197
- // ── Lists ─────────────────────────────────────────────────────────────
198
-
199
- describe('lexicalToKoguma — lists', () => {
200
- test('unordered list', () => {
201
- const state = {
202
- root: {
203
- type: 'root',
204
- version: 1,
205
- children: [
206
- {
207
- type: 'list',
208
- version: 1,
209
- listType: 'bullet',
210
- start: 1,
211
- tag: 'ul',
212
- children: [
213
- {
214
- type: 'listitem',
215
- version: 1,
216
- value: 1,
217
- children: [
218
- { type: 'text', text: 'Item 1', format: 0, version: 1 }
219
- ]
220
- }
221
- ]
222
- }
223
- ]
224
- }
225
- };
226
- const doc = lexicalToKoguma(state);
227
- const list = doc.nodes[0] as {
228
- type: string;
229
- ordered: boolean;
230
- items: unknown[];
231
- };
232
- expect(list.type).toBe('list');
233
- expect(list.ordered).toBe(false);
234
- expect(list.items).toHaveLength(1);
235
- });
236
-
237
- test('ordered list', () => {
238
- const state = {
239
- root: {
240
- type: 'root',
241
- version: 1,
242
- children: [
243
- {
244
- type: 'list',
245
- version: 1,
246
- listType: 'number',
247
- start: 1,
248
- tag: 'ol',
249
- children: [
250
- {
251
- type: 'listitem',
252
- version: 1,
253
- value: 1,
254
- children: [
255
- { type: 'text', text: 'First', format: 0, version: 1 }
256
- ]
257
- }
258
- ]
259
- }
260
- ]
261
- }
262
- };
263
- const doc = lexicalToKoguma(state);
264
- expect((doc.nodes[0] as { ordered: boolean }).ordered).toBe(true);
265
- });
266
-
267
- test('checklist — checked item', () => {
268
- const state = {
269
- root: {
270
- type: 'root',
271
- version: 1,
272
- children: [
273
- {
274
- type: 'list',
275
- version: 1,
276
- listType: 'check',
277
- start: 1,
278
- tag: 'ul',
279
- children: [
280
- {
281
- type: 'listitem',
282
- version: 1,
283
- value: 1,
284
- checked: true,
285
- children: [
286
- { type: 'text', text: 'Done', format: 0, version: 1 }
287
- ]
288
- }
289
- ]
290
- }
291
- ]
292
- }
293
- };
294
- const doc = lexicalToKoguma(state);
295
- const list = doc.nodes[0] as { items: Array<{ checked?: boolean }> };
296
- expect(list.items[0].checked).toBe(true);
297
- });
298
-
299
- test('checklist — unchecked item', () => {
300
- const state = {
301
- root: {
302
- type: 'root',
303
- version: 1,
304
- children: [
305
- {
306
- type: 'list',
307
- version: 1,
308
- listType: 'check',
309
- start: 1,
310
- tag: 'ul',
311
- children: [
312
- {
313
- type: 'listitem',
314
- version: 1,
315
- value: 1,
316
- checked: false,
317
- children: [
318
- { type: 'text', text: 'Pending', format: 0, version: 1 }
319
- ]
320
- }
321
- ]
322
- }
323
- ]
324
- }
325
- };
326
- const doc = lexicalToKoguma(state);
327
- const list = doc.nodes[0] as { items: Array<{ checked?: boolean }> };
328
- expect(list.items[0].checked).toBe(false);
329
- });
330
-
331
- test('nested list', () => {
332
- const state = {
333
- root: {
334
- type: 'root',
335
- version: 1,
336
- children: [
337
- {
338
- type: 'list',
339
- version: 1,
340
- listType: 'bullet',
341
- start: 1,
342
- tag: 'ul',
343
- children: [
344
- {
345
- type: 'listitem',
346
- version: 1,
347
- value: 1,
348
- children: [
349
- { type: 'text', text: 'Parent', format: 0, version: 1 },
350
- {
351
- type: 'list',
352
- version: 1,
353
- listType: 'bullet',
354
- start: 1,
355
- tag: 'ul',
356
- children: [
357
- {
358
- type: 'listitem',
359
- version: 1,
360
- value: 1,
361
- children: [
362
- { type: 'text', text: 'Child', format: 0, version: 1 }
363
- ]
364
- }
365
- ]
366
- }
367
- ]
368
- }
369
- ]
370
- }
371
- ]
372
- }
373
- };
374
- const doc = lexicalToKoguma(state);
375
- const list = doc.nodes[0] as {
376
- items: Array<{ nestedList?: { items: unknown[] } }>;
377
- };
378
- expect(list.items[0].nestedList).toBeDefined();
379
- expect(list.items[0].nestedList!.items).toHaveLength(1);
380
- });
381
- });
382
-
383
- // ── Quote ─────────────────────────────────────────────────────────────
384
-
385
- describe('lexicalToKoguma — quote', () => {
386
- test('blockquote wrapping a paragraph', () => {
387
- const state = {
388
- root: {
389
- type: 'root',
390
- version: 1,
391
- children: [
392
- {
393
- type: 'quote',
394
- version: 1,
395
- children: [
396
- {
397
- type: 'paragraph',
398
- version: 1,
399
- format: 0,
400
- children: [
401
- { type: 'text', text: 'A quote', format: 0, version: 1 }
402
- ]
403
- }
404
- ]
405
- }
406
- ]
407
- }
408
- };
409
- const doc = lexicalToKoguma(state);
410
- const q = doc.nodes[0] as {
411
- type: string;
412
- children: Array<{ text: string }>;
413
- };
414
- expect(q.type).toBe('quote');
415
- expect(q.children[0].text).toBe('A quote');
416
- });
417
- });
418
-
419
- // ── Code ─────────────────────────────────────────────────────────────
420
-
421
- describe('lexicalToKoguma — code block', () => {
422
- test('extracts text from CodeHighlightNode children', () => {
423
- const state = {
424
- root: {
425
- type: 'root',
426
- version: 1,
427
- children: [
428
- {
429
- type: 'code',
430
- version: 1,
431
- language: 'typescript',
432
- children: [
433
- {
434
- type: 'code-highlight',
435
- text: 'const',
436
- highlightType: 'keyword',
437
- version: 1
438
- },
439
- { type: 'text', text: ' x = 1', format: 0, version: 1 }
440
- ]
441
- }
442
- ]
443
- }
444
- };
445
- const doc = lexicalToKoguma(state);
446
- const code = doc.nodes[0] as {
447
- type: string;
448
- language?: string;
449
- text: string;
450
- };
451
- expect(code.type).toBe('code');
452
- expect(code.language).toBe('typescript');
453
- expect(code.text).toBe('const x = 1');
454
- });
455
-
456
- test('code block with linebreak', () => {
457
- const state = {
458
- root: {
459
- type: 'root',
460
- version: 1,
461
- children: [
462
- {
463
- type: 'code',
464
- version: 1,
465
- language: 'javascript',
466
- children: [
467
- { type: 'code-highlight', text: 'function foo()', version: 1 },
468
- { type: 'linebreak', version: 1 },
469
- { type: 'code-highlight', text: ' return 1', version: 1 }
470
- ]
471
- }
472
- ]
473
- }
474
- };
475
- const doc = lexicalToKoguma(state);
476
- const code = doc.nodes[0] as { text: string };
477
- expect(code.text).toBe('function foo()\n return 1');
478
- });
479
- });
480
-
481
- // ── Horizontal Rule ───────────────────────────────────────────────────
482
-
483
- describe('lexicalToKoguma — hr', () => {
484
- test('horizontal rule', () => {
485
- const state = {
486
- root: {
487
- type: 'root',
488
- version: 1,
489
- children: [{ type: 'horizontalrule', version: 1, children: [] }]
490
- }
491
- };
492
- const doc = lexicalToKoguma(state);
493
- expect(doc.nodes[0].type).toBe('hr');
494
- });
495
- });
496
-
497
- // ── Links ─────────────────────────────────────────────────────────────
498
-
499
- describe('lexicalToKoguma — links', () => {
500
- test('link node', () => {
501
- const state = {
502
- root: {
503
- type: 'root',
504
- version: 1,
505
- children: [
506
- {
507
- type: 'paragraph',
508
- version: 1,
509
- format: 0,
510
- children: [
511
- {
512
- type: 'link',
513
- version: 1,
514
- url: 'https://example.com',
515
- target: null,
516
- children: [
517
- { type: 'text', text: 'Example', format: 0, version: 1 }
518
- ]
519
- }
520
- ]
521
- }
522
- ]
523
- }
524
- };
525
- const doc = lexicalToKoguma(state);
526
- const p = doc.nodes[0] as {
527
- children: Array<{ type: string; url: string; newTab?: boolean }>;
528
- };
529
- const link = p.children[0];
530
- expect(link.type).toBe('link');
531
- expect(link.url).toBe('https://example.com');
532
- expect(link.newTab).toBeUndefined();
533
- });
534
-
535
- test('link with newTab=true', () => {
536
- const state = {
537
- root: {
538
- type: 'root',
539
- version: 1,
540
- children: [
541
- {
542
- type: 'paragraph',
543
- version: 1,
544
- format: 0,
545
- children: [
546
- {
547
- type: 'link',
548
- version: 1,
549
- url: 'https://example.com',
550
- newTab: true,
551
- children: [
552
- { type: 'text', text: 'Link', format: 0, version: 1 }
553
- ]
554
- }
555
- ]
556
- }
557
- ]
558
- }
559
- };
560
- const doc = lexicalToKoguma(state);
561
- const p = doc.nodes[0] as { children: Array<{ newTab?: boolean }> };
562
- expect(p.children[0].newTab).toBe(true);
563
- });
564
-
565
- test('autolink node', () => {
566
- const state = {
567
- root: {
568
- type: 'root',
569
- version: 1,
570
- children: [
571
- {
572
- type: 'paragraph',
573
- version: 1,
574
- format: 0,
575
- children: [
576
- {
577
- type: 'autolink',
578
- version: 1,
579
- url: 'https://auto.com',
580
- target: null,
581
- children: [
582
- { type: 'text', text: 'auto.com', format: 0, version: 1 }
583
- ]
584
- }
585
- ]
586
- }
587
- ]
588
- }
589
- };
590
- const doc = lexicalToKoguma(state);
591
- const p = doc.nodes[0] as { children: Array<{ type: string }> };
592
- expect(p.children[0].type).toBe('link');
593
- });
594
- });
595
-
596
- // ── Line break ────────────────────────────────────────────────────────
597
-
598
- describe('lexicalToKoguma — line-break', () => {
599
- test('linebreak node produces line-break inline', () => {
600
- const state = {
601
- root: {
602
- type: 'root',
603
- version: 1,
604
- children: [
605
- {
606
- type: 'paragraph',
607
- version: 1,
608
- format: 0,
609
- children: [
610
- { type: 'text', text: 'Line 1', format: 0, version: 1 },
611
- { type: 'linebreak', version: 1 },
612
- { type: 'text', text: 'Line 2', format: 0, version: 1 }
613
- ]
614
- }
615
- ]
616
- }
617
- };
618
- const doc = lexicalToKoguma(state);
619
- const p = doc.nodes[0] as { children: Array<{ type: string }> };
620
- expect(p.children[1].type).toBe('line-break');
621
- });
622
- });
623
-
624
- // ── Table ─────────────────────────────────────────────────────────────
625
-
626
- describe('lexicalToKoguma — table', () => {
627
- test('basic table with header and body rows', () => {
628
- const state = {
629
- root: {
630
- type: 'root',
631
- version: 1,
632
- children: [
633
- {
634
- type: 'table',
635
- version: 1,
636
- children: [
637
- {
638
- type: 'tablerow',
639
- version: 1,
640
- children: [
641
- {
642
- type: 'tablecell',
643
- version: 1,
644
- children: [
645
- { type: 'text', text: 'Name', format: 0, version: 1 }
646
- ]
647
- },
648
- {
649
- type: 'tablecell',
650
- version: 1,
651
- children: [
652
- { type: 'text', text: 'Age', format: 0, version: 1 }
653
- ]
654
- }
655
- ]
656
- },
657
- {
658
- type: 'tablerow',
659
- version: 1,
660
- children: [
661
- {
662
- type: 'tablecell',
663
- version: 1,
664
- children: [
665
- { type: 'text', text: 'Alice', format: 0, version: 1 }
666
- ]
667
- },
668
- {
669
- type: 'tablecell',
670
- version: 1,
671
- children: [
672
- { type: 'text', text: '30', format: 0, version: 1 }
673
- ]
674
- }
675
- ]
676
- }
677
- ]
678
- }
679
- ]
680
- }
681
- };
682
- const doc = lexicalToKoguma(state);
683
- const table = doc.nodes[0] as {
684
- type: string;
685
- rows: Array<{ isHeader?: boolean; cells: unknown[] }>;
686
- };
687
- expect(table.type).toBe('table');
688
- expect(table.rows).toHaveLength(2);
689
- expect(table.rows[0].isHeader).toBe(true);
690
- expect(table.rows[1].isHeader).toBe(false);
691
- expect(table.rows[0].cells).toHaveLength(2);
692
- });
693
- });
694
-
695
- // ── Layout ────────────────────────────────────────────────────────────
696
-
697
- describe('lexicalToKoguma — layout', () => {
698
- test('layout container with two columns', () => {
699
- const state = {
700
- root: {
701
- type: 'root',
702
- version: 1,
703
- children: [
704
- {
705
- type: 'layoutcontainer',
706
- version: 1,
707
- children: [
708
- {
709
- type: 'layoutitem',
710
- version: 1,
711
- children: [
712
- {
713
- type: 'paragraph',
714
- version: 1,
715
- format: 0,
716
- children: [
717
- { type: 'text', text: 'Column 1', format: 0, version: 1 }
718
- ]
719
- }
720
- ]
721
- },
722
- {
723
- type: 'layoutitem',
724
- version: 1,
725
- children: [
726
- {
727
- type: 'paragraph',
728
- version: 1,
729
- format: 0,
730
- children: [
731
- { type: 'text', text: 'Column 2', format: 0, version: 1 }
732
- ]
733
- }
734
- ]
735
- }
736
- ]
737
- }
738
- ]
739
- }
740
- };
741
- const doc = lexicalToKoguma(state);
742
- const layout = doc.nodes[0] as { type: string; columns: unknown[][] };
743
- expect(layout.type).toBe('layout');
744
- expect(layout.columns).toHaveLength(2);
745
- });
746
- });
747
-
748
- // ── Custom / embed nodes ──────────────────────────────────────────────
749
-
750
- describe('lexicalToKoguma — custom and embed nodes', () => {
751
- test('unknown block node falls through to custom', () => {
752
- const state = {
753
- root: {
754
- type: 'root',
755
- version: 1,
756
- children: [
757
- { type: 'tweet', version: 1, tweetId: '123456', children: [] }
758
- ]
759
- }
760
- };
761
- const doc = lexicalToKoguma(state);
762
- const node = doc.nodes[0] as {
763
- type: string;
764
- name: string;
765
- data: Record<string, unknown>;
766
- };
767
- expect(node.type).toBe('custom');
768
- expect(node.name).toBe('tweet');
769
- expect(node.data.tweetId).toBe('123456');
770
- });
771
-
772
- test('hashtag node produces custom inline', () => {
773
- const state = {
774
- root: {
775
- type: 'root',
776
- version: 1,
777
- children: [
778
- {
779
- type: 'paragraph',
780
- version: 1,
781
- format: 0,
782
- children: [
783
- { type: 'hashtag', version: 1, hashtag: 'react', text: '#react' }
784
- ]
785
- }
786
- ]
787
- }
788
- };
789
- const doc = lexicalToKoguma(state);
790
- const p = doc.nodes[0] as {
791
- children: Array<{
792
- type: string;
793
- name: string;
794
- data: Record<string, unknown>;
795
- }>;
796
- };
797
- expect(p.children[0].type).toBe('custom');
798
- expect(p.children[0].name).toBe('hashtag');
799
- expect(p.children[0].data.tag).toBe('react');
800
- });
801
-
802
- test('emoji node flattens to text', () => {
803
- const state = {
804
- root: {
805
- type: 'root',
806
- version: 1,
807
- children: [
808
- {
809
- type: 'paragraph',
810
- version: 1,
811
- format: 0,
812
- children: [{ type: 'emoji', version: 1, text: '😀' }]
813
- }
814
- ]
815
- }
816
- };
817
- const doc = lexicalToKoguma(state);
818
- const p = doc.nodes[0] as {
819
- children: Array<{ type: string; text: string }>;
820
- };
821
- expect(p.children[0].type).toBe('text');
822
- expect(p.children[0].text).toBe('😀');
823
- });
824
-
825
- test('keyword and autocomplete nodes are stripped', () => {
826
- const state = {
827
- root: {
828
- type: 'root',
829
- version: 1,
830
- children: [
831
- {
832
- type: 'paragraph',
833
- version: 1,
834
- format: 0,
835
- children: [
836
- { type: 'text', text: 'Hello', format: 0, version: 1 },
837
- { type: 'keyword', version: 1, text: 'keyword' },
838
- { type: 'autocomplete', version: 1, text: 'ghost' }
839
- ]
840
- }
841
- ]
842
- }
843
- };
844
- const doc = lexicalToKoguma(state);
845
- const p = doc.nodes[0] as { children: unknown[] };
846
- expect(p.children).toHaveLength(1); // only the text node
847
- });
848
-
849
- test('overflow and autocomplete block nodes are stripped', () => {
850
- const state = {
851
- root: {
852
- type: 'root',
853
- version: 1,
854
- children: [
855
- {
856
- type: 'paragraph',
857
- version: 1,
858
- format: 0,
859
- children: [{ type: 'text', text: 'Hi', format: 0, version: 1 }]
860
- },
861
- { type: 'overflow', version: 1, children: [] },
862
- { type: 'autocomplete', version: 1, children: [] }
863
- ]
864
- }
865
- };
866
- const doc = lexicalToKoguma(state);
867
- expect(doc.nodes).toHaveLength(1); // only the paragraph
868
- });
869
- });
870
-
871
- // ── Image ─────────────────────────────────────────────────────────────
872
-
873
- describe('lexicalToKoguma — image', () => {
874
- test('block image', () => {
875
- const state = {
876
- root: {
877
- type: 'root',
878
- version: 1,
879
- children: [
880
- {
881
- type: 'image',
882
- version: 1,
883
- src: 'https://example.com/img.jpg',
884
- altText: 'A photo',
885
- width: 800,
886
- height: 600,
887
- children: []
888
- }
889
- ]
890
- }
891
- };
892
- const doc = lexicalToKoguma(state);
893
- const img = doc.nodes[0] as {
894
- type: string;
895
- url: string;
896
- alt?: string;
897
- width?: number;
898
- height?: number;
899
- };
900
- expect(img.type).toBe('image');
901
- expect(img.url).toBe('https://example.com/img.jpg');
902
- expect(img.alt).toBe('A photo');
903
- expect(img.width).toBe(800);
904
- expect(img.height).toBe(600);
905
- });
906
- });