draftly 2.0.0 → 2.1.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/{chunk-BWJLMREN.cjs → chunk-EQUQHE2E.cjs} +26 -24
- package/dist/chunk-EQUQHE2E.cjs.map +1 -0
- package/dist/{chunk-L2XSK57Y.js → chunk-NRPI5O6Y.js} +1267 -551
- package/dist/chunk-NRPI5O6Y.js.map +1 -0
- package/dist/{chunk-W5ALMXG2.cjs → chunk-OMFUE4AQ.cjs} +1304 -588
- package/dist/chunk-OMFUE4AQ.cjs.map +1 -0
- package/dist/{chunk-EEHILRG5.js → chunk-TD3L5C45.js} +13 -11
- package/dist/chunk-TD3L5C45.js.map +1 -0
- package/dist/{chunk-ZUI3GI3W.js → chunk-UCHBDJ4R.js} +22 -20
- package/dist/chunk-UCHBDJ4R.js.map +1 -0
- package/dist/{chunk-TBVZEK2H.cjs → chunk-W75QUUQC.cjs} +13 -11
- package/dist/chunk-W75QUUQC.cjs.map +1 -0
- package/dist/editor/index.cjs +16 -16
- package/dist/editor/index.js +2 -2
- package/dist/index.cjs +33 -33
- package/dist/index.js +3 -3
- package/dist/plugins/index.cjs +19 -19
- package/dist/plugins/index.d.cts +77 -42
- package/dist/plugins/index.d.ts +77 -42
- package/dist/plugins/index.js +3 -3
- package/dist/preview/index.cjs +1 -1
- package/dist/preview/index.js +1 -1
- package/package.json +1 -1
- package/src/editor/draftly.ts +29 -27
- package/src/editor/utils.ts +13 -11
- package/src/plugins/table-plugin.ts +1759 -900
- package/dist/chunk-BWJLMREN.cjs.map +0 -1
- package/dist/chunk-EEHILRG5.js.map +0 -1
- package/dist/chunk-L2XSK57Y.js.map +0 -1
- package/dist/chunk-TBVZEK2H.cjs.map +0 -1
- package/dist/chunk-W5ALMXG2.cjs.map +0 -1
- package/dist/chunk-ZUI3GI3W.js.map +0 -1
|
@@ -1,900 +1,1759 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { syntaxTree } from "@codemirror/language";
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
)
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
return
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
"
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
}
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
const
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
}
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
}
|
|
594
|
-
}
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
}
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
}
|
|
644
|
-
}
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
}
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
}
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
}
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
1
|
+
import { Annotation, EditorState, Extension, Prec, Range, RangeSet } from "@codemirror/state";
|
|
2
|
+
import { syntaxTree } from "@codemirror/language";
|
|
3
|
+
import { BlockWrapper, Decoration, EditorView, KeyBinding, WidgetType, keymap } from "@codemirror/view";
|
|
4
|
+
import { SyntaxNode } from "@lezer/common";
|
|
5
|
+
import { MarkdownConfig, Table } from "@lezer/markdown";
|
|
6
|
+
import { createTheme } from "../editor";
|
|
7
|
+
import { DraftlyConfig } from "../editor/draftly";
|
|
8
|
+
import { DecorationContext, DecorationPlugin, PluginContext } from "../editor/plugin";
|
|
9
|
+
import { ThemeEnum } from "../editor/utils";
|
|
10
|
+
import { PreviewRenderer } from "../preview/renderer";
|
|
11
|
+
|
|
12
|
+
type Alignment = "left" | "center" | "right";
|
|
13
|
+
type TableRowKind = "header" | "body";
|
|
14
|
+
|
|
15
|
+
interface ParsedTable {
|
|
16
|
+
headers: string[];
|
|
17
|
+
alignments: Alignment[];
|
|
18
|
+
rows: string[][];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
interface PreviewContextLike {
|
|
22
|
+
sliceDoc(from: number, to: number): string;
|
|
23
|
+
sanitize(html: string): string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
interface TableCellInfo {
|
|
27
|
+
rowKind: TableRowKind;
|
|
28
|
+
rowIndex: number;
|
|
29
|
+
columnIndex: number;
|
|
30
|
+
from: number;
|
|
31
|
+
to: number;
|
|
32
|
+
contentFrom: number;
|
|
33
|
+
contentTo: number;
|
|
34
|
+
lineFrom: number;
|
|
35
|
+
lineNumber: number;
|
|
36
|
+
rawText: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
interface TableInfo {
|
|
40
|
+
from: number;
|
|
41
|
+
to: number;
|
|
42
|
+
startLineNumber: number;
|
|
43
|
+
delimiterLineNumber: number;
|
|
44
|
+
endLineNumber: number;
|
|
45
|
+
columnCount: number;
|
|
46
|
+
alignments: Alignment[];
|
|
47
|
+
cellsByRow: TableCellInfo[][];
|
|
48
|
+
headerCells: TableCellInfo[];
|
|
49
|
+
bodyCells: TableCellInfo[][];
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const BREAK_TAG = "<br />";
|
|
53
|
+
const BREAK_TAG_REGEX = /<br\s*\/?>/gi;
|
|
54
|
+
const DELIMITER_CELL_PATTERN = /^:?-{3,}:?$/;
|
|
55
|
+
const TABLE_SUB_NODE_NAMES = new Set(["TableHeader", "TableDelimiter", "TableRow", "TableCell"]);
|
|
56
|
+
const TABLE_TEMPLATE: ParsedTable = {
|
|
57
|
+
headers: ["Header 1", "Header 2", "Header 3"],
|
|
58
|
+
alignments: ["left", "left", "left"],
|
|
59
|
+
rows: [["", "", ""]],
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const normalizeAnnotation = Annotation.define<boolean>();
|
|
63
|
+
const repairSelectionAnnotation = Annotation.define<boolean>();
|
|
64
|
+
const pipeReplace = Decoration.replace({});
|
|
65
|
+
const delimiterReplace = Decoration.replace({});
|
|
66
|
+
|
|
67
|
+
const tableBlockWrapper = BlockWrapper.create({
|
|
68
|
+
tagName: "div",
|
|
69
|
+
attributes: { class: "cm-draftly-table-wrapper" },
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
class TableBreakWidget extends WidgetType {
|
|
73
|
+
/** Reuses the same widget instance for identical break markers. */
|
|
74
|
+
override eq(): boolean {
|
|
75
|
+
return true;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Renders an inline `<br />` placeholder inside a table cell. */
|
|
79
|
+
toDOM(): HTMLElement {
|
|
80
|
+
const span = document.createElement("span");
|
|
81
|
+
span.className = "cm-draftly-table-break";
|
|
82
|
+
span.setAttribute("aria-label", "line break");
|
|
83
|
+
span.appendChild(document.createElement("br"));
|
|
84
|
+
return span;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Allows the editor to observe events on the rendered break widget. */
|
|
88
|
+
override ignoreEvent(): boolean {
|
|
89
|
+
return false;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
class TableControlsWidget extends WidgetType {
|
|
94
|
+
constructor(
|
|
95
|
+
private readonly onAddRow: (view: EditorView) => void,
|
|
96
|
+
private readonly onAddColumn: (view: EditorView) => void
|
|
97
|
+
) {
|
|
98
|
+
super();
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** Forces the control widget to be recreated so handlers stay current. */
|
|
102
|
+
override eq(): boolean {
|
|
103
|
+
return false;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** Renders the hover controls used to append rows and columns. */
|
|
107
|
+
toDOM(view: EditorView): HTMLElement {
|
|
108
|
+
const anchor = document.createElement("span");
|
|
109
|
+
anchor.className = "cm-draftly-table-controls-anchor";
|
|
110
|
+
anchor.setAttribute("aria-hidden", "true");
|
|
111
|
+
|
|
112
|
+
const rightButton = this.createButton("Add column", "cm-draftly-table-control cm-draftly-table-control-column");
|
|
113
|
+
rightButton.addEventListener("mousedown", (event) => {
|
|
114
|
+
event.preventDefault();
|
|
115
|
+
event.stopPropagation();
|
|
116
|
+
this.onAddColumn(view);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
const bottomButton = this.createButton("Add row", "cm-draftly-table-control cm-draftly-table-control-row");
|
|
120
|
+
bottomButton.addEventListener("mousedown", (event) => {
|
|
121
|
+
event.preventDefault();
|
|
122
|
+
event.stopPropagation();
|
|
123
|
+
this.onAddRow(view);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
anchor.append(rightButton, bottomButton);
|
|
127
|
+
return anchor;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** Lets button events bubble through the widget. */
|
|
131
|
+
override ignoreEvent(): boolean {
|
|
132
|
+
return false;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** Builds a single control button with the provided label and class. */
|
|
136
|
+
private createButton(label: string, className: string): HTMLButtonElement {
|
|
137
|
+
const button = document.createElement("button");
|
|
138
|
+
button.type = "button";
|
|
139
|
+
button.className = className;
|
|
140
|
+
button.setAttribute("tabindex", "-1");
|
|
141
|
+
button.setAttribute("aria-label", label);
|
|
142
|
+
button.textContent = "+";
|
|
143
|
+
return button;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** Returns whether the character at the given index is backslash-escaped. */
|
|
148
|
+
function isEscaped(text: string, index: number): boolean {
|
|
149
|
+
let slashCount = 0;
|
|
150
|
+
for (let i = index - 1; i >= 0 && text[i] === "\\"; i--) {
|
|
151
|
+
slashCount++;
|
|
152
|
+
}
|
|
153
|
+
return slashCount % 2 === 1;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/** Collects the positions of every unescaped pipe character in a line. */
|
|
157
|
+
function getPipePositions(lineText: string): number[] {
|
|
158
|
+
const positions: number[] = [];
|
|
159
|
+
for (let index = 0; index < lineText.length; index++) {
|
|
160
|
+
if (lineText[index] === "|" && !isEscaped(lineText, index)) {
|
|
161
|
+
positions.push(index);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
return positions;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** Splits a markdown table row into raw cell strings. */
|
|
168
|
+
function splitTableLine(lineText: string): string[] {
|
|
169
|
+
const cells: string[] = [];
|
|
170
|
+
const trimmed = lineText.trim();
|
|
171
|
+
|
|
172
|
+
if (!trimmed.includes("|")) {
|
|
173
|
+
return [trimmed];
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
let current = "";
|
|
177
|
+
for (let index = 0; index < trimmed.length; index++) {
|
|
178
|
+
const char = trimmed[index]!;
|
|
179
|
+
if (char === "|" && !isEscaped(trimmed, index)) {
|
|
180
|
+
cells.push(current);
|
|
181
|
+
current = "";
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
current += char;
|
|
185
|
+
}
|
|
186
|
+
cells.push(current);
|
|
187
|
+
|
|
188
|
+
if (trimmed.startsWith("|")) {
|
|
189
|
+
cells.shift();
|
|
190
|
+
}
|
|
191
|
+
if (trimmed.endsWith("|")) {
|
|
192
|
+
cells.pop();
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
return cells;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/** Checks whether the given line can participate in a table block. */
|
|
199
|
+
function isTableRowLine(lineText: string): boolean {
|
|
200
|
+
return getPipePositions(lineText.trim()).length > 0;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/** Parses a delimiter cell token into a table alignment value. */
|
|
204
|
+
function parseAlignment(cell: string): Alignment {
|
|
205
|
+
const trimmed = cell.trim();
|
|
206
|
+
const left = trimmed.startsWith(":");
|
|
207
|
+
const right = trimmed.endsWith(":");
|
|
208
|
+
if (left && right) return "center";
|
|
209
|
+
if (right) return "right";
|
|
210
|
+
return "left";
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/** Parses the delimiter line and returns per-column alignments. */
|
|
214
|
+
function parseDelimiterAlignments(lineText: string): Alignment[] | null {
|
|
215
|
+
const cells = splitTableLine(lineText).map((cell) => cell.trim());
|
|
216
|
+
if (cells.length === 0 || !cells.every((cell) => DELIMITER_CELL_PATTERN.test(cell))) {
|
|
217
|
+
return null;
|
|
218
|
+
}
|
|
219
|
+
return cells.map(parseAlignment);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/** Splits a table node slice into its table lines and any trailing markdown. */
|
|
223
|
+
function splitTableAndTrailingMarkdown(markdown: string): { tableMarkdown: string; trailingMarkdown: string } {
|
|
224
|
+
const lines = markdown.split("\n");
|
|
225
|
+
if (lines.length < 2) {
|
|
226
|
+
return { tableMarkdown: markdown, trailingMarkdown: "" };
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const headerLine = lines[0] || "";
|
|
230
|
+
const delimiterLine = lines[1] || "";
|
|
231
|
+
if (!isTableRowLine(headerLine) || !parseDelimiterAlignments(delimiterLine)) {
|
|
232
|
+
return { tableMarkdown: markdown, trailingMarkdown: "" };
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
let endIndex = 1;
|
|
236
|
+
for (let index = 2; index < lines.length; index++) {
|
|
237
|
+
if (!isTableRowLine(lines[index] || "")) {
|
|
238
|
+
break;
|
|
239
|
+
}
|
|
240
|
+
endIndex = index;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
return {
|
|
244
|
+
tableMarkdown: lines.slice(0, endIndex + 1).join("\n"),
|
|
245
|
+
trailingMarkdown: lines.slice(endIndex + 1).join("\n"),
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/** Normalizes every supported `<br>` form to the canonical `<br />` token. */
|
|
250
|
+
function canonicalizeBreakTags(text: string): string {
|
|
251
|
+
return text.replace(BREAK_TAG_REGEX, BREAK_TAG);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/** Escapes literal pipe characters so cell content stays GFM-compatible. */
|
|
255
|
+
function escapeUnescapedPipes(text: string): string {
|
|
256
|
+
let result = "";
|
|
257
|
+
for (let index = 0; index < text.length; index++) {
|
|
258
|
+
const char = text[index]!;
|
|
259
|
+
if (char === "|" && !isEscaped(text, index)) {
|
|
260
|
+
result += "\\|";
|
|
261
|
+
continue;
|
|
262
|
+
}
|
|
263
|
+
result += char;
|
|
264
|
+
}
|
|
265
|
+
return result;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/** Trims and normalizes cell content before it is written back to markdown. */
|
|
269
|
+
function normalizeCellContent(text: string): string {
|
|
270
|
+
const normalizedBreaks = canonicalizeBreakTags(text.trim());
|
|
271
|
+
if (!normalizedBreaks) {
|
|
272
|
+
return "";
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
const parts = normalizedBreaks.split(BREAK_TAG_REGEX).map((part) => escapeUnescapedPipes(part.trim()));
|
|
276
|
+
if (parts.length === 1) {
|
|
277
|
+
return parts[0] || "";
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
return parts.join(` ${BREAK_TAG} `).trim();
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/** Measures the visible width of a cell for markdown alignment output. */
|
|
284
|
+
function renderWidth(text: string): number {
|
|
285
|
+
return canonicalizeBreakTags(text).replace(BREAK_TAG, " ").replace(/\\\|/g, "|").length;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/** Pads a cell according to its alignment for normalized markdown output. */
|
|
289
|
+
function padCell(text: string, width: number, alignment: Alignment): string {
|
|
290
|
+
const safeWidth = Math.max(width, renderWidth(text));
|
|
291
|
+
const difference = safeWidth - renderWidth(text);
|
|
292
|
+
if (difference <= 0) {
|
|
293
|
+
return text;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
if (alignment === "right") {
|
|
297
|
+
return " ".repeat(difference) + text;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
if (alignment === "center") {
|
|
301
|
+
const left = Math.floor(difference / 2);
|
|
302
|
+
const right = difference - left;
|
|
303
|
+
return " ".repeat(left) + text + " ".repeat(right);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
return text + " ".repeat(difference);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/** Builds the markdown delimiter token for a column. */
|
|
310
|
+
function delimiterCell(width: number, alignment: Alignment): string {
|
|
311
|
+
const hyphenCount = Math.max(width, 3);
|
|
312
|
+
if (alignment === "center") {
|
|
313
|
+
return ":" + "-".repeat(Math.max(1, hyphenCount - 2)) + ":";
|
|
314
|
+
}
|
|
315
|
+
if (alignment === "right") {
|
|
316
|
+
return "-".repeat(Math.max(2, hyphenCount - 1)) + ":";
|
|
317
|
+
}
|
|
318
|
+
return "-".repeat(hyphenCount);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/** Parses a markdown table block into header, alignment, and body rows. */
|
|
322
|
+
function parseTableMarkdown(markdown: string): ParsedTable | null {
|
|
323
|
+
const { tableMarkdown } = splitTableAndTrailingMarkdown(markdown);
|
|
324
|
+
const lines = tableMarkdown.split("\n");
|
|
325
|
+
if (lines.length < 2) {
|
|
326
|
+
return null;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
const headers = splitTableLine(lines[0] || "").map((cell) => cell.trim());
|
|
330
|
+
const alignments = parseDelimiterAlignments(lines[1] || "");
|
|
331
|
+
if (!alignments) {
|
|
332
|
+
return null;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
const rows = lines
|
|
336
|
+
.slice(2)
|
|
337
|
+
.filter((line) => isTableRowLine(line))
|
|
338
|
+
.map((line) => splitTableLine(line).map((cell) => cell.trim()));
|
|
339
|
+
|
|
340
|
+
return { headers, alignments, rows };
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/** Expands all rows so the parsed table has a consistent column count. */
|
|
344
|
+
function normalizeParsedTable(parsed: ParsedTable): ParsedTable {
|
|
345
|
+
const columnCount = Math.max(
|
|
346
|
+
parsed.headers.length,
|
|
347
|
+
parsed.alignments.length,
|
|
348
|
+
...parsed.rows.map((row) => row.length),
|
|
349
|
+
1
|
|
350
|
+
);
|
|
351
|
+
|
|
352
|
+
const headers = Array.from({ length: columnCount }, (_, index) => normalizeCellContent(parsed.headers[index] || ""));
|
|
353
|
+
const alignments = Array.from({ length: columnCount }, (_, index) => parsed.alignments[index] || "left");
|
|
354
|
+
const rows = parsed.rows.map((row) =>
|
|
355
|
+
Array.from({ length: columnCount }, (_, index) => normalizeCellContent(row[index] || ""))
|
|
356
|
+
);
|
|
357
|
+
|
|
358
|
+
return { headers, alignments, rows };
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/** Formats a parsed table back into normalized GFM markdown. */
|
|
362
|
+
function formatTableMarkdown(parsed: ParsedTable): string {
|
|
363
|
+
const normalized = normalizeParsedTable(parsed);
|
|
364
|
+
const widths = normalized.headers.map((header, index) =>
|
|
365
|
+
Math.max(renderWidth(header), ...normalized.rows.map((row) => renderWidth(row[index] || "")), 3)
|
|
366
|
+
);
|
|
367
|
+
|
|
368
|
+
const formatRow = (cells: string[]) =>
|
|
369
|
+
`| ${cells.map((cell, index) => padCell(cell, widths[index] || 3, normalized.alignments[index] || "left")).join(" | ")} |`;
|
|
370
|
+
|
|
371
|
+
const headerLine = formatRow(normalized.headers);
|
|
372
|
+
const delimiterLine = `| ${normalized.alignments
|
|
373
|
+
.map((alignment, index) => delimiterCell(widths[index] || 3, alignment))
|
|
374
|
+
.join(" | ")} |`;
|
|
375
|
+
const bodyLines = normalized.rows.map((row) => formatRow(row));
|
|
376
|
+
|
|
377
|
+
return [headerLine, delimiterLine, ...bodyLines].join("\n");
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
/** Creates a blank row with the requested number of columns. */
|
|
381
|
+
function buildEmptyRow(columnCount: number): string[] {
|
|
382
|
+
return Array.from({ length: columnCount }, () => "");
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
/** Creates a preview renderer that skips paragraph wrapping inside cells. */
|
|
386
|
+
function createPreviewRenderer(markdown: string, config?: DraftlyConfig): PreviewRenderer {
|
|
387
|
+
const plugins = (config?.plugins || []).filter((plugin) => plugin.name !== "paragraph");
|
|
388
|
+
return new PreviewRenderer(markdown, plugins, config?.markdown || [], config?.theme || ThemeEnum.AUTO, true);
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
/** Removes a single top-level paragraph wrapper from preview HTML. */
|
|
392
|
+
function stripSingleParagraph(html: string): string {
|
|
393
|
+
const trimmed = html.trim();
|
|
394
|
+
const match = trimmed.match(/^<p\b[^>]*>([\s\S]*)<\/p>$/i);
|
|
395
|
+
return match?.[1] || trimmed;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
/** Renders one table cell through the preview pipeline. */
|
|
399
|
+
async function renderCellToHtml(text: string, config?: DraftlyConfig): Promise<string> {
|
|
400
|
+
if (!text.trim()) {
|
|
401
|
+
return " ";
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
return stripSingleParagraph(await createPreviewRenderer(text, config).render());
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
/** Renders a parsed table into semantic preview HTML. */
|
|
408
|
+
async function renderTableToHtml(parsed: ParsedTable, config?: DraftlyConfig): Promise<string> {
|
|
409
|
+
const normalized = normalizeParsedTable(parsed);
|
|
410
|
+
let html = '<div class="cm-draftly-table-widget"><table class="cm-draftly-table cm-draftly-table-preview">';
|
|
411
|
+
html += '<thead><tr class="cm-draftly-table-row cm-draftly-table-header-row">';
|
|
412
|
+
|
|
413
|
+
for (let index = 0; index < normalized.headers.length; index++) {
|
|
414
|
+
const alignment = normalized.alignments[index] || "left";
|
|
415
|
+
const content = await renderCellToHtml(normalized.headers[index] || "", config);
|
|
416
|
+
html += `<th class="cm-draftly-table-cell cm-draftly-table-th${
|
|
417
|
+
alignment === "center"
|
|
418
|
+
? " cm-draftly-table-cell-center"
|
|
419
|
+
: alignment === "right"
|
|
420
|
+
? " cm-draftly-table-cell-right"
|
|
421
|
+
: ""
|
|
422
|
+
}${index === normalized.headers.length - 1 ? " cm-draftly-table-cell-last" : ""}">${content}</th>`;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
html += "</tr></thead><tbody>";
|
|
426
|
+
|
|
427
|
+
for (let rowIndex = 0; rowIndex < normalized.rows.length; rowIndex++) {
|
|
428
|
+
const row = normalized.rows[rowIndex] || [];
|
|
429
|
+
html += `<tr class="cm-draftly-table-row cm-draftly-table-body-row${
|
|
430
|
+
rowIndex % 2 === 1 ? " cm-draftly-table-row-even" : ""
|
|
431
|
+
}${rowIndex === normalized.rows.length - 1 ? " cm-draftly-table-row-last" : ""}">`;
|
|
432
|
+
|
|
433
|
+
for (let index = 0; index < normalized.headers.length; index++) {
|
|
434
|
+
const alignment = normalized.alignments[index] || "left";
|
|
435
|
+
const content = await renderCellToHtml(row[index] || "", config);
|
|
436
|
+
html += `<td class="cm-draftly-table-cell${
|
|
437
|
+
alignment === "center"
|
|
438
|
+
? " cm-draftly-table-cell-center"
|
|
439
|
+
: alignment === "right"
|
|
440
|
+
? " cm-draftly-table-cell-right"
|
|
441
|
+
: ""
|
|
442
|
+
}${index === normalized.headers.length - 1 ? " cm-draftly-table-cell-last" : ""}">${content}</td>`;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
html += "</tr>";
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
html += "</tbody></table></div>";
|
|
449
|
+
return html;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
/** Finds the visible content bounds inside a raw table cell span. */
|
|
453
|
+
function getVisibleBounds(rawCellText: string): { startOffset: number; endOffset: number } {
|
|
454
|
+
const leading = rawCellText.length - rawCellText.trimStart().length;
|
|
455
|
+
const trailing = rawCellText.length - rawCellText.trimEnd().length;
|
|
456
|
+
const trimmedLength = rawCellText.trim().length;
|
|
457
|
+
|
|
458
|
+
if (trimmedLength === 0) {
|
|
459
|
+
const placeholderOffset = Math.min(Math.floor(rawCellText.length / 2), Math.max(rawCellText.length - 1, 0));
|
|
460
|
+
return {
|
|
461
|
+
startOffset: placeholderOffset,
|
|
462
|
+
endOffset: Math.min(placeholderOffset + 1, rawCellText.length),
|
|
463
|
+
};
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
return {
|
|
467
|
+
startOffset: leading,
|
|
468
|
+
endOffset: rawCellText.length - trailing,
|
|
469
|
+
};
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
/** Returns whether every cell in a body row is empty. */
|
|
473
|
+
function isBodyRowEmpty(row: TableCellInfo[]): boolean {
|
|
474
|
+
return row.every((cell) => normalizeCellContent(cell.rawText) === "");
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
/** Converts the live editor table model into a serializable table structure. */
|
|
478
|
+
function buildTableFromInfo(tableInfo: TableInfo): ParsedTable {
|
|
479
|
+
return {
|
|
480
|
+
headers: tableInfo.headerCells.map((cell) => normalizeCellContent(cell.rawText)),
|
|
481
|
+
alignments: [...tableInfo.alignments],
|
|
482
|
+
rows: tableInfo.bodyCells.map((row) => row.map((cell) => normalizeCellContent(cell.rawText))),
|
|
483
|
+
};
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
/** Maps a logical row index to its physical line index in formatted markdown. */
|
|
487
|
+
function getRowLineIndex(rowIndex: number): number {
|
|
488
|
+
return rowIndex === 0 ? 0 : rowIndex + 1;
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
/** Resolves the caret anchor for a cell inside normalized table markdown. */
|
|
492
|
+
function getCellAnchorInFormattedTable(
|
|
493
|
+
formattedTable: string,
|
|
494
|
+
rowIndex: number,
|
|
495
|
+
columnIndex: number,
|
|
496
|
+
offset = 0
|
|
497
|
+
): number {
|
|
498
|
+
const lines = formattedTable.split("\n");
|
|
499
|
+
const lineIndex = getRowLineIndex(rowIndex);
|
|
500
|
+
const lineText = lines[lineIndex] || "";
|
|
501
|
+
const pipes = getPipePositions(lineText);
|
|
502
|
+
|
|
503
|
+
if (pipes.length < columnIndex + 2) {
|
|
504
|
+
return formattedTable.length;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
const rawFrom = pipes[columnIndex]! + 1;
|
|
508
|
+
const rawTo = pipes[columnIndex + 1]!;
|
|
509
|
+
const visible = getVisibleBounds(lineText.slice(rawFrom, rawTo));
|
|
510
|
+
const lineOffset = lines.slice(0, lineIndex).reduce((sum, line) => sum + line.length + 1, 0);
|
|
511
|
+
return (
|
|
512
|
+
lineOffset +
|
|
513
|
+
Math.min(rawFrom + visible.startOffset + offset, rawFrom + Math.max(visible.endOffset - 1, visible.startOffset))
|
|
514
|
+
);
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
/** Wraps a table replacement with the required blank spacer lines. */
|
|
518
|
+
function createTableInsert(state: EditorState, from: number, to: number, tableMarkdown: string) {
|
|
519
|
+
let insert = tableMarkdown;
|
|
520
|
+
let prefixLength = 0;
|
|
521
|
+
|
|
522
|
+
const startLine = state.doc.lineAt(from);
|
|
523
|
+
if (startLine.number === 1 || state.doc.line(startLine.number - 1).text.trim() !== "") {
|
|
524
|
+
insert = "\n" + insert;
|
|
525
|
+
prefixLength = 1;
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
const endLine = state.doc.lineAt(Math.max(from, to));
|
|
529
|
+
if (endLine.number === state.doc.lines || state.doc.line(endLine.number + 1).text.trim() !== "") {
|
|
530
|
+
insert += "\n";
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
return { from, to, insert, prefixLength };
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
/** Builds a live table model from the current editor document. */
|
|
537
|
+
function readTableInfo(state: EditorState, nodeFrom: number, nodeTo: number): TableInfo | null {
|
|
538
|
+
const startLine = state.doc.lineAt(nodeFrom);
|
|
539
|
+
const endLine = state.doc.lineAt(nodeTo);
|
|
540
|
+
const delimiterLineNumber = startLine.number + 1;
|
|
541
|
+
if (delimiterLineNumber > endLine.number) {
|
|
542
|
+
return null;
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
const delimiterLine = state.doc.line(delimiterLineNumber);
|
|
546
|
+
const alignments = parseDelimiterAlignments(delimiterLine.text);
|
|
547
|
+
if (!alignments) {
|
|
548
|
+
return null;
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
let effectiveEndLineNumber = delimiterLineNumber;
|
|
552
|
+
for (let lineNumber = delimiterLineNumber + 1; lineNumber <= endLine.number; lineNumber++) {
|
|
553
|
+
const line = state.doc.line(lineNumber);
|
|
554
|
+
if (!isTableRowLine(line.text)) {
|
|
555
|
+
break;
|
|
556
|
+
}
|
|
557
|
+
effectiveEndLineNumber = lineNumber;
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
const cellsByRow: TableCellInfo[][] = [];
|
|
561
|
+
for (let lineNumber = startLine.number; lineNumber <= effectiveEndLineNumber; lineNumber++) {
|
|
562
|
+
if (lineNumber === delimiterLineNumber) {
|
|
563
|
+
continue;
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
const line = state.doc.line(lineNumber);
|
|
567
|
+
const pipes = getPipePositions(line.text);
|
|
568
|
+
if (pipes.length < 2) {
|
|
569
|
+
return null;
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
const isHeader = lineNumber === startLine.number;
|
|
573
|
+
const rowIndex = isHeader ? 0 : cellsByRow.length;
|
|
574
|
+
const cells: TableCellInfo[] = [];
|
|
575
|
+
|
|
576
|
+
for (let columnIndex = 0; columnIndex < pipes.length - 1; columnIndex++) {
|
|
577
|
+
const from = line.from + pipes[columnIndex]! + 1;
|
|
578
|
+
const to = line.from + pipes[columnIndex + 1]!;
|
|
579
|
+
const rawText = line.text.slice(pipes[columnIndex]! + 1, pipes[columnIndex + 1]);
|
|
580
|
+
const visible = getVisibleBounds(rawText);
|
|
581
|
+
|
|
582
|
+
cells.push({
|
|
583
|
+
rowKind: isHeader ? "header" : "body",
|
|
584
|
+
rowIndex,
|
|
585
|
+
columnIndex,
|
|
586
|
+
from,
|
|
587
|
+
to,
|
|
588
|
+
contentFrom: from + visible.startOffset,
|
|
589
|
+
contentTo: from + visible.endOffset,
|
|
590
|
+
lineFrom: line.from,
|
|
591
|
+
lineNumber,
|
|
592
|
+
rawText,
|
|
593
|
+
});
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
cellsByRow.push(cells);
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
if (cellsByRow.length === 0) {
|
|
600
|
+
return null;
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
return {
|
|
604
|
+
from: startLine.from,
|
|
605
|
+
to: state.doc.line(effectiveEndLineNumber).to,
|
|
606
|
+
startLineNumber: startLine.number,
|
|
607
|
+
delimiterLineNumber,
|
|
608
|
+
endLineNumber: effectiveEndLineNumber,
|
|
609
|
+
columnCount: cellsByRow[0]!.length,
|
|
610
|
+
alignments: Array.from({ length: cellsByRow[0]!.length }, (_, index) => alignments[index] || "left"),
|
|
611
|
+
cellsByRow,
|
|
612
|
+
headerCells: cellsByRow[0]!,
|
|
613
|
+
bodyCells: cellsByRow.slice(1),
|
|
614
|
+
};
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
/** Finds the table model that contains the given document position. */
|
|
618
|
+
function getTableInfoAtPosition(state: EditorState, position: number): TableInfo | null {
|
|
619
|
+
let resolved: TableInfo | null = null;
|
|
620
|
+
|
|
621
|
+
syntaxTree(state).iterate({
|
|
622
|
+
enter: (node) => {
|
|
623
|
+
if (resolved || node.name !== "Table") {
|
|
624
|
+
return;
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
const info = readTableInfo(state, node.from, node.to);
|
|
628
|
+
if (info && position >= info.from && position <= info.to) {
|
|
629
|
+
resolved = info;
|
|
630
|
+
}
|
|
631
|
+
},
|
|
632
|
+
});
|
|
633
|
+
|
|
634
|
+
return resolved;
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
/** Returns the table cell containing the given cursor position. */
|
|
638
|
+
function findCellAtPosition(tableInfo: TableInfo, position: number): TableCellInfo | null {
|
|
639
|
+
for (const row of tableInfo.cellsByRow) {
|
|
640
|
+
for (const cell of row) {
|
|
641
|
+
if (position >= cell.from && position <= cell.to) {
|
|
642
|
+
return cell;
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
for (const row of tableInfo.cellsByRow) {
|
|
648
|
+
for (const cell of row) {
|
|
649
|
+
if (position >= cell.from - 1 && position <= cell.to + 1) {
|
|
650
|
+
return cell;
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
let nearestCell: TableCellInfo | null = null;
|
|
656
|
+
let nearestDistance = Number.POSITIVE_INFINITY;
|
|
657
|
+
for (const row of tableInfo.cellsByRow) {
|
|
658
|
+
for (const cell of row) {
|
|
659
|
+
const distance = Math.min(Math.abs(position - cell.from), Math.abs(position - cell.to));
|
|
660
|
+
if (distance < nearestDistance) {
|
|
661
|
+
nearestCell = cell;
|
|
662
|
+
nearestDistance = distance;
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
return nearestCell;
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
/** Clamps a document position into the editable content span of a cell. */
|
|
671
|
+
function clampCellPosition(cell: TableCellInfo, position: number): number {
|
|
672
|
+
const cellEnd = Math.max(cell.contentFrom, cell.contentTo);
|
|
673
|
+
return Math.max(cell.contentFrom, Math.min(position, cellEnd));
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
/** Collects all `<br />` token ranges from the current table. */
|
|
677
|
+
function collectBreakRanges(tableInfo: TableInfo): Array<{ from: number; to: number }> {
|
|
678
|
+
const ranges: Array<{ from: number; to: number }> = [];
|
|
679
|
+
|
|
680
|
+
for (const row of tableInfo.cellsByRow) {
|
|
681
|
+
for (const cell of row) {
|
|
682
|
+
let match: RegExpExecArray | null;
|
|
683
|
+
const regex = new RegExp(BREAK_TAG_REGEX);
|
|
684
|
+
while ((match = regex.exec(cell.rawText)) !== null) {
|
|
685
|
+
ranges.push({
|
|
686
|
+
from: cell.from + match.index,
|
|
687
|
+
to: cell.from + match.index + match[0].length,
|
|
688
|
+
});
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
return ranges;
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
const lineDecorations = {
|
|
697
|
+
header: Decoration.line({ class: "cm-draftly-table-row cm-draftly-table-header-row" }),
|
|
698
|
+
delimiter: Decoration.line({ class: "cm-draftly-table-row cm-draftly-table-delimiter-row" }),
|
|
699
|
+
body: Decoration.line({ class: "cm-draftly-table-row cm-draftly-table-body-row" }),
|
|
700
|
+
even: Decoration.line({ class: "cm-draftly-table-row cm-draftly-table-body-row cm-draftly-table-row-even" }),
|
|
701
|
+
last: Decoration.line({ class: "cm-draftly-table-row-last" }),
|
|
702
|
+
};
|
|
703
|
+
|
|
704
|
+
const cellDecorations = {
|
|
705
|
+
"th-left": Decoration.mark({ class: "cm-draftly-table-cell cm-draftly-table-th" }),
|
|
706
|
+
"th-center": Decoration.mark({ class: "cm-draftly-table-cell cm-draftly-table-th cm-draftly-table-cell-center" }),
|
|
707
|
+
"th-right": Decoration.mark({ class: "cm-draftly-table-cell cm-draftly-table-th cm-draftly-table-cell-right" }),
|
|
708
|
+
"th-left-last": Decoration.mark({ class: "cm-draftly-table-cell cm-draftly-table-th cm-draftly-table-cell-last" }),
|
|
709
|
+
"th-center-last": Decoration.mark({
|
|
710
|
+
class: "cm-draftly-table-cell cm-draftly-table-th cm-draftly-table-cell-center cm-draftly-table-cell-last",
|
|
711
|
+
}),
|
|
712
|
+
"th-right-last": Decoration.mark({
|
|
713
|
+
class: "cm-draftly-table-cell cm-draftly-table-th cm-draftly-table-cell-right cm-draftly-table-cell-last",
|
|
714
|
+
}),
|
|
715
|
+
"td-left": Decoration.mark({ class: "cm-draftly-table-cell" }),
|
|
716
|
+
"td-center": Decoration.mark({ class: "cm-draftly-table-cell cm-draftly-table-cell-center" }),
|
|
717
|
+
"td-right": Decoration.mark({ class: "cm-draftly-table-cell cm-draftly-table-cell-right" }),
|
|
718
|
+
"td-left-last": Decoration.mark({ class: "cm-draftly-table-cell cm-draftly-table-cell-last" }),
|
|
719
|
+
"td-center-last": Decoration.mark({
|
|
720
|
+
class: "cm-draftly-table-cell cm-draftly-table-cell-center cm-draftly-table-cell-last",
|
|
721
|
+
}),
|
|
722
|
+
"td-right-last": Decoration.mark({
|
|
723
|
+
class: "cm-draftly-table-cell cm-draftly-table-cell-right cm-draftly-table-cell-last",
|
|
724
|
+
}),
|
|
725
|
+
} as const;
|
|
726
|
+
|
|
727
|
+
type CellDecorationKey = keyof typeof cellDecorations;
|
|
728
|
+
|
|
729
|
+
function getCellDecoration(isHeader: boolean, alignment: Alignment, isLastCell: boolean): Decoration {
|
|
730
|
+
const key = `${isHeader ? "th" : "td"}-${alignment}${isLastCell ? "-last" : ""}` as CellDecorationKey;
|
|
731
|
+
return cellDecorations[key];
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
export class TablePlugin extends DecorationPlugin {
|
|
735
|
+
readonly name = "table";
|
|
736
|
+
readonly version = "2.0.0";
|
|
737
|
+
override decorationPriority = 20;
|
|
738
|
+
override readonly requiredNodes = ["Table", "TableHeader", "TableDelimiter", "TableRow", "TableCell"] as const;
|
|
739
|
+
|
|
740
|
+
private draftlyConfig: DraftlyConfig | undefined;
|
|
741
|
+
private pendingNormalizationView: EditorView | null = null;
|
|
742
|
+
private pendingPaddingView: EditorView | null = null;
|
|
743
|
+
private pendingSelectionRepairView: EditorView | null = null;
|
|
744
|
+
|
|
745
|
+
/** Stores the editor config for preview rendering and shared behavior. */
|
|
746
|
+
override onRegister(context: PluginContext): void {
|
|
747
|
+
super.onRegister(context);
|
|
748
|
+
this.draftlyConfig = context.config;
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
/** Exposes the plugin theme used for editor and preview styling. */
|
|
752
|
+
override get theme() {
|
|
753
|
+
return theme;
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
/** Enables GFM table parsing for the editor and preview renderer. */
|
|
757
|
+
override getMarkdownConfig(): MarkdownConfig {
|
|
758
|
+
return Table;
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
/** Registers block wrappers and atomic ranges for the table UI. */
|
|
762
|
+
override getExtensions(): Extension[] {
|
|
763
|
+
return [
|
|
764
|
+
Prec.highest(keymap.of(this.buildTableKeymap())),
|
|
765
|
+
EditorView.blockWrappers.of((view) => this.computeBlockWrappers(view)),
|
|
766
|
+
EditorView.atomicRanges.of((view) => this.computeAtomicRanges(view)),
|
|
767
|
+
EditorView.domEventHandlers({
|
|
768
|
+
keydown: (event, view) => this.handleDomKeydown(view, event),
|
|
769
|
+
}),
|
|
770
|
+
];
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
/** Provides the table-specific keyboard shortcuts and navigation. */
|
|
774
|
+
override getKeymap(): KeyBinding[] {
|
|
775
|
+
return [];
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
/** Builds the high-priority key bindings used inside tables. */
|
|
779
|
+
private buildTableKeymap(): KeyBinding[] {
|
|
780
|
+
return [
|
|
781
|
+
{ key: "Mod-Shift-t", run: (view) => this.insertTable(view), preventDefault: true },
|
|
782
|
+
{ key: "Mod-Alt-ArrowDown", run: (view) => this.addRow(view), preventDefault: true },
|
|
783
|
+
{ key: "Mod-Alt-ArrowRight", run: (view) => this.addColumn(view), preventDefault: true },
|
|
784
|
+
{ key: "Mod-Alt-Backspace", run: (view) => this.removeRow(view), preventDefault: true },
|
|
785
|
+
{ key: "Mod-Alt-Delete", run: (view) => this.removeColumn(view), preventDefault: true },
|
|
786
|
+
{ key: "Tab", run: (view) => this.handleTab(view, false) },
|
|
787
|
+
{ key: "Shift-Tab", run: (view) => this.handleTab(view, true) },
|
|
788
|
+
{ key: "ArrowLeft", run: (view) => this.handleArrowHorizontal(view, false) },
|
|
789
|
+
{ key: "ArrowRight", run: (view) => this.handleArrowHorizontal(view, true) },
|
|
790
|
+
{ key: "ArrowUp", run: (view) => this.handleArrowVertical(view, false) },
|
|
791
|
+
{ key: "ArrowDown", run: (view) => this.handleArrowVertical(view, true) },
|
|
792
|
+
{ key: "Enter", run: (view) => this.handleEnter(view) },
|
|
793
|
+
{ key: "Shift-Enter", run: (view) => this.insertBreakTag(view), preventDefault: true },
|
|
794
|
+
{ key: "Backspace", run: (view) => this.handleBreakDeletion(view, false) },
|
|
795
|
+
{ key: "Delete", run: (view) => this.handleBreakDeletion(view, true) },
|
|
796
|
+
];
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
/** Schedules an initial normalization pass once the view is ready. */
|
|
800
|
+
override onViewReady(view: EditorView): void {
|
|
801
|
+
this.scheduleNormalization(view);
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
/** Re-schedules normalization after user-driven document changes. */
|
|
805
|
+
override onViewUpdate(update: import("@codemirror/view").ViewUpdate): void {
|
|
806
|
+
if (update.docChanged && !update.transactions.some((transaction) => transaction.annotation(normalizeAnnotation))) {
|
|
807
|
+
this.schedulePadding(update.view);
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
if (
|
|
811
|
+
update.selectionSet &&
|
|
812
|
+
!update.transactions.some((transaction) => transaction.annotation(repairSelectionAnnotation))
|
|
813
|
+
) {
|
|
814
|
+
this.scheduleSelectionRepair(update.view);
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
/** Intercepts table-specific DOM key handling before browser defaults run. */
|
|
819
|
+
private handleDomKeydown(view: EditorView, event: KeyboardEvent): boolean {
|
|
820
|
+
if (event.defaultPrevented || event.isComposing || event.altKey || event.metaKey || event.ctrlKey) {
|
|
821
|
+
return false;
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
let handled = false;
|
|
825
|
+
|
|
826
|
+
if (event.key === "Tab") {
|
|
827
|
+
handled = this.handleTab(view, event.shiftKey);
|
|
828
|
+
} else if (event.key === "Enter" && event.shiftKey) {
|
|
829
|
+
handled = this.insertBreakTag(view);
|
|
830
|
+
} else if (event.key === "Enter") {
|
|
831
|
+
handled = this.handleEnter(view);
|
|
832
|
+
} else if (event.key === "ArrowLeft") {
|
|
833
|
+
handled = this.handleArrowHorizontal(view, false);
|
|
834
|
+
} else if (event.key === "ArrowRight") {
|
|
835
|
+
handled = this.handleArrowHorizontal(view, true);
|
|
836
|
+
} else if (event.key === "ArrowUp") {
|
|
837
|
+
handled = this.handleArrowVertical(view, false);
|
|
838
|
+
} else if (event.key === "ArrowDown") {
|
|
839
|
+
handled = this.handleArrowVertical(view, true);
|
|
840
|
+
} else if (event.key === "Backspace") {
|
|
841
|
+
handled = this.handleBreakDeletion(view, false);
|
|
842
|
+
} else if (event.key === "Delete") {
|
|
843
|
+
handled = this.handleBreakDeletion(view, true);
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
if (handled) {
|
|
847
|
+
event.preventDefault();
|
|
848
|
+
event.stopPropagation();
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
return handled;
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
/** Builds the visual table decorations for every parsed table block. */
|
|
855
|
+
override buildDecorations(ctx: DecorationContext): void {
|
|
856
|
+
const { view, decorations } = ctx;
|
|
857
|
+
|
|
858
|
+
syntaxTree(view.state).iterate({
|
|
859
|
+
enter: (node) => {
|
|
860
|
+
if (node.name !== "Table") {
|
|
861
|
+
return;
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
const tableInfo = readTableInfo(view.state, node.from, node.to);
|
|
865
|
+
if (tableInfo) {
|
|
866
|
+
this.decorateTable(view, decorations, tableInfo);
|
|
867
|
+
}
|
|
868
|
+
},
|
|
869
|
+
});
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
/** Renders the full table node to semantic preview HTML. */
|
|
873
|
+
override async renderToHTML(node: SyntaxNode, _children: string, ctx: PreviewContextLike): Promise<string | null> {
|
|
874
|
+
if (node.name === "Table") {
|
|
875
|
+
const content = ctx.sliceDoc(node.from, node.to);
|
|
876
|
+
const { tableMarkdown, trailingMarkdown } = splitTableAndTrailingMarkdown(content);
|
|
877
|
+
const parsed = parseTableMarkdown(tableMarkdown);
|
|
878
|
+
if (!parsed) {
|
|
879
|
+
return null;
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
const tableHtml = await renderTableToHtml(parsed, this.draftlyConfig);
|
|
883
|
+
if (!trailingMarkdown.trim()) {
|
|
884
|
+
return tableHtml;
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
return tableHtml + (await createPreviewRenderer(trailingMarkdown, this.draftlyConfig).render());
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
if (TABLE_SUB_NODE_NAMES.has(node.name)) {
|
|
891
|
+
return "";
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
return null;
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
/** Computes the block wrapper ranges used to group table lines. */
|
|
898
|
+
private computeBlockWrappers(view: EditorView): RangeSet<BlockWrapper> {
|
|
899
|
+
const wrappers: Range<BlockWrapper>[] = [];
|
|
900
|
+
|
|
901
|
+
syntaxTree(view.state).iterate({
|
|
902
|
+
enter: (node) => {
|
|
903
|
+
if (node.name !== "Table") {
|
|
904
|
+
return;
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
const tableInfo = readTableInfo(view.state, node.from, node.to);
|
|
908
|
+
if (tableInfo) {
|
|
909
|
+
wrappers.push(tableBlockWrapper.range(tableInfo.from, tableInfo.to));
|
|
910
|
+
}
|
|
911
|
+
},
|
|
912
|
+
});
|
|
913
|
+
|
|
914
|
+
return BlockWrapper.set(wrappers, true);
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
/** Computes atomic ranges for delimiters and inline break tags. */
|
|
918
|
+
private computeAtomicRanges(view: EditorView): RangeSet<Decoration> {
|
|
919
|
+
const ranges: Range<Decoration>[] = [];
|
|
920
|
+
|
|
921
|
+
syntaxTree(view.state).iterate({
|
|
922
|
+
enter: (node) => {
|
|
923
|
+
if (node.name !== "Table") {
|
|
924
|
+
return;
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
const tableInfo = readTableInfo(view.state, node.from, node.to);
|
|
928
|
+
if (!tableInfo) {
|
|
929
|
+
return;
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
for (let lineNumber = tableInfo.startLineNumber; lineNumber <= tableInfo.endLineNumber; lineNumber++) {
|
|
933
|
+
const line = view.state.doc.line(lineNumber);
|
|
934
|
+
if (lineNumber === tableInfo.delimiterLineNumber) {
|
|
935
|
+
ranges.push(delimiterReplace.range(line.from, line.to));
|
|
936
|
+
continue;
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
const pipes = getPipePositions(line.text);
|
|
940
|
+
for (const pipe of pipes) {
|
|
941
|
+
ranges.push(pipeReplace.range(line.from + pipe, line.from + pipe + 1));
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
for (let columnIndex = 0; columnIndex < pipes.length - 1; columnIndex++) {
|
|
945
|
+
const rawFrom = pipes[columnIndex]! + 1;
|
|
946
|
+
const rawTo = pipes[columnIndex + 1]!;
|
|
947
|
+
const rawText = line.text.slice(rawFrom, rawTo);
|
|
948
|
+
const visible = getVisibleBounds(rawText);
|
|
949
|
+
|
|
950
|
+
if (visible.startOffset > 0) {
|
|
951
|
+
ranges.push(pipeReplace.range(line.from + rawFrom, line.from + rawFrom + visible.startOffset));
|
|
952
|
+
}
|
|
953
|
+
if (visible.endOffset < rawText.length) {
|
|
954
|
+
ranges.push(pipeReplace.range(line.from + rawFrom + visible.endOffset, line.from + rawTo));
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
let match: RegExpExecArray | null;
|
|
958
|
+
const regex = new RegExp(BREAK_TAG_REGEX);
|
|
959
|
+
while ((match = regex.exec(rawText)) !== null) {
|
|
960
|
+
ranges.push(
|
|
961
|
+
Decoration.replace({ widget: new TableBreakWidget() }).range(
|
|
962
|
+
line.from + rawFrom + match.index,
|
|
963
|
+
line.from + rawFrom + match.index + match[0].length
|
|
964
|
+
)
|
|
965
|
+
);
|
|
966
|
+
}
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
},
|
|
970
|
+
});
|
|
971
|
+
|
|
972
|
+
return RangeSet.of(ranges, true);
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
/** Applies row, cell, and control decorations for a single table. */
|
|
976
|
+
private decorateTable(view: EditorView, decorations: Range<Decoration>[], tableInfo: TableInfo): void {
|
|
977
|
+
for (let lineNumber = tableInfo.startLineNumber; lineNumber <= tableInfo.endLineNumber; lineNumber++) {
|
|
978
|
+
const line = view.state.doc.line(lineNumber);
|
|
979
|
+
const isHeader = lineNumber === tableInfo.startLineNumber;
|
|
980
|
+
const isDelimiter = lineNumber === tableInfo.delimiterLineNumber;
|
|
981
|
+
const isLastBody = !isHeader && !isDelimiter && lineNumber === tableInfo.endLineNumber;
|
|
982
|
+
const bodyIndex = isHeader || isDelimiter ? -1 : lineNumber - tableInfo.delimiterLineNumber - 1;
|
|
983
|
+
|
|
984
|
+
if (isHeader) {
|
|
985
|
+
decorations.push(lineDecorations.header.range(line.from));
|
|
986
|
+
} else if (isDelimiter) {
|
|
987
|
+
decorations.push(lineDecorations.delimiter.range(line.from));
|
|
988
|
+
} else if (bodyIndex % 2 === 1) {
|
|
989
|
+
decorations.push(lineDecorations.even.range(line.from));
|
|
990
|
+
} else {
|
|
991
|
+
decorations.push(lineDecorations.body.range(line.from));
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
if (isLastBody) {
|
|
995
|
+
decorations.push(lineDecorations.last.range(line.from));
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
if (isDelimiter) {
|
|
999
|
+
decorations.push(delimiterReplace.range(line.from, line.to));
|
|
1000
|
+
continue;
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
this.decorateLine(decorations, line.from, line.text, tableInfo.alignments, isHeader);
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
decorations.push(
|
|
1007
|
+
Decoration.widget({
|
|
1008
|
+
widget: new TableControlsWidget(
|
|
1009
|
+
(view) => {
|
|
1010
|
+
const liveTable = getTableInfoAtPosition(view.state, tableInfo.from);
|
|
1011
|
+
if (liveTable) {
|
|
1012
|
+
this.appendRow(view, liveTable, liveTable.columnCount - 1);
|
|
1013
|
+
}
|
|
1014
|
+
},
|
|
1015
|
+
(view) => {
|
|
1016
|
+
const liveTable = getTableInfoAtPosition(view.state, tableInfo.from);
|
|
1017
|
+
if (liveTable) {
|
|
1018
|
+
this.appendColumn(view, liveTable);
|
|
1019
|
+
}
|
|
1020
|
+
}
|
|
1021
|
+
),
|
|
1022
|
+
side: 1,
|
|
1023
|
+
}).range(tableInfo.to)
|
|
1024
|
+
);
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
/** Applies the visual cell decorations for a single table row line. */
|
|
1028
|
+
private decorateLine(
|
|
1029
|
+
decorations: Range<Decoration>[],
|
|
1030
|
+
lineFrom: number,
|
|
1031
|
+
lineText: string,
|
|
1032
|
+
alignments: Alignment[],
|
|
1033
|
+
isHeader: boolean
|
|
1034
|
+
): void {
|
|
1035
|
+
const pipes = getPipePositions(lineText);
|
|
1036
|
+
if (pipes.length < 2) {
|
|
1037
|
+
return;
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
for (const pipe of pipes) {
|
|
1041
|
+
decorations.push(pipeReplace.range(lineFrom + pipe, lineFrom + pipe + 1));
|
|
1042
|
+
}
|
|
1043
|
+
|
|
1044
|
+
for (let columnIndex = 0; columnIndex < pipes.length - 1; columnIndex++) {
|
|
1045
|
+
const rawFrom = pipes[columnIndex]! + 1;
|
|
1046
|
+
const rawTo = pipes[columnIndex + 1]!;
|
|
1047
|
+
const rawText = lineText.slice(rawFrom, rawTo);
|
|
1048
|
+
const visible = getVisibleBounds(rawText);
|
|
1049
|
+
const absoluteFrom = lineFrom + rawFrom;
|
|
1050
|
+
const absoluteTo = lineFrom + rawTo;
|
|
1051
|
+
|
|
1052
|
+
if (visible.startOffset > 0) {
|
|
1053
|
+
decorations.push(pipeReplace.range(absoluteFrom, absoluteFrom + visible.startOffset));
|
|
1054
|
+
}
|
|
1055
|
+
if (visible.endOffset < rawText.length) {
|
|
1056
|
+
decorations.push(pipeReplace.range(absoluteFrom + visible.endOffset, absoluteTo));
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
decorations.push(
|
|
1060
|
+
getCellDecoration(isHeader, alignments[columnIndex] || "left", columnIndex === pipes.length - 2).range(
|
|
1061
|
+
absoluteFrom,
|
|
1062
|
+
absoluteTo
|
|
1063
|
+
)
|
|
1064
|
+
);
|
|
1065
|
+
|
|
1066
|
+
let match: RegExpExecArray | null;
|
|
1067
|
+
const regex = new RegExp(BREAK_TAG_REGEX);
|
|
1068
|
+
while ((match = regex.exec(rawText)) !== null) {
|
|
1069
|
+
decorations.push(
|
|
1070
|
+
Decoration.replace({ widget: new TableBreakWidget() }).range(
|
|
1071
|
+
absoluteFrom + match.index,
|
|
1072
|
+
absoluteFrom + match.index + match[0].length
|
|
1073
|
+
)
|
|
1074
|
+
);
|
|
1075
|
+
}
|
|
1076
|
+
}
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
/** Normalizes every parsed table block back into canonical markdown. */
|
|
1080
|
+
private normalizeTables(view: EditorView): void {
|
|
1081
|
+
const changes: Array<{ from: number; to: number; insert: string }> = [];
|
|
1082
|
+
|
|
1083
|
+
syntaxTree(view.state).iterate({
|
|
1084
|
+
enter: (node) => {
|
|
1085
|
+
if (node.name !== "Table") {
|
|
1086
|
+
return;
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1089
|
+
const content = view.state.sliceDoc(node.from, node.to);
|
|
1090
|
+
const { tableMarkdown } = splitTableAndTrailingMarkdown(content);
|
|
1091
|
+
const parsed = parseTableMarkdown(tableMarkdown);
|
|
1092
|
+
if (!parsed) {
|
|
1093
|
+
return;
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1096
|
+
const formatted = formatTableMarkdown(parsed);
|
|
1097
|
+
const change = createTableInsert(view.state, node.from, node.from + tableMarkdown.length, formatted);
|
|
1098
|
+
if (
|
|
1099
|
+
change.insert !== tableMarkdown ||
|
|
1100
|
+
change.from !== node.from ||
|
|
1101
|
+
change.to !== node.from + tableMarkdown.length
|
|
1102
|
+
) {
|
|
1103
|
+
changes.push({
|
|
1104
|
+
from: change.from,
|
|
1105
|
+
to: change.to,
|
|
1106
|
+
insert: change.insert,
|
|
1107
|
+
});
|
|
1108
|
+
}
|
|
1109
|
+
},
|
|
1110
|
+
});
|
|
1111
|
+
|
|
1112
|
+
if (changes.length > 0) {
|
|
1113
|
+
view.dispatch({
|
|
1114
|
+
changes: changes.sort((left, right) => right.from - left.from),
|
|
1115
|
+
annotations: normalizeAnnotation.of(true),
|
|
1116
|
+
});
|
|
1117
|
+
}
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1120
|
+
/** Defers table normalization until the current update cycle is finished. */
|
|
1121
|
+
private scheduleNormalization(view: EditorView): void {
|
|
1122
|
+
if (this.pendingNormalizationView === view) {
|
|
1123
|
+
return;
|
|
1124
|
+
}
|
|
1125
|
+
|
|
1126
|
+
this.pendingNormalizationView = view;
|
|
1127
|
+
queueMicrotask(() => {
|
|
1128
|
+
if (this.pendingNormalizationView !== view) {
|
|
1129
|
+
return;
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1132
|
+
this.pendingNormalizationView = null;
|
|
1133
|
+
this.normalizeTables(view);
|
|
1134
|
+
});
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1137
|
+
/** Adds missing spacer lines above and below tables after edits. */
|
|
1138
|
+
private ensureTablePadding(view: EditorView): void {
|
|
1139
|
+
const changes: Array<{ from: number; to: number; insert: string }> = [];
|
|
1140
|
+
|
|
1141
|
+
syntaxTree(view.state).iterate({
|
|
1142
|
+
enter: (node) => {
|
|
1143
|
+
if (node.name !== "Table") {
|
|
1144
|
+
return;
|
|
1145
|
+
}
|
|
1146
|
+
|
|
1147
|
+
const tableInfo = readTableInfo(view.state, node.from, node.to);
|
|
1148
|
+
if (!tableInfo) {
|
|
1149
|
+
return;
|
|
1150
|
+
}
|
|
1151
|
+
|
|
1152
|
+
const startLine = view.state.doc.lineAt(tableInfo.from);
|
|
1153
|
+
if (startLine.number === 1) {
|
|
1154
|
+
changes.push({ from: startLine.from, to: startLine.from, insert: "\n" });
|
|
1155
|
+
} else {
|
|
1156
|
+
const previousLine = view.state.doc.line(startLine.number - 1);
|
|
1157
|
+
if (previousLine.text.trim() !== "") {
|
|
1158
|
+
changes.push({ from: startLine.from, to: startLine.from, insert: "\n" });
|
|
1159
|
+
}
|
|
1160
|
+
}
|
|
1161
|
+
|
|
1162
|
+
const endLine = view.state.doc.lineAt(tableInfo.to);
|
|
1163
|
+
if (endLine.number === view.state.doc.lines) {
|
|
1164
|
+
changes.push({ from: endLine.to, to: endLine.to, insert: "\n" });
|
|
1165
|
+
} else {
|
|
1166
|
+
const nextLine = view.state.doc.line(endLine.number + 1);
|
|
1167
|
+
if (nextLine.text.trim() !== "") {
|
|
1168
|
+
changes.push({ from: endLine.to, to: endLine.to, insert: "\n" });
|
|
1169
|
+
}
|
|
1170
|
+
}
|
|
1171
|
+
},
|
|
1172
|
+
});
|
|
1173
|
+
|
|
1174
|
+
if (changes.length > 0) {
|
|
1175
|
+
view.dispatch({
|
|
1176
|
+
changes: changes.sort((left, right) => right.from - left.from),
|
|
1177
|
+
annotations: normalizeAnnotation.of(true),
|
|
1178
|
+
});
|
|
1179
|
+
}
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1182
|
+
/** Schedules a padding-only pass after the current update cycle finishes. */
|
|
1183
|
+
private schedulePadding(view: EditorView): void {
|
|
1184
|
+
if (this.pendingPaddingView === view) {
|
|
1185
|
+
return;
|
|
1186
|
+
}
|
|
1187
|
+
|
|
1188
|
+
this.pendingPaddingView = view;
|
|
1189
|
+
queueMicrotask(() => {
|
|
1190
|
+
if (this.pendingPaddingView !== view) {
|
|
1191
|
+
return;
|
|
1192
|
+
}
|
|
1193
|
+
|
|
1194
|
+
this.pendingPaddingView = null;
|
|
1195
|
+
this.ensureTablePadding(view);
|
|
1196
|
+
});
|
|
1197
|
+
}
|
|
1198
|
+
|
|
1199
|
+
/** Repairs carets that land in hidden table markup instead of editable cell content. */
|
|
1200
|
+
private ensureTableSelection(view: EditorView): void {
|
|
1201
|
+
const selection = view.state.selection.main;
|
|
1202
|
+
if (!selection.empty) {
|
|
1203
|
+
return;
|
|
1204
|
+
}
|
|
1205
|
+
|
|
1206
|
+
const tableInfo = getTableInfoAtPosition(view.state, selection.head);
|
|
1207
|
+
if (!tableInfo) {
|
|
1208
|
+
return;
|
|
1209
|
+
}
|
|
1210
|
+
|
|
1211
|
+
const cell = findCellAtPosition(tableInfo, selection.head);
|
|
1212
|
+
if (!cell) {
|
|
1213
|
+
return;
|
|
1214
|
+
}
|
|
1215
|
+
|
|
1216
|
+
const anchor = clampCellPosition(cell, selection.head);
|
|
1217
|
+
if (anchor === selection.head) {
|
|
1218
|
+
return;
|
|
1219
|
+
}
|
|
1220
|
+
|
|
1221
|
+
view.dispatch({
|
|
1222
|
+
selection: { anchor },
|
|
1223
|
+
annotations: repairSelectionAnnotation.of(true),
|
|
1224
|
+
scrollIntoView: true,
|
|
1225
|
+
});
|
|
1226
|
+
}
|
|
1227
|
+
|
|
1228
|
+
/** Schedules table selection repair after the current update finishes. */
|
|
1229
|
+
private scheduleSelectionRepair(view: EditorView): void {
|
|
1230
|
+
if (this.pendingSelectionRepairView === view) {
|
|
1231
|
+
return;
|
|
1232
|
+
}
|
|
1233
|
+
|
|
1234
|
+
this.pendingSelectionRepairView = view;
|
|
1235
|
+
queueMicrotask(() => {
|
|
1236
|
+
if (this.pendingSelectionRepairView !== view) {
|
|
1237
|
+
return;
|
|
1238
|
+
}
|
|
1239
|
+
|
|
1240
|
+
this.pendingSelectionRepairView = null;
|
|
1241
|
+
this.ensureTableSelection(view);
|
|
1242
|
+
});
|
|
1243
|
+
}
|
|
1244
|
+
|
|
1245
|
+
/** Rewrites a table block and restores the caret to a target cell position. */
|
|
1246
|
+
private replaceTable(
|
|
1247
|
+
view: EditorView,
|
|
1248
|
+
tableInfo: TableInfo,
|
|
1249
|
+
parsed: ParsedTable,
|
|
1250
|
+
targetRowIndex: number,
|
|
1251
|
+
targetColumnIndex: number,
|
|
1252
|
+
offset = 0
|
|
1253
|
+
): void {
|
|
1254
|
+
const formatted = formatTableMarkdown(parsed);
|
|
1255
|
+
const change = createTableInsert(view.state, tableInfo.from, tableInfo.to, formatted);
|
|
1256
|
+
const selection =
|
|
1257
|
+
change.from +
|
|
1258
|
+
change.prefixLength +
|
|
1259
|
+
getCellAnchorInFormattedTable(
|
|
1260
|
+
formatted,
|
|
1261
|
+
Math.max(0, targetRowIndex),
|
|
1262
|
+
Math.max(0, Math.min(targetColumnIndex, Math.max(parsed.headers.length - 1, 0))),
|
|
1263
|
+
Math.max(0, offset)
|
|
1264
|
+
);
|
|
1265
|
+
|
|
1266
|
+
view.dispatch({
|
|
1267
|
+
changes: { from: change.from, to: change.to, insert: change.insert },
|
|
1268
|
+
selection: { anchor: selection },
|
|
1269
|
+
});
|
|
1270
|
+
}
|
|
1271
|
+
|
|
1272
|
+
/** Inserts an empty body row below the given logical row index. */
|
|
1273
|
+
private insertRowBelow(view: EditorView, tableInfo: TableInfo, afterRowIndex: number, targetColumn: number): void {
|
|
1274
|
+
const parsed = normalizeParsedTable(buildTableFromInfo(tableInfo));
|
|
1275
|
+
const insertBodyIndex = Math.max(0, Math.min(afterRowIndex, parsed.rows.length));
|
|
1276
|
+
parsed.rows.splice(insertBodyIndex, 0, buildEmptyRow(tableInfo.columnCount));
|
|
1277
|
+
this.replaceTable(view, tableInfo, parsed, insertBodyIndex + 1, targetColumn);
|
|
1278
|
+
}
|
|
1279
|
+
|
|
1280
|
+
/** Inserts a starter table near the current cursor line. */
|
|
1281
|
+
private insertTable(view: EditorView): boolean {
|
|
1282
|
+
const { state } = view;
|
|
1283
|
+
const cursor = state.selection.main.head;
|
|
1284
|
+
const line = state.doc.lineAt(cursor);
|
|
1285
|
+
const insertAt = line.text.trim() ? line.to : line.from;
|
|
1286
|
+
const formatted = formatTableMarkdown(TABLE_TEMPLATE);
|
|
1287
|
+
const change = createTableInsert(state, insertAt, insertAt, formatted);
|
|
1288
|
+
const selection = change.from + change.prefixLength + getCellAnchorInFormattedTable(formatted, 0, 0);
|
|
1289
|
+
|
|
1290
|
+
view.dispatch({
|
|
1291
|
+
changes: { from: change.from, to: change.to, insert: change.insert },
|
|
1292
|
+
selection: { anchor: selection },
|
|
1293
|
+
});
|
|
1294
|
+
|
|
1295
|
+
return true;
|
|
1296
|
+
}
|
|
1297
|
+
|
|
1298
|
+
/** Adds a new empty body row to the active table. */
|
|
1299
|
+
private addRow(view: EditorView): boolean {
|
|
1300
|
+
const tableInfo = this.getTableAtCursor(view);
|
|
1301
|
+
if (!tableInfo) {
|
|
1302
|
+
return false;
|
|
1303
|
+
}
|
|
1304
|
+
|
|
1305
|
+
const cell = this.getCurrentCell(view, tableInfo);
|
|
1306
|
+
this.appendRow(view, tableInfo, cell?.columnIndex || 0);
|
|
1307
|
+
return true;
|
|
1308
|
+
}
|
|
1309
|
+
|
|
1310
|
+
/** Appends a row and keeps the caret in the requested column. */
|
|
1311
|
+
private appendRow(view: EditorView, tableInfo: TableInfo, targetColumn: number): void {
|
|
1312
|
+
this.insertRowBelow(view, tableInfo, tableInfo.bodyCells.length, targetColumn);
|
|
1313
|
+
}
|
|
1314
|
+
|
|
1315
|
+
/** Inserts a new column after the current column. */
|
|
1316
|
+
private addColumn(view: EditorView): boolean {
|
|
1317
|
+
const tableInfo = this.getTableAtCursor(view);
|
|
1318
|
+
if (!tableInfo) {
|
|
1319
|
+
return false;
|
|
1320
|
+
}
|
|
1321
|
+
|
|
1322
|
+
const cell = this.getCurrentCell(view, tableInfo);
|
|
1323
|
+
const insertAfter = cell?.columnIndex ?? tableInfo.columnCount - 1;
|
|
1324
|
+
const parsed = normalizeParsedTable(buildTableFromInfo(tableInfo));
|
|
1325
|
+
|
|
1326
|
+
parsed.headers.splice(insertAfter + 1, 0, "");
|
|
1327
|
+
parsed.alignments.splice(insertAfter + 1, 0, "left");
|
|
1328
|
+
for (const row of parsed.rows) {
|
|
1329
|
+
row.splice(insertAfter + 1, 0, "");
|
|
1330
|
+
}
|
|
1331
|
+
|
|
1332
|
+
this.replaceTable(view, tableInfo, parsed, cell?.rowIndex || 0, insertAfter + 1);
|
|
1333
|
+
|
|
1334
|
+
return true;
|
|
1335
|
+
}
|
|
1336
|
+
|
|
1337
|
+
/** Appends a new column at the far right of the table. */
|
|
1338
|
+
private appendColumn(view: EditorView, tableInfo: TableInfo): void {
|
|
1339
|
+
const parsed = normalizeParsedTable(buildTableFromInfo(tableInfo));
|
|
1340
|
+
parsed.headers.push("");
|
|
1341
|
+
parsed.alignments.push("left");
|
|
1342
|
+
for (const row of parsed.rows) {
|
|
1343
|
+
row.push("");
|
|
1344
|
+
}
|
|
1345
|
+
|
|
1346
|
+
this.replaceTable(view, tableInfo, parsed, 0, parsed.headers.length - 1);
|
|
1347
|
+
}
|
|
1348
|
+
|
|
1349
|
+
/** Removes the current body row or clears the last remaining row. */
|
|
1350
|
+
private removeRow(view: EditorView): boolean {
|
|
1351
|
+
const tableInfo = this.getTableAtCursor(view);
|
|
1352
|
+
if (!tableInfo) {
|
|
1353
|
+
return false;
|
|
1354
|
+
}
|
|
1355
|
+
|
|
1356
|
+
const cell = this.getCurrentCell(view, tableInfo);
|
|
1357
|
+
if (!cell || cell.rowKind !== "body") {
|
|
1358
|
+
return false;
|
|
1359
|
+
}
|
|
1360
|
+
|
|
1361
|
+
const parsed = normalizeParsedTable(buildTableFromInfo(tableInfo));
|
|
1362
|
+
const bodyIndex = cell.rowIndex - 1;
|
|
1363
|
+
if (bodyIndex < 0 || bodyIndex >= parsed.rows.length) {
|
|
1364
|
+
return false;
|
|
1365
|
+
}
|
|
1366
|
+
|
|
1367
|
+
if (parsed.rows.length === 1) {
|
|
1368
|
+
parsed.rows[0] = buildEmptyRow(tableInfo.columnCount);
|
|
1369
|
+
} else {
|
|
1370
|
+
parsed.rows.splice(bodyIndex, 1);
|
|
1371
|
+
}
|
|
1372
|
+
|
|
1373
|
+
const nextRowIndex = Math.max(1, Math.min(cell.rowIndex, parsed.rows.length));
|
|
1374
|
+
this.replaceTable(view, tableInfo, parsed, nextRowIndex, Math.min(cell.columnIndex, tableInfo.columnCount - 1));
|
|
1375
|
+
|
|
1376
|
+
return true;
|
|
1377
|
+
}
|
|
1378
|
+
|
|
1379
|
+
/** Removes the current column when the table has more than one column. */
|
|
1380
|
+
private removeColumn(view: EditorView): boolean {
|
|
1381
|
+
const tableInfo = this.getTableAtCursor(view);
|
|
1382
|
+
if (!tableInfo || tableInfo.columnCount <= 1) {
|
|
1383
|
+
return false;
|
|
1384
|
+
}
|
|
1385
|
+
|
|
1386
|
+
const cell = this.getCurrentCell(view, tableInfo);
|
|
1387
|
+
const removeAt = cell?.columnIndex ?? tableInfo.columnCount - 1;
|
|
1388
|
+
const parsed = normalizeParsedTable(buildTableFromInfo(tableInfo));
|
|
1389
|
+
|
|
1390
|
+
parsed.headers.splice(removeAt, 1);
|
|
1391
|
+
parsed.alignments.splice(removeAt, 1);
|
|
1392
|
+
for (const row of parsed.rows) {
|
|
1393
|
+
row.splice(removeAt, 1);
|
|
1394
|
+
}
|
|
1395
|
+
|
|
1396
|
+
this.replaceTable(view, tableInfo, parsed, cell?.rowIndex || 0, Math.min(removeAt, parsed.headers.length - 1));
|
|
1397
|
+
|
|
1398
|
+
return true;
|
|
1399
|
+
}
|
|
1400
|
+
|
|
1401
|
+
/** Moves to the next or previous logical cell with Tab navigation. */
|
|
1402
|
+
private handleTab(view: EditorView, backwards: boolean): boolean {
|
|
1403
|
+
const tableInfo = this.getTableAtCursor(view);
|
|
1404
|
+
if (!tableInfo) {
|
|
1405
|
+
return false;
|
|
1406
|
+
}
|
|
1407
|
+
|
|
1408
|
+
const cell = this.getCurrentCell(view, tableInfo);
|
|
1409
|
+
if (!cell) {
|
|
1410
|
+
return false;
|
|
1411
|
+
}
|
|
1412
|
+
|
|
1413
|
+
const cells = tableInfo.cellsByRow.flat();
|
|
1414
|
+
const currentIndex = cells.findIndex((candidate) => candidate.from === cell.from && candidate.to === cell.to);
|
|
1415
|
+
if (currentIndex < 0) {
|
|
1416
|
+
return false;
|
|
1417
|
+
}
|
|
1418
|
+
|
|
1419
|
+
const nextIndex = backwards ? currentIndex - 1 : currentIndex + 1;
|
|
1420
|
+
if (nextIndex < 0) {
|
|
1421
|
+
return true;
|
|
1422
|
+
}
|
|
1423
|
+
if (nextIndex >= cells.length) {
|
|
1424
|
+
this.appendRow(view, tableInfo, 0);
|
|
1425
|
+
return true;
|
|
1426
|
+
}
|
|
1427
|
+
|
|
1428
|
+
this.moveSelectionToCell(view, cells[nextIndex]!);
|
|
1429
|
+
return true;
|
|
1430
|
+
}
|
|
1431
|
+
|
|
1432
|
+
/** Moves horizontally between adjacent cells when the caret hits an edge. */
|
|
1433
|
+
private handleArrowHorizontal(view: EditorView, forward: boolean): boolean {
|
|
1434
|
+
const tableInfo = this.getTableAtCursor(view);
|
|
1435
|
+
if (!tableInfo) {
|
|
1436
|
+
return false;
|
|
1437
|
+
}
|
|
1438
|
+
|
|
1439
|
+
const cell = this.getCurrentCell(view, tableInfo);
|
|
1440
|
+
if (!cell) {
|
|
1441
|
+
return false;
|
|
1442
|
+
}
|
|
1443
|
+
|
|
1444
|
+
const cursor = view.state.selection.main.head;
|
|
1445
|
+
const rightEdge = Math.max(cell.contentFrom, cell.contentTo);
|
|
1446
|
+
if (forward && cursor < rightEdge) {
|
|
1447
|
+
return false;
|
|
1448
|
+
}
|
|
1449
|
+
if (!forward && cursor > cell.contentFrom) {
|
|
1450
|
+
return false;
|
|
1451
|
+
}
|
|
1452
|
+
|
|
1453
|
+
const row = tableInfo.cellsByRow[cell.rowIndex] || [];
|
|
1454
|
+
const nextCell = row[cell.columnIndex + (forward ? 1 : -1)];
|
|
1455
|
+
if (!nextCell) {
|
|
1456
|
+
return false;
|
|
1457
|
+
}
|
|
1458
|
+
|
|
1459
|
+
this.moveSelectionToCell(view, nextCell);
|
|
1460
|
+
return true;
|
|
1461
|
+
}
|
|
1462
|
+
|
|
1463
|
+
/** Moves vertically between rows while keeping the current column. */
|
|
1464
|
+
private handleArrowVertical(view: EditorView, forward: boolean): boolean {
|
|
1465
|
+
const tableInfo = this.getTableAtCursor(view);
|
|
1466
|
+
if (!tableInfo) {
|
|
1467
|
+
return false;
|
|
1468
|
+
}
|
|
1469
|
+
|
|
1470
|
+
const cell = this.getCurrentCell(view, tableInfo);
|
|
1471
|
+
if (!cell) {
|
|
1472
|
+
return false;
|
|
1473
|
+
}
|
|
1474
|
+
|
|
1475
|
+
const nextRow = tableInfo.cellsByRow[cell.rowIndex + (forward ? 1 : -1)];
|
|
1476
|
+
if (!nextRow) {
|
|
1477
|
+
return false;
|
|
1478
|
+
}
|
|
1479
|
+
|
|
1480
|
+
const nextCell = nextRow[cell.columnIndex];
|
|
1481
|
+
if (!nextCell) {
|
|
1482
|
+
return false;
|
|
1483
|
+
}
|
|
1484
|
+
|
|
1485
|
+
this.moveSelectionToCell(view, nextCell);
|
|
1486
|
+
return true;
|
|
1487
|
+
}
|
|
1488
|
+
|
|
1489
|
+
/** Advances downward on Enter and manages the trailing empty row behavior. */
|
|
1490
|
+
private handleEnter(view: EditorView): boolean {
|
|
1491
|
+
const tableInfo = this.getTableAtCursor(view);
|
|
1492
|
+
if (!tableInfo) {
|
|
1493
|
+
return false;
|
|
1494
|
+
}
|
|
1495
|
+
|
|
1496
|
+
const cell = this.getCurrentCell(view, tableInfo);
|
|
1497
|
+
if (!cell) {
|
|
1498
|
+
return false;
|
|
1499
|
+
}
|
|
1500
|
+
|
|
1501
|
+
if (cell.rowKind === "body") {
|
|
1502
|
+
const currentRow = tableInfo.bodyCells[cell.rowIndex - 1];
|
|
1503
|
+
if (currentRow && isBodyRowEmpty(currentRow)) {
|
|
1504
|
+
const parsed = normalizeParsedTable(buildTableFromInfo(tableInfo));
|
|
1505
|
+
parsed.rows.splice(cell.rowIndex - 1, 1);
|
|
1506
|
+
const formatted = formatTableMarkdown(parsed);
|
|
1507
|
+
const change = createTableInsert(view.state, tableInfo.from, tableInfo.to, formatted);
|
|
1508
|
+
const anchor = Math.min(change.from + change.insert.length, view.state.doc.length + change.insert.length);
|
|
1509
|
+
|
|
1510
|
+
view.dispatch({
|
|
1511
|
+
changes: { from: change.from, to: change.to, insert: change.insert },
|
|
1512
|
+
selection: { anchor },
|
|
1513
|
+
});
|
|
1514
|
+
return true;
|
|
1515
|
+
}
|
|
1516
|
+
}
|
|
1517
|
+
|
|
1518
|
+
if (cell.rowKind === "body" && cell.rowIndex === tableInfo.cellsByRow.length - 1) {
|
|
1519
|
+
const parsed = normalizeParsedTable(buildTableFromInfo(tableInfo));
|
|
1520
|
+
parsed.rows.push(buildEmptyRow(tableInfo.columnCount));
|
|
1521
|
+
this.replaceTable(view, tableInfo, parsed, parsed.rows.length, cell.columnIndex);
|
|
1522
|
+
return true;
|
|
1523
|
+
}
|
|
1524
|
+
|
|
1525
|
+
this.insertRowBelow(view, tableInfo, cell.rowIndex, cell.columnIndex);
|
|
1526
|
+
return true;
|
|
1527
|
+
}
|
|
1528
|
+
|
|
1529
|
+
/** Inserts a canonical `<br />` token inside the current table cell. */
|
|
1530
|
+
private insertBreakTag(view: EditorView): boolean {
|
|
1531
|
+
const tableInfo = this.getTableAtCursor(view);
|
|
1532
|
+
if (!tableInfo) {
|
|
1533
|
+
return false;
|
|
1534
|
+
}
|
|
1535
|
+
|
|
1536
|
+
const selection = view.state.selection.main;
|
|
1537
|
+
view.dispatch({
|
|
1538
|
+
changes: { from: selection.from, to: selection.to, insert: BREAK_TAG },
|
|
1539
|
+
selection: { anchor: selection.from + BREAK_TAG.length },
|
|
1540
|
+
});
|
|
1541
|
+
return true;
|
|
1542
|
+
}
|
|
1543
|
+
|
|
1544
|
+
/** Deletes a whole `<br />` token when backspace or delete hits it. */
|
|
1545
|
+
private handleBreakDeletion(view: EditorView, forward: boolean): boolean {
|
|
1546
|
+
const tableInfo = this.getTableAtCursor(view);
|
|
1547
|
+
if (!tableInfo) {
|
|
1548
|
+
return false;
|
|
1549
|
+
}
|
|
1550
|
+
|
|
1551
|
+
const selection = view.state.selection.main;
|
|
1552
|
+
const cursor = selection.head;
|
|
1553
|
+
|
|
1554
|
+
for (const range of collectBreakRanges(tableInfo)) {
|
|
1555
|
+
const within = cursor > range.from && cursor < range.to;
|
|
1556
|
+
const matchesBackspace = !forward && cursor === range.to;
|
|
1557
|
+
const matchesDelete = forward && cursor === range.from;
|
|
1558
|
+
const overlapsSelection = !selection.empty && selection.from <= range.from && selection.to >= range.to;
|
|
1559
|
+
|
|
1560
|
+
if (within || matchesBackspace || matchesDelete || overlapsSelection) {
|
|
1561
|
+
view.dispatch({
|
|
1562
|
+
changes: { from: range.from, to: range.to, insert: "" },
|
|
1563
|
+
selection: { anchor: range.from },
|
|
1564
|
+
});
|
|
1565
|
+
return true;
|
|
1566
|
+
}
|
|
1567
|
+
}
|
|
1568
|
+
|
|
1569
|
+
return false;
|
|
1570
|
+
}
|
|
1571
|
+
|
|
1572
|
+
/** Moves the current selection anchor into a target cell. */
|
|
1573
|
+
private moveSelectionToCell(view: EditorView, cell: TableCellInfo, offset = 0): void {
|
|
1574
|
+
const end = Math.max(cell.contentFrom, cell.contentTo);
|
|
1575
|
+
view.dispatch({
|
|
1576
|
+
selection: { anchor: Math.min(cell.contentFrom + offset, end) },
|
|
1577
|
+
scrollIntoView: true,
|
|
1578
|
+
});
|
|
1579
|
+
}
|
|
1580
|
+
|
|
1581
|
+
/** Returns the table currently containing the editor cursor. */
|
|
1582
|
+
private getTableAtCursor(view: EditorView): TableInfo | null {
|
|
1583
|
+
return getTableInfoAtPosition(view.state, view.state.selection.main.head);
|
|
1584
|
+
}
|
|
1585
|
+
|
|
1586
|
+
/** Returns the active cell under the current selection head. */
|
|
1587
|
+
private getCurrentCell(view: EditorView, tableInfo: TableInfo): TableCellInfo | null {
|
|
1588
|
+
return findCellAtPosition(tableInfo, view.state.selection.main.head);
|
|
1589
|
+
}
|
|
1590
|
+
}
|
|
1591
|
+
|
|
1592
|
+
const theme = createTheme({
|
|
1593
|
+
default: {
|
|
1594
|
+
".cm-draftly-table-wrapper, .cm-draftly-table-widget": {
|
|
1595
|
+
display: "table",
|
|
1596
|
+
width: "100%",
|
|
1597
|
+
borderCollapse: "separate",
|
|
1598
|
+
borderSpacing: "0",
|
|
1599
|
+
position: "relative",
|
|
1600
|
+
overflow: "visible",
|
|
1601
|
+
border: "1px solid var(--color-border, #d7dee7)",
|
|
1602
|
+
borderRadius: "0.75rem",
|
|
1603
|
+
backgroundColor: "var(--color-background, #ffffff)",
|
|
1604
|
+
|
|
1605
|
+
"& .cm-draftly-table": {
|
|
1606
|
+
width: "100%",
|
|
1607
|
+
borderCollapse: "separate",
|
|
1608
|
+
borderSpacing: "0",
|
|
1609
|
+
tableLayout: "auto",
|
|
1610
|
+
},
|
|
1611
|
+
|
|
1612
|
+
"& .cm-draftly-table-row": {
|
|
1613
|
+
display: "table-row !important",
|
|
1614
|
+
},
|
|
1615
|
+
|
|
1616
|
+
"& .cm-draftly-table-header-row": {
|
|
1617
|
+
backgroundColor: "rgba(15, 23, 42, 0.04)",
|
|
1618
|
+
},
|
|
1619
|
+
|
|
1620
|
+
"& .cm-draftly-table-row-even": {
|
|
1621
|
+
backgroundColor: "rgba(15, 23, 42, 0.02)",
|
|
1622
|
+
},
|
|
1623
|
+
|
|
1624
|
+
"& .cm-draftly-table-delimiter-row": {
|
|
1625
|
+
display: "none !important",
|
|
1626
|
+
},
|
|
1627
|
+
|
|
1628
|
+
"& .cm-draftly-table-cell": {
|
|
1629
|
+
display: "table-cell",
|
|
1630
|
+
minWidth: "4rem",
|
|
1631
|
+
minHeight: "2.5rem",
|
|
1632
|
+
height: "2.75rem",
|
|
1633
|
+
padding: "0.5rem 0.875rem",
|
|
1634
|
+
verticalAlign: "top",
|
|
1635
|
+
borderRight: "1px solid var(--color-border, #d7dee7)",
|
|
1636
|
+
borderBottom: "1px solid var(--color-border, #d7dee7)",
|
|
1637
|
+
whiteSpace: "normal",
|
|
1638
|
+
overflowWrap: "break-word",
|
|
1639
|
+
wordBreak: "normal",
|
|
1640
|
+
lineHeight: "1.6",
|
|
1641
|
+
},
|
|
1642
|
+
|
|
1643
|
+
"& .cm-draftly-table-body-row": {
|
|
1644
|
+
minHeight: "2.75rem",
|
|
1645
|
+
},
|
|
1646
|
+
|
|
1647
|
+
"& .cm-draftly-table-cell .cm-draftly-code-inline": {
|
|
1648
|
+
whiteSpace: "normal",
|
|
1649
|
+
overflowWrap: "anywhere",
|
|
1650
|
+
},
|
|
1651
|
+
|
|
1652
|
+
"& .cm-draftly-table-th": {
|
|
1653
|
+
fontWeight: "600",
|
|
1654
|
+
borderBottomWidth: "2px",
|
|
1655
|
+
},
|
|
1656
|
+
|
|
1657
|
+
"& .cm-draftly-table-cell-last": {
|
|
1658
|
+
borderRight: "none",
|
|
1659
|
+
},
|
|
1660
|
+
|
|
1661
|
+
"& .cm-draftly-table-row-last .cm-draftly-table-cell": {
|
|
1662
|
+
borderBottom: "none",
|
|
1663
|
+
},
|
|
1664
|
+
|
|
1665
|
+
"& .cm-draftly-table-cell-center": {
|
|
1666
|
+
textAlign: "center",
|
|
1667
|
+
},
|
|
1668
|
+
|
|
1669
|
+
"& .cm-draftly-table-cell-right": {
|
|
1670
|
+
textAlign: "right",
|
|
1671
|
+
},
|
|
1672
|
+
|
|
1673
|
+
"& .cm-draftly-table-break": {
|
|
1674
|
+
display: "inline",
|
|
1675
|
+
},
|
|
1676
|
+
|
|
1677
|
+
"& .cm-draftly-table-controls-anchor": {
|
|
1678
|
+
position: "absolute",
|
|
1679
|
+
inset: "0",
|
|
1680
|
+
pointerEvents: "none",
|
|
1681
|
+
},
|
|
1682
|
+
|
|
1683
|
+
"& .cm-draftly-table-control": {
|
|
1684
|
+
position: "absolute",
|
|
1685
|
+
width: "1.75rem",
|
|
1686
|
+
height: "1.75rem",
|
|
1687
|
+
border: "1px solid var(--color-border, #d7dee7)",
|
|
1688
|
+
borderRadius: "999px",
|
|
1689
|
+
backgroundColor: "var(--color-background, #ffffff)",
|
|
1690
|
+
color: "var(--color-text, #0f172a)",
|
|
1691
|
+
boxShadow: "0 10px 24px rgba(15, 23, 42, 0.12)",
|
|
1692
|
+
display: "inline-flex",
|
|
1693
|
+
alignItems: "center",
|
|
1694
|
+
justifyContent: "center",
|
|
1695
|
+
opacity: "0",
|
|
1696
|
+
pointerEvents: "auto",
|
|
1697
|
+
transition: "opacity 120ms ease, transform 120ms ease, background-color 120ms ease",
|
|
1698
|
+
},
|
|
1699
|
+
|
|
1700
|
+
"& .cm-draftly-table-control:hover": {
|
|
1701
|
+
backgroundColor: "rgba(15, 23, 42, 0.05)",
|
|
1702
|
+
},
|
|
1703
|
+
|
|
1704
|
+
"& .cm-draftly-table-control-column": {
|
|
1705
|
+
top: "50%",
|
|
1706
|
+
right: "-0.95rem",
|
|
1707
|
+
transform: "translate(0.35rem, -50%)",
|
|
1708
|
+
},
|
|
1709
|
+
|
|
1710
|
+
"& .cm-draftly-table-control-row": {
|
|
1711
|
+
left: "50%",
|
|
1712
|
+
bottom: "-0.95rem",
|
|
1713
|
+
transform: "translate(-50%, 0.35rem)",
|
|
1714
|
+
},
|
|
1715
|
+
|
|
1716
|
+
"&:hover .cm-draftly-table-control, &:focus-within .cm-draftly-table-control": {
|
|
1717
|
+
opacity: "1",
|
|
1718
|
+
},
|
|
1719
|
+
|
|
1720
|
+
"&:hover .cm-draftly-table-control-column, &:focus-within .cm-draftly-table-control-column": {
|
|
1721
|
+
transform: "translate(0, -50%)",
|
|
1722
|
+
},
|
|
1723
|
+
|
|
1724
|
+
"&:hover .cm-draftly-table-control-row, &:focus-within .cm-draftly-table-control-row": {
|
|
1725
|
+
transform: "translate(-50%, 0)",
|
|
1726
|
+
},
|
|
1727
|
+
},
|
|
1728
|
+
},
|
|
1729
|
+
|
|
1730
|
+
dark: {
|
|
1731
|
+
".cm-draftly-table-wrapper, .cm-draftly-table-widget": {
|
|
1732
|
+
borderColor: "var(--color-border, #30363d)",
|
|
1733
|
+
backgroundColor: "var(--color-background, #0d1117)",
|
|
1734
|
+
|
|
1735
|
+
"& .cm-draftly-table-header-row": {
|
|
1736
|
+
backgroundColor: "rgba(255, 255, 255, 0.05)",
|
|
1737
|
+
},
|
|
1738
|
+
|
|
1739
|
+
"& .cm-draftly-table-row-even": {
|
|
1740
|
+
backgroundColor: "rgba(255, 255, 255, 0.025)",
|
|
1741
|
+
},
|
|
1742
|
+
|
|
1743
|
+
"& .cm-draftly-table-cell": {
|
|
1744
|
+
borderColor: "var(--color-border, #30363d)",
|
|
1745
|
+
},
|
|
1746
|
+
|
|
1747
|
+
"& .cm-draftly-table-control": {
|
|
1748
|
+
borderColor: "var(--color-border, #30363d)",
|
|
1749
|
+
backgroundColor: "var(--color-background, #161b22)",
|
|
1750
|
+
color: "var(--color-text, #e6edf3)",
|
|
1751
|
+
boxShadow: "0 12px 28px rgba(0, 0, 0, 0.35)",
|
|
1752
|
+
},
|
|
1753
|
+
|
|
1754
|
+
"& .cm-draftly-table-control:hover": {
|
|
1755
|
+
backgroundColor: "rgba(255, 255, 255, 0.08)",
|
|
1756
|
+
},
|
|
1757
|
+
},
|
|
1758
|
+
},
|
|
1759
|
+
});
|