veryfront 0.1.37 → 0.1.39

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.
@@ -1,4353 +0,0 @@
1
- export interface StudioBridgeOptions {
2
- projectId: string;
3
- pageId: string;
4
- pagePath?: string;
5
- wsUrl?: string;
6
- yjsGuid?: string;
7
- debugSkipInit?: boolean;
8
- debugExposeInternals?: boolean;
9
- }
10
-
11
- export function generateStudioBridgeScript(options: StudioBridgeOptions): string {
12
- return `(function() {
13
- 'use strict';
14
-
15
- const PROJECT_ID = ${JSON.stringify(options.projectId)};
16
- const PAGE_ID = ${JSON.stringify(options.pageId)};
17
- const PAGE_PATH = ${JSON.stringify(options.pagePath ?? options.pageId)};
18
- const WS_URL = ${JSON.stringify(options.wsUrl ?? "")};
19
- const YJS_GUID = ${JSON.stringify(options.yjsGuid ?? "")};
20
- const DEBUG_SKIP_INIT = ${options.debugSkipInit ? "true" : "false"};
21
- const DEBUG_EXPOSE_INTERNALS = ${options.debugExposeInternals ? "true" : "false"};
22
-
23
- const DATA_VF_ID = 'data-vf-id';
24
- const DATA_VF_SELECTOR = 'data-vf-selector';
25
- const DATA_VF_TEXT = 'data-vf-text';
26
- const DATA_VF_IGNORE = 'data-vf-ignore';
27
-
28
- const DATA_NODE_ID = 'data-node-id';
29
- const DATA_NODE_LINE = 'data-node-line';
30
- const DATA_NODE_COLUMN = 'data-node-column';
31
- const DATA_NODE_END_LINE = 'data-node-end-line';
32
- const DATA_NODE_END_COLUMN = 'data-node-end-column';
33
-
34
- let inspectMode = false;
35
- let selectedNodeId = null;
36
- let hoveredNodeId = null;
37
- let lastTreeSignature = '';
38
-
39
- let hoverOverlay = null;
40
- let selectionOverlay = null;
41
- let markdownEditorRoot = null;
42
- let markdownEditorSurface = null;
43
- let markdownEditorTextarea = null;
44
- let markdownEditButton = null;
45
- let markdownFileId = null;
46
- let markdownSyncTimer = null;
47
- let markdownSelectionSyncTimer = null;
48
- let markdownPersistStatus = null;
49
- let markdownPresenceRoot = null;
50
- let markdownSelectionsRoot = null;
51
- let markdownSelectionOverlayRoot = null;
52
- let markdownOverlaySelections = [];
53
- let markdownSelectionOverlayRenderFrame = null;
54
- let markdownSlashMenuRoot = null;
55
- let markdownSlashMenuTimer = null;
56
- let markdownSlashMenuContext = null;
57
- let markdownSlashMenuCommands = [];
58
- let markdownSlashMenuActiveIndex = 0;
59
- let markdownInlineToolbarRoot = null;
60
- let markdownInlineToolbarFrame = null;
61
- let markdownBlockDragHandle = null;
62
- let markdownBlockDropIndicator = null;
63
- let markdownBlockDropLabel = null;
64
- let markdownBlockDragGhost = null;
65
- let markdownBlockDragSourceIndex = -1;
66
- let markdownBlockDropSlotIndex = -1;
67
- let markdownBlockHandleHoverIndex = -1;
68
- let markdownBlockDragActive = false;
69
- let markdownMdxBlocksRoot = null;
70
- let markdownLexicalApi = null;
71
- let markdownLexicalSetupPromise = null;
72
- let markdownCurrentContent = '';
73
- let markdownCurrentEditorContent = '';
74
- let markdownLexicalRenderedContent = null;
75
- let markdownApplyingRemoteUpdate = false;
76
- let markdownFrontmatter = '';
77
- let markdownRawBlocks = [];
78
- let markdownRawBlockTokenPrefix = 'VF_RAW_BLOCK';
79
- let markdownLatestMdxBlocks = [];
80
- let markdownLatestMdxImportMap = {};
81
- let markdownLatestPresenceUsers = [];
82
- let markdownLatestSelections = [];
83
- let markdownHasUnsavedChanges = false;
84
- let markdownSaveInProgress = false;
85
- let markdownYDoc = null;
86
- let markdownYProvider = null;
87
- let markdownYText = null;
88
- let markdownYjsConnected = false;
89
- let markdownYjsSetupId = 0;
90
- let markdownYjsY = null;
91
- let markdownPendingSelection = null;
92
- const LEXICAL_YJS_ORIGIN = 'lexical-yjs-binding';
93
-
94
- const MARKDOWN_SLASH_COMMANDS = [
95
- {
96
- id: 'heading-1',
97
- label: 'Heading 1',
98
- description: 'Create a top-level heading',
99
- aliases: ['h1', 'heading', 'title']
100
- },
101
- {
102
- id: 'heading-2',
103
- label: 'Heading 2',
104
- description: 'Create a second-level heading',
105
- aliases: ['h2', 'heading2', 'subheading']
106
- },
107
- {
108
- id: 'heading-3',
109
- label: 'Heading 3',
110
- description: 'Create a third-level heading',
111
- aliases: ['h3', 'heading3']
112
- },
113
- {
114
- id: 'bulleted-list',
115
- label: 'Bulleted list',
116
- description: 'Start a bullet list item',
117
- aliases: ['list', 'bullet', 'ul']
118
- },
119
- {
120
- id: 'numbered-list',
121
- label: 'Numbered list',
122
- description: 'Start a numbered list item',
123
- aliases: ['olist', 'numbered', 'ol']
124
- },
125
- {
126
- id: 'quote-block',
127
- label: 'Quote',
128
- description: 'Insert a block quote line',
129
- aliases: ['quote', 'blockquote']
130
- },
131
- {
132
- id: 'code-block',
133
- label: 'Code block',
134
- description: 'Insert a fenced code block',
135
- aliases: ['code', 'fence', 'snippet']
136
- },
137
- {
138
- id: 'image',
139
- label: 'Image',
140
- description: 'Insert markdown image syntax',
141
- aliases: ['image', 'img', 'photo']
142
- }
143
- ];
144
-
145
- function debounce(fn, ms) {
146
- let timer;
147
- return function(...args) {
148
- clearTimeout(timer);
149
- timer = setTimeout(function() {
150
- fn.apply(this, args);
151
- }, ms);
152
- };
153
- }
154
-
155
- function injectOverlayStyles() {
156
- if (document.getElementById('vf-overlay-styles')) return;
157
-
158
- const style = document.createElement('style');
159
- style.id = 'vf-overlay-styles';
160
- style.textContent = \`
161
- .vf-overlay {
162
- position: fixed;
163
- pointer-events: none;
164
- z-index: 99999;
165
- box-sizing: border-box;
166
- transition: all 0.05s ease-out;
167
- }
168
- .vf-overlay-hover {
169
- border: 2px solid #0081F8;
170
- background: rgba(0, 129, 248, 0.05);
171
- }
172
- .vf-overlay-selection {
173
- border: 2px solid #0081F8;
174
- background: rgba(0, 129, 248, 0.1);
175
- }
176
- .vf-overlay-label {
177
- position: absolute;
178
- top: -22px;
179
- left: -2px;
180
- background: #0081F8;
181
- color: white;
182
- font-size: 11px;
183
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
184
- padding: 2px 6px;
185
- border-radius: 3px 3px 0 0;
186
- white-space: nowrap;
187
- pointer-events: none;
188
- }
189
- .vf-overlay-label-bottom {
190
- top: auto;
191
- bottom: -22px;
192
- border-radius: 0 0 3px 3px;
193
- }
194
-
195
- .vf-markdown-edit-button {
196
- position: fixed;
197
- right: 16px;
198
- bottom: 16px;
199
- z-index: 100001;
200
- border: 1px solid rgba(0, 0, 0, 0.2);
201
- border-radius: 8px;
202
- background: #111827;
203
- color: #ffffff;
204
- font-size: 13px;
205
- line-height: 1;
206
- padding: 10px 12px;
207
- cursor: pointer;
208
- box-shadow: 0 10px 20px rgba(0, 0, 0, 0.15);
209
- }
210
-
211
- .vf-markdown-editor {
212
- position: fixed;
213
- inset: 0;
214
- z-index: 100000;
215
- background: var(--vf-markdown-editor-bg, #ffffff);
216
- display: none;
217
- }
218
-
219
- .vf-markdown-editor__toolbar {
220
- display: flex;
221
- justify-content: space-between;
222
- align-items: center;
223
- gap: 8px;
224
- padding: 12px 16px;
225
- border-bottom: 1px solid rgba(0, 0, 0, 0.12);
226
- background: rgba(255, 255, 255, 0.94);
227
- position: sticky;
228
- top: 0;
229
- }
230
-
231
- .vf-markdown-editor__title {
232
- font-size: 12px;
233
- color: #374151;
234
- font-weight: 600;
235
- display: inline-flex;
236
- flex-direction: column;
237
- gap: 2px;
238
- }
239
-
240
- .vf-markdown-editor__title-main {
241
- font-size: 12px;
242
- font-weight: 700;
243
- }
244
-
245
- .vf-markdown-editor__title-hints {
246
- font-size: 10px;
247
- font-weight: 500;
248
- color: #6b7280;
249
- }
250
-
251
- .vf-markdown-editor__actions {
252
- display: inline-flex;
253
- align-items: center;
254
- gap: 8px;
255
- }
256
-
257
- .vf-markdown-editor__status {
258
- font-size: 12px;
259
- color: #6b7280;
260
- }
261
-
262
- .vf-markdown-editor__status[data-state='saving'] {
263
- color: #b45309;
264
- }
265
-
266
- .vf-markdown-editor__status[data-state='saved'] {
267
- color: #15803d;
268
- }
269
-
270
- .vf-markdown-editor__status[data-state='error'] {
271
- color: #b91c1c;
272
- }
273
-
274
- .vf-markdown-editor__presence {
275
- display: none;
276
- align-items: center;
277
- gap: 6px;
278
- }
279
-
280
- .vf-markdown-editor__presence-pill {
281
- border: 1px solid rgba(0, 0, 0, 0.16);
282
- border-left-width: 4px;
283
- border-radius: 999px;
284
- padding: 2px 8px;
285
- font-size: 11px;
286
- color: #111827;
287
- background: #ffffff;
288
- }
289
-
290
- .vf-markdown-editor__presence-pill[data-current='true'] {
291
- font-weight: 600;
292
- }
293
-
294
- .vf-markdown-editor__presence-pill[data-agent='true'] {
295
- font-style: italic;
296
- }
297
-
298
- .vf-markdown-editor__selections {
299
- display: none;
300
- align-items: center;
301
- gap: 6px;
302
- }
303
-
304
- .vf-markdown-editor__selection-pill {
305
- border: 1px solid rgba(0, 0, 0, 0.16);
306
- border-left-width: 4px;
307
- border-radius: 999px;
308
- padding: 2px 8px;
309
- font-size: 11px;
310
- color: #111827;
311
- background: #ffffff;
312
- }
313
-
314
- .vf-markdown-editor__exit {
315
- border: 1px solid rgba(0, 0, 0, 0.16);
316
- border-radius: 6px;
317
- background: #ffffff;
318
- color: #111827;
319
- font-size: 12px;
320
- padding: 6px 10px;
321
- cursor: pointer;
322
- }
323
-
324
- .vf-markdown-editor__history {
325
- border: 1px solid rgba(0, 0, 0, 0.16);
326
- border-radius: 6px;
327
- background: #ffffff;
328
- color: #111827;
329
- font-size: 13px;
330
- line-height: 1;
331
- min-width: 28px;
332
- height: 28px;
333
- padding: 0 8px;
334
- cursor: pointer;
335
- }
336
-
337
- .vf-markdown-editor__surface-wrap {
338
- position: relative;
339
- height: calc(100vh - 52px);
340
- }
341
-
342
- .vf-markdown-editor__surface {
343
- width: 100%;
344
- max-width: 980px;
345
- margin: 0 auto;
346
- height: 100%;
347
- overflow: auto;
348
- outline: none;
349
- position: relative;
350
- z-index: 1;
351
- background: transparent;
352
- padding: 32px 40px;
353
- box-sizing: border-box;
354
- }
355
-
356
- .vf-markdown-editor__selection-overlay {
357
- position: absolute;
358
- inset: 0;
359
- pointer-events: none;
360
- z-index: 2;
361
- display: none;
362
- }
363
-
364
- .vf-markdown-editor__selection-highlight {
365
- position: absolute;
366
- border-radius: 3px;
367
- opacity: 0.26;
368
- }
369
-
370
- .vf-markdown-editor__selection-caret {
371
- position: absolute;
372
- width: 2px;
373
- border-radius: 1px;
374
- }
375
-
376
- .vf-markdown-editor__selection-label {
377
- position: absolute;
378
- transform: translateY(-100%);
379
- margin-top: -4px;
380
- border-radius: 999px;
381
- padding: 1px 7px;
382
- font-size: 10px;
383
- line-height: 1.4;
384
- white-space: nowrap;
385
- color: #ffffff;
386
- box-shadow: 0 2px 8px rgba(0, 0, 0, 0.16);
387
- }
388
-
389
- .vf-markdown-editor__slash-menu {
390
- position: fixed;
391
- z-index: 100005;
392
- min-width: 220px;
393
- max-width: 300px;
394
- border: 1px solid rgba(17, 24, 39, 0.16);
395
- border-radius: 10px;
396
- background: #ffffff;
397
- box-shadow: 0 12px 28px rgba(0, 0, 0, 0.18);
398
- padding: 6px;
399
- display: none;
400
- }
401
-
402
- .vf-markdown-editor__slash-item {
403
- display: block;
404
- width: 100%;
405
- border: 0;
406
- border-radius: 8px;
407
- background: transparent;
408
- text-align: left;
409
- padding: 8px 10px;
410
- cursor: pointer;
411
- }
412
-
413
- .vf-markdown-editor__slash-item:hover,
414
- .vf-markdown-editor__slash-item[data-active='true'] {
415
- background: rgba(0, 129, 248, 0.12);
416
- }
417
-
418
- .vf-markdown-editor__slash-item-title {
419
- display: block;
420
- font-size: 12px;
421
- font-weight: 600;
422
- color: #111827;
423
- line-height: 1.35;
424
- }
425
-
426
- .vf-markdown-editor__slash-item-desc {
427
- display: block;
428
- margin-top: 2px;
429
- font-size: 11px;
430
- color: #6b7280;
431
- line-height: 1.35;
432
- }
433
-
434
- .vf-markdown-editor__inline-toolbar {
435
- position: fixed;
436
- z-index: 100006;
437
- display: none;
438
- align-items: center;
439
- gap: 2px;
440
- border: 1px solid rgba(17, 24, 39, 0.16);
441
- border-radius: 8px;
442
- background: #ffffff;
443
- box-shadow: 0 8px 20px rgba(0, 0, 0, 0.16);
444
- padding: 4px;
445
- }
446
-
447
- .vf-markdown-editor__inline-button {
448
- border: 0;
449
- border-radius: 6px;
450
- background: transparent;
451
- color: #111827;
452
- font-size: 12px;
453
- font-weight: 600;
454
- line-height: 1;
455
- min-width: 26px;
456
- height: 24px;
457
- padding: 0 7px;
458
- cursor: pointer;
459
- }
460
-
461
- .vf-markdown-editor__inline-button:hover {
462
- background: rgba(0, 129, 248, 0.12);
463
- }
464
-
465
- .vf-markdown-editor__block-handle {
466
- position: fixed;
467
- z-index: 100007;
468
- display: none;
469
- border: 1px solid rgba(17, 24, 39, 0.18);
470
- border-radius: 6px;
471
- background: #ffffff;
472
- color: #374151;
473
- font-size: 12px;
474
- font-weight: 700;
475
- line-height: 1;
476
- width: 28px;
477
- height: 28px;
478
- padding: 0;
479
- cursor: grab;
480
- box-shadow: 0 2px 8px rgba(0, 0, 0, 0.14);
481
- }
482
-
483
- .vf-markdown-editor__block-handle:hover {
484
- background: rgba(0, 129, 248, 0.12);
485
- }
486
-
487
- .vf-markdown-editor__block-handle[data-dragging='true'] {
488
- cursor: grabbing;
489
- }
490
-
491
- .vf-markdown-editor__block-drop-indicator {
492
- position: fixed;
493
- z-index: 100006;
494
- display: none;
495
- height: 2px;
496
- border-radius: 999px;
497
- background: #0081f8;
498
- box-shadow: 0 1px 6px rgba(0, 129, 248, 0.5);
499
- }
500
-
501
- .vf-markdown-editor__block-drop-label {
502
- position: fixed;
503
- z-index: 100007;
504
- display: none;
505
- border-radius: 999px;
506
- border: 1px solid rgba(0, 129, 248, 0.24);
507
- background: rgba(255, 255, 255, 0.96);
508
- color: #0f172a;
509
- font-size: 11px;
510
- font-weight: 600;
511
- line-height: 1.2;
512
- padding: 3px 8px;
513
- box-shadow: 0 2px 8px rgba(0, 0, 0, 0.14);
514
- }
515
-
516
- .vf-markdown-editor__block-drag-ghost {
517
- position: fixed;
518
- top: -9999px;
519
- left: -9999px;
520
- width: 260px;
521
- border: 1px solid rgba(17, 24, 39, 0.22);
522
- border-radius: 10px;
523
- background: #ffffff;
524
- box-shadow: 0 10px 24px rgba(0, 0, 0, 0.2);
525
- padding: 8px 10px;
526
- }
527
-
528
- .vf-markdown-editor__block-drag-ghost-title {
529
- display: block;
530
- font-size: 11px;
531
- font-weight: 700;
532
- color: #1e293b;
533
- }
534
-
535
- .vf-markdown-editor__block-drag-ghost-text {
536
- display: block;
537
- margin-top: 4px;
538
- font-size: 11px;
539
- color: #475569;
540
- line-height: 1.35;
541
- }
542
-
543
- .vf-markdown-editor__mdx-blocks {
544
- display: none;
545
- gap: 8px;
546
- padding: 8px 16px;
547
- border-bottom: 1px solid rgba(0, 0, 0, 0.08);
548
- background: rgba(245, 247, 250, 0.95);
549
- overflow-x: auto;
550
- }
551
-
552
- .vf-markdown-editor__mdx-block {
553
- display: inline-flex;
554
- align-items: center;
555
- gap: 8px;
556
- border: 1px solid rgba(0, 0, 0, 0.16);
557
- border-radius: 8px;
558
- background: #ffffff;
559
- padding: 6px 8px;
560
- }
561
-
562
- .vf-markdown-editor__mdx-block-label {
563
- font-size: 11px;
564
- color: #334155;
565
- white-space: nowrap;
566
- }
567
-
568
- .vf-markdown-editor__mdx-note {
569
- font-size: 10px;
570
- color: #6b7280;
571
- white-space: nowrap;
572
- }
573
-
574
- .vf-markdown-editor__mdx-open {
575
- border: 1px solid rgba(0, 0, 0, 0.16);
576
- border-radius: 6px;
577
- background: #ffffff;
578
- color: #0f172a;
579
- font-size: 11px;
580
- line-height: 1;
581
- padding: 5px 7px;
582
- cursor: pointer;
583
- white-space: nowrap;
584
- }
585
-
586
- .vf-markdown-editor__surface [data-lexical-editor] {
587
- outline: none;
588
- }
589
-
590
- .vf-markdown-editor__surface p:empty::before {
591
- content: '';
592
- display: inline-block;
593
- }
594
-
595
- .vf-markdown-editor__surface p {
596
- min-height: 1.5em;
597
- }
598
-
599
- .vf-markdown-editor__textarea {
600
- width: 100%;
601
- height: calc(100vh - 52px);
602
- border: 0;
603
- outline: none;
604
- resize: none;
605
- display: none;
606
- font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace;
607
- font-size: 14px;
608
- line-height: 1.6;
609
- color: #111827;
610
- background: transparent;
611
- padding: 16px;
612
- box-sizing: border-box;
613
- }
614
-
615
- [data-theme='dark'] .vf-markdown-editor {
616
- --vf-markdown-editor-bg: #0b1220;
617
- }
618
-
619
- [data-theme='dark'] .vf-markdown-editor__toolbar {
620
- border-bottom-color: rgba(255, 255, 255, 0.18);
621
- background: rgba(17, 24, 39, 0.92);
622
- }
623
-
624
- [data-theme='dark'] .vf-markdown-editor__title {
625
- color: #d1d5db;
626
- }
627
-
628
- [data-theme='dark'] .vf-markdown-editor__title-hints {
629
- color: #9ca3af;
630
- }
631
-
632
- [data-theme='dark'] .vf-markdown-editor__exit {
633
- background: #111827;
634
- border-color: rgba(255, 255, 255, 0.2);
635
- color: #f9fafb;
636
- }
637
-
638
- [data-theme='dark'] .vf-markdown-editor__history {
639
- background: #111827;
640
- border-color: rgba(255, 255, 255, 0.2);
641
- color: #f9fafb;
642
- }
643
-
644
- [data-theme='dark'] .vf-markdown-editor__status {
645
- color: #9ca3af;
646
- }
647
-
648
- [data-theme='dark'] .vf-markdown-editor__status[data-state='saving'] {
649
- color: #fbbf24;
650
- }
651
-
652
- [data-theme='dark'] .vf-markdown-editor__status[data-state='saved'] {
653
- color: #4ade80;
654
- }
655
-
656
- [data-theme='dark'] .vf-markdown-editor__status[data-state='error'] {
657
- color: #f87171;
658
- }
659
-
660
- [data-theme='dark'] .vf-markdown-editor__presence-pill {
661
- border-color: rgba(255, 255, 255, 0.2);
662
- color: #f9fafb;
663
- background: #111827;
664
- }
665
-
666
- [data-theme='dark'] .vf-markdown-editor__selection-pill {
667
- border-color: rgba(255, 255, 255, 0.2);
668
- color: #f9fafb;
669
- background: #111827;
670
- }
671
-
672
- [data-theme='dark'] .vf-markdown-editor__textarea {
673
- color: #f9fafb;
674
- }
675
-
676
- [data-theme='dark'] .vf-markdown-editor__slash-menu {
677
- border-color: rgba(255, 255, 255, 0.2);
678
- background: #111827;
679
- }
680
-
681
- [data-theme='dark'] .vf-markdown-editor__slash-item:hover,
682
- [data-theme='dark'] .vf-markdown-editor__slash-item[data-active='true'] {
683
- background: rgba(59, 130, 246, 0.24);
684
- }
685
-
686
- [data-theme='dark'] .vf-markdown-editor__slash-item-title {
687
- color: #f9fafb;
688
- }
689
-
690
- [data-theme='dark'] .vf-markdown-editor__slash-item-desc {
691
- color: #9ca3af;
692
- }
693
-
694
- [data-theme='dark'] .vf-markdown-editor__inline-toolbar {
695
- border-color: rgba(255, 255, 255, 0.2);
696
- background: #111827;
697
- }
698
-
699
- [data-theme='dark'] .vf-markdown-editor__inline-button {
700
- color: #f9fafb;
701
- }
702
-
703
- [data-theme='dark'] .vf-markdown-editor__inline-button:hover {
704
- background: rgba(59, 130, 246, 0.24);
705
- }
706
-
707
- [data-theme='dark'] .vf-markdown-editor__block-handle {
708
- border-color: rgba(255, 255, 255, 0.22);
709
- background: #111827;
710
- color: #d1d5db;
711
- }
712
-
713
- [data-theme='dark'] .vf-markdown-editor__block-handle:hover {
714
- background: rgba(59, 130, 246, 0.24);
715
- }
716
-
717
- [data-theme='dark'] .vf-markdown-editor__block-drop-label {
718
- border-color: rgba(59, 130, 246, 0.35);
719
- background: rgba(17, 24, 39, 0.94);
720
- color: #e5e7eb;
721
- }
722
-
723
- [data-theme='dark'] .vf-markdown-editor__block-drag-ghost {
724
- border-color: rgba(255, 255, 255, 0.24);
725
- background: #111827;
726
- }
727
-
728
- [data-theme='dark'] .vf-markdown-editor__block-drag-ghost-title {
729
- color: #e5e7eb;
730
- }
731
-
732
- [data-theme='dark'] .vf-markdown-editor__block-drag-ghost-text {
733
- color: #94a3b8;
734
- }
735
-
736
- [data-theme='dark'] .vf-markdown-editor__mdx-blocks {
737
- border-bottom-color: rgba(255, 255, 255, 0.12);
738
- background: rgba(2, 6, 23, 0.7);
739
- }
740
-
741
- [data-theme='dark'] .vf-markdown-editor__mdx-block {
742
- border-color: rgba(255, 255, 255, 0.22);
743
- background: #111827;
744
- }
745
-
746
- [data-theme='dark'] .vf-markdown-editor__mdx-block-label {
747
- color: #cbd5e1;
748
- }
749
-
750
- [data-theme='dark'] .vf-markdown-editor__mdx-note {
751
- color: #94a3b8;
752
- }
753
-
754
- [data-theme='dark'] .vf-markdown-editor__mdx-open {
755
- border-color: rgba(255, 255, 255, 0.22);
756
- background: #0b1220;
757
- color: #e5e7eb;
758
- }
759
- \`;
760
- try {
761
- document.head.appendChild(style);
762
- if (!style.sheet) {
763
- console.warn('[StudioBridge] Inline style injection may be blocked by CSP (style-src).');
764
- }
765
- } catch (error) {
766
- console.warn('[StudioBridge] Failed to inject bridge styles. This may be caused by CSP style-src restrictions.', error);
767
- }
768
- }
769
-
770
- function createOverlay(type) {
771
- const overlay = document.createElement('div');
772
- overlay.className = 'vf-overlay vf-overlay-' + type;
773
- overlay.setAttribute(DATA_VF_IGNORE, 'true');
774
-
775
- const label = document.createElement('div');
776
- label.className = 'vf-overlay-label';
777
- overlay.appendChild(label);
778
-
779
- overlay.style.display = 'none';
780
- document.body.appendChild(overlay);
781
- return overlay;
782
- }
783
-
784
- function hideOverlay(overlay) {
785
- if (overlay) overlay.style.display = 'none';
786
- }
787
-
788
- function positionOverlay(overlay, element, nodeName) {
789
- if (!overlay) return;
790
- if (!element) {
791
- hideOverlay(overlay);
792
- return;
793
- }
794
-
795
- const rect = element.getBoundingClientRect();
796
-
797
- overlay.style.display = 'block';
798
- overlay.style.top = rect.top + 'px';
799
- overlay.style.left = rect.left + 'px';
800
- overlay.style.width = rect.width + 'px';
801
- overlay.style.height = rect.height + 'px';
802
-
803
- const label = overlay.querySelector('.vf-overlay-label');
804
- if (!label) return;
805
-
806
- label.textContent = nodeName || element.tagName.toLowerCase();
807
- if (rect.top < 24) {
808
- label.classList.add('vf-overlay-label-bottom');
809
- } else {
810
- label.classList.remove('vf-overlay-label-bottom');
811
- }
812
- }
813
-
814
- function getNodeName(element) {
815
- const vfId = element.getAttribute(DATA_VF_ID);
816
- if (vfId) return vfId.split('_')[0];
817
- return element.tagName.toLowerCase();
818
- }
819
-
820
- function findElementById(nodeId) {
821
- if (!nodeId) return null;
822
- return (
823
- document.querySelector('[' + DATA_VF_ID + '="' + nodeId + '"]') ||
824
- document.querySelector('[' + DATA_VF_SELECTOR + '="' + nodeId + '"]') ||
825
- document.querySelector('[' + DATA_NODE_ID + '="' + nodeId + '"]')
826
- );
827
- }
828
-
829
- function postToStudio(message) {
830
- if (!window.parent || window.parent === window) return;
831
- try {
832
- window.parent.postMessage(message, '*');
833
- } catch (e) {
834
- console.debug('[StudioBridge] postMessage failed:', e);
835
- }
836
- }
837
-
838
- function isFromStudio(event) {
839
- try {
840
- const url = new URL(event.origin || '');
841
- const host = url.hostname;
842
- return (
843
- host === 'localhost' ||
844
- host.endsWith('.veryfront.org') || host === 'veryfront.org' ||
845
- host.endsWith('.veryfront.com') || host === 'veryfront.com' ||
846
- host.endsWith('.veryfront.dev') || host === 'veryfront.dev'
847
- );
848
- } catch (e) {
849
- return false;
850
- }
851
- }
852
-
853
- const originalConsole = {};
854
- const consoleMethods = ['log', 'debug', 'info', 'warn', 'error', 'table', 'clear', 'dir'];
855
- let logCounter = 0;
856
-
857
- function setupConsoleCapture() {
858
- consoleMethods.forEach(method => {
859
- originalConsole[method] = console[method];
860
- console[method] = function(...args) {
861
- originalConsole[method].apply(console, args);
862
-
863
- const logId = 'vf-' + Date.now() + '-' + ++logCounter;
864
-
865
- const formattedData = args.map(arg => {
866
- try {
867
- if (arg instanceof Error) {
868
- return { __isError: true, message: arg.message, stack: arg.stack, name: arg.name };
869
- }
870
- if (arg === undefined) return { __isUndefined: true };
871
- if (arg === null) return null;
872
- if (typeof arg === 'function') return { __isFunction: true, name: arg.name || 'anonymous' };
873
- if (typeof arg === 'symbol') return { __isSymbol: true, description: arg.description };
874
- if (typeof arg === 'object') return JSON.parse(JSON.stringify(arg));
875
- return arg;
876
- } catch (e) {
877
- return String(arg);
878
- }
879
- });
880
-
881
- postToStudio({
882
- action: 'logEvent',
883
- value: {
884
- id: logId,
885
- method: method,
886
- data: formattedData,
887
- timestamp: new Date().toISOString()
888
- }
889
- });
890
- };
891
- });
892
- }
893
-
894
- function setupErrorHandling() {
895
- function hideOverlays() {
896
- hideOverlay(hoverOverlay);
897
- hideOverlay(selectionOverlay);
898
- }
899
-
900
- window.addEventListener('error', function(event) {
901
- hideOverlays();
902
- postToStudio({
903
- action: 'runtimeError',
904
- url: window.location.href,
905
- errors: [
906
- {
907
- type: 'error',
908
- message: event.message,
909
- file: event.filename,
910
- line: event.lineno,
911
- column: event.colno
912
- }
913
- ]
914
- });
915
- });
916
-
917
- window.addEventListener('unhandledrejection', function(event) {
918
- hideOverlays();
919
- const reason = event.reason;
920
- postToStudio({
921
- action: 'runtimeError',
922
- url: window.location.href,
923
- errors: [
924
- {
925
- type: 'error',
926
- message: reason instanceof Error ? reason.message : String(reason),
927
- file: reason instanceof Error ? reason.stack : undefined
928
- }
929
- ]
930
- });
931
- });
932
- }
933
-
934
- const DOM_IGNORE_TAGS = ['SCRIPT', 'STYLE', 'LINK', 'META', 'NOSCRIPT'];
935
-
936
- function isValidElement(el) {
937
- return (
938
- el &&
939
- el.nodeType === Node.ELEMENT_NODE &&
940
- !DOM_IGNORE_TAGS.includes(el.tagName) &&
941
- !el.hasAttribute(DATA_VF_IGNORE) &&
942
- el.style.display !== 'none'
943
- );
944
- }
945
-
946
- function getNodeType(el) {
947
- const tagName = el.tagName.toLowerCase();
948
-
949
- const vfId = el.getAttribute(DATA_VF_ID) || '';
950
- if (vfId && /^[A-Z]/.test(vfId)) return 'component';
951
- if (el.hasAttribute(DATA_VF_TEXT)) return 'text';
952
-
953
- if (['h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'p', 'blockquote', 'ul', 'ol', 'li', 'pre', 'code'].includes(tagName)) {
954
- return 'markdown';
955
- }
956
-
957
- return 'element';
958
- }
959
-
960
- function buildNavigatorTree(root) {
961
- let nodeIndex = 0;
962
-
963
- function processElement(el, parentId) {
964
- if (!isValidElement(el)) {
965
- const children = [];
966
- Array.from(el.children || []).forEach(child => {
967
- children.push(...processElement(child, parentId));
968
- });
969
- return children;
970
- }
971
-
972
- let id = el.getAttribute(DATA_VF_ID) || el.getAttribute(DATA_NODE_ID) || el.getAttribute(DATA_VF_SELECTOR);
973
- if (!id) {
974
- id = 'vf-' + el.tagName.toLowerCase() + '-' + ++nodeIndex;
975
- el.setAttribute(DATA_VF_SELECTOR, id);
976
- }
977
-
978
- const vfId = el.getAttribute(DATA_VF_ID);
979
- const name = vfId ? vfId.split('_')[0] : el.tagName.toLowerCase();
980
-
981
- const node = {
982
- id: id,
983
- name: name,
984
- type: getNodeType(el),
985
- path: PAGE_PATH,
986
- parentId: parentId,
987
- start: {
988
- line: parseInt(el.getAttribute(DATA_NODE_LINE) || '0', 10),
989
- column: parseInt(el.getAttribute(DATA_NODE_COLUMN) || '0', 10)
990
- },
991
- end: {
992
- line: parseInt(el.getAttribute(DATA_NODE_END_LINE) || '0', 10),
993
- column: parseInt(el.getAttribute(DATA_NODE_END_COLUMN) || '0', 10)
994
- },
995
- children: [],
996
- text: el.hasAttribute(DATA_VF_TEXT) ? el.textContent?.trim() : undefined,
997
- isRemote: false
998
- };
999
-
1000
- Array.from(el.children || []).forEach(child => {
1001
- node.children.push(...processElement(child, id));
1002
- });
1003
-
1004
- return [node];
1005
- }
1006
-
1007
- const rootNode = {
1008
- id: 'root',
1009
- name: 'root',
1010
- type: 'root',
1011
- path: '',
1012
- parentId: '',
1013
- start: { line: 0, column: 0 },
1014
- end: { line: 0, column: 0 },
1015
- children: []
1016
- };
1017
-
1018
- Array.from(root.children || []).forEach(child => {
1019
- rootNode.children.push(...processElement(child, 'root'));
1020
- });
1021
-
1022
- return rootNode;
1023
- }
1024
-
1025
- function createTreeSignature(root) {
1026
- const validElements = Array.from(root.querySelectorAll('*')).filter(el => isValidElement(el));
1027
- return validElements.length + '-' + validElements.map(el => el.tagName).join('');
1028
- }
1029
-
1030
- let treeUpdateTimer = null;
1031
- let mutationObserver = null;
1032
-
1033
- function sendTreeUpdate() {
1034
- const root = document.getElementById('root') || document.body;
1035
- if (!root) return;
1036
-
1037
- const signature = createTreeSignature(root);
1038
- if (signature === lastTreeSignature) return;
1039
- lastTreeSignature = signature;
1040
-
1041
- postToStudio({
1042
- action: 'treeUpdated',
1043
- id: PAGE_ID,
1044
- url: window.location.href,
1045
- tree: buildNavigatorTree(root),
1046
- sourceHash: window.__VERYFRONT_SOURCE_HASH__ || null
1047
- });
1048
- }
1049
-
1050
- function debouncedTreeUpdate() {
1051
- if (treeUpdateTimer) clearTimeout(treeUpdateTimer);
1052
- treeUpdateTimer = setTimeout(sendTreeUpdate, 150);
1053
- }
1054
-
1055
- function setupMutationObserver() {
1056
- const root = document.getElementById('root') || document.body;
1057
- if (!root) return;
1058
-
1059
- mutationObserver = new MutationObserver(function(mutations) {
1060
- const hasRelevantChanges = mutations.some(m => m.type === 'childList' || m.type === 'characterData');
1061
- if (hasRelevantChanges) debouncedTreeUpdate();
1062
- });
1063
-
1064
- mutationObserver.observe(root, { childList: true, characterData: true, subtree: true });
1065
- sendTreeUpdate();
1066
- }
1067
-
1068
- function showOverlay(overlay, nodeId) {
1069
- if (!nodeId) {
1070
- hideOverlay(overlay);
1071
- return;
1072
- }
1073
-
1074
- const el = findElementById(nodeId);
1075
- if (!el) {
1076
- hideOverlay(overlay);
1077
- return;
1078
- }
1079
-
1080
- positionOverlay(overlay, el, getNodeName(el));
1081
- }
1082
-
1083
- function showHoverOverlay(nodeId) {
1084
- showOverlay(hoverOverlay, nodeId);
1085
- }
1086
-
1087
- function showSelectionOverlay(nodeId) {
1088
- showOverlay(selectionOverlay, nodeId);
1089
- }
1090
-
1091
- function scrollToElement(nodeId) {
1092
- const el =
1093
- document.querySelector('[' + DATA_VF_ID + '="' + nodeId + '"]') ||
1094
- document.querySelector('[' + DATA_NODE_ID + '="' + nodeId + '"]') ||
1095
- document.querySelector('[' + DATA_VF_SELECTOR + '*="' + nodeId + '"]');
1096
-
1097
- if (el) el.scrollIntoView({ behavior: 'smooth', block: 'center' });
1098
- }
1099
-
1100
- function setupInspectMode() {
1101
- const INSPECTABLE_SELECTOR = '[' + DATA_VF_ID + '], [' + DATA_VF_SELECTOR + '], [' + DATA_NODE_ID + ']';
1102
-
1103
- function getElementId(el) {
1104
- return el.getAttribute(DATA_VF_ID) || el.getAttribute(DATA_NODE_ID) || el.getAttribute(DATA_VF_SELECTOR);
1105
- }
1106
-
1107
- document.addEventListener(
1108
- 'click',
1109
- function(event) {
1110
- if (!inspectMode) return;
1111
-
1112
- event.preventDefault();
1113
- event.stopPropagation();
1114
-
1115
- const target = event.target.closest(INSPECTABLE_SELECTOR);
1116
- if (!target) {
1117
- selectedNodeId = null;
1118
- hideOverlay(selectionOverlay);
1119
- postToStudio({ action: 'setSelectedNode', id: null });
1120
- return;
1121
- }
1122
-
1123
- const id = getElementId(target);
1124
- selectedNodeId = id;
1125
- showSelectionOverlay(id);
1126
- postToStudio({ action: 'setSelectedNode', id: id });
1127
- },
1128
- true
1129
- );
1130
-
1131
- document.addEventListener('pointerover', function(event) {
1132
- if (!inspectMode || event.pointerType === 'touch') return;
1133
-
1134
- const target = event.target.closest(INSPECTABLE_SELECTOR);
1135
- if (!target) return;
1136
-
1137
- const id = getElementId(target);
1138
- if (id === hoveredNodeId) return;
1139
-
1140
- hoveredNodeId = id;
1141
- showHoverOverlay(id);
1142
- });
1143
-
1144
- document.addEventListener('pointerout', function(event) {
1145
- if (!inspectMode || event.pointerType === 'touch') return;
1146
-
1147
- const target = event.target.closest(INSPECTABLE_SELECTOR);
1148
- if (!target) return;
1149
-
1150
- const relatedTarget = event.relatedTarget;
1151
- if (relatedTarget && target.contains(relatedTarget)) return;
1152
-
1153
- hoveredNodeId = null;
1154
- hideOverlay(hoverOverlay);
1155
- });
1156
-
1157
- const updateOverlays = debounce(function() {
1158
- if (inspectMode && hoveredNodeId) showHoverOverlay(hoveredNodeId);
1159
- if (selectedNodeId) showSelectionOverlay(selectedNodeId);
1160
- }, 16);
1161
-
1162
- window.addEventListener('scroll', updateOverlays, true);
1163
- window.addEventListener('resize', updateOverlays);
1164
- }
1165
-
1166
- function setColorMode(mode) {
1167
- document.documentElement.setAttribute('data-theme', mode);
1168
- document.documentElement.classList.remove('light', 'dark');
1169
- document.documentElement.classList.add(mode);
1170
- }
1171
-
1172
- function isMarkdownPage() {
1173
- if (typeof PAGE_PATH !== 'string') {
1174
- return false;
1175
- }
1176
- const lowerPath = PAGE_PATH.toLowerCase();
1177
- return lowerPath.endsWith('.md') || lowerPath.endsWith('.mdx');
1178
- }
1179
-
1180
- function isMdxPage() {
1181
- return typeof PAGE_PATH === 'string' && PAGE_PATH.toLowerCase().endsWith('.mdx');
1182
- }
1183
-
1184
- function openFilePathInStudio(filePath, lineNumber, symbolName) {
1185
- if (typeof filePath !== 'string' || !filePath) {
1186
- return;
1187
- }
1188
- const safeLine = Number.isFinite(lineNumber) ? Math.max(1, Math.trunc(lineNumber)) : 1;
1189
- const payload = {
1190
- action: 'openFile',
1191
- filePath: filePath,
1192
- lineNumber: safeLine,
1193
- columnNumber: 1
1194
- };
1195
- if (typeof symbolName === 'string' && symbolName.trim()) {
1196
- payload.symbolName = symbolName.trim();
1197
- }
1198
- postToStudio(payload);
1199
- }
1200
-
1201
- function openMarkdownSourceInStudio(lineNumber) {
1202
- openFilePathInStudio(PAGE_PATH, lineNumber);
1203
- }
1204
-
1205
- function normalizePathSegments(segments) {
1206
- const stack = [];
1207
- for (const segment of segments) {
1208
- if (!segment || segment === '.') {
1209
- continue;
1210
- }
1211
- if (segment === '..') {
1212
- if (stack.length > 0) {
1213
- stack.pop();
1214
- }
1215
- continue;
1216
- }
1217
- stack.push(segment);
1218
- }
1219
- return stack;
1220
- }
1221
-
1222
- function resolveImportPathForPage(importPath) {
1223
- const sourcePath = typeof importPath === 'string' ? importPath.trim() : '';
1224
- if (!sourcePath) {
1225
- return '';
1226
- }
1227
-
1228
- if (sourcePath.startsWith('@/') || sourcePath.startsWith('~/')) {
1229
- return sourcePath.slice(2);
1230
- }
1231
-
1232
- if (sourcePath.startsWith('/')) {
1233
- let normalizedPath = sourcePath;
1234
- while (normalizedPath.startsWith('/')) {
1235
- normalizedPath = normalizedPath.slice(1);
1236
- }
1237
- return normalizedPath;
1238
- }
1239
-
1240
- if (!PAGE_PATH || !sourcePath.startsWith('.')) {
1241
- return sourcePath;
1242
- }
1243
-
1244
- const baseParts = String(PAGE_PATH).split('/');
1245
- baseParts.pop();
1246
- const resolved = normalizePathSegments(baseParts.concat(sourcePath.split('/')));
1247
- return resolved.join('/');
1248
- }
1249
-
1250
- function isLikelyProjectImportPath(importPath) {
1251
- if (typeof importPath !== 'string') {
1252
- return false;
1253
- }
1254
- const value = importPath.trim();
1255
- if (!value) {
1256
- return false;
1257
- }
1258
- return (
1259
- value.startsWith('.') ||
1260
- value.startsWith('/') ||
1261
- value.startsWith('@/') ||
1262
- value.startsWith('~/')
1263
- );
1264
- }
1265
-
1266
- function guessStudioFilePath(filePath) {
1267
- const sourcePath = typeof filePath === 'string' ? filePath.trim() : '';
1268
- if (!sourcePath) {
1269
- return '';
1270
- }
1271
-
1272
- const hasKnownExtension = sourcePath.match(/\\.(tsx?|jsx?|mdx?|json|css|scss|sass|less)$/i);
1273
- if (hasKnownExtension) {
1274
- return sourcePath;
1275
- }
1276
-
1277
- if (sourcePath.endsWith('/')) {
1278
- return sourcePath + 'index.tsx';
1279
- }
1280
-
1281
- return sourcePath + '.tsx';
1282
- }
1283
-
1284
- function parseMdxImportMap(content) {
1285
- const source = typeof content === 'string' ? content : '';
1286
- const importMap = {};
1287
- if (!source) {
1288
- return importMap;
1289
- }
1290
-
1291
- const stripImportComments = function(specifierText) {
1292
- return String(specifierText || '')
1293
- .replace(/\\/\\*[\\s\\S]*?\\*\\//g, ' ')
1294
- .replace(/\\/\\/[^\\n\\r]*/g, ' ');
1295
- };
1296
-
1297
- const normalizeImportSpecifier = function(specifierText) {
1298
- return stripImportComments(specifierText)
1299
- .replace(/\\s+/g, ' ')
1300
- .trim();
1301
- };
1302
-
1303
- const setImportEntry = function(localName, resolvedPath, symbolName, importKind) {
1304
- const key = typeof localName === 'string' ? localName.trim() : '';
1305
- const filePath = typeof resolvedPath === 'string' ? resolvedPath.trim() : '';
1306
- if (!key || !filePath) {
1307
- return;
1308
- }
1309
- importMap[key] = {
1310
- filePath: filePath,
1311
- symbolName: typeof symbolName === 'string' ? symbolName.trim() : '',
1312
- importKind: typeof importKind === 'string' ? importKind : 'unknown'
1313
- };
1314
- };
1315
-
1316
- const mapNamedImports = function(namedSpecifier, resolvedPath) {
1317
- const text = String(namedSpecifier || '').trim();
1318
- if (!text.startsWith('{') || !text.endsWith('}')) {
1319
- return;
1320
- }
1321
- const named = text.slice(1, -1).split(',');
1322
- for (const entry of named) {
1323
- const part = entry.trim();
1324
- if (!part) {
1325
- continue;
1326
- }
1327
- const normalizedPart = normalizeImportSpecifier(part).trim();
1328
- if (!normalizedPart || /^type\\s+/.test(normalizedPart)) {
1329
- continue;
1330
- }
1331
- const aliasMatch = normalizedPart.match(/^([A-Za-z_$][\\w$]*)\\s+as\\s+([A-Za-z_$][\\w$]*)$/);
1332
- const sourceName = aliasMatch ? aliasMatch[1] : normalizedPart;
1333
- const localName = aliasMatch ? aliasMatch[2] : normalizedPart;
1334
- if (localName) {
1335
- const isDefaultAlias = sourceName === 'default';
1336
- setImportEntry(localName, resolvedPath, isDefaultAlias ? '' : sourceName, isDefaultAlias ? 'default' : 'named');
1337
- }
1338
- }
1339
- };
1340
-
1341
- const importPattern = /^import\\s+([\\s\\S]*?)\\s+from\\s+['\"]([^'\"]+)['\"]\\s*;?/gm;
1342
- let match = importPattern.exec(source);
1343
- while (match) {
1344
- const specifier = normalizeImportSpecifier(match[1] || '');
1345
- if (!specifier) {
1346
- match = importPattern.exec(source);
1347
- continue;
1348
- }
1349
- const typeOnlySpecifier = specifier.startsWith('type ');
1350
- const normalizedSpecifier = typeOnlySpecifier ? specifier.slice(5).trim() : specifier;
1351
- if (typeOnlySpecifier) {
1352
- match = importPattern.exec(source);
1353
- continue;
1354
- }
1355
- const rawImportPath = String(match[2] || '').trim();
1356
- if (!isLikelyProjectImportPath(rawImportPath)) {
1357
- match = importPattern.exec(source);
1358
- continue;
1359
- }
1360
- const resolvedPath = guessStudioFilePath(resolveImportPathForPage(rawImportPath));
1361
- if (!resolvedPath) {
1362
- match = importPattern.exec(source);
1363
- continue;
1364
- }
1365
-
1366
- if (normalizedSpecifier.startsWith('{') && normalizedSpecifier.endsWith('}')) {
1367
- mapNamedImports(normalizedSpecifier, resolvedPath);
1368
- } else if (normalizedSpecifier.startsWith('* as ')) {
1369
- const namespaceName = normalizedSpecifier.slice(5).trim();
1370
- if (namespaceName) {
1371
- setImportEntry(namespaceName, resolvedPath, '', 'namespace');
1372
- }
1373
- } else {
1374
- const commaIndex = normalizedSpecifier.indexOf(',');
1375
- if (commaIndex >= 0) {
1376
- const defaultPart = normalizedSpecifier.slice(0, commaIndex).trim();
1377
- const restPart = normalizedSpecifier.slice(commaIndex + 1).trim();
1378
- const normalizedDefaultPart = defaultPart.trim();
1379
- if (normalizedDefaultPart && !/^type\\s+/.test(normalizedDefaultPart)) {
1380
- setImportEntry(normalizedDefaultPart, resolvedPath, '', 'default');
1381
- }
1382
- if (restPart.startsWith('{')) {
1383
- mapNamedImports(restPart, resolvedPath);
1384
- } else if (restPart.startsWith('* as ')) {
1385
- const namespaceName = restPart.slice(5).trim();
1386
- if (namespaceName) {
1387
- setImportEntry(namespaceName, resolvedPath, '', 'namespace');
1388
- }
1389
- }
1390
- } else {
1391
- const defaultPart = normalizedSpecifier.trim();
1392
- const normalizedDefaultPart = defaultPart.trim();
1393
- if (normalizedDefaultPart && !/^type\\s+/.test(normalizedDefaultPart)) {
1394
- setImportEntry(normalizedDefaultPart, resolvedPath, '', 'default');
1395
- }
1396
- }
1397
- }
1398
-
1399
- match = importPattern.exec(source);
1400
- }
1401
-
1402
- return importMap;
1403
- }
1404
-
1405
- function postMarkdownEditorReady() {
1406
- if (!markdownFileId) {
1407
- return;
1408
- }
1409
- postToStudio({
1410
- action: 'markdownEditorReady',
1411
- fileId: markdownFileId,
1412
- filePath: PAGE_PATH
1413
- });
1414
- }
1415
-
1416
- function scheduleMarkdownSync(content) {
1417
- if (!markdownFileId) {
1418
- return;
1419
- }
1420
- if (markdownSyncTimer) {
1421
- clearTimeout(markdownSyncTimer);
1422
- }
1423
- markdownSyncTimer = setTimeout(function() {
1424
- postToStudio({
1425
- action: 'markdownContentChange',
1426
- fileId: markdownFileId,
1427
- filePath: PAGE_PATH,
1428
- content: content
1429
- });
1430
- }, 120);
1431
- }
1432
-
1433
- function computeTextDiff(oldText, newText) {
1434
- var prefixLen = 0;
1435
- var minLen = Math.min(oldText.length, newText.length);
1436
- while (prefixLen < minLen && oldText.charCodeAt(prefixLen) === newText.charCodeAt(prefixLen)) {
1437
- prefixLen++;
1438
- }
1439
- var suffixLen = 0;
1440
- var maxSuffix = minLen - prefixLen;
1441
- while (suffixLen < maxSuffix &&
1442
- oldText.charCodeAt(oldText.length - 1 - suffixLen) === newText.charCodeAt(newText.length - 1 - suffixLen)) {
1443
- suffixLen++;
1444
- }
1445
- return {
1446
- index: prefixLen,
1447
- deleteCount: oldText.length - prefixLen - suffixLen,
1448
- insertText: newText.slice(prefixLen, suffixLen > 0 ? newText.length - suffixLen : undefined)
1449
- };
1450
- }
1451
-
1452
- function syncLocalChangeToYText(fullContent) {
1453
- if (!markdownYText || !markdownYDoc) {
1454
- return;
1455
- }
1456
- var currentYContent = markdownYText.toString();
1457
- if (currentYContent === fullContent) {
1458
- return;
1459
- }
1460
- var diff = computeTextDiff(currentYContent, fullContent);
1461
- if (diff.deleteCount === 0 && diff.insertText === '') {
1462
- return;
1463
- }
1464
- markdownYDoc.transact(function() {
1465
- if (diff.deleteCount > 0) {
1466
- markdownYText.delete(diff.index, diff.deleteCount);
1467
- }
1468
- if (diff.insertText) {
1469
- markdownYText.insert(diff.index, diff.insertText);
1470
- }
1471
- }, LEXICAL_YJS_ORIGIN);
1472
- }
1473
-
1474
- function setupMarkdownYjsConnection(config) {
1475
- if (markdownYDoc) {
1476
- return;
1477
- }
1478
-
1479
- var setupId = ++markdownYjsSetupId;
1480
-
1481
- Promise.all([
1482
- import('https://esm.sh/yjs@13.6.28?target=es2022'),
1483
- import('https://esm.sh/y-websocket@2.1.0?deps=yjs@13.6.28&target=es2022')
1484
- ]).then(function(modules) {
1485
- // Abort if edit mode was closed while imports were loading
1486
- if (setupId !== markdownYjsSetupId) {
1487
- return;
1488
- }
1489
-
1490
- var Y = modules[0];
1491
- var WebsocketProvider = modules[1].WebsocketProvider;
1492
- markdownYjsY = Y;
1493
-
1494
- var doc = new Y.Doc({ guid: config.guid });
1495
- // Cookie auth: authToken cookie on .veryfront.com is sent automatically
1496
- // with the WebSocket upgrade request. No explicit token param needed.
1497
- var provider = new WebsocketProvider(config.wsUrl, config.guid, doc, {
1498
- resyncInterval: -1
1499
- });
1500
-
1501
- var ytext = doc.getText(config.fileId);
1502
-
1503
- markdownYDoc = doc;
1504
- markdownYProvider = provider;
1505
- markdownYText = ytext;
1506
-
1507
- // Filter non-binary messages to prevent y-websocket parse errors
1508
- provider.on('status', function(event) {
1509
- console.debug('[StudioBridge] Yjs status:', event.status);
1510
- if (event.status === 'connected' && provider.ws) {
1511
- var origOnMessage = provider.ws.onmessage;
1512
- provider.ws.onmessage = function(wsEvent) {
1513
- if (typeof wsEvent.data === 'string') {
1514
- return;
1515
- }
1516
- if (origOnMessage) {
1517
- origOnMessage.call(provider.ws, wsEvent);
1518
- }
1519
- };
1520
- }
1521
- });
1522
-
1523
- // Extract user identity from authToken JWT cookie for presence
1524
- var presenceUser = { id: 'preview-' + Math.random().toString(36).slice(2), name: 'Preview' };
1525
- try {
1526
- var cookieMatch = document.cookie.match(/authToken=([^;]+)/);
1527
- if (cookieMatch) {
1528
- var parts = cookieMatch[1].split('.');
1529
- if (parts.length === 3) {
1530
- var payload = JSON.parse(atob(parts[1].replace(/-/g, '+').replace(/_/g, '/')));
1531
- if (payload.userId) {
1532
- presenceUser.id = payload.userId;
1533
- }
1534
- if (payload.email) {
1535
- var local = payload.email.split('@')[0] || '';
1536
- if (local.includes('.') || local.includes('_')) {
1537
- presenceUser.name = local.split(/[._]/).map(function(p) { return p.charAt(0).toUpperCase() + p.slice(1); }).join(' ');
1538
- } else {
1539
- presenceUser.name = local.charAt(0).toUpperCase() + local.slice(1);
1540
- }
1541
- }
1542
- }
1543
- }
1544
- } catch (e) {
1545
- // Fall back to defaults on any parse error
1546
- }
1547
-
1548
- // Set local user on awareness for presence
1549
- provider.awareness.setLocalStateField('user', {
1550
- id: presenceUser.id,
1551
- name: presenceUser.name,
1552
- color: '#10b981'
1553
- });
1554
-
1555
- // Observe awareness for remote presence and selection changes
1556
- function syncAwareness() {
1557
- var states = Array.from(provider.awareness.getStates().entries());
1558
-
1559
- // Sync presence users
1560
- var users = [];
1561
- for (var i = 0; i < states.length; i++) {
1562
- var clientId = states[i][0];
1563
- var state = states[i][1];
1564
- var user = state.user;
1565
- if (!user || typeof user.name !== 'string') {
1566
- continue;
1567
- }
1568
- users.push({
1569
- id: user.id || String(clientId),
1570
- name: user.name,
1571
- color: user.color || '#6b7280',
1572
- isCurrentUser: clientId === provider.awareness.clientID,
1573
- isAgent: user.isAgent || false
1574
- });
1575
- }
1576
- setMarkdownPresence(users);
1577
-
1578
- // Sync remote selections
1579
- var selections = [];
1580
- for (var j = 0; j < states.length; j++) {
1581
- var cId = states[j][0];
1582
- var st = states[j][1];
1583
- var u = st.user;
1584
- var ranges = st.selection;
1585
- if (!u || !Array.isArray(ranges) || ranges.length === 0) {
1586
- continue;
1587
- }
1588
- for (var k = 0; k < ranges.length; k++) {
1589
- var range = ranges[k];
1590
- var anchorPos = Y.createAbsolutePositionFromRelativePosition(range.anchor, doc);
1591
- var markerPos = Y.createAbsolutePositionFromRelativePosition(range.marker, doc);
1592
- if (!anchorPos || !markerPos || anchorPos.type !== ytext || markerPos.type !== ytext) {
1593
- continue;
1594
- }
1595
- selections.push({
1596
- id: u.id || String(cId),
1597
- name: u.name || 'Anonymous',
1598
- color: u.color || '#6b7280',
1599
- isCurrentUser: cId === provider.awareness.clientID,
1600
- start: Math.min(anchorPos.index, markerPos.index),
1601
- end: Math.max(anchorPos.index, markerPos.index)
1602
- });
1603
- }
1604
- }
1605
- setMarkdownSelections(selections);
1606
- }
1607
-
1608
- provider.awareness.on('change', syncAwareness);
1609
-
1610
- provider.on('sync', function(synced) {
1611
- if (synced && !markdownYjsConnected) {
1612
- markdownYjsConnected = true;
1613
-
1614
- var ytextContent = ytext.toString();
1615
- if (markdownCurrentContent && markdownCurrentContent !== ytextContent) {
1616
- // User typed before sync completed — push local edits to Y.Text
1617
- syncLocalChangeToYText(markdownCurrentContent);
1618
- } else if (ytextContent) {
1619
- // No local edits — seed editor from Y.Text
1620
- applyMarkdownContent(ytextContent);
1621
- }
1622
-
1623
- // Replay any selection queued before Yjs was ready
1624
- if (markdownPendingSelection) {
1625
- var ps = markdownPendingSelection;
1626
- markdownPendingSelection = null;
1627
- var cs = Math.max(0, Math.min(ytext.length, ps.start));
1628
- var ce = Math.max(0, Math.min(ytext.length, ps.end));
1629
- provider.awareness.setLocalStateField('selection', [{
1630
- anchor: Y.createRelativePositionFromTypeIndex(ytext, cs),
1631
- marker: Y.createRelativePositionFromTypeIndex(ytext, ce)
1632
- }]);
1633
- }
1634
-
1635
- // Observe Y.Text for remote changes (from other users / Monaco)
1636
- ytext.observe(function(event) {
1637
- if (event.transaction.origin === LEXICAL_YJS_ORIGIN) {
1638
- return;
1639
- }
1640
- var fullContent = ytext.toString();
1641
- if (fullContent === markdownCurrentContent) {
1642
- return;
1643
- }
1644
- applyMarkdownContent(fullContent);
1645
- });
1646
-
1647
- // Initial awareness sync after Yjs is connected
1648
- syncAwareness();
1649
-
1650
- console.debug('[StudioBridge] Yjs synced, bound to Y.Text for fileId:', config.fileId);
1651
- }
1652
- });
1653
- }).catch(function(error) {
1654
- console.error('[StudioBridge] Failed to setup Yjs connection:', error);
1655
- });
1656
- }
1657
-
1658
- function disposeMarkdownYjs() {
1659
- markdownYjsSetupId++;
1660
- if (markdownYProvider) {
1661
- markdownYProvider.disconnect();
1662
- markdownYProvider.destroy();
1663
- markdownYProvider = null;
1664
- }
1665
- if (markdownYDoc) {
1666
- markdownYDoc.destroy();
1667
- markdownYDoc = null;
1668
- }
1669
- markdownYText = null;
1670
- markdownYjsConnected = false;
1671
- markdownYjsY = null;
1672
- }
1673
-
1674
- function getTextOffsetWithinRoot(root, targetNode, targetOffset) {
1675
- if (!root || !targetNode) {
1676
- return 0;
1677
- }
1678
-
1679
- if (!root.contains(targetNode)) {
1680
- return 0;
1681
- }
1682
-
1683
- try {
1684
- const range = document.createRange();
1685
- range.selectNodeContents(root);
1686
- range.setEnd(targetNode, targetOffset);
1687
- return range.toString().length;
1688
- } catch {
1689
- return 0;
1690
- }
1691
- }
1692
-
1693
- function getMarkdownEditorSelection() {
1694
- if (markdownLexicalApi && markdownEditorSurface) {
1695
- const selection = window.getSelection();
1696
- if (!selection || selection.rangeCount === 0) {
1697
- return null;
1698
- }
1699
-
1700
- const range = selection.getRangeAt(0);
1701
- if (
1702
- !markdownEditorSurface.contains(range.startContainer) ||
1703
- !markdownEditorSurface.contains(range.endContainer)
1704
- ) {
1705
- return null;
1706
- }
1707
-
1708
- const start = getTextOffsetWithinRoot(
1709
- markdownEditorSurface,
1710
- range.startContainer,
1711
- range.startOffset
1712
- );
1713
- const end = getTextOffsetWithinRoot(
1714
- markdownEditorSurface,
1715
- range.endContainer,
1716
- range.endOffset
1717
- );
1718
- return {
1719
- start: Math.max(0, Math.min(start, end)),
1720
- end: Math.max(0, Math.max(start, end))
1721
- };
1722
- }
1723
-
1724
- if (markdownEditorTextarea) {
1725
- const start = typeof markdownEditorTextarea.selectionStart === 'number'
1726
- ? markdownEditorTextarea.selectionStart
1727
- : 0;
1728
- const end = typeof markdownEditorTextarea.selectionEnd === 'number'
1729
- ? markdownEditorTextarea.selectionEnd
1730
- : start;
1731
-
1732
- return {
1733
- start: Math.max(0, Math.min(start, end)),
1734
- end: Math.max(0, Math.max(start, end))
1735
- };
1736
- }
1737
-
1738
- return null;
1739
- }
1740
-
1741
- function getMarkdownRawBlockLength(index) {
1742
- const rawBlock = markdownRawBlocks[index];
1743
- if (typeof rawBlock !== 'string') {
1744
- return 0;
1745
- }
1746
- return rawBlock.length;
1747
- }
1748
-
1749
- function escapeRegexText(value) {
1750
- const text = String(value || '');
1751
- let escaped = '';
1752
- for (let i = 0; i < text.length; i += 1) {
1753
- const char = text[i];
1754
- if ('\\\\^$.*+?()[]{}|'.indexOf(char) >= 0) {
1755
- escaped += '\\\\' + char;
1756
- } else {
1757
- escaped += char;
1758
- }
1759
- }
1760
- return escaped;
1761
- }
1762
-
1763
- function getMarkdownRawBlockTokenPattern() {
1764
- const prefix = typeof markdownRawBlockTokenPrefix === 'string' && markdownRawBlockTokenPrefix
1765
- ? markdownRawBlockTokenPrefix
1766
- : 'VF_RAW_BLOCK';
1767
- const escapedPrefix = escapeRegexText(prefix);
1768
- return new RegExp('\\\\[\\\\[' + escapedPrefix + '_(\\\\d+)\\\\]\\\\]', 'g');
1769
- }
1770
-
1771
- function editorOffsetToBodyOffset(editorOffset, bias) {
1772
- const editorContent = typeof markdownCurrentEditorContent === 'string'
1773
- ? markdownCurrentEditorContent
1774
- : '';
1775
- const maxOffset = editorContent.length;
1776
- const safeOffset = Math.max(0, Math.min(maxOffset, Math.trunc(editorOffset || 0)));
1777
- const tokenPattern = getMarkdownRawBlockTokenPattern();
1778
- let diffBefore = 0;
1779
- let match = tokenPattern.exec(editorContent);
1780
-
1781
- while (match) {
1782
- const token = match[0];
1783
- const tokenStartEditor = match.index;
1784
- const tokenEndEditor = tokenStartEditor + token.length;
1785
- const rawLength = getMarkdownRawBlockLength(Number(match[1]));
1786
- const tokenDelta = rawLength - token.length;
1787
- const tokenStartBody = tokenStartEditor + diffBefore;
1788
-
1789
- if (safeOffset >= tokenEndEditor) {
1790
- diffBefore += tokenDelta;
1791
- match = tokenPattern.exec(editorContent);
1792
- continue;
1793
- }
1794
-
1795
- if (safeOffset > tokenStartEditor) {
1796
- if (bias === 'end') {
1797
- return tokenStartBody + rawLength;
1798
- }
1799
- return tokenStartBody;
1800
- }
1801
- break;
1802
- }
1803
-
1804
- return safeOffset + diffBefore;
1805
- }
1806
-
1807
- function bodyOffsetToEditorOffset(bodyOffset, bias) {
1808
- const editorContent = typeof markdownCurrentEditorContent === 'string'
1809
- ? markdownCurrentEditorContent
1810
- : '';
1811
- const safeBodyOffset = Math.max(0, Math.trunc(bodyOffset || 0));
1812
- const tokenPattern = getMarkdownRawBlockTokenPattern();
1813
- let diffBefore = 0;
1814
- let match = tokenPattern.exec(editorContent);
1815
-
1816
- while (match) {
1817
- const token = match[0];
1818
- const tokenStartEditor = match.index;
1819
- const tokenEndEditor = tokenStartEditor + token.length;
1820
- const rawLength = getMarkdownRawBlockLength(Number(match[1]));
1821
- const tokenStartBody = tokenStartEditor + diffBefore;
1822
- const tokenEndBody = tokenStartBody + rawLength;
1823
-
1824
- if (safeBodyOffset > tokenEndBody) {
1825
- diffBefore += rawLength - token.length;
1826
- match = tokenPattern.exec(editorContent);
1827
- continue;
1828
- }
1829
-
1830
- if (safeBodyOffset >= tokenStartBody && safeBodyOffset <= tokenEndBody) {
1831
- if (bias === 'end') {
1832
- return tokenEndEditor;
1833
- }
1834
- return tokenStartEditor;
1835
- }
1836
-
1837
- const mappedOffset = safeBodyOffset - diffBefore;
1838
- return Math.max(0, Math.min(editorContent.length, mappedOffset));
1839
- }
1840
-
1841
- const mappedOffset = safeBodyOffset - diffBefore;
1842
- return Math.max(0, Math.min(editorContent.length, mappedOffset));
1843
- }
1844
-
1845
- function editorOffsetToSourceOffset(editorOffset, bias) {
1846
- const frontmatterLength = typeof markdownFrontmatter === 'string' ? markdownFrontmatter.length : 0;
1847
- const bodyOffset = editorOffsetToBodyOffset(editorOffset, bias);
1848
- return Math.max(0, frontmatterLength + bodyOffset);
1849
- }
1850
-
1851
- function sourceSelectionToEditorRange(start, end) {
1852
- const frontmatterLength = typeof markdownFrontmatter === 'string' ? markdownFrontmatter.length : 0;
1853
- const safeStart = Math.max(0, Math.trunc(start || 0));
1854
- const safeEnd = Math.max(0, Math.trunc(end || 0));
1855
- const sourceStart = Math.min(safeStart, safeEnd);
1856
- const sourceEnd = Math.max(safeStart, safeEnd);
1857
-
1858
- if (sourceEnd <= frontmatterLength) {
1859
- return null;
1860
- }
1861
-
1862
- const bodyStart = Math.max(0, sourceStart - frontmatterLength);
1863
- const bodyEnd = Math.max(0, sourceEnd - frontmatterLength);
1864
- const editorStart = bodyOffsetToEditorOffset(bodyStart, 'start');
1865
- const editorEnd = bodyOffsetToEditorOffset(bodyEnd, 'end');
1866
-
1867
- return {
1868
- start: Math.max(0, Math.min(editorStart, editorEnd)),
1869
- end: Math.max(0, Math.max(editorStart, editorEnd))
1870
- };
1871
- }
1872
-
1873
- function setMarkdownEditorSelection(start, end) {
1874
- const safeStart = Math.max(0, Math.trunc(start || 0));
1875
- const endValue = typeof end === 'number' ? end : safeStart;
1876
- const safeEnd = Math.max(0, Math.trunc(endValue));
1877
-
1878
- if (markdownLexicalApi && markdownEditorSurface) {
1879
- const selection = window.getSelection();
1880
- if (!selection) {
1881
- return;
1882
- }
1883
-
1884
- const anchor = resolveMarkdownTextPoint(markdownEditorSurface, safeStart);
1885
- const focus = resolveMarkdownTextPoint(markdownEditorSurface, safeEnd);
1886
- try {
1887
- const range = document.createRange();
1888
- range.setStart(anchor.node, anchor.offset);
1889
- range.setEnd(focus.node, focus.offset);
1890
- selection.removeAllRanges();
1891
- selection.addRange(range);
1892
- } catch {
1893
- // Ignore when point resolution races with Lexical DOM updates.
1894
- }
1895
- return;
1896
- }
1897
-
1898
- if (markdownEditorTextarea) {
1899
- const max = markdownEditorTextarea.value.length;
1900
- markdownEditorTextarea.setSelectionRange(Math.min(safeStart, max), Math.min(safeEnd, max));
1901
- }
1902
- }
1903
-
1904
- function hideMarkdownSlashMenu() {
1905
- markdownSlashMenuContext = null;
1906
- markdownSlashMenuCommands = [];
1907
- markdownSlashMenuActiveIndex = 0;
1908
- if (!markdownSlashMenuRoot) {
1909
- return;
1910
- }
1911
- markdownSlashMenuRoot.style.display = 'none';
1912
- markdownSlashMenuRoot.textContent = '';
1913
- }
1914
-
1915
- function getMarkdownSlashCommandInsert(id, indent) {
1916
- const prefix = typeof indent === 'string' ? indent : '';
1917
- if (id === 'heading-1') {
1918
- const text = prefix + '# ';
1919
- return { text: text, caretOffset: text.length };
1920
- }
1921
- if (id === 'heading-2') {
1922
- const text = prefix + '## ';
1923
- return { text: text, caretOffset: text.length };
1924
- }
1925
- if (id === 'heading-3') {
1926
- const text = prefix + '### ';
1927
- return { text: text, caretOffset: text.length };
1928
- }
1929
- if (id === 'bulleted-list') {
1930
- const text = prefix + '- ';
1931
- return { text: text, caretOffset: text.length };
1932
- }
1933
- if (id === 'numbered-list') {
1934
- const text = prefix + '1. ';
1935
- return { text: text, caretOffset: text.length };
1936
- }
1937
- if (id === 'quote-block') {
1938
- const text = prefix + '> ';
1939
- return { text: text, caretOffset: text.length };
1940
- }
1941
- if (id === 'code-block') {
1942
- const fence = String.fromCharCode(96, 96, 96);
1943
- const text = prefix + fence + '\\n' + prefix + '\\n' + prefix + fence;
1944
- return {
1945
- text: text,
1946
- caretOffset: (prefix + fence + '\\n' + prefix).length
1947
- };
1948
- }
1949
- if (id === 'image') {
1950
- const text = prefix + '![alt text](https://)';
1951
- return {
1952
- text: text,
1953
- caretOffset: (prefix + '![alt text](').length
1954
- };
1955
- }
1956
- return null;
1957
- }
1958
-
1959
- function applyMarkdownSlashCommand(index) {
1960
- if (!markdownSlashMenuContext || markdownSlashMenuCommands.length === 0) {
1961
- return false;
1962
- }
1963
-
1964
- const command = markdownSlashMenuCommands[index];
1965
- if (!command) {
1966
- return false;
1967
- }
1968
-
1969
- const insert = getMarkdownSlashCommandInsert(command.id, markdownSlashMenuContext.indent);
1970
- if (!insert) {
1971
- return false;
1972
- }
1973
-
1974
- const editorContent = typeof markdownCurrentEditorContent === 'string'
1975
- ? markdownCurrentEditorContent
1976
- : '';
1977
- const before = editorContent.slice(0, markdownSlashMenuContext.lineStart);
1978
- const after = editorContent.slice(markdownSlashMenuContext.caret);
1979
- const nextEditorContent = before + insert.text + after;
1980
- const nextCaret = before.length + insert.caretOffset;
1981
- const nextFullContent = composeMarkdownContent(restoreRawBlocksFromEditor(nextEditorContent));
1982
- const hasChanged = nextFullContent !== markdownCurrentContent;
1983
-
1984
- applyMarkdownContent(nextFullContent);
1985
- if (hasChanged) {
1986
- markdownHasUnsavedChanges = true;
1987
- scheduleMarkdownSync(nextFullContent);
1988
- }
1989
-
1990
- setTimeout(function() {
1991
- focusMarkdownEditor();
1992
- setMarkdownEditorSelection(nextCaret, nextCaret);
1993
- scheduleMarkdownSelectionSync();
1994
- scheduleMarkdownSelectionOverlayRender();
1995
- scheduleMarkdownSlashMenuUpdate();
1996
- }, 0);
1997
-
1998
- hideMarkdownSlashMenu();
1999
- return true;
2000
- }
2001
-
2002
- function renderMarkdownSlashMenu() {
2003
- if (!markdownSlashMenuRoot || !markdownSlashMenuContext || markdownSlashMenuCommands.length === 0) {
2004
- hideMarkdownSlashMenu();
2005
- return;
2006
- }
2007
-
2008
- markdownSlashMenuRoot.textContent = '';
2009
-
2010
- const maxLeft = Math.max(8, window.innerWidth - 312);
2011
- const maxTop = Math.max(8, window.innerHeight - 220);
2012
- const left = Math.max(8, Math.min(maxLeft, markdownSlashMenuContext.anchorLeft));
2013
- const top = Math.max(8, Math.min(maxTop, markdownSlashMenuContext.anchorTop));
2014
- markdownSlashMenuRoot.style.left = left + 'px';
2015
- markdownSlashMenuRoot.style.top = top + 'px';
2016
-
2017
- markdownSlashMenuCommands.forEach(function(command, index) {
2018
- const item = document.createElement('button');
2019
- item.type = 'button';
2020
- item.className = 'vf-markdown-editor__slash-item';
2021
- item.setAttribute('data-active', index === markdownSlashMenuActiveIndex ? 'true' : 'false');
2022
- item.setAttribute(DATA_VF_IGNORE, 'true');
2023
- item.addEventListener('mousedown', function(event) {
2024
- event.preventDefault();
2025
- });
2026
- item.addEventListener('click', function(event) {
2027
- event.preventDefault();
2028
- markdownSlashMenuActiveIndex = index;
2029
- applyMarkdownSlashCommand(markdownSlashMenuActiveIndex);
2030
- });
2031
-
2032
- const title = document.createElement('span');
2033
- title.className = 'vf-markdown-editor__slash-item-title';
2034
- title.textContent = command.label;
2035
-
2036
- const description = document.createElement('span');
2037
- description.className = 'vf-markdown-editor__slash-item-desc';
2038
- description.textContent = command.description;
2039
-
2040
- item.appendChild(title);
2041
- item.appendChild(description);
2042
- markdownSlashMenuRoot.appendChild(item);
2043
- });
2044
-
2045
- markdownSlashMenuRoot.style.display = 'block';
2046
- }
2047
-
2048
- function updateMarkdownSlashMenu() {
2049
- if (
2050
- !markdownEditorRoot ||
2051
- markdownEditorRoot.style.display !== 'block' ||
2052
- !markdownLexicalApi ||
2053
- !markdownEditorSurface ||
2054
- markdownEditorSurface.style.display === 'none'
2055
- ) {
2056
- hideMarkdownSlashMenu();
2057
- return;
2058
- }
2059
-
2060
- const selection = getMarkdownEditorSelection();
2061
- if (!selection || selection.start !== selection.end) {
2062
- hideMarkdownSlashMenu();
2063
- return;
2064
- }
2065
-
2066
- const caret = selection.start;
2067
- const editorContent = typeof markdownCurrentEditorContent === 'string'
2068
- ? markdownCurrentEditorContent
2069
- : '';
2070
- const lineStart = editorContent.lastIndexOf('\\n', Math.max(0, caret - 1)) + 1;
2071
- const line = editorContent.slice(lineStart, caret);
2072
- const match = line.match(/^(\\s*)\\/([a-z0-9-]*)$/i);
2073
- if (!match) {
2074
- hideMarkdownSlashMenu();
2075
- return;
2076
- }
2077
-
2078
- const query = (match[2] || '').toLowerCase();
2079
- const commands = MARKDOWN_SLASH_COMMANDS.filter(function(command) {
2080
- if (!query) {
2081
- return true;
2082
- }
2083
- if (command.label.toLowerCase().includes(query)) {
2084
- return true;
2085
- }
2086
- return command.aliases.some(function(alias) {
2087
- return alias.startsWith(query);
2088
- });
2089
- });
2090
-
2091
- if (commands.length === 0) {
2092
- hideMarkdownSlashMenu();
2093
- return;
2094
- }
2095
-
2096
- const domSelection = window.getSelection();
2097
- if (!domSelection || domSelection.rangeCount === 0) {
2098
- hideMarkdownSlashMenu();
2099
- return;
2100
- }
2101
- const caretRect = domSelection.getRangeAt(0).getBoundingClientRect();
2102
- const anchorLeft = caretRect.left;
2103
- const anchorTop = caretRect.bottom + 8;
2104
-
2105
- markdownSlashMenuCommands = commands.slice(0, 8);
2106
- markdownSlashMenuActiveIndex = Math.max(0, Math.min(markdownSlashMenuActiveIndex, markdownSlashMenuCommands.length - 1));
2107
- markdownSlashMenuContext = {
2108
- lineStart: lineStart,
2109
- caret: caret,
2110
- indent: match[1] || '',
2111
- query: query,
2112
- anchorLeft: anchorLeft,
2113
- anchorTop: anchorTop
2114
- };
2115
- renderMarkdownSlashMenu();
2116
- }
2117
-
2118
- function scheduleMarkdownSlashMenuUpdate() {
2119
- if (markdownSlashMenuTimer) {
2120
- clearTimeout(markdownSlashMenuTimer);
2121
- }
2122
- markdownSlashMenuTimer = setTimeout(function() {
2123
- markdownSlashMenuTimer = null;
2124
- updateMarkdownSlashMenu();
2125
- }, 0);
2126
- }
2127
-
2128
- function handleMarkdownSlashMenuKeydown(event) {
2129
- if (!markdownSlashMenuRoot || markdownSlashMenuRoot.style.display !== 'block' || markdownSlashMenuCommands.length === 0) {
2130
- return false;
2131
- }
2132
-
2133
- if (event.key === 'ArrowDown') {
2134
- event.preventDefault();
2135
- markdownSlashMenuActiveIndex = (markdownSlashMenuActiveIndex + 1) % markdownSlashMenuCommands.length;
2136
- renderMarkdownSlashMenu();
2137
- return true;
2138
- }
2139
-
2140
- if (event.key === 'ArrowUp') {
2141
- event.preventDefault();
2142
- markdownSlashMenuActiveIndex = (markdownSlashMenuActiveIndex - 1 + markdownSlashMenuCommands.length) % markdownSlashMenuCommands.length;
2143
- renderMarkdownSlashMenu();
2144
- return true;
2145
- }
2146
-
2147
- if (event.key === 'Enter' || event.key === 'Tab') {
2148
- event.preventDefault();
2149
- return applyMarkdownSlashCommand(markdownSlashMenuActiveIndex);
2150
- }
2151
-
2152
- if (event.key === 'Escape') {
2153
- event.preventDefault();
2154
- hideMarkdownSlashMenu();
2155
- return true;
2156
- }
2157
-
2158
- return false;
2159
- }
2160
-
2161
- function hideMarkdownInlineToolbar() {
2162
- if (!markdownInlineToolbarRoot) {
2163
- return;
2164
- }
2165
- markdownInlineToolbarRoot.style.display = 'none';
2166
- }
2167
-
2168
- function toggleMarkdownInlineFormat(format) {
2169
- if (!markdownLexicalApi || !markdownLexicalApi.editor || !markdownLexicalApi.lexicalModule) {
2170
- return;
2171
- }
2172
- if (typeof format !== 'string' || !format) {
2173
- return;
2174
- }
2175
-
2176
- markdownLexicalApi.editor.focus();
2177
- markdownLexicalApi.editor.dispatchCommand(markdownLexicalApi.lexicalModule.FORMAT_TEXT_COMMAND, format);
2178
- scheduleMarkdownInlineToolbarUpdate();
2179
- }
2180
-
2181
- function updateMarkdownInlineToolbar() {
2182
- if (
2183
- !markdownInlineToolbarRoot ||
2184
- !markdownEditorRoot ||
2185
- markdownEditorRoot.style.display !== 'block' ||
2186
- !markdownLexicalApi ||
2187
- !markdownEditorSurface ||
2188
- markdownEditorSurface.style.display === 'none'
2189
- ) {
2190
- hideMarkdownInlineToolbar();
2191
- return;
2192
- }
2193
-
2194
- const selection = window.getSelection();
2195
- if (!selection || selection.rangeCount === 0 || selection.isCollapsed) {
2196
- hideMarkdownInlineToolbar();
2197
- return;
2198
- }
2199
-
2200
- const range = selection.getRangeAt(0);
2201
- if (
2202
- !markdownEditorSurface.contains(range.startContainer) ||
2203
- !markdownEditorSurface.contains(range.endContainer)
2204
- ) {
2205
- hideMarkdownInlineToolbar();
2206
- return;
2207
- }
2208
-
2209
- const rect = range.getBoundingClientRect();
2210
- if (!rect || rect.width <= 0 || rect.height <= 0) {
2211
- hideMarkdownInlineToolbar();
2212
- return;
2213
- }
2214
-
2215
- const left = Math.max(8, Math.min(window.innerWidth - 180, rect.left + rect.width / 2 - 66));
2216
- const top = Math.max(8, Math.min(window.innerHeight - 56, rect.top - 44));
2217
- markdownInlineToolbarRoot.style.left = left + 'px';
2218
- markdownInlineToolbarRoot.style.top = top + 'px';
2219
- markdownInlineToolbarRoot.style.display = 'inline-flex';
2220
- }
2221
-
2222
- function scheduleMarkdownInlineToolbarUpdate() {
2223
- if (markdownInlineToolbarFrame) {
2224
- cancelAnimationFrame(markdownInlineToolbarFrame);
2225
- }
2226
-
2227
- markdownInlineToolbarFrame = requestAnimationFrame(function() {
2228
- markdownInlineToolbarFrame = null;
2229
- updateMarkdownInlineToolbar();
2230
- });
2231
- }
2232
-
2233
- function getMarkdownTopLevelBlocks() {
2234
- if (!markdownEditorSurface) {
2235
- return [];
2236
- }
2237
-
2238
- return Array.from(markdownEditorSurface.children).filter(function(node) {
2239
- return node && node.nodeType === Node.ELEMENT_NODE;
2240
- });
2241
- }
2242
-
2243
- function hideMarkdownBlockDragHandle() {
2244
- markdownBlockHandleHoverIndex = -1;
2245
- if (!markdownBlockDragHandle) {
2246
- return;
2247
- }
2248
- markdownBlockDragHandle.style.display = 'none';
2249
- markdownBlockDragHandle.removeAttribute('data-block-index');
2250
- }
2251
-
2252
- function hideMarkdownBlockDropIndicator() {
2253
- markdownBlockDropSlotIndex = -1;
2254
- if (markdownBlockDropIndicator) {
2255
- markdownBlockDropIndicator.style.display = 'none';
2256
- }
2257
- if (markdownBlockDropLabel) {
2258
- markdownBlockDropLabel.style.display = 'none';
2259
- markdownBlockDropLabel.textContent = '';
2260
- }
2261
- }
2262
-
2263
- function hideMarkdownBlockDragUi() {
2264
- markdownBlockDragActive = false;
2265
- markdownBlockDragSourceIndex = -1;
2266
- if (markdownBlockDragHandle) {
2267
- markdownBlockDragHandle.setAttribute('data-dragging', 'false');
2268
- }
2269
- removeMarkdownDragGhost();
2270
- hideMarkdownBlockDropIndicator();
2271
- hideMarkdownBlockDragHandle();
2272
- }
2273
-
2274
- function getMarkdownBlockElementFromNode(node) {
2275
- if (!markdownEditorSurface || !node) {
2276
- return null;
2277
- }
2278
-
2279
- let current = node.nodeType === Node.ELEMENT_NODE ? node : node.parentElement;
2280
- while (current && current.parentElement !== markdownEditorSurface) {
2281
- current = current.parentElement;
2282
- }
2283
-
2284
- if (!current || current.parentElement !== markdownEditorSurface) {
2285
- return null;
2286
- }
2287
- return current;
2288
- }
2289
-
2290
- function getMarkdownBlockTypeInfo(block) {
2291
- if (!block || !block.tagName) {
2292
- return { label: 'block', color: '#0081f8' };
2293
- }
2294
-
2295
- const tag = block.tagName.toLowerCase();
2296
- if (tag === 'h1') {
2297
- return { label: 'heading 1', color: '#7c3aed' };
2298
- }
2299
- if (tag === 'h2') {
2300
- return { label: 'heading 2', color: '#7c3aed' };
2301
- }
2302
- if (tag === 'h3') {
2303
- return { label: 'heading 3', color: '#7c3aed' };
2304
- }
2305
- if (tag === 'ul' || tag === 'ol') {
2306
- return { label: 'list', color: '#0d9488' };
2307
- }
2308
- if (tag === 'blockquote') {
2309
- return { label: 'quote', color: '#2563eb' };
2310
- }
2311
- if (tag === 'pre') {
2312
- return { label: 'code block', color: '#ea580c' };
2313
- }
2314
- if (tag === 'img' || tag === 'figure') {
2315
- return { label: 'image', color: '#db2777' };
2316
- }
2317
- if (tag === 'p') {
2318
- return { label: 'paragraph', color: '#16a34a' };
2319
- }
2320
- return { label: tag, color: '#0081f8' };
2321
- }
2322
-
2323
- function getMarkdownBlockPreviewText(block) {
2324
- if (!block) {
2325
- return '';
2326
- }
2327
- const text = String(block.textContent || '').replace(new RegExp('\\s+', 'g'), ' ').trim();
2328
- if (!text) {
2329
- return 'Empty block';
2330
- }
2331
- if (text.length > 84) {
2332
- return text.slice(0, 84) + '...';
2333
- }
2334
- return text;
2335
- }
2336
-
2337
- function removeMarkdownDragGhost() {
2338
- if (!markdownBlockDragGhost) {
2339
- return;
2340
- }
2341
- markdownBlockDragGhost.remove();
2342
- markdownBlockDragGhost = null;
2343
- }
2344
-
2345
- function createMarkdownDragGhost(block) {
2346
- const typeInfo = getMarkdownBlockTypeInfo(block);
2347
- const ghost = document.createElement('div');
2348
- ghost.className = 'vf-markdown-editor__block-drag-ghost';
2349
- ghost.setAttribute(DATA_VF_IGNORE, 'true');
2350
-
2351
- const title = document.createElement('span');
2352
- title.className = 'vf-markdown-editor__block-drag-ghost-title';
2353
- title.setAttribute(DATA_VF_IGNORE, 'true');
2354
- title.textContent = 'Moving ' + typeInfo.label;
2355
-
2356
- const text = document.createElement('span');
2357
- text.className = 'vf-markdown-editor__block-drag-ghost-text';
2358
- text.setAttribute(DATA_VF_IGNORE, 'true');
2359
- text.textContent = getMarkdownBlockPreviewText(block);
2360
-
2361
- ghost.appendChild(title);
2362
- ghost.appendChild(text);
2363
- return ghost;
2364
- }
2365
-
2366
- function getLineNumberForOffset(text, offset) {
2367
- const source = typeof text === 'string' ? text : '';
2368
- const maxOffset = Math.max(0, Math.min(source.length, Math.trunc(offset || 0)));
2369
- let line = 1;
2370
- for (let i = 0; i < maxOffset; i += 1) {
2371
- if (source.charCodeAt(i) === 10) {
2372
- line += 1;
2373
- }
2374
- }
2375
- return line;
2376
- }
2377
-
2378
- function getMdxComponentName(blockText) {
2379
- const source = typeof blockText === 'string' ? blockText : '';
2380
- const fence = String.fromCharCode(96, 96, 96);
2381
- const componentMatch = source.match(/<\\s*([A-Z][\\w.]*)/);
2382
- if (componentMatch && componentMatch[1]) {
2383
- return componentMatch[1];
2384
- }
2385
- if (source.trim().startsWith(fence + 'tsx')) {
2386
- return 'tsx block';
2387
- }
2388
- if (source.trim().startsWith(fence + 'jsx')) {
2389
- return 'jsx block';
2390
- }
2391
- return 'component block';
2392
- }
2393
-
2394
- function getMdxBlockOpenUiState(block) {
2395
- const hasResolvedTarget = !!(block && typeof block.filePath === 'string' && block.filePath.trim());
2396
- return {
2397
- hasResolvedTarget: hasResolvedTarget,
2398
- buttonLabel: hasResolvedTarget ? 'Edit in Studio' : 'Open MDX source',
2399
- showUnresolvedNote: !hasResolvedTarget
2400
- };
2401
- }
2402
-
2403
- function setMarkdownMdxBlocks(blocks) {
2404
- markdownLatestMdxBlocks = Array.isArray(blocks) ? blocks : [];
2405
- if (!markdownMdxBlocksRoot) {
2406
- return;
2407
- }
2408
-
2409
- markdownMdxBlocksRoot.textContent = '';
2410
- if (!isMdxPage() || markdownLatestMdxBlocks.length === 0) {
2411
- markdownMdxBlocksRoot.style.display = 'none';
2412
- return;
2413
- }
2414
-
2415
- markdownMdxBlocksRoot.style.display = 'flex';
2416
- for (const block of markdownLatestMdxBlocks.slice(0, 8)) {
2417
- if (!block || typeof block.label !== 'string') {
2418
- continue;
2419
- }
2420
-
2421
- const item = document.createElement('div');
2422
- item.className = 'vf-markdown-editor__mdx-block';
2423
- item.setAttribute(DATA_VF_IGNORE, 'true');
2424
-
2425
- const label = document.createElement('div');
2426
- label.className = 'vf-markdown-editor__mdx-block-label';
2427
- label.setAttribute(DATA_VF_IGNORE, 'true');
2428
- const safeLine = Number.isFinite(block.lineNumber) ? Math.max(1, Math.trunc(block.lineNumber)) : 1;
2429
- label.textContent = block.label + ' (line ' + String(safeLine) + ')';
2430
- const openUiState = getMdxBlockOpenUiState(block);
2431
-
2432
- const openButton = document.createElement('button');
2433
- openButton.type = 'button';
2434
- openButton.className = 'vf-markdown-editor__mdx-open';
2435
- openButton.setAttribute(DATA_VF_IGNORE, 'true');
2436
- openButton.textContent = openUiState.buttonLabel;
2437
- if (openUiState.showUnresolvedNote) {
2438
- openButton.title = 'Component import could not be resolved. Opening current MDX source.';
2439
- }
2440
- openButton.addEventListener('click', function() {
2441
- const targetFile = typeof block.filePath === 'string' && block.filePath ? block.filePath : PAGE_PATH;
2442
- const targetLine = targetFile === PAGE_PATH ? safeLine : 1;
2443
- const targetSymbol = targetFile === PAGE_PATH
2444
- ? ''
2445
- : (typeof block.symbolName === 'string' ? block.symbolName : '');
2446
- openFilePathInStudio(targetFile, targetLine, targetSymbol);
2447
- });
2448
-
2449
- item.appendChild(label);
2450
- if (openUiState.showUnresolvedNote) {
2451
- const fallbackNote = document.createElement('span');
2452
- fallbackNote.className = 'vf-markdown-editor__mdx-note';
2453
- fallbackNote.setAttribute(DATA_VF_IGNORE, 'true');
2454
- fallbackNote.textContent = 'Unresolved import';
2455
- item.appendChild(fallbackNote);
2456
- }
2457
- item.appendChild(openButton);
2458
- markdownMdxBlocksRoot.appendChild(item);
2459
- }
2460
-
2461
- if (markdownMdxBlocksRoot.childNodes.length === 0) {
2462
- markdownMdxBlocksRoot.style.display = 'none';
2463
- }
2464
- }
2465
-
2466
- function getMarkdownBlockHoverIndexFromPointer(targetNode, clientX, clientY) {
2467
- const blocks = getMarkdownTopLevelBlocks();
2468
- if (blocks.length === 0) {
2469
- return -1;
2470
- }
2471
-
2472
- const directBlock = getMarkdownBlockElementFromNode(targetNode);
2473
- const directIndex = blocks.indexOf(directBlock);
2474
- if (directIndex >= 0) {
2475
- return directIndex;
2476
- }
2477
-
2478
- if (!markdownEditorSurface) {
2479
- return -1;
2480
- }
2481
- const surfaceRect = markdownEditorSurface.getBoundingClientRect();
2482
- const leftBoundary = surfaceRect.left - 44;
2483
- const rightBoundary = surfaceRect.left + Math.min(96, surfaceRect.width * 0.35);
2484
- if (clientX < leftBoundary || clientX > rightBoundary) {
2485
- return -1;
2486
- }
2487
-
2488
- for (let i = 0; i < blocks.length; i += 1) {
2489
- const rect = blocks[i].getBoundingClientRect();
2490
- if (clientY >= rect.top - 4 && clientY <= rect.bottom + 4) {
2491
- return i;
2492
- }
2493
- }
2494
-
2495
- const firstRect = blocks[0].getBoundingClientRect();
2496
- const lastRect = blocks[blocks.length - 1].getBoundingClientRect();
2497
- if (clientY < firstRect.top) {
2498
- return 0;
2499
- }
2500
- if (clientY > lastRect.bottom) {
2501
- return blocks.length - 1;
2502
- }
2503
- return -1;
2504
- }
2505
-
2506
- function positionMarkdownBlockDragHandle(block, index) {
2507
- if (!markdownBlockDragHandle || !block || !markdownEditorRoot || markdownEditorRoot.style.display !== 'block') {
2508
- hideMarkdownBlockDragHandle();
2509
- return;
2510
- }
2511
-
2512
- const rect = block.getBoundingClientRect();
2513
- const surfaceRect = markdownEditorSurface ? markdownEditorSurface.getBoundingClientRect() : null;
2514
- if (!surfaceRect || rect.width <= 0 || rect.height <= 0) {
2515
- hideMarkdownBlockDragHandle();
2516
- return;
2517
- }
2518
-
2519
- // Ignore blocks outside the visible scroll viewport.
2520
- if (rect.bottom < surfaceRect.top || rect.top > surfaceRect.bottom) {
2521
- hideMarkdownBlockDragHandle();
2522
- return;
2523
- }
2524
-
2525
- const left = Math.max(6, rect.left - 36);
2526
- const top = Math.max(6, rect.top + 1);
2527
- markdownBlockDragHandle.style.left = left + 'px';
2528
- markdownBlockDragHandle.style.top = top + 'px';
2529
- markdownBlockDragHandle.style.display = 'block';
2530
- markdownBlockDragHandle.setAttribute('data-block-index', String(index));
2531
- markdownBlockHandleHoverIndex = index;
2532
- }
2533
-
2534
- function refreshMarkdownBlockDragHandlePosition() {
2535
- if (markdownBlockDragActive || markdownBlockHandleHoverIndex < 0) {
2536
- return;
2537
- }
2538
- const blocks = getMarkdownTopLevelBlocks();
2539
- const block = blocks[markdownBlockHandleHoverIndex];
2540
- if (!block) {
2541
- hideMarkdownBlockDragHandle();
2542
- return;
2543
- }
2544
- positionMarkdownBlockDragHandle(block, markdownBlockHandleHoverIndex);
2545
- }
2546
-
2547
- function getMarkdownDropSlotIndexFromPointer(clientY) {
2548
- const blocks = getMarkdownTopLevelBlocks();
2549
- if (blocks.length === 0) {
2550
- return -1;
2551
- }
2552
-
2553
- for (let i = 0; i < blocks.length; i += 1) {
2554
- const rect = blocks[i].getBoundingClientRect();
2555
- const midpoint = rect.top + rect.height / 2;
2556
- if (clientY < midpoint) {
2557
- return i;
2558
- }
2559
- }
2560
- return blocks.length;
2561
- }
2562
-
2563
- function autoScrollMarkdownSurfaceDuringDrag(clientY) {
2564
- if (!markdownEditorSurface) {
2565
- return;
2566
- }
2567
-
2568
- const rect = markdownEditorSurface.getBoundingClientRect();
2569
- const threshold = 42;
2570
- const maxStep = 20;
2571
- let delta = 0;
2572
-
2573
- if (clientY < rect.top + threshold) {
2574
- const distance = rect.top + threshold - clientY;
2575
- delta = -Math.min(maxStep, Math.max(2, Math.floor(distance / 3)));
2576
- } else if (clientY > rect.bottom - threshold) {
2577
- const distance = clientY - (rect.bottom - threshold);
2578
- delta = Math.min(maxStep, Math.max(2, Math.floor(distance / 3)));
2579
- }
2580
-
2581
- if (delta !== 0) {
2582
- markdownEditorSurface.scrollTop += delta;
2583
- refreshMarkdownBlockDragHandlePosition();
2584
- }
2585
- }
2586
-
2587
- function showMarkdownBlockDropIndicator(slotIndex) {
2588
- if (!markdownBlockDropIndicator || !markdownEditorSurface) {
2589
- return;
2590
- }
2591
-
2592
- const blocks = getMarkdownTopLevelBlocks();
2593
- if (blocks.length === 0) {
2594
- hideMarkdownBlockDropIndicator();
2595
- return;
2596
- }
2597
-
2598
- const safeSlot = Math.max(0, Math.min(blocks.length, Math.trunc(slotIndex || 0)));
2599
- const surfaceRect = markdownEditorSurface.getBoundingClientRect();
2600
- let top = surfaceRect.top + 6;
2601
-
2602
- if (safeSlot >= blocks.length) {
2603
- const lastRect = blocks[blocks.length - 1].getBoundingClientRect();
2604
- top = lastRect.bottom + 1;
2605
- } else {
2606
- const rect = blocks[safeSlot].getBoundingClientRect();
2607
- top = rect.top - 1;
2608
- }
2609
-
2610
- markdownBlockDropIndicator.style.left = Math.max(8, surfaceRect.left + 8) + 'px';
2611
- markdownBlockDropIndicator.style.top = Math.max(8, top) + 'px';
2612
- markdownBlockDropIndicator.style.width = Math.max(40, surfaceRect.width - 16) + 'px';
2613
- markdownBlockDropIndicator.style.display = 'block';
2614
- markdownBlockDropSlotIndex = safeSlot;
2615
-
2616
- const dropType = safeSlot >= blocks.length
2617
- ? { label: 'end of document', color: '#0284c7' }
2618
- : getMarkdownBlockTypeInfo(blocks[safeSlot]);
2619
- markdownBlockDropIndicator.style.background = dropType.color;
2620
- markdownBlockDropIndicator.style.boxShadow = '0 1px 6px ' + dropType.color;
2621
-
2622
- if (markdownBlockDropLabel) {
2623
- markdownBlockDropLabel.textContent = safeSlot >= blocks.length
2624
- ? 'Drop at end'
2625
- : 'Drop before ' + dropType.label;
2626
- markdownBlockDropLabel.style.left = Math.max(8, surfaceRect.left + 8) + 'px';
2627
- markdownBlockDropLabel.style.top = Math.max(8, top - 26) + 'px';
2628
- markdownBlockDropLabel.style.borderColor = dropType.color;
2629
- markdownBlockDropLabel.style.display = 'block';
2630
- }
2631
- }
2632
-
2633
- function moveMarkdownLexicalBlock(sourceIndex, targetSlotIndex) {
2634
- if (!markdownLexicalApi || !markdownLexicalApi.editor || !markdownLexicalApi.lexicalModule) {
2635
- return false;
2636
- }
2637
-
2638
- const source = Math.trunc(sourceIndex);
2639
- const targetSlot = Math.trunc(targetSlotIndex);
2640
- if (!Number.isInteger(source) || !Number.isInteger(targetSlot)) {
2641
- return false;
2642
- }
2643
-
2644
- let didMove = false;
2645
- markdownLexicalApi.editor.update(function() {
2646
- const root = markdownLexicalApi.lexicalModule.$getRoot();
2647
- const children = root.getChildren();
2648
- const maxSlot = children.length;
2649
- if (source < 0 || source >= maxSlot || targetSlot < 0 || targetSlot > maxSlot) {
2650
- return;
2651
- }
2652
-
2653
- let adjustedSlot = targetSlot;
2654
- if (source < adjustedSlot) {
2655
- adjustedSlot -= 1;
2656
- }
2657
- if (adjustedSlot === source) {
2658
- return;
2659
- }
2660
-
2661
- const node = children[source];
2662
- node.remove();
2663
- const afterRemoval = root.getChildren();
2664
- if (adjustedSlot >= afterRemoval.length) {
2665
- root.append(node);
2666
- } else {
2667
- afterRemoval[adjustedSlot].insertBefore(node);
2668
- }
2669
- didMove = true;
2670
- });
2671
-
2672
- if (didMove) {
2673
- scheduleMarkdownSelectionSync();
2674
- scheduleMarkdownSelectionOverlayRender();
2675
- scheduleMarkdownSlashMenuUpdate();
2676
- scheduleMarkdownInlineToolbarUpdate();
2677
- }
2678
- return didMove;
2679
- }
2680
-
2681
- function getMarkdownCurrentBlockIndexFromSelection() {
2682
- if (!markdownEditorSurface) {
2683
- return -1;
2684
- }
2685
- const selection = window.getSelection();
2686
- if (!selection || selection.rangeCount === 0) {
2687
- return -1;
2688
- }
2689
-
2690
- const range = selection.getRangeAt(0);
2691
- if (!markdownEditorSurface.contains(range.startContainer)) {
2692
- return -1;
2693
- }
2694
-
2695
- const block = getMarkdownBlockElementFromNode(range.startContainer);
2696
- if (!block) {
2697
- return -1;
2698
- }
2699
-
2700
- const blocks = getMarkdownTopLevelBlocks();
2701
- return blocks.indexOf(block);
2702
- }
2703
-
2704
- function moveMarkdownCurrentBlockByDelta(delta) {
2705
- const blocks = getMarkdownTopLevelBlocks();
2706
- if (blocks.length <= 1) {
2707
- return false;
2708
- }
2709
-
2710
- const index = getMarkdownCurrentBlockIndexFromSelection();
2711
- if (index < 0) {
2712
- return false;
2713
- }
2714
-
2715
- const step = Math.sign(delta);
2716
- if (step === 0) {
2717
- return false;
2718
- }
2719
-
2720
- let targetSlot = index;
2721
- if (step < 0) {
2722
- targetSlot = Math.max(0, index - 1);
2723
- } else {
2724
- targetSlot = Math.min(blocks.length, index + 2);
2725
- }
2726
-
2727
- const moved = moveMarkdownLexicalBlock(index, targetSlot);
2728
- if (moved) {
2729
- setTimeout(function() {
2730
- const nextBlocks = getMarkdownTopLevelBlocks();
2731
- const nextIndex = Math.max(0, Math.min(nextBlocks.length - 1, index + step));
2732
- const nextBlock = nextBlocks[nextIndex];
2733
- if (nextBlock) {
2734
- positionMarkdownBlockDragHandle(nextBlock, nextIndex);
2735
- }
2736
- }, 0);
2737
- }
2738
- return moved;
2739
- }
2740
-
2741
- function scheduleMarkdownSelectionSync() {
2742
- if (!markdownFileId) {
2743
- return;
2744
- }
2745
-
2746
- if (markdownSelectionSyncTimer) {
2747
- clearTimeout(markdownSelectionSyncTimer);
2748
- }
2749
-
2750
- markdownSelectionSyncTimer = setTimeout(function() {
2751
- const selection = getMarkdownEditorSelection();
2752
- if (!selection) {
2753
- return;
2754
- }
2755
-
2756
- const start = editorOffsetToSourceOffset(selection.start, 'start');
2757
- const end = editorOffsetToSourceOffset(selection.end, 'end');
2758
-
2759
- // Set local selection on Yjs awareness directly
2760
- if (markdownYjsConnected && markdownYText && markdownYjsY && markdownYProvider) {
2761
- var clampedStart = Math.max(0, Math.min(markdownYText.length, start));
2762
- var clampedEnd = Math.max(0, Math.min(markdownYText.length, end));
2763
- markdownYProvider.awareness.setLocalStateField('selection', [{
2764
- anchor: markdownYjsY.createRelativePositionFromTypeIndex(markdownYText, clampedStart),
2765
- marker: markdownYjsY.createRelativePositionFromTypeIndex(markdownYText, clampedEnd)
2766
- }]);
2767
- markdownPendingSelection = null;
2768
- } else {
2769
- // Queue selection for replay after Yjs connects
2770
- markdownPendingSelection = { start: start, end: end };
2771
- }
2772
- }, 80);
2773
- }
2774
-
2775
- function clearMarkdownSelectionSync() {
2776
- if (markdownYProvider) {
2777
- markdownYProvider.awareness.setLocalStateField('selection', null);
2778
- }
2779
- }
2780
-
2781
- function clearMarkdownSelectionOverlay() {
2782
- if (!markdownSelectionOverlayRoot) {
2783
- return;
2784
- }
2785
- markdownSelectionOverlayRoot.textContent = '';
2786
- markdownSelectionOverlayRoot.style.display = 'none';
2787
- }
2788
-
2789
- function resolveMarkdownTextPoint(root, rawOffset) {
2790
- const offset = Math.max(0, Math.trunc(rawOffset || 0));
2791
- const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT);
2792
- let remaining = offset;
2793
- let lastTextNode = null;
2794
- let node = walker.nextNode();
2795
-
2796
- while (node) {
2797
- lastTextNode = node;
2798
- const textLength = node.textContent ? node.textContent.length : 0;
2799
- if (remaining <= textLength) {
2800
- return { node: node, offset: remaining };
2801
- }
2802
- remaining -= textLength;
2803
- node = walker.nextNode();
2804
- }
2805
-
2806
- if (lastTextNode) {
2807
- const textLength = lastTextNode.textContent ? lastTextNode.textContent.length : 0;
2808
- return { node: lastTextNode, offset: textLength };
2809
- }
2810
-
2811
- return {
2812
- node: root,
2813
- offset: offset > 0 ? root.childNodes.length : 0
2814
- };
2815
- }
2816
-
2817
- function createMarkdownEditorRange(start, end) {
2818
- if (!markdownEditorSurface) {
2819
- return null;
2820
- }
2821
-
2822
- const safeStart = Math.max(0, Math.min(start, end));
2823
- const safeEnd = Math.max(0, Math.max(start, end));
2824
- const startPoint = resolveMarkdownTextPoint(markdownEditorSurface, safeStart);
2825
- const endPoint = resolveMarkdownTextPoint(markdownEditorSurface, safeEnd);
2826
-
2827
- try {
2828
- const range = document.createRange();
2829
- range.setStart(startPoint.node, startPoint.offset);
2830
- range.setEnd(endPoint.node, endPoint.offset);
2831
- return range;
2832
- } catch {
2833
- return null;
2834
- }
2835
- }
2836
-
2837
- function toMarkdownOverlayRect(rect, surfaceRect) {
2838
- const rawLeft = rect.left - surfaceRect.left;
2839
- const rawTop = rect.top - surfaceRect.top;
2840
- const rawRight = rawLeft + rect.width;
2841
- const rawBottom = rawTop + rect.height;
2842
-
2843
- const left = Math.max(0, rawLeft);
2844
- const top = Math.max(0, rawTop);
2845
- const right = Math.min(surfaceRect.width, rawRight);
2846
- const bottom = Math.min(surfaceRect.height, rawBottom);
2847
- const width = right - left;
2848
- const height = bottom - top;
2849
-
2850
- if (width <= 0 || height <= 0) {
2851
- return null;
2852
- }
2853
-
2854
- return {
2855
- left: left,
2856
- top: top,
2857
- width: width,
2858
- height: height
2859
- };
2860
- }
2861
-
2862
- function renderMarkdownSelectionOverlay() {
2863
- if (!markdownSelectionOverlayRoot) {
2864
- return;
2865
- }
2866
-
2867
- if (
2868
- !markdownEditorRoot ||
2869
- markdownEditorRoot.style.display !== 'block' ||
2870
- !markdownEditorSurface ||
2871
- !markdownLexicalApi ||
2872
- !Array.isArray(markdownOverlaySelections) ||
2873
- markdownOverlaySelections.length === 0
2874
- ) {
2875
- clearMarkdownSelectionOverlay();
2876
- return;
2877
- }
2878
-
2879
- const surfaceRect = markdownEditorSurface.getBoundingClientRect();
2880
- if (surfaceRect.width <= 0 || surfaceRect.height <= 0) {
2881
- clearMarkdownSelectionOverlay();
2882
- return;
2883
- }
2884
-
2885
- const computedStyle = window.getComputedStyle(markdownEditorSurface);
2886
- const lineHeight = Math.max(14, Number.parseFloat(computedStyle.lineHeight || '0') || 22);
2887
- markdownSelectionOverlayRoot.textContent = '';
2888
- markdownSelectionOverlayRoot.style.display = 'block';
2889
-
2890
- for (const selection of markdownOverlaySelections) {
2891
- if (!selection || typeof selection.start !== 'number' || typeof selection.end !== 'number') {
2892
- continue;
2893
- }
2894
-
2895
- const range = createMarkdownEditorRange(selection.start, selection.end);
2896
- if (!range) {
2897
- continue;
2898
- }
2899
-
2900
- const color = typeof selection.color === 'string' && selection.color ? selection.color : '#6b7280';
2901
- const name = typeof selection.name === 'string' && selection.name ? selection.name : 'Anonymous';
2902
- let labelAnchor = null;
2903
-
2904
- if (selection.start === selection.end) {
2905
- const caretRect = range.getBoundingClientRect();
2906
- const clippedCaret = toMarkdownOverlayRect(
2907
- {
2908
- left: caretRect.left,
2909
- top: caretRect.top,
2910
- width: 2,
2911
- height: Math.max(caretRect.height, lineHeight)
2912
- },
2913
- surfaceRect
2914
- );
2915
-
2916
- if (!clippedCaret) {
2917
- continue;
2918
- }
2919
-
2920
- const caret = document.createElement('div');
2921
- caret.className = 'vf-markdown-editor__selection-caret';
2922
- caret.setAttribute(DATA_VF_IGNORE, 'true');
2923
- caret.style.left = clippedCaret.left + 'px';
2924
- caret.style.top = clippedCaret.top + 'px';
2925
- caret.style.height = clippedCaret.height + 'px';
2926
- caret.style.background = color;
2927
- markdownSelectionOverlayRoot.appendChild(caret);
2928
-
2929
- labelAnchor = { left: clippedCaret.left, top: clippedCaret.top };
2930
- } else {
2931
- const rectList = Array.from(range.getClientRects());
2932
- for (const rect of rectList) {
2933
- const clippedRect = toMarkdownOverlayRect(rect, surfaceRect);
2934
- if (!clippedRect) {
2935
- continue;
2936
- }
2937
-
2938
- const highlight = document.createElement('div');
2939
- highlight.className = 'vf-markdown-editor__selection-highlight';
2940
- highlight.setAttribute(DATA_VF_IGNORE, 'true');
2941
- highlight.style.left = clippedRect.left + 'px';
2942
- highlight.style.top = clippedRect.top + 'px';
2943
- highlight.style.width = clippedRect.width + 'px';
2944
- highlight.style.height = clippedRect.height + 'px';
2945
- highlight.style.background = color;
2946
- markdownSelectionOverlayRoot.appendChild(highlight);
2947
-
2948
- if (!labelAnchor) {
2949
- labelAnchor = { left: clippedRect.left, top: clippedRect.top };
2950
- }
2951
- }
2952
- }
2953
-
2954
- if (!labelAnchor) {
2955
- continue;
2956
- }
2957
-
2958
- const label = document.createElement('div');
2959
- label.className = 'vf-markdown-editor__selection-label';
2960
- label.setAttribute(DATA_VF_IGNORE, 'true');
2961
- label.textContent = name;
2962
- label.style.left = labelAnchor.left + 'px';
2963
- label.style.top = labelAnchor.top + 'px';
2964
- label.style.background = color;
2965
- markdownSelectionOverlayRoot.appendChild(label);
2966
- }
2967
-
2968
- if (markdownSelectionOverlayRoot.childNodes.length === 0) {
2969
- clearMarkdownSelectionOverlay();
2970
- }
2971
- }
2972
-
2973
- function scheduleMarkdownSelectionOverlayRender() {
2974
- if (markdownSelectionOverlayRenderFrame) {
2975
- cancelAnimationFrame(markdownSelectionOverlayRenderFrame);
2976
- }
2977
-
2978
- markdownSelectionOverlayRenderFrame = requestAnimationFrame(function() {
2979
- markdownSelectionOverlayRenderFrame = null;
2980
- renderMarkdownSelectionOverlay();
2981
- });
2982
- }
2983
-
2984
- function extractMarkdownParts(content) {
2985
- if (typeof content !== 'string') {
2986
- return {
2987
- frontmatter: '',
2988
- body: ''
2989
- };
2990
- }
2991
-
2992
- const frontmatterPattern = new RegExp(
2993
- '^---[ \\\\t]*\\\\r?\\\\n[\\\\s\\\\S]*?\\\\r?\\\\n---[ \\\\t]*(?:\\\\r?\\\\n)?'
2994
- );
2995
- const match = content.match(frontmatterPattern);
2996
- if (!match) {
2997
- return {
2998
- frontmatter: '',
2999
- body: content
3000
- };
3001
- }
3002
-
3003
- return {
3004
- frontmatter: match[0],
3005
- body: content.slice(match[0].length)
3006
- };
3007
- }
3008
-
3009
- function composeMarkdownContent(body) {
3010
- const safeBody = typeof body === 'string' ? body : '';
3011
- if (!markdownFrontmatter) {
3012
- return safeBody;
3013
- }
3014
- if (!safeBody) {
3015
- return markdownFrontmatter;
3016
- }
3017
- if (markdownFrontmatter.endsWith('\\n')) {
3018
- return markdownFrontmatter + safeBody;
3019
- }
3020
- return markdownFrontmatter + '\\n' + safeBody;
3021
- }
3022
-
3023
- function extractRawBlocksForEditor(body, mdxImportMap) {
3024
- const source = typeof body === 'string' ? body : '';
3025
- const rawBlocks = [];
3026
- const mdxBlocks = [];
3027
- const tokenPrefix = 'VF_RAW_BLOCK_' + Date.now().toString(36) + '_' + Math.random().toString(36).slice(2, 8);
3028
- const trackMdxBlocks = isMdxPage();
3029
- const importMap = mdxImportMap && typeof mdxImportMap === 'object' ? mdxImportMap : {};
3030
- const createToken = function(index) {
3031
- return '[[' + tokenPrefix + '_' + index + ']]';
3032
- };
3033
- const registerMdxBlock = function(rawBlock, tokenIndex, offset, inputText) {
3034
- if (!trackMdxBlocks) {
3035
- return;
3036
- }
3037
-
3038
- const trimmed = String(rawBlock || '').trimStart();
3039
- const fence = String.fromCharCode(96, 96, 96);
3040
- const startsWithTsxFence = trimmed.startsWith(fence + 'tsx') || trimmed.startsWith(fence + 'jsx');
3041
- const startsWithUpperTag = /^<\\s*[A-Z]/.test(trimmed);
3042
- const hasTsxProps = trimmed.indexOf('{') >= 0 && trimmed.indexOf('}') >= 0;
3043
-
3044
- if (!startsWithTsxFence && !startsWithUpperTag && !hasTsxProps) {
3045
- return;
3046
- }
3047
-
3048
- const label = startsWithTsxFence
3049
- ? 'TSX block'
3050
- : 'JSX ' + getMdxComponentName(trimmed);
3051
- const componentName = getMdxComponentName(trimmed);
3052
- const componentNamePattern = /^[A-Z][\\w$]*(?:\\.[A-Z][\\w$]*)*$/;
3053
- const normalizedComponentName = componentNamePattern.test(componentName) ? componentName : '';
3054
- const componentParts = normalizedComponentName ? normalizedComponentName.split('.') : [];
3055
- const namespaceName = componentParts.length > 0 ? componentParts[0] : '';
3056
- const fallbackSymbol = componentParts.length > 0 ? componentParts[componentParts.length - 1] : '';
3057
- const directEntry = normalizedComponentName ? importMap[normalizedComponentName] : null;
3058
- const namespaceEntry = !directEntry && namespaceName ? importMap[namespaceName] : null;
3059
- const importEntry = directEntry || namespaceEntry || null;
3060
- const entryPath = importEntry && typeof importEntry.filePath === 'string'
3061
- ? importEntry.filePath
3062
- : (
3063
- typeof importEntry === 'string'
3064
- ? importEntry
3065
- : ''
3066
- );
3067
- const entrySymbol = importEntry && typeof importEntry.symbolName === 'string'
3068
- ? importEntry.symbolName
3069
- : '';
3070
- const entryKind = importEntry && typeof importEntry.importKind === 'string'
3071
- ? importEntry.importKind
3072
- : '';
3073
- const componentPath = entryPath || '';
3074
- let componentSymbol = fallbackSymbol;
3075
- if (entrySymbol) {
3076
- componentSymbol = entrySymbol;
3077
- } else if (entryKind === 'namespace' && componentParts.length > 1) {
3078
- componentSymbol = componentParts[componentParts.length - 1];
3079
- }
3080
- mdxBlocks.push({
3081
- tokenIndex: tokenIndex,
3082
- label: label,
3083
- lineNumber: getLineNumberForOffset(inputText, offset),
3084
- filePath: componentPath,
3085
- symbolName: componentSymbol
3086
- });
3087
- };
3088
- const replaceWithToken = function(match, leadingNewline, offset, inputText) {
3089
- const safeLeading = typeof leadingNewline === 'string' ? leadingNewline : '';
3090
- const tokenIndex = rawBlocks.length;
3091
- const rawBlock = typeof match === 'string' ? match.trimStart() : '';
3092
- rawBlocks.push(rawBlock);
3093
- registerMdxBlock(rawBlock, tokenIndex, Math.max(0, (offset || 0) + safeLeading.length), inputText || source);
3094
- return safeLeading + createToken(tokenIndex);
3095
- };
3096
-
3097
- const mermaidFencePattern = new RegExp(
3098
- '(^|\\\\n)\\\\x60\\\\x60\\\\x60mermaid[^\\\\n]*\\\\n[\\\\s\\\\S]*?\\\\n\\\\x60\\\\x60\\\\x60(?=\\\\n|$)',
3099
- 'g'
3100
- );
3101
- const tsxFencePattern = new RegExp(
3102
- '(^|\\\\n)\\\\x60\\\\x60\\\\x60(?:tsx|jsx)[^\\\\n]*\\\\n[\\\\s\\\\S]*?\\\\n\\\\x60\\\\x60\\\\x60(?=\\\\n|$)',
3103
- 'g'
3104
- );
3105
- const htmlBlockPattern = new RegExp(
3106
- '(^|\\\\n)<[A-Za-z][\\\\w:-]*(?:\\\\s[^>\\\\n]*)?>[\\\\s\\\\S]*?<\\\\/[A-Za-z][\\\\w:-]*>(?=\\\\n|$)',
3107
- 'g'
3108
- );
3109
- const htmlSelfClosingPattern = new RegExp(
3110
- '(^|\\\\n)<[A-Za-z][\\\\w:-]*(?:\\\\s[^>\\\\n]*)?\\\\/>(?=\\\\n|$)',
3111
- 'g'
3112
- );
3113
-
3114
- let editorBody = source.replace(
3115
- mermaidFencePattern,
3116
- replaceWithToken
3117
- );
3118
-
3119
- editorBody = editorBody.replace(
3120
- tsxFencePattern,
3121
- replaceWithToken
3122
- );
3123
-
3124
- editorBody = editorBody.replace(
3125
- htmlBlockPattern,
3126
- replaceWithToken
3127
- );
3128
-
3129
- editorBody = editorBody.replace(
3130
- htmlSelfClosingPattern,
3131
- replaceWithToken
3132
- );
3133
-
3134
- return {
3135
- editorBody: editorBody,
3136
- rawBlocks: rawBlocks,
3137
- mdxBlocks: mdxBlocks,
3138
- tokenPrefix: tokenPrefix
3139
- };
3140
- }
3141
-
3142
- function restoreRawBlocksFromEditor(editorBody) {
3143
- const source = typeof editorBody === 'string' ? editorBody : '';
3144
- if (!source || markdownRawBlocks.length === 0) {
3145
- return source;
3146
- }
3147
- const rawBlockTokenPattern = getMarkdownRawBlockTokenPattern();
3148
-
3149
- return source.replace(rawBlockTokenPattern, function(match, indexText) {
3150
- const index = Number(indexText);
3151
- if (!Number.isInteger(index) || index < 0 || index >= markdownRawBlocks.length) {
3152
- return match;
3153
- }
3154
- const rawBlock = markdownRawBlocks[index];
3155
- return typeof rawBlock === 'string' ? rawBlock : match;
3156
- });
3157
- }
3158
-
3159
- function handleMarkdownLocalChange(content) {
3160
- if (typeof content !== 'string') {
3161
- return;
3162
- }
3163
-
3164
- markdownCurrentEditorContent = content;
3165
- const restoredBody = restoreRawBlocksFromEditor(content);
3166
- const fullContent = composeMarkdownContent(restoredBody);
3167
- if (fullContent === markdownCurrentContent) {
3168
- return;
3169
- }
3170
- markdownCurrentContent = fullContent;
3171
- markdownHasUnsavedChanges = true;
3172
- if (markdownYjsConnected) {
3173
- syncLocalChangeToYText(fullContent);
3174
- }
3175
- scheduleMarkdownSync(fullContent);
3176
- scheduleMarkdownSelectionOverlayRender();
3177
- }
3178
-
3179
- function saveMarkdownContent() {
3180
- if (!markdownHasUnsavedChanges) {
3181
- return;
3182
- }
3183
- markdownSaveInProgress = true;
3184
- setMarkdownPersistStatus('saving');
3185
- if (markdownSyncTimer) {
3186
- clearTimeout(markdownSyncTimer);
3187
- markdownSyncTimer = null;
3188
- }
3189
- postToStudio({
3190
- action: 'markdownContentChange',
3191
- fileId: markdownFileId,
3192
- filePath: PAGE_PATH,
3193
- content: markdownCurrentContent,
3194
- save: true
3195
- });
3196
- markdownHasUnsavedChanges = false;
3197
- }
3198
-
3199
- function setupMarkdownLexicalEditor() {
3200
- if (!markdownEditorSurface || markdownLexicalApi || markdownLexicalSetupPromise) {
3201
- return;
3202
- }
3203
-
3204
- markdownLexicalSetupPromise = Promise.all([
3205
- import('https://esm.sh/lexical@0.21.0?target=es2022'),
3206
- import('https://esm.sh/@lexical/rich-text@0.21.0?target=es2022'),
3207
- import('https://esm.sh/@lexical/list@0.21.0?target=es2022'),
3208
- import('https://esm.sh/@lexical/markdown@0.21.0?target=es2022'),
3209
- import('https://esm.sh/@lexical/history@0.21.0?target=es2022')
3210
- ]).then(function(modules) {
3211
- if (!markdownEditorSurface) {
3212
- return;
3213
- }
3214
-
3215
- const lexicalModule = modules[0];
3216
- const richTextModule = modules[1];
3217
- const listModule = modules[2];
3218
- const markdownModule = modules[3];
3219
- const historyModule = modules[4];
3220
-
3221
- const editor = lexicalModule.createEditor({
3222
- namespace: 'veryfront-markdown-preview',
3223
- nodes: [
3224
- richTextModule.HeadingNode,
3225
- richTextModule.QuoteNode,
3226
- listModule.ListNode,
3227
- listModule.ListItemNode
3228
- ],
3229
- onError: function(error) {
3230
- console.error('[StudioBridge] Markdown Lexical error', error);
3231
- }
3232
- });
3233
-
3234
- const unregisterRichText = richTextModule.registerRichText(editor);
3235
- const unregisterList = listModule.registerList(editor);
3236
- const unregisterHistory = historyModule.registerHistory(
3237
- editor,
3238
- historyModule.createEmptyHistoryState(),
3239
- 1000
3240
- );
3241
- const unregisterUpdate = editor.registerUpdateListener(function(update) {
3242
- if (markdownApplyingRemoteUpdate) {
3243
- return;
3244
- }
3245
-
3246
- let nextContent = '';
3247
- update.editorState.read(function() {
3248
- nextContent = markdownModule.$convertToMarkdownString(markdownModule.TRANSFORMERS, undefined, true);
3249
- });
3250
- const restoredBody = restoreRawBlocksFromEditor(nextContent);
3251
- const fullContent = composeMarkdownContent(restoredBody);
3252
-
3253
- if (fullContent === markdownLexicalRenderedContent) {
3254
- return;
3255
- }
3256
- markdownLexicalRenderedContent = fullContent;
3257
-
3258
- handleMarkdownLocalChange(nextContent);
3259
- scheduleMarkdownSlashMenuUpdate();
3260
- scheduleMarkdownInlineToolbarUpdate();
3261
- });
3262
-
3263
- editor.setRootElement(markdownEditorSurface);
3264
- editor.update(function() {
3265
- const root = lexicalModule.$getRoot();
3266
- if (root.getChildrenSize() === 0) {
3267
- root.append(lexicalModule.$createParagraphNode());
3268
- }
3269
- });
3270
- markdownLexicalApi = {
3271
- editor: editor,
3272
- lexicalModule: lexicalModule,
3273
- markdownModule: markdownModule,
3274
- unregisterRichText: unregisterRichText,
3275
- unregisterList: unregisterList,
3276
- unregisterHistory: unregisterHistory,
3277
- unregisterUpdate: unregisterUpdate
3278
- };
3279
-
3280
- markdownEditorSurface.style.display = 'block';
3281
- if (markdownEditorTextarea) {
3282
- markdownEditorTextarea.style.display = 'none';
3283
- }
3284
-
3285
- applyMarkdownContent(markdownCurrentContent);
3286
- hideMarkdownBlockDropIndicator();
3287
- }).catch(function(error) {
3288
- console.warn(
3289
- '[StudioBridge] Failed to load Lexical markdown editor; falling back to textarea',
3290
- error
3291
- );
3292
- if (markdownEditorSurface) {
3293
- markdownEditorSurface.style.display = 'none';
3294
- }
3295
- if (markdownEditorTextarea) {
3296
- markdownEditorTextarea.style.display = 'block';
3297
- }
3298
- hideMarkdownSlashMenu();
3299
- hideMarkdownInlineToolbar();
3300
- hideMarkdownBlockDragUi();
3301
- clearMarkdownSelectionOverlay();
3302
- });
3303
- }
3304
-
3305
- function focusMarkdownEditor() {
3306
- if (markdownLexicalApi && markdownEditorSurface) {
3307
- markdownEditorSurface.focus();
3308
- return;
3309
- }
3310
- if (markdownEditorTextarea) {
3311
- markdownEditorTextarea.focus();
3312
- }
3313
- }
3314
-
3315
- function applyMarkdownHistoryCommand(command) {
3316
- if (!markdownLexicalApi || !markdownLexicalApi.editor || !markdownLexicalApi.lexicalModule) {
3317
- return;
3318
- }
3319
- if (!command) {
3320
- return;
3321
- }
3322
-
3323
- markdownLexicalApi.editor.focus();
3324
- markdownLexicalApi.editor.dispatchCommand(command, undefined);
3325
- scheduleMarkdownSelectionSync();
3326
- scheduleMarkdownSelectionOverlayRender();
3327
- scheduleMarkdownSlashMenuUpdate();
3328
- scheduleMarkdownInlineToolbarUpdate();
3329
- }
3330
-
3331
- function applyMarkdownContent(content) {
3332
- if (typeof content !== 'string') {
3333
- return;
3334
- }
3335
-
3336
- if (markdownLexicalApi && markdownLexicalRenderedContent === content) {
3337
- console.debug('[StudioBridge] applyMarkdownContent: skipped (content unchanged)');
3338
- markdownCurrentContent = content;
3339
- scheduleMarkdownSelectionOverlayRender();
3340
- scheduleMarkdownSlashMenuUpdate();
3341
- scheduleMarkdownInlineToolbarUpdate();
3342
- hideMarkdownBlockDropIndicator();
3343
- return;
3344
- }
3345
-
3346
- console.debug('[StudioBridge] applyMarkdownContent: rebuilding Lexical DOM, content length:', content.length, 'rendered match:', markdownLexicalRenderedContent === content, 'current match:', markdownCurrentContent === content);
3347
-
3348
- const mdxImportMap = parseMdxImportMap(content);
3349
- const parts = extractMarkdownParts(content);
3350
- const extracted = extractRawBlocksForEditor(parts.body, mdxImportMap);
3351
- const editorContent = extracted.editorBody;
3352
- const mdxBlocks = Array.isArray(extracted.mdxBlocks) ? extracted.mdxBlocks : [];
3353
- markdownFrontmatter = parts.frontmatter;
3354
- markdownRawBlocks = extracted.rawBlocks;
3355
- markdownRawBlockTokenPrefix = extracted.tokenPrefix;
3356
- markdownLatestMdxImportMap = mdxImportMap;
3357
- setMarkdownMdxBlocks(mdxBlocks);
3358
-
3359
- markdownCurrentContent = content;
3360
- markdownCurrentEditorContent = editorContent;
3361
-
3362
- if (markdownLexicalApi) {
3363
- markdownApplyingRemoteUpdate = true;
3364
- try {
3365
- markdownLexicalRenderedContent = content;
3366
- markdownLexicalApi.editor.update(function() {
3367
- const lexicalModule = markdownLexicalApi.lexicalModule;
3368
- const markdownModule = markdownLexicalApi.markdownModule;
3369
- const root = lexicalModule.$getRoot();
3370
- root.clear();
3371
- markdownModule.$convertFromMarkdownString(editorContent, markdownModule.TRANSFORMERS, undefined, true);
3372
- if (root.getChildrenSize() === 0) {
3373
- root.append(lexicalModule.$createParagraphNode());
3374
- }
3375
- }, { discrete: true });
3376
- } finally {
3377
- markdownApplyingRemoteUpdate = false;
3378
- }
3379
- scheduleMarkdownSelectionOverlayRender();
3380
- scheduleMarkdownSlashMenuUpdate();
3381
- scheduleMarkdownInlineToolbarUpdate();
3382
- hideMarkdownBlockDropIndicator();
3383
- return;
3384
- }
3385
-
3386
- if (!markdownEditorTextarea || markdownEditorTextarea.value === editorContent) {
3387
- clearMarkdownSelectionOverlay();
3388
- hideMarkdownSlashMenu();
3389
- hideMarkdownInlineToolbar();
3390
- hideMarkdownBlockDragUi();
3391
- return;
3392
- }
3393
-
3394
- const selectionStart = markdownEditorTextarea.selectionStart;
3395
- const selectionEnd = markdownEditorTextarea.selectionEnd;
3396
- markdownEditorTextarea.value = editorContent;
3397
-
3398
- if (typeof selectionStart === 'number' && typeof selectionEnd === 'number') {
3399
- const max = markdownEditorTextarea.value.length;
3400
- markdownEditorTextarea.setSelectionRange(Math.min(selectionStart, max), Math.min(selectionEnd, max));
3401
- }
3402
- clearMarkdownSelectionOverlay();
3403
- hideMarkdownSlashMenu();
3404
- hideMarkdownInlineToolbar();
3405
- hideMarkdownBlockDragUi();
3406
- }
3407
-
3408
- function setMarkdownPersistStatus(status) {
3409
- if (!markdownPersistStatus) {
3410
- return;
3411
- }
3412
-
3413
- const nextStatus = status === 'saving' || status === 'saved' || status === 'error'
3414
- ? status
3415
- : 'saved';
3416
-
3417
- markdownPersistStatus.setAttribute('data-state', nextStatus);
3418
- if (nextStatus === 'saving') {
3419
- markdownPersistStatus.textContent = 'Saving...';
3420
- return;
3421
- }
3422
- if (nextStatus === 'error') {
3423
- markdownPersistStatus.textContent = 'Save failed';
3424
- return;
3425
- }
3426
- markdownPersistStatus.textContent = 'Saved';
3427
- }
3428
-
3429
- function setMarkdownPresence(users) {
3430
- markdownLatestPresenceUsers = Array.isArray(users) ? users : [];
3431
- if (!markdownPresenceRoot) {
3432
- return;
3433
- }
3434
-
3435
- markdownPresenceRoot.textContent = '';
3436
- if (!Array.isArray(users) || users.length === 0) {
3437
- markdownPresenceRoot.style.display = 'none';
3438
- return;
3439
- }
3440
-
3441
- const visibleUsers = users.filter(function(user) {
3442
- return user && typeof user.name === 'string';
3443
- }).slice(0, 4);
3444
-
3445
- if (visibleUsers.length === 0) {
3446
- markdownPresenceRoot.style.display = 'none';
3447
- return;
3448
- }
3449
-
3450
- markdownPresenceRoot.style.display = 'inline-flex';
3451
-
3452
- for (const user of visibleUsers) {
3453
- const pill = document.createElement('div');
3454
- pill.className = 'vf-markdown-editor__presence-pill';
3455
- pill.setAttribute(DATA_VF_IGNORE, 'true');
3456
- pill.setAttribute('data-current', user.isCurrentUser ? 'true' : 'false');
3457
- pill.setAttribute('data-agent', user.isAgent ? 'true' : 'false');
3458
- pill.textContent = user.isCurrentUser ? 'You' : user.name;
3459
-
3460
- const color = typeof user.color === 'string' && user.color ? user.color : '#6b7280';
3461
- pill.style.borderLeftColor = color;
3462
-
3463
- markdownPresenceRoot.appendChild(pill);
3464
- }
3465
-
3466
- if (users.length > visibleUsers.length) {
3467
- const extra = document.createElement('div');
3468
- extra.className = 'vf-markdown-editor__presence-pill';
3469
- extra.setAttribute(DATA_VF_IGNORE, 'true');
3470
- extra.textContent = '+' + String(users.length - visibleUsers.length);
3471
- markdownPresenceRoot.appendChild(extra);
3472
- }
3473
- }
3474
-
3475
- function setMarkdownSelections(selections) {
3476
- markdownLatestSelections = Array.isArray(selections) ? selections : [];
3477
- if (!markdownSelectionsRoot) {
3478
- return;
3479
- }
3480
-
3481
- markdownSelectionsRoot.textContent = '';
3482
- markdownOverlaySelections = [];
3483
- if (!Array.isArray(selections) || selections.length === 0) {
3484
- markdownSelectionsRoot.style.display = 'none';
3485
- clearMarkdownSelectionOverlay();
3486
- return;
3487
- }
3488
-
3489
- const visibleSelections = selections.filter(function(selection) {
3490
- return (
3491
- selection &&
3492
- typeof selection.name === 'string' &&
3493
- typeof selection.start === 'number' &&
3494
- typeof selection.end === 'number'
3495
- );
3496
- }).slice(0, 4);
3497
-
3498
- if (visibleSelections.length === 0) {
3499
- markdownSelectionsRoot.style.display = 'none';
3500
- clearMarkdownSelectionOverlay();
3501
- return;
3502
- }
3503
-
3504
- markdownSelectionsRoot.style.display = 'inline-flex';
3505
-
3506
- for (const selection of visibleSelections) {
3507
- const pill = document.createElement('div');
3508
- pill.className = 'vf-markdown-editor__selection-pill';
3509
- pill.setAttribute(DATA_VF_IGNORE, 'true');
3510
- const color = typeof selection.color === 'string' && selection.color ? selection.color : '#6b7280';
3511
- const displayName = selection.isCurrentUser ? 'You' : selection.name;
3512
- pill.style.borderLeftColor = color;
3513
-
3514
- const start = Math.max(0, Math.trunc(selection.start));
3515
- const end = Math.max(0, Math.trunc(selection.end));
3516
- const rangeLabel = start === end ? '@' + String(start) : String(start) + '-' + String(end);
3517
- pill.textContent = displayName + ' ' + rangeLabel;
3518
- markdownSelectionsRoot.appendChild(pill);
3519
-
3520
- const editorRange = sourceSelectionToEditorRange(start, end);
3521
- if (!editorRange) {
3522
- continue;
3523
- }
3524
-
3525
- markdownOverlaySelections.push({
3526
- name: displayName,
3527
- color: color,
3528
- start: editorRange.start,
3529
- end: editorRange.end
3530
- });
3531
- }
3532
-
3533
- if (selections.length > visibleSelections.length) {
3534
- const extra = document.createElement('div');
3535
- extra.className = 'vf-markdown-editor__selection-pill';
3536
- extra.setAttribute(DATA_VF_IGNORE, 'true');
3537
- extra.textContent = '+' + String(selections.length - visibleSelections.length);
3538
- markdownSelectionsRoot.appendChild(extra);
3539
- }
3540
-
3541
- scheduleMarkdownSelectionOverlayRender();
3542
- }
3543
-
3544
- function ensureMarkdownEditor() {
3545
- if (markdownEditorRoot) {
3546
- return markdownEditorRoot;
3547
- }
3548
-
3549
- const editorRoot = document.createElement('div');
3550
- editorRoot.className = 'vf-markdown-editor';
3551
- editorRoot.setAttribute(DATA_VF_IGNORE, 'true');
3552
-
3553
- const toolbar = document.createElement('div');
3554
- toolbar.className = 'vf-markdown-editor__toolbar';
3555
- toolbar.setAttribute(DATA_VF_IGNORE, 'true');
3556
-
3557
- const title = document.createElement('div');
3558
- title.className = 'vf-markdown-editor__title';
3559
- title.setAttribute(DATA_VF_IGNORE, 'true');
3560
-
3561
- const titleMain = document.createElement('div');
3562
- titleMain.className = 'vf-markdown-editor__title-main';
3563
- titleMain.setAttribute(DATA_VF_IGNORE, 'true');
3564
- titleMain.textContent = 'Markdown editor';
3565
-
3566
- const titleHints = document.createElement('div');
3567
- titleHints.className = 'vf-markdown-editor__title-hints';
3568
- titleHints.setAttribute(DATA_VF_IGNORE, 'true');
3569
- titleHints.textContent = '/ commands | Shift+Alt+Up/Down move block | Undo/Redo';
3570
-
3571
- title.appendChild(titleMain);
3572
- title.appendChild(titleHints);
3573
-
3574
- const actions = document.createElement('div');
3575
- actions.className = 'vf-markdown-editor__actions';
3576
- actions.setAttribute(DATA_VF_IGNORE, 'true');
3577
-
3578
- const status = document.createElement('div');
3579
- status.className = 'vf-markdown-editor__status';
3580
- status.setAttribute(DATA_VF_IGNORE, 'true');
3581
- status.textContent = '';
3582
- status.setAttribute('data-state', '');
3583
-
3584
- const presence = document.createElement('div');
3585
- presence.className = 'vf-markdown-editor__presence';
3586
- presence.setAttribute(DATA_VF_IGNORE, 'true');
3587
-
3588
- const selections = document.createElement('div');
3589
- selections.className = 'vf-markdown-editor__selections';
3590
- selections.setAttribute(DATA_VF_IGNORE, 'true');
3591
-
3592
- const undoButton = document.createElement('button');
3593
- undoButton.type = 'button';
3594
- undoButton.className = 'vf-markdown-editor__history';
3595
- undoButton.setAttribute(DATA_VF_IGNORE, 'true');
3596
- undoButton.setAttribute('title', 'Undo');
3597
- undoButton.textContent = 'Undo';
3598
- undoButton.addEventListener('click', function() {
3599
- if (!markdownLexicalApi || !markdownLexicalApi.lexicalModule) {
3600
- return;
3601
- }
3602
- applyMarkdownHistoryCommand(markdownLexicalApi.lexicalModule.UNDO_COMMAND);
3603
- });
3604
-
3605
- const redoButton = document.createElement('button');
3606
- redoButton.type = 'button';
3607
- redoButton.className = 'vf-markdown-editor__history';
3608
- redoButton.setAttribute(DATA_VF_IGNORE, 'true');
3609
- redoButton.setAttribute('title', 'Redo');
3610
- redoButton.textContent = 'Redo';
3611
- redoButton.addEventListener('click', function() {
3612
- if (!markdownLexicalApi || !markdownLexicalApi.lexicalModule) {
3613
- return;
3614
- }
3615
- applyMarkdownHistoryCommand(markdownLexicalApi.lexicalModule.REDO_COMMAND);
3616
- });
3617
-
3618
- const openStudioButton = document.createElement('button');
3619
- openStudioButton.type = 'button';
3620
- openStudioButton.className = 'vf-markdown-editor__history';
3621
- openStudioButton.setAttribute(DATA_VF_IGNORE, 'true');
3622
- openStudioButton.setAttribute('title', 'Open file in Studio');
3623
- openStudioButton.textContent = 'Open';
3624
- openStudioButton.addEventListener('click', function() {
3625
- openMarkdownSourceInStudio(1);
3626
- });
3627
-
3628
- const exitButton = document.createElement('button');
3629
- exitButton.type = 'button';
3630
- exitButton.className = 'vf-markdown-editor__exit';
3631
- exitButton.setAttribute(DATA_VF_IGNORE, 'true');
3632
- exitButton.textContent = 'Done';
3633
- exitButton.addEventListener('click', function() {
3634
- setMarkdownEditMode(false);
3635
- });
3636
-
3637
- actions.appendChild(status);
3638
- actions.appendChild(presence);
3639
- actions.appendChild(selections);
3640
- actions.appendChild(undoButton);
3641
- actions.appendChild(redoButton);
3642
- actions.appendChild(openStudioButton);
3643
- actions.appendChild(exitButton);
3644
-
3645
- toolbar.appendChild(title);
3646
- toolbar.appendChild(actions);
3647
-
3648
- const mdxBlocks = document.createElement('div');
3649
- mdxBlocks.className = 'vf-markdown-editor__mdx-blocks';
3650
- mdxBlocks.setAttribute(DATA_VF_IGNORE, 'true');
3651
-
3652
- const surface = document.createElement('div');
3653
- surface.className = 'vf-markdown-editor__surface markdown-body';
3654
- surface.setAttribute(DATA_VF_IGNORE, 'true');
3655
- surface.setAttribute('contenteditable', 'true');
3656
- surface.setAttribute('aria-label', 'Markdown editor');
3657
- surface.addEventListener('keyup', scheduleMarkdownSelectionSync);
3658
- surface.addEventListener('mouseup', scheduleMarkdownSelectionSync);
3659
- surface.addEventListener('input', function() {
3660
- scheduleMarkdownSelectionSync();
3661
- scheduleMarkdownSlashMenuUpdate();
3662
- });
3663
- surface.addEventListener('keyup', scheduleMarkdownSlashMenuUpdate);
3664
- surface.addEventListener('mouseup', scheduleMarkdownSlashMenuUpdate);
3665
- surface.addEventListener('keydown', handleMarkdownSlashMenuKeydown);
3666
- surface.addEventListener('keydown', function(event) {
3667
- if (!event.shiftKey || !event.altKey) {
3668
- return;
3669
- }
3670
- if (event.key === 'ArrowUp') {
3671
- const moved = moveMarkdownCurrentBlockByDelta(-1);
3672
- if (moved) {
3673
- event.preventDefault();
3674
- }
3675
- return;
3676
- }
3677
- if (event.key === 'ArrowDown') {
3678
- const moved = moveMarkdownCurrentBlockByDelta(1);
3679
- if (moved) {
3680
- event.preventDefault();
3681
- }
3682
- }
3683
- });
3684
- surface.addEventListener('keydown', function(event) {
3685
- if ((event.metaKey || event.ctrlKey) && event.key === 's') {
3686
- event.preventDefault();
3687
- saveMarkdownContent();
3688
- }
3689
- });
3690
- surface.addEventListener('scroll', scheduleMarkdownSelectionOverlayRender);
3691
- surface.addEventListener('scroll', scheduleMarkdownSlashMenuUpdate);
3692
- surface.addEventListener('keyup', scheduleMarkdownInlineToolbarUpdate);
3693
- surface.addEventListener('mouseup', scheduleMarkdownInlineToolbarUpdate);
3694
- surface.addEventListener('input', scheduleMarkdownInlineToolbarUpdate);
3695
- surface.addEventListener('scroll', scheduleMarkdownInlineToolbarUpdate);
3696
- surface.addEventListener('scroll', refreshMarkdownBlockDragHandlePosition);
3697
-
3698
- const surfaceWrap = document.createElement('div');
3699
- surfaceWrap.className = 'vf-markdown-editor__surface-wrap';
3700
- surfaceWrap.setAttribute(DATA_VF_IGNORE, 'true');
3701
-
3702
- const selectionOverlay = document.createElement('div');
3703
- selectionOverlay.className = 'vf-markdown-editor__selection-overlay';
3704
- selectionOverlay.setAttribute(DATA_VF_IGNORE, 'true');
3705
-
3706
- const slashMenu = document.createElement('div');
3707
- slashMenu.className = 'vf-markdown-editor__slash-menu';
3708
- slashMenu.setAttribute(DATA_VF_IGNORE, 'true');
3709
-
3710
- const inlineToolbar = document.createElement('div');
3711
- inlineToolbar.className = 'vf-markdown-editor__inline-toolbar';
3712
- inlineToolbar.setAttribute(DATA_VF_IGNORE, 'true');
3713
-
3714
- const boldButton = document.createElement('button');
3715
- boldButton.type = 'button';
3716
- boldButton.className = 'vf-markdown-editor__inline-button';
3717
- boldButton.setAttribute(DATA_VF_IGNORE, 'true');
3718
- boldButton.textContent = 'B';
3719
- boldButton.addEventListener('mousedown', function(event) {
3720
- event.preventDefault();
3721
- });
3722
- boldButton.addEventListener('click', function(event) {
3723
- event.preventDefault();
3724
- toggleMarkdownInlineFormat('bold');
3725
- });
3726
-
3727
- const italicButton = document.createElement('button');
3728
- italicButton.type = 'button';
3729
- italicButton.className = 'vf-markdown-editor__inline-button';
3730
- italicButton.setAttribute(DATA_VF_IGNORE, 'true');
3731
- italicButton.textContent = 'I';
3732
- italicButton.addEventListener('mousedown', function(event) {
3733
- event.preventDefault();
3734
- });
3735
- italicButton.addEventListener('click', function(event) {
3736
- event.preventDefault();
3737
- toggleMarkdownInlineFormat('italic');
3738
- });
3739
-
3740
- const codeButton = document.createElement('button');
3741
- codeButton.type = 'button';
3742
- codeButton.className = 'vf-markdown-editor__inline-button';
3743
- codeButton.setAttribute(DATA_VF_IGNORE, 'true');
3744
- codeButton.textContent = '</>';
3745
- codeButton.addEventListener('mousedown', function(event) {
3746
- event.preventDefault();
3747
- });
3748
- codeButton.addEventListener('click', function(event) {
3749
- event.preventDefault();
3750
- toggleMarkdownInlineFormat('code');
3751
- });
3752
-
3753
- inlineToolbar.appendChild(boldButton);
3754
- inlineToolbar.appendChild(italicButton);
3755
- inlineToolbar.appendChild(codeButton);
3756
-
3757
- const blockDragHandle = document.createElement('button');
3758
- blockDragHandle.type = 'button';
3759
- blockDragHandle.className = 'vf-markdown-editor__block-handle';
3760
- blockDragHandle.setAttribute(DATA_VF_IGNORE, 'true');
3761
- blockDragHandle.textContent = '::';
3762
- blockDragHandle.draggable = true;
3763
- blockDragHandle.setAttribute('data-dragging', 'false');
3764
- blockDragHandle.addEventListener('dragstart', function(event) {
3765
- const indexText = blockDragHandle.getAttribute('data-block-index');
3766
- const index = Number(indexText);
3767
- if (!Number.isInteger(index)) {
3768
- event.preventDefault();
3769
- return;
3770
- }
3771
- markdownBlockDragSourceIndex = index;
3772
- markdownBlockDragActive = true;
3773
- blockDragHandle.setAttribute('data-dragging', 'true');
3774
- if (event.dataTransfer) {
3775
- event.dataTransfer.effectAllowed = 'move';
3776
- event.dataTransfer.setData('text/plain', String(index));
3777
-
3778
- const blocks = getMarkdownTopLevelBlocks();
3779
- const block = blocks[index];
3780
- removeMarkdownDragGhost();
3781
- if (block) {
3782
- const ghost = createMarkdownDragGhost(block);
3783
- document.body.appendChild(ghost);
3784
- markdownBlockDragGhost = ghost;
3785
- event.dataTransfer.setDragImage(ghost, 14, 14);
3786
- }
3787
- }
3788
- showMarkdownBlockDropIndicator(index);
3789
- });
3790
- blockDragHandle.addEventListener('mouseenter', function() {
3791
- if (markdownBlockHandleHoverIndex >= 0) {
3792
- blockDragHandle.style.display = 'block';
3793
- }
3794
- });
3795
- blockDragHandle.addEventListener('mouseleave', function(event) {
3796
- if (markdownBlockDragActive) {
3797
- return;
3798
- }
3799
- const next = event.relatedTarget;
3800
- if (next && markdownEditorSurface && markdownEditorSurface.contains(next)) {
3801
- return;
3802
- }
3803
- hideMarkdownBlockDragHandle();
3804
- });
3805
- blockDragHandle.addEventListener('dragend', function() {
3806
- hideMarkdownBlockDragUi();
3807
- });
3808
-
3809
- const blockDropIndicator = document.createElement('div');
3810
- blockDropIndicator.className = 'vf-markdown-editor__block-drop-indicator';
3811
- blockDropIndicator.setAttribute(DATA_VF_IGNORE, 'true');
3812
-
3813
- const blockDropLabel = document.createElement('div');
3814
- blockDropLabel.className = 'vf-markdown-editor__block-drop-label';
3815
- blockDropLabel.setAttribute(DATA_VF_IGNORE, 'true');
3816
-
3817
- surfaceWrap.appendChild(surface);
3818
- surfaceWrap.appendChild(selectionOverlay);
3819
-
3820
- const textarea = document.createElement('textarea');
3821
- textarea.className = 'vf-markdown-editor__textarea';
3822
- textarea.setAttribute(DATA_VF_IGNORE, 'true');
3823
- textarea.setAttribute('aria-label', 'Markdown editor');
3824
- textarea.spellcheck = false;
3825
- textarea.addEventListener('input', function() {
3826
- handleMarkdownLocalChange(textarea.value);
3827
- scheduleMarkdownSelectionSync();
3828
- hideMarkdownSlashMenu();
3829
- });
3830
- textarea.addEventListener('select', scheduleMarkdownSelectionSync);
3831
- textarea.addEventListener('keyup', scheduleMarkdownSelectionSync);
3832
- textarea.addEventListener('click', scheduleMarkdownSelectionSync);
3833
- textarea.addEventListener('input', clearMarkdownSelectionOverlay);
3834
- textarea.addEventListener('keydown', function() {
3835
- hideMarkdownSlashMenu();
3836
- hideMarkdownInlineToolbar();
3837
- hideMarkdownBlockDragUi();
3838
- });
3839
-
3840
- surface.addEventListener('mousemove', function(event) {
3841
- if (markdownBlockDragActive) {
3842
- return;
3843
- }
3844
-
3845
- const index = getMarkdownBlockHoverIndexFromPointer(event.target, event.clientX, event.clientY);
3846
- if (index < 0) {
3847
- hideMarkdownBlockDragHandle();
3848
- return;
3849
- }
3850
- const blocks = getMarkdownTopLevelBlocks();
3851
- const block = blocks[index];
3852
- if (!block) {
3853
- hideMarkdownBlockDragHandle();
3854
- return;
3855
- }
3856
- positionMarkdownBlockDragHandle(block, index);
3857
- });
3858
-
3859
- surface.addEventListener('mouseleave', function(event) {
3860
- if (!markdownBlockDragActive) {
3861
- const next = event.relatedTarget;
3862
- if (next && markdownBlockDragHandle && (next === markdownBlockDragHandle || markdownBlockDragHandle.contains(next))) {
3863
- return;
3864
- }
3865
- hideMarkdownBlockDragHandle();
3866
- }
3867
- });
3868
-
3869
- surface.addEventListener('dragover', function(event) {
3870
- if (!markdownBlockDragActive) {
3871
- return;
3872
- }
3873
- event.preventDefault();
3874
- if (event.dataTransfer) {
3875
- event.dataTransfer.dropEffect = 'move';
3876
- }
3877
- autoScrollMarkdownSurfaceDuringDrag(event.clientY);
3878
- const slotIndex = getMarkdownDropSlotIndexFromPointer(event.clientY);
3879
- if (slotIndex >= 0) {
3880
- showMarkdownBlockDropIndicator(slotIndex);
3881
- }
3882
- });
3883
-
3884
- surface.addEventListener('drop', function(event) {
3885
- if (!markdownBlockDragActive) {
3886
- return;
3887
- }
3888
- event.preventDefault();
3889
-
3890
- const fallbackSlot = getMarkdownDropSlotIndexFromPointer(event.clientY);
3891
- const slotIndex = markdownBlockDropSlotIndex >= 0 ? markdownBlockDropSlotIndex : fallbackSlot;
3892
- const sourceIndex = markdownBlockDragSourceIndex;
3893
- hideMarkdownBlockDragUi();
3894
- if (sourceIndex < 0 || slotIndex < 0) {
3895
- return;
3896
- }
3897
- moveMarkdownLexicalBlock(sourceIndex, slotIndex);
3898
- });
3899
-
3900
- document.addEventListener('selectionchange', function() {
3901
- if (!markdownEditorRoot || markdownEditorRoot.style.display !== 'block') {
3902
- return;
3903
- }
3904
- scheduleMarkdownSelectionSync();
3905
- scheduleMarkdownSelectionOverlayRender();
3906
- scheduleMarkdownSlashMenuUpdate();
3907
- scheduleMarkdownInlineToolbarUpdate();
3908
- });
3909
-
3910
- window.addEventListener('resize', scheduleMarkdownSelectionOverlayRender);
3911
- window.addEventListener('resize', scheduleMarkdownSlashMenuUpdate);
3912
- window.addEventListener('resize', scheduleMarkdownInlineToolbarUpdate);
3913
- window.addEventListener('resize', hideMarkdownBlockDragUi);
3914
-
3915
- editorRoot.appendChild(toolbar);
3916
- editorRoot.appendChild(mdxBlocks);
3917
- editorRoot.appendChild(surfaceWrap);
3918
- editorRoot.appendChild(textarea);
3919
- editorRoot.appendChild(slashMenu);
3920
- editorRoot.appendChild(inlineToolbar);
3921
- editorRoot.appendChild(blockDragHandle);
3922
- editorRoot.appendChild(blockDropIndicator);
3923
- editorRoot.appendChild(blockDropLabel);
3924
- document.body.appendChild(editorRoot);
3925
-
3926
- markdownEditorRoot = editorRoot;
3927
- markdownEditorSurface = surface;
3928
- markdownEditorTextarea = textarea;
3929
- markdownPersistStatus = status;
3930
- markdownPresenceRoot = presence;
3931
- markdownSelectionsRoot = selections;
3932
- markdownMdxBlocksRoot = mdxBlocks;
3933
- markdownSelectionOverlayRoot = selectionOverlay;
3934
- markdownSlashMenuRoot = slashMenu;
3935
- markdownInlineToolbarRoot = inlineToolbar;
3936
- markdownBlockDragHandle = blockDragHandle;
3937
- markdownBlockDropIndicator = blockDropIndicator;
3938
- markdownBlockDropLabel = blockDropLabel;
3939
- setMarkdownMdxBlocks(markdownLatestMdxBlocks);
3940
- setMarkdownPresence(markdownLatestPresenceUsers);
3941
- setMarkdownSelections(markdownLatestSelections);
3942
- setupMarkdownLexicalEditor();
3943
- applyMarkdownContent(markdownCurrentContent);
3944
-
3945
- return editorRoot;
3946
- }
3947
-
3948
- function setMarkdownEditMode(enabled) {
3949
- const markdownBody = document.getElementById('markdown-body');
3950
- if (!markdownBody || !isMarkdownPage()) {
3951
- return;
3952
- }
3953
-
3954
- if (enabled) {
3955
- ensureMarkdownEditor();
3956
- setupMarkdownLexicalEditor();
3957
- markdownBody.style.display = 'none';
3958
- markdownEditorRoot.style.display = 'block';
3959
- markdownHasUnsavedChanges = false;
3960
- focusMarkdownEditor();
3961
- scheduleMarkdownSelectionSync();
3962
- scheduleMarkdownSelectionOverlayRender();
3963
- scheduleMarkdownSlashMenuUpdate();
3964
- scheduleMarkdownInlineToolbarUpdate();
3965
- postMarkdownEditorReady();
3966
-
3967
- // Self-connect to Yjs when server-injected config is available
3968
- if (WS_URL && YJS_GUID && !markdownYDoc) {
3969
- setupMarkdownYjsConnection({
3970
- wsUrl: WS_URL,
3971
- guid: YJS_GUID,
3972
- fileId: markdownFileId,
3973
- });
3974
- }
3975
- } else {
3976
- markdownBody.style.display = '';
3977
- if (markdownEditorRoot) {
3978
- markdownEditorRoot.style.display = 'none';
3979
- }
3980
- hideMarkdownSlashMenu();
3981
- hideMarkdownInlineToolbar();
3982
- hideMarkdownBlockDragUi();
3983
- markdownOverlaySelections = [];
3984
- clearMarkdownSelectionOverlay();
3985
- clearMarkdownSelectionSync();
3986
- disposeMarkdownYjs();
3987
- }
3988
-
3989
- const nextUrl = new URL(window.location.href);
3990
- if (enabled) {
3991
- nextUrl.searchParams.set('edit', 'true');
3992
- } else {
3993
- nextUrl.searchParams.delete('edit');
3994
- }
3995
- window.history.replaceState(window.history.state, '', nextUrl.toString());
3996
- }
3997
-
3998
- function ensureMarkdownEditButton() {
3999
- if (markdownEditButton || !isMarkdownPage()) {
4000
- return;
4001
- }
4002
-
4003
- const button = document.createElement('button');
4004
- button.type = 'button';
4005
- button.className = 'vf-markdown-edit-button';
4006
- button.textContent = 'Edit';
4007
- button.setAttribute(DATA_VF_IGNORE, 'true');
4008
- button.addEventListener('click', function() {
4009
- setMarkdownEditMode(true);
4010
- });
4011
-
4012
- document.body.appendChild(button);
4013
- markdownEditButton = button;
4014
- }
4015
-
4016
- function setupMarkdownEditor(params) {
4017
- if (!isMarkdownPage()) {
4018
- return;
4019
- }
4020
-
4021
- markdownFileId = params.get('vf_file_id') || PAGE_ID || null;
4022
- ensureMarkdownEditButton();
4023
-
4024
- if (params.get('edit') === 'true') {
4025
- setMarkdownEditMode(true);
4026
- }
4027
- }
4028
-
4029
- let html2canvasLoaded = false;
4030
- let html2canvasPromise = null;
4031
-
4032
- function loadHtml2Canvas() {
4033
- if (html2canvasLoaded) return Promise.resolve();
4034
- if (html2canvasPromise) return html2canvasPromise;
4035
-
4036
- html2canvasPromise = new Promise((resolve, reject) => {
4037
- const script = document.createElement('script');
4038
- script.src = 'https://cdn.jsdelivr.net/npm/html2canvas-pro@2.0.0/dist/html2canvas-pro.min.js';
4039
- script.onload = () => {
4040
- html2canvasLoaded = true;
4041
- resolve();
4042
- };
4043
- script.onerror = (event) => {
4044
- console.warn(
4045
- '[StudioBridge] Failed to load html2canvas script. This may be caused by CSP script-src restrictions.',
4046
- event
4047
- );
4048
- reject(new Error('Failed to load html2canvas script'));
4049
- };
4050
- try {
4051
- document.head.appendChild(script);
4052
- } catch (error) {
4053
- console.warn(
4054
- '[StudioBridge] Failed to append html2canvas script element. This may be caused by CSP script-src restrictions.',
4055
- error
4056
- );
4057
- reject(error instanceof Error ? error : new Error('Failed to append html2canvas script element'));
4058
- }
4059
- });
4060
-
4061
- return html2canvasPromise;
4062
- }
4063
-
4064
- async function captureScreenshot(options) {
4065
- const { scrollTo, fullPage, quality = 0.8 } = options || {};
4066
- const originalScrollY = window.scrollY;
4067
-
4068
- try {
4069
- await loadHtml2Canvas();
4070
-
4071
- if (typeof scrollTo === 'number') {
4072
- window.scrollTo(0, scrollTo);
4073
- await new Promise(r => setTimeout(r, 150));
4074
- }
4075
-
4076
- const canvasOptions = {
4077
- useCORS: true,
4078
- logging: false,
4079
- scale: window.devicePixelRatio || 1
4080
- };
4081
-
4082
- if (fullPage) {
4083
- canvasOptions.height = document.documentElement.scrollHeight;
4084
- canvasOptions.windowHeight = document.documentElement.scrollHeight;
4085
- canvasOptions.y = 0;
4086
- window.scrollTo(0, 0);
4087
- await new Promise(r => setTimeout(r, 100));
4088
- }
4089
-
4090
- const html2canvasFn = window.html2canvas.default || window.html2canvas;
4091
- const canvas = await html2canvasFn(document.body, canvasOptions);
4092
-
4093
- if (!canvas || canvas.width === 0 || canvas.height === 0) {
4094
- console.error('[bridge] html2canvas produced empty canvas:', canvas?.width, 'x', canvas?.height);
4095
- window.scrollTo(0, originalScrollY);
4096
- return {
4097
- success: false,
4098
- error: 'html2canvas produced empty canvas (0x0 dimensions)'
4099
- };
4100
- }
4101
-
4102
- const dataUrl = canvas.toDataURL('image/png', quality);
4103
-
4104
- if (!dataUrl || !dataUrl.startsWith('data:image/') || dataUrl.length < 100) {
4105
- console.error('[bridge] html2canvas produced invalid data URL:', dataUrl?.substring(0, 50));
4106
- window.scrollTo(0, originalScrollY);
4107
- return {
4108
- success: false,
4109
- error: 'html2canvas produced invalid image data'
4110
- };
4111
- }
4112
-
4113
- window.scrollTo(0, originalScrollY);
4114
-
4115
- return {
4116
- success: true,
4117
- data: dataUrl,
4118
- width: canvas.width,
4119
- height: canvas.height,
4120
- scrollY: window.scrollY,
4121
- totalHeight: document.documentElement.scrollHeight,
4122
- viewportHeight: window.innerHeight,
4123
- url: window.location.href
4124
- };
4125
- } catch (error) {
4126
- console.error('[bridge] html2canvas error:', error);
4127
- window.scrollTo(0, originalScrollY);
4128
- return {
4129
- success: false,
4130
- error: error.message || String(error)
4131
- };
4132
- }
4133
- }
4134
-
4135
- async function captureMultipleSections(sectionCount) {
4136
- const originalScrollY = window.scrollY;
4137
- const results = [];
4138
- const totalHeight = document.documentElement.scrollHeight;
4139
- const viewportHeight = window.innerHeight;
4140
- const sections = sectionCount || Math.ceil(totalHeight / viewportHeight);
4141
-
4142
- try {
4143
- for (let i = 0; i < sections; i++) {
4144
- const scrollY = Math.min(i * viewportHeight, totalHeight - viewportHeight);
4145
- const result = await captureScreenshot({ scrollTo: scrollY });
4146
- if (result.success) {
4147
- results.push({ ...result, section: i + 1, totalSections: sections });
4148
- }
4149
- }
4150
- } finally {
4151
- window.scrollTo(0, originalScrollY);
4152
- }
4153
-
4154
- return results;
4155
- }
4156
-
4157
- function handleStudioMessage(event) {
4158
- if (!isFromStudio(event)) return;
4159
-
4160
- const message = event.data;
4161
- if (!message?.action) return;
4162
-
4163
- switch (message.action) {
4164
- case 'routeChange':
4165
- if (message.url) {
4166
- postToStudio({ action: 'onPageTransitionStart', url: message.url, projectId: PROJECT_ID });
4167
- window.location.href = message.url;
4168
- }
4169
- return;
4170
-
4171
- case 'reload':
4172
- window.location.reload();
4173
- return;
4174
-
4175
- case 'goBack':
4176
- window.history.back();
4177
- return;
4178
-
4179
- case 'goForward':
4180
- window.history.forward();
4181
- return;
4182
-
4183
- case 'colorMode':
4184
- setColorMode(message.value);
4185
- return;
4186
-
4187
- case 'toggleInspectMode':
4188
- inspectMode = message.value;
4189
- if (inspectMode) return;
4190
-
4191
- hideOverlay(hoverOverlay);
4192
- hoveredNodeId = null;
4193
-
4194
- if (!message.deselectElements) return;
4195
-
4196
- hideOverlay(selectionOverlay);
4197
- selectedNodeId = null;
4198
- return;
4199
-
4200
- case 'setSelectedNode':
4201
- selectedNodeId = message.id;
4202
- showSelectionOverlay(message.id);
4203
- if (message.scroll) scrollToElement(message.id);
4204
- return;
4205
-
4206
- case 'setHoveredNode':
4207
- if (!inspectMode) showHoverOverlay(message.id);
4208
- return;
4209
-
4210
- case 'setMarkdownPersistState':
4211
- if (!isMarkdownPage()) {
4212
- return;
4213
- }
4214
- if (message.fileId && markdownFileId && message.fileId !== markdownFileId) {
4215
- return;
4216
- }
4217
- if (markdownSaveInProgress) {
4218
- setMarkdownPersistStatus(message.status || 'saved');
4219
- if (message.status === 'saved' || message.status === 'error') {
4220
- markdownSaveInProgress = false;
4221
- markdownHasUnsavedChanges = false;
4222
- }
4223
- }
4224
- return;
4225
-
4226
- case 'screenshot':
4227
- (async function() {
4228
- if (message.multipleSections) {
4229
- const results = await captureMultipleSections(message.sectionCount);
4230
- postToStudio({
4231
- action: 'screenshotResult',
4232
- requestId: message.requestId,
4233
- multiple: true,
4234
- results: results
4235
- });
4236
- return;
4237
- }
4238
-
4239
- const result = await captureScreenshot(message.options);
4240
- postToStudio({
4241
- action: 'screenshotResult',
4242
- requestId: message.requestId,
4243
- multiple: false,
4244
- ...result
4245
- });
4246
- })();
4247
- return;
4248
-
4249
- default:
4250
- console.debug('[StudioBridge] Unknown action:', message.action);
4251
- return;
4252
- }
4253
- }
4254
-
4255
- function notifyAppLoaded() {
4256
- postToStudio({ action: 'appLoaded', url: window.location.href });
4257
-
4258
- postToStudio({
4259
- action: 'appUpdated',
4260
- url: window.location.href,
4261
- id: PAGE_ID,
4262
- isInitialLoad: true,
4263
- errors: [],
4264
- warnings: []
4265
- });
4266
-
4267
- postToStudio({
4268
- action: 'onPageTransitionEnd',
4269
- url: window.location.href,
4270
- projectId: PROJECT_ID,
4271
- id: PAGE_ID,
4272
- params: {}
4273
- });
4274
- }
4275
-
4276
- function notifyAppUnloaded() {
4277
- postToStudio({ action: 'appUnloaded', url: window.location.href });
4278
- }
4279
-
4280
- function init() {
4281
- const params = new URLSearchParams(window.location.search);
4282
- const studioEmbed = params.get('studio_embed') === 'true';
4283
- const isStandalone = window.parent === window && !studioEmbed;
4284
-
4285
- if (isStandalone) {
4286
- // Allow standalone markdown editing when WS_URL is available (server-injected Yjs config)
4287
- if (!WS_URL) {
4288
- console.debug('[StudioBridge] Not in iframe and not studio_embed mode, skipping initialization');
4289
- return;
4290
- }
4291
- }
4292
-
4293
- console.debug('[StudioBridge] Initializing...');
4294
-
4295
- // Only set up Studio interaction features when embedded in Studio
4296
- if (!isStandalone) {
4297
- injectOverlayStyles();
4298
- hoverOverlay = createOverlay('hover');
4299
- selectionOverlay = createOverlay('selection');
4300
-
4301
- setupConsoleCapture();
4302
- setupErrorHandling();
4303
- setupInspectMode();
4304
- }
4305
-
4306
- setupMarkdownEditor(params);
4307
-
4308
- window.addEventListener('message', handleStudioMessage);
4309
-
4310
- if (!isStandalone) {
4311
- // IMPORTANT: notifyAppLoaded() must be called BEFORE setupMutationObserver()
4312
- // because notifyAppLoaded sends onPageTransitionEnd which sets previewId,
4313
- // and treeUpdated (from setupMutationObserver) requires previewId to be set
4314
- if (document.readyState === 'loading') {
4315
- document.addEventListener('DOMContentLoaded', function() {
4316
- notifyAppLoaded();
4317
- setupMutationObserver();
4318
- });
4319
- } else {
4320
- notifyAppLoaded();
4321
- setupMutationObserver();
4322
- }
4323
-
4324
- window.addEventListener('beforeunload', notifyAppUnloaded);
4325
- }
4326
-
4327
- const colorMode = params.get('color_mode');
4328
- if (colorMode) setColorMode(colorMode);
4329
-
4330
- if (!isStandalone) {
4331
- const inspectModeParam = params.get('inspect_mode');
4332
- if (inspectModeParam === 'true') {
4333
- inspectMode = true;
4334
- console.debug('[StudioBridge] Inspect mode enabled from query param');
4335
- }
4336
- }
4337
-
4338
- console.debug('[StudioBridge] Initialized successfully');
4339
- }
4340
-
4341
- if (DEBUG_EXPOSE_INTERNALS && typeof window !== 'undefined') {
4342
- window.__VF_STUDIO_BRIDGE_DEBUG = {
4343
- parseMdxImportMap: parseMdxImportMap,
4344
- extractRawBlocksForEditor: extractRawBlocksForEditor,
4345
- getMdxBlockOpenUiState: getMdxBlockOpenUiState
4346
- };
4347
- }
4348
-
4349
- if (!DEBUG_SKIP_INIT) {
4350
- init();
4351
- }
4352
- })();`;
4353
- }