@mdzip/editor 1.3.1 → 1.3.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +200 -4
- package/dist/archive-utils.d.ts.map +1 -1
- package/dist/archive-utils.js +1 -34
- package/dist/archive-utils.js.map +1 -1
- package/dist/asset-cache.d.ts +92 -0
- package/dist/asset-cache.d.ts.map +1 -0
- package/dist/asset-cache.js +392 -0
- package/dist/asset-cache.js.map +1 -0
- package/dist/diff-view-css.d.ts +2 -0
- package/dist/diff-view-css.d.ts.map +1 -0
- package/dist/diff-view-css.js +49 -0
- package/dist/diff-view-css.js.map +1 -0
- package/dist/diff-view.d.ts +112 -0
- package/dist/diff-view.d.ts.map +1 -0
- package/dist/diff-view.js +573 -0
- package/dist/diff-view.js.map +1 -0
- package/dist/diff.d.ts +2 -1
- package/dist/diff.d.ts.map +1 -1
- package/dist/diff.js +3 -1
- package/dist/diff.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/library-info.d.ts +37 -0
- package/dist/library-info.d.ts.map +1 -0
- package/dist/library-info.js +46 -0
- package/dist/library-info.js.map +1 -0
- package/dist/mermaid.d.ts +46 -0
- package/dist/mermaid.d.ts.map +1 -0
- package/dist/mermaid.js +107 -0
- package/dist/mermaid.js.map +1 -0
- package/dist/preview.d.ts +10 -0
- package/dist/preview.d.ts.map +1 -0
- package/dist/preview.js +9 -0
- package/dist/preview.js.map +1 -0
- package/dist/rendering.d.ts +175 -2
- package/dist/rendering.d.ts.map +1 -1
- package/dist/rendering.js +132 -14
- package/dist/rendering.js.map +1 -1
- package/dist/view-css.d.ts.map +1 -1
- package/dist/view-css.js +133 -1
- package/dist/view-css.js.map +1 -1
- package/dist/view.d.ts +148 -1
- package/dist/view.d.ts.map +1 -1
- package/dist/view.js +805 -22
- package/dist/view.js.map +1 -1
- package/dist/workspace.d.ts +15 -0
- package/dist/workspace.d.ts.map +1 -1
- package/dist/workspace.js +42 -4
- package/dist/workspace.js.map +1 -1
- package/package.json +26 -4
package/dist/view.js
CHANGED
|
@@ -6,13 +6,19 @@ import { EditorView, dropCursor, keymap, lineNumbers } from '@codemirror/view';
|
|
|
6
6
|
import { tags } from '@lezer/highlight';
|
|
7
7
|
import { Bold, ChevronDown, Code, Columns2, Eye, File, FileBraces, FileImage, Folder, FolderOpen, Hash, Heading1, ImagePlus, Info, Italic, Link2Off, Link, List, ListOrdered, Moon, PanelLeft, Quote, Save, SquarePen, Strikethrough, Sun, ZoomIn } from 'lucide';
|
|
8
8
|
import { browserClipboardHasImage, readBrowserClipboardImage } from './browser.js';
|
|
9
|
+
import { MdzipAssetSession, mdzipArchiveSourceId } from './asset-cache.js';
|
|
9
10
|
import { MD_MARKDOWN_ICON } from './icons/md-markdown.js';
|
|
11
|
+
import { MDZIP_RUNTIME_LIBRARIES } from './library-info.js';
|
|
10
12
|
import { MdzipWorkspaceService, extensionForMime, normalizeArchivePath, relativeArchivePath } from './workspace.js';
|
|
11
13
|
import { buildMdzipNavTree, canEditMdzipPath, escapeHtml, isOrphanedMdzipAsset, mdzipEntryIconKind, isMdzipManifestPath, resolveMdzipArchiveLinkTarget, renderMdzipPreviewHtml } from './workspace-view.js';
|
|
14
|
+
import { MdzipRenderingService, defaultSafeMarkdownRenderer } from './rendering.js';
|
|
12
15
|
import { WORKSPACE_CSS } from './view-css.js';
|
|
13
16
|
const STYLE_ATTR = 'data-mdzip-ws-styles';
|
|
14
17
|
const IMAGE_EXTENSIONS = /\.(jpg|jpeg|png|gif|webp|svg|bmp|ico|tiff?)$/i;
|
|
15
18
|
const isImageFile = (path) => IMAGE_EXTENSIONS.test(path);
|
|
19
|
+
function isThenable(value) {
|
|
20
|
+
return typeof value?.then === 'function';
|
|
21
|
+
}
|
|
16
22
|
const NAV_ICON_CLASS = 'nav-lucide-icon';
|
|
17
23
|
const TOOLBAR_ICON_CLASS = 'toggle-icon';
|
|
18
24
|
const MANIFEST_ICON_HTML = lucideIcon(FileBraces, NAV_ICON_CLASS);
|
|
@@ -396,6 +402,7 @@ function navNodeOrder(a, b) {
|
|
|
396
402
|
export class MdzipWorkspaceView {
|
|
397
403
|
constructor(container, options = {}) {
|
|
398
404
|
this.workspace = null;
|
|
405
|
+
this.assetSession = null;
|
|
399
406
|
this.unsub = null;
|
|
400
407
|
this.layout = 'split';
|
|
401
408
|
this.navVisible = true;
|
|
@@ -414,6 +421,12 @@ export class MdzipWorkspaceView {
|
|
|
414
421
|
this.pendingNewFolders = new Set();
|
|
415
422
|
this.pendingReplacePath = null;
|
|
416
423
|
this.conversionHookPending = false;
|
|
424
|
+
this.conversionDocumentGeneration = 0;
|
|
425
|
+
// Monotonic token bumped on every openArchive/openWorkspace call. The async
|
|
426
|
+
// parse for a superseded call must not win the race and overwrite the latest
|
|
427
|
+
// input, so each call captures its token and discards its result once a newer
|
|
428
|
+
// open has started.
|
|
429
|
+
this.openGeneration = 0;
|
|
417
430
|
this.dragSourcePath = null;
|
|
418
431
|
this.dragOverElement = null;
|
|
419
432
|
this.tooltipState = null;
|
|
@@ -423,6 +436,25 @@ export class MdzipWorkspaceView {
|
|
|
423
436
|
this.readOnlyCompartment = new Compartment();
|
|
424
437
|
this.updatingCm = false;
|
|
425
438
|
this.syncing = false;
|
|
439
|
+
this.markdownExtensions = [];
|
|
440
|
+
this.entryRenderers = [];
|
|
441
|
+
this.renderingService = new MdzipRenderingService();
|
|
442
|
+
// Preview render memo: the preview pipeline only re-runs when one of these
|
|
443
|
+
// inputs actually changed, so unrelated snapshot renders (dialogs, nav,
|
|
444
|
+
// layout toggles) never reset preview DOM or re-run extension mounts.
|
|
445
|
+
this.previewMemo = null;
|
|
446
|
+
this.previewGeneration = 0;
|
|
447
|
+
this.previewAbort = null;
|
|
448
|
+
this.previewHandles = [];
|
|
449
|
+
// Whether the latest preview generation has finished mounting and hydrating
|
|
450
|
+
// its images; drives `whenRendered()` and gates the hydration callback.
|
|
451
|
+
this.previewHydrated = false;
|
|
452
|
+
this.renderedWaiters = [];
|
|
453
|
+
this.entryState = null;
|
|
454
|
+
// Negative match cache: with only non-matching renderers registered,
|
|
455
|
+
// matches() does not re-run on every snapshot render of the same entry.
|
|
456
|
+
this.entryMatchMissKey = null;
|
|
457
|
+
this.entryGeneration = 0;
|
|
426
458
|
this.options = options;
|
|
427
459
|
this.controlPolicy = resolveMdzipControlPolicy(options.controls);
|
|
428
460
|
this.navigationMode = options.navigationMode ?? 'editor';
|
|
@@ -432,6 +464,10 @@ export class MdzipWorkspaceView {
|
|
|
432
464
|
? 'dark'
|
|
433
465
|
: 'light');
|
|
434
466
|
this.navVisible = options.navigationButtonActive ?? this.navVisible;
|
|
467
|
+
this.markdownRenderer = options.markdownRenderer;
|
|
468
|
+
this.markdownExtensions = options.markdownExtensions ?? [];
|
|
469
|
+
this.entryRenderers = options.entryRenderers ?? [];
|
|
470
|
+
this.renderingService = new MdzipRenderingService(this.markdownRenderer ?? defaultSafeMarkdownRenderer, this.markdownExtensions);
|
|
435
471
|
injectStyles(container.ownerDocument);
|
|
436
472
|
container.replaceChildren();
|
|
437
473
|
container.innerHTML = SHELL_HTML;
|
|
@@ -468,6 +504,7 @@ export class MdzipWorkspaceView {
|
|
|
468
504
|
this.elSplitResizer = q('[data-ref="split-resizer"]');
|
|
469
505
|
this.elPreviewPane = q('[data-ref="preview-pane"]');
|
|
470
506
|
this.elPreviewContent = q('[data-ref="preview-content"]');
|
|
507
|
+
this.elEntryPane = q('[data-ref="entry-pane"]');
|
|
471
508
|
this.elTitleDialog = q('[data-ref="title-dialog"]');
|
|
472
509
|
this.elTitleInput = q('[data-ref="title-input"]');
|
|
473
510
|
this.elTitleValidation = q('[data-ref="title-validation"]');
|
|
@@ -475,6 +512,7 @@ export class MdzipWorkspaceView {
|
|
|
475
512
|
this.elTitleResetBtn = q('[data-ref="title-reset-btn"]');
|
|
476
513
|
this.elMetadataDialog = q('[data-ref="metadata-dialog"]');
|
|
477
514
|
this.elMetadataList = q('[data-ref="metadata-list"]');
|
|
515
|
+
this.elLibraryList = q('[data-ref="library-list"]');
|
|
478
516
|
this.elConversionDialog = q('[data-ref="conversion-dialog"]');
|
|
479
517
|
this.elConversionConfirmBtn = q('[data-ref="conversion-confirm-btn"]');
|
|
480
518
|
this.elNavMenu = q('[data-ref="nav-menu"]');
|
|
@@ -504,20 +542,47 @@ export class MdzipWorkspaceView {
|
|
|
504
542
|
await this.openArchive(bytes, options);
|
|
505
543
|
}
|
|
506
544
|
async openArchive(bytes, options = {}) {
|
|
545
|
+
const generation = ++this.openGeneration;
|
|
507
546
|
this.unsub?.();
|
|
547
|
+
this.resetRenderingState();
|
|
508
548
|
this.cmEditor?.destroy();
|
|
509
549
|
this.cmEditor = null;
|
|
510
550
|
this.workspace = null;
|
|
551
|
+
this.replaceAssetSession(null);
|
|
552
|
+
this.conversionDocumentGeneration += 1;
|
|
511
553
|
try {
|
|
512
554
|
const ws = await MdzipWorkspaceService.open(bytes, options);
|
|
555
|
+
if (generation !== this.openGeneration) {
|
|
556
|
+
// A newer open() started while this parse was in flight; discard this
|
|
557
|
+
// stale result so the latest input remains authoritative.
|
|
558
|
+
return;
|
|
559
|
+
}
|
|
513
560
|
this.workspace = ws;
|
|
561
|
+
this.replaceAssetSession(new MdzipAssetSession(ws, ws.snapshot().workspace.assets, this.elRoot.ownerDocument, {
|
|
562
|
+
cache: this.options.assetCache,
|
|
563
|
+
sourceId: () => mdzipArchiveSourceId(bytes),
|
|
564
|
+
onFailed: this.options.onFailed
|
|
565
|
+
}));
|
|
514
566
|
const snap = ws.snapshot();
|
|
515
567
|
if (snap.sourceFormat === 'markdown') {
|
|
516
568
|
this.navVisible = false;
|
|
517
569
|
}
|
|
518
570
|
this.layout = this.validLayoutForSnapshot(this.options.initialLayout ?? defaultLayoutForPolicy(this.controlPolicy), snap);
|
|
519
|
-
|
|
571
|
+
await this.ensureCmEditor();
|
|
572
|
+
if (generation !== this.openGeneration) {
|
|
573
|
+
return;
|
|
574
|
+
}
|
|
520
575
|
this.unsub = ws.subscribe((event) => {
|
|
576
|
+
if (event.changes.includes('asset')) {
|
|
577
|
+
this.replaceAssetSession(new MdzipAssetSession(ws, event.snapshot.workspace.assets, this.elRoot.ownerDocument, {
|
|
578
|
+
cache: this.options.assetCache,
|
|
579
|
+
sourceId: () => mdzipArchiveSourceId(event.snapshot.archiveBytes),
|
|
580
|
+
onFailed: this.options.onFailed
|
|
581
|
+
}));
|
|
582
|
+
}
|
|
583
|
+
if (event.changes.includes('workspace') || event.changes.includes('document')) {
|
|
584
|
+
this.conversionDocumentGeneration += 1;
|
|
585
|
+
}
|
|
521
586
|
this.render();
|
|
522
587
|
void this.notifyChanged(event);
|
|
523
588
|
});
|
|
@@ -525,6 +590,9 @@ export class MdzipWorkspaceView {
|
|
|
525
590
|
void this.notifyChanged(this.initialWorkspaceEvent(ws.snapshot()));
|
|
526
591
|
}
|
|
527
592
|
catch (error) {
|
|
593
|
+
if (generation !== this.openGeneration) {
|
|
594
|
+
return;
|
|
595
|
+
}
|
|
528
596
|
this.options.onFailed?.(error);
|
|
529
597
|
}
|
|
530
598
|
}
|
|
@@ -555,17 +623,46 @@ export class MdzipWorkspaceView {
|
|
|
555
623
|
* `ERR_LAZY_TEXT_UNAVAILABLE` instead of silently producing an empty file.
|
|
556
624
|
*/
|
|
557
625
|
async openWorkspace(workspace, options = {}) {
|
|
626
|
+
const generation = ++this.openGeneration;
|
|
558
627
|
this.unsub?.();
|
|
628
|
+
this.resetRenderingState();
|
|
559
629
|
this.cmEditor?.destroy();
|
|
560
630
|
this.cmEditor = null;
|
|
561
631
|
this.workspace = null;
|
|
632
|
+
this.replaceAssetSession(null);
|
|
633
|
+
this.conversionDocumentGeneration += 1;
|
|
562
634
|
try {
|
|
563
635
|
const ws = await MdzipWorkspaceService.openWorkspace(workspace, options);
|
|
636
|
+
if (generation !== this.openGeneration) {
|
|
637
|
+
// A newer open started while this parse was in flight; discard it.
|
|
638
|
+
return;
|
|
639
|
+
}
|
|
564
640
|
this.workspace = ws;
|
|
641
|
+
this.replaceAssetSession(new MdzipAssetSession(ws, ws.snapshot().workspace.assets, this.elRoot.ownerDocument, {
|
|
642
|
+
cache: this.options.assetCache,
|
|
643
|
+
sourceId: options.assetSourceId
|
|
644
|
+
?? (options.archiveBytes ? () => mdzipArchiveSourceId(options.archiveBytes) : undefined),
|
|
645
|
+
onFailed: this.options.onFailed
|
|
646
|
+
}));
|
|
565
647
|
const snap = ws.snapshot();
|
|
566
648
|
this.layout = this.validLayoutForSnapshot(this.options.initialLayout ?? defaultLayoutForPolicy(this.controlPolicy), snap);
|
|
567
|
-
|
|
649
|
+
await this.ensureCmEditor();
|
|
650
|
+
if (generation !== this.openGeneration) {
|
|
651
|
+
return;
|
|
652
|
+
}
|
|
568
653
|
this.unsub = ws.subscribe((event) => {
|
|
654
|
+
if (event.changes.includes('asset')) {
|
|
655
|
+
this.replaceAssetSession(new MdzipAssetSession(ws, event.snapshot.workspace.assets, this.elRoot.ownerDocument, {
|
|
656
|
+
cache: this.options.assetCache,
|
|
657
|
+
sourceId: event.snapshot.archiveBytes.length
|
|
658
|
+
? () => mdzipArchiveSourceId(event.snapshot.archiveBytes)
|
|
659
|
+
: undefined,
|
|
660
|
+
onFailed: this.options.onFailed
|
|
661
|
+
}));
|
|
662
|
+
}
|
|
663
|
+
if (event.changes.includes('workspace') || event.changes.includes('document')) {
|
|
664
|
+
this.conversionDocumentGeneration += 1;
|
|
665
|
+
}
|
|
569
666
|
this.render();
|
|
570
667
|
void this.notifyChanged(event);
|
|
571
668
|
});
|
|
@@ -573,6 +670,9 @@ export class MdzipWorkspaceView {
|
|
|
573
670
|
void this.notifyChanged(this.initialWorkspaceEvent(ws.snapshot()));
|
|
574
671
|
}
|
|
575
672
|
catch (error) {
|
|
673
|
+
if (generation !== this.openGeneration) {
|
|
674
|
+
return;
|
|
675
|
+
}
|
|
576
676
|
this.options.onFailed?.(error);
|
|
577
677
|
}
|
|
578
678
|
}
|
|
@@ -613,14 +713,18 @@ export class MdzipWorkspaceView {
|
|
|
613
713
|
return this.workspace?.listAssets() ?? [];
|
|
614
714
|
}
|
|
615
715
|
canExecuteCommand(command) {
|
|
716
|
+
// Availability is currently uniform across commands; the parameter is
|
|
717
|
+
// kept so per-command policies stay a non-breaking change.
|
|
718
|
+
void command;
|
|
616
719
|
const snapshot = this.workspace?.snapshot();
|
|
617
|
-
if (!snapshot ||
|
|
720
|
+
if (!snapshot || snapshot.mode === 'read-only'
|
|
618
721
|
|| snapshot.currentPathType !== 'markdown') {
|
|
619
722
|
return false;
|
|
620
723
|
}
|
|
621
724
|
return true;
|
|
622
725
|
}
|
|
623
726
|
async executeCommand(command, file) {
|
|
727
|
+
await this.ensureCmEditor(true);
|
|
624
728
|
if (!this.canExecuteCommand(command)) {
|
|
625
729
|
return false;
|
|
626
730
|
}
|
|
@@ -660,12 +764,18 @@ export class MdzipWorkspaceView {
|
|
|
660
764
|
this.cmEditor?.focus();
|
|
661
765
|
}
|
|
662
766
|
destroy() {
|
|
767
|
+
this.conversionDocumentGeneration += 1;
|
|
663
768
|
try {
|
|
664
769
|
this.unsub?.();
|
|
665
770
|
}
|
|
666
771
|
catch {
|
|
667
772
|
// Ignore subscription cleanup errors
|
|
668
773
|
}
|
|
774
|
+
this.resetPreviewState();
|
|
775
|
+
this.replaceAssetSession(null);
|
|
776
|
+
this.teardownEntryRenderer();
|
|
777
|
+
// Release any whenRendered() waiters so their promises do not hang.
|
|
778
|
+
this.flushRenderedWaiters();
|
|
669
779
|
try {
|
|
670
780
|
this.cmEditor?.destroy();
|
|
671
781
|
}
|
|
@@ -679,6 +789,550 @@ export class MdzipWorkspaceView {
|
|
|
679
789
|
// Ignore DOM cleanup errors
|
|
680
790
|
}
|
|
681
791
|
}
|
|
792
|
+
/**
|
|
793
|
+
* Replaces the rendering configuration without recreating the view (and
|
|
794
|
+
* therefore without re-opening the workspace). Wrappers call this when
|
|
795
|
+
* renderer props change; the cost is one preview re-render and entry
|
|
796
|
+
* renderer re-match.
|
|
797
|
+
*/
|
|
798
|
+
setRenderingOptions(options) {
|
|
799
|
+
if ('markdownRenderer' in options) {
|
|
800
|
+
this.markdownRenderer = options.markdownRenderer ?? undefined;
|
|
801
|
+
}
|
|
802
|
+
if (options.markdownExtensions) {
|
|
803
|
+
this.markdownExtensions = options.markdownExtensions;
|
|
804
|
+
}
|
|
805
|
+
if (options.entryRenderers) {
|
|
806
|
+
this.entryRenderers = options.entryRenderers;
|
|
807
|
+
}
|
|
808
|
+
this.renderingService = new MdzipRenderingService(this.markdownRenderer ?? defaultSafeMarkdownRenderer, this.markdownExtensions);
|
|
809
|
+
this.resetPreviewState();
|
|
810
|
+
this.teardownEntryRenderer();
|
|
811
|
+
this.entryMatchMissKey = null;
|
|
812
|
+
this.render();
|
|
813
|
+
}
|
|
814
|
+
/**
|
|
815
|
+
* Tears down all custom rendering state: aborts in-flight renders, destroys
|
|
816
|
+
* mounted extension and entry renderer handles, and clears memo caches.
|
|
817
|
+
* Used when the workspace is replaced or the view is destroyed.
|
|
818
|
+
*/
|
|
819
|
+
resetRenderingState() {
|
|
820
|
+
this.resetPreviewState();
|
|
821
|
+
this.teardownEntryRenderer();
|
|
822
|
+
this.entryMatchMissKey = null;
|
|
823
|
+
}
|
|
824
|
+
replaceAssetSession(session) {
|
|
825
|
+
this.assetSession?.destroy();
|
|
826
|
+
this.assetSession = session;
|
|
827
|
+
this.resetPreviewState();
|
|
828
|
+
}
|
|
829
|
+
resetPreviewState() {
|
|
830
|
+
this.previewAbort?.abort();
|
|
831
|
+
this.previewAbort = null;
|
|
832
|
+
this.previewGeneration += 1;
|
|
833
|
+
this.destroyPreviewHandles();
|
|
834
|
+
this.previewMemo = null;
|
|
835
|
+
}
|
|
836
|
+
destroyPreviewHandles() {
|
|
837
|
+
const handles = this.previewHandles;
|
|
838
|
+
this.previewHandles = [];
|
|
839
|
+
for (const handle of handles) {
|
|
840
|
+
try {
|
|
841
|
+
handle.destroy();
|
|
842
|
+
}
|
|
843
|
+
catch (error) {
|
|
844
|
+
this.options.onFailed?.(error);
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
updatePreview(snapshot, entryClaimed) {
|
|
849
|
+
if (entryClaimed) {
|
|
850
|
+
// The entry renderer owns the pane stack; release preview resources so
|
|
851
|
+
// a later fallback re-renders from scratch.
|
|
852
|
+
if (this.previewMemo || this.previewHandles.length > 0 || this.previewAbort) {
|
|
853
|
+
this.resetPreviewState();
|
|
854
|
+
this.elPreviewContent.replaceChildren();
|
|
855
|
+
}
|
|
856
|
+
// There is no built-in preview to wait for; release any waiters.
|
|
857
|
+
this.previewHydrated = true;
|
|
858
|
+
this.flushRenderedWaiters();
|
|
859
|
+
return;
|
|
860
|
+
}
|
|
861
|
+
const memo = this.previewMemo;
|
|
862
|
+
if (memo
|
|
863
|
+
&& memo.path === snapshot.currentPath
|
|
864
|
+
&& memo.pathType === snapshot.currentPathType
|
|
865
|
+
&& memo.text === snapshot.currentText
|
|
866
|
+
&& memo.colorScheme === this.colorScheme) {
|
|
867
|
+
// Nothing that feeds the preview changed; keep the existing DOM and any
|
|
868
|
+
// mounted extension handles.
|
|
869
|
+
return;
|
|
870
|
+
}
|
|
871
|
+
this.previewAbort?.abort();
|
|
872
|
+
this.previewAbort = null;
|
|
873
|
+
this.destroyPreviewHandles();
|
|
874
|
+
const generation = ++this.previewGeneration;
|
|
875
|
+
this.previewHydrated = false;
|
|
876
|
+
this.previewMemo = {
|
|
877
|
+
path: snapshot.currentPath,
|
|
878
|
+
pathType: snapshot.currentPathType,
|
|
879
|
+
text: snapshot.currentText,
|
|
880
|
+
colorScheme: this.colorScheme
|
|
881
|
+
};
|
|
882
|
+
if (snapshot.currentPathType !== 'markdown') {
|
|
883
|
+
if (snapshot.currentPathType === 'image' && this.assetSession) {
|
|
884
|
+
const abort = new AbortController();
|
|
885
|
+
this.previewAbort = abort;
|
|
886
|
+
void this.assetSession.resolve(snapshot.currentPath, snapshot.currentPath).then((src) => {
|
|
887
|
+
if (generation !== this.previewGeneration || abort.signal.aborted)
|
|
888
|
+
return;
|
|
889
|
+
this.elPreviewContent.innerHTML = src
|
|
890
|
+
? `<div class="asset-preview-wrap"><img class="asset-preview-image" src="${escapeHtml(src)}" alt="${escapeHtml(snapshot.currentPath)}"></div>`
|
|
891
|
+
: renderMdzipPreviewHtml(snapshot);
|
|
892
|
+
this.firePreviewRendered(snapshot, generation);
|
|
893
|
+
this.fireAssetsHydrated(snapshot, generation);
|
|
894
|
+
}).catch((error) => this.options.onFailed?.(error));
|
|
895
|
+
}
|
|
896
|
+
else {
|
|
897
|
+
this.elPreviewContent.innerHTML = renderMdzipPreviewHtml(snapshot);
|
|
898
|
+
this.firePreviewRendered(snapshot, generation);
|
|
899
|
+
this.fireAssetsHydrated(snapshot, generation);
|
|
900
|
+
}
|
|
901
|
+
return;
|
|
902
|
+
}
|
|
903
|
+
const abort = new AbortController();
|
|
904
|
+
this.previewAbort = abort;
|
|
905
|
+
const context = this.createMarkdownContext(snapshot, abort.signal);
|
|
906
|
+
let result;
|
|
907
|
+
try {
|
|
908
|
+
result = this.renderingService.renderMarkdown(snapshot.currentText, context);
|
|
909
|
+
}
|
|
910
|
+
catch (error) {
|
|
911
|
+
this.options.onFailed?.(error);
|
|
912
|
+
this.elPreviewContent.innerHTML = renderMdzipPreviewHtml(snapshot);
|
|
913
|
+
this.firePreviewRendered(snapshot, generation);
|
|
914
|
+
this.fireAssetsHydrated(snapshot, generation);
|
|
915
|
+
return;
|
|
916
|
+
}
|
|
917
|
+
if (typeof result === 'string') {
|
|
918
|
+
// Sync fast path: no microtask hop when every pipeline stage is sync.
|
|
919
|
+
this.applyPreviewHtml(result, snapshot, context, generation);
|
|
920
|
+
return;
|
|
921
|
+
}
|
|
922
|
+
void result.then((html) => {
|
|
923
|
+
if (generation !== this.previewGeneration || abort.signal.aborted) {
|
|
924
|
+
return; // Stale: the selection or content moved on while rendering.
|
|
925
|
+
}
|
|
926
|
+
this.applyPreviewHtml(html, snapshot, context, generation);
|
|
927
|
+
}).catch((error) => {
|
|
928
|
+
if (generation !== this.previewGeneration || abort.signal.aborted) {
|
|
929
|
+
return;
|
|
930
|
+
}
|
|
931
|
+
if (error?.name !== 'AbortError') {
|
|
932
|
+
this.options.onFailed?.(error);
|
|
933
|
+
}
|
|
934
|
+
});
|
|
935
|
+
}
|
|
936
|
+
applyPreviewHtml(html, snapshot, context, generation) {
|
|
937
|
+
// When the preview references archive images, mount the text immediately
|
|
938
|
+
// and hydrate each image progressively (reserving layout space from its
|
|
939
|
+
// sniffed intrinsic size), rather than blocking the whole preview on image
|
|
940
|
+
// resolution. Other markdown mounts synchronously.
|
|
941
|
+
if (this.assetSession && /<img\b/i.test(html)) {
|
|
942
|
+
this.mountProgressivePreview(html, snapshot, context, generation);
|
|
943
|
+
return;
|
|
944
|
+
}
|
|
945
|
+
this.mountPreviewHtml(html, snapshot, context, generation);
|
|
946
|
+
}
|
|
947
|
+
mountPreviewHtml(html, snapshot, context, generation) {
|
|
948
|
+
this.elPreviewContent.innerHTML = html;
|
|
949
|
+
this.mountPreviewExtensions(context, generation);
|
|
950
|
+
// No archive images to resolve on this path: the preview is ready now.
|
|
951
|
+
this.firePreviewRendered(snapshot, generation);
|
|
952
|
+
this.fireAssetsHydrated(snapshot, generation);
|
|
953
|
+
}
|
|
954
|
+
/**
|
|
955
|
+
* Mounts the rendered text immediately with image placeholders, then swaps
|
|
956
|
+
* each archive image in as it resolves. `onPreviewRendered` fires once the
|
|
957
|
+
* text is in the DOM; `onAssetsHydrated` fires once every referenced image
|
|
958
|
+
* has resolved and had its final `src` assigned (or there are none).
|
|
959
|
+
*/
|
|
960
|
+
mountProgressivePreview(html, snapshot, context, generation) {
|
|
961
|
+
this.elPreviewContent.innerHTML = html;
|
|
962
|
+
const session = this.assetSession;
|
|
963
|
+
const document = this.elPreviewContent.ownerDocument;
|
|
964
|
+
const pending = [];
|
|
965
|
+
for (const image of Array.from(this.elPreviewContent.querySelectorAll('img'))) {
|
|
966
|
+
const source = image.getAttribute('src');
|
|
967
|
+
// Leave external, protocol-relative, data, and fragment URLs untouched.
|
|
968
|
+
if (!source || /^(?:[a-z][a-z\d+.-]*:|\/\/|#)/i.test(source)) {
|
|
969
|
+
continue;
|
|
970
|
+
}
|
|
971
|
+
// Drop the archive-relative src so the browser does not fetch the bad
|
|
972
|
+
// path. Wrap the image in a collapsed slot so the text stays compact and
|
|
973
|
+
// immediately readable; the slot eases open to the reserved height once
|
|
974
|
+
// the image resolves.
|
|
975
|
+
image.removeAttribute('src');
|
|
976
|
+
image.classList.add('mdzip-image-loading');
|
|
977
|
+
const slot = document.createElement('span');
|
|
978
|
+
slot.className = 'mdzip-image-slot';
|
|
979
|
+
image.parentNode?.insertBefore(slot, image);
|
|
980
|
+
slot.appendChild(image);
|
|
981
|
+
pending.push({ image, slot, source });
|
|
982
|
+
}
|
|
983
|
+
this.mountPreviewExtensions(context, generation);
|
|
984
|
+
this.firePreviewRendered(snapshot, generation);
|
|
985
|
+
if (!session || pending.length === 0) {
|
|
986
|
+
this.fireAssetsHydrated(snapshot, generation);
|
|
987
|
+
return;
|
|
988
|
+
}
|
|
989
|
+
let remaining = pending.length;
|
|
990
|
+
const settle = () => {
|
|
991
|
+
remaining -= 1;
|
|
992
|
+
if (remaining === 0) {
|
|
993
|
+
this.fireAssetsHydrated(snapshot, generation);
|
|
994
|
+
}
|
|
995
|
+
};
|
|
996
|
+
for (const { image, slot, source } of pending) {
|
|
997
|
+
void session.resolveImage(source, context.currentPath).then((resolved) => {
|
|
998
|
+
if (generation !== this.previewGeneration || context.signal.aborted) {
|
|
999
|
+
settle();
|
|
1000
|
+
return;
|
|
1001
|
+
}
|
|
1002
|
+
if (!resolved) {
|
|
1003
|
+
image.classList.remove('mdzip-image-loading');
|
|
1004
|
+
this.openImageSlot(slot);
|
|
1005
|
+
settle();
|
|
1006
|
+
return;
|
|
1007
|
+
}
|
|
1008
|
+
// Size the reserved box from the sniffed dimensions so the slot eases
|
|
1009
|
+
// open to the image's exact height in a single slide — and the pixels
|
|
1010
|
+
// drop into an already-correct box with no further reflow.
|
|
1011
|
+
if (resolved.width && resolved.height) {
|
|
1012
|
+
image.setAttribute('width', String(resolved.width));
|
|
1013
|
+
image.setAttribute('height', String(resolved.height));
|
|
1014
|
+
}
|
|
1015
|
+
this.attachImageLoadHandlers(image, source, resolved.url, context, generation);
|
|
1016
|
+
image.setAttribute('src', resolved.url);
|
|
1017
|
+
this.openImageSlot(slot);
|
|
1018
|
+
settle();
|
|
1019
|
+
}).catch((error) => {
|
|
1020
|
+
if (error?.name !== 'AbortError') {
|
|
1021
|
+
this.options.onFailed?.(error);
|
|
1022
|
+
}
|
|
1023
|
+
image.classList.remove('mdzip-image-loading');
|
|
1024
|
+
this.openImageSlot(slot);
|
|
1025
|
+
settle();
|
|
1026
|
+
});
|
|
1027
|
+
}
|
|
1028
|
+
}
|
|
1029
|
+
/**
|
|
1030
|
+
* Reveals a collapsed image slot. Flushing layout before toggling the class
|
|
1031
|
+
* lets the `0fr -> 1fr` grid transition run from the collapsed state instead
|
|
1032
|
+
* of being coalesced into the initial mount; CSS snaps it open instantly
|
|
1033
|
+
* under `prefers-reduced-motion`.
|
|
1034
|
+
*/
|
|
1035
|
+
openImageSlot(slot) {
|
|
1036
|
+
void slot.offsetHeight;
|
|
1037
|
+
slot.classList.add('mdzip-image-open');
|
|
1038
|
+
}
|
|
1039
|
+
/**
|
|
1040
|
+
* Wires `<img>` load/error handling for a resolved archive image.
|
|
1041
|
+
*
|
|
1042
|
+
* `resolveImage()` already succeeded, so an `error` here is environmental
|
|
1043
|
+
* rather than a missing asset — most often a host whose CSP `img-src` blocks
|
|
1044
|
+
* the `blob:` object URL. When that happens we retry once with a `data:` URL
|
|
1045
|
+
* (which many such hosts still permit), and only if that also fails do we
|
|
1046
|
+
* surface the failure via `onFailed` instead of leaving a silent blank box.
|
|
1047
|
+
*/
|
|
1048
|
+
attachImageLoadHandlers(image, source, resolvedUrl, context, generation) {
|
|
1049
|
+
const clear = () => image.classList.remove('mdzip-image-loading');
|
|
1050
|
+
image.addEventListener('load', clear, { once: true });
|
|
1051
|
+
image.addEventListener('error', () => {
|
|
1052
|
+
const session = this.assetSession;
|
|
1053
|
+
if (resolvedUrl.startsWith('blob:') && session) {
|
|
1054
|
+
void session.resolveDataUrl(source, context.currentPath).then((dataUrl) => {
|
|
1055
|
+
if (generation !== this.previewGeneration || context.signal.aborted) {
|
|
1056
|
+
clear();
|
|
1057
|
+
return;
|
|
1058
|
+
}
|
|
1059
|
+
if (dataUrl && dataUrl !== resolvedUrl) {
|
|
1060
|
+
image.addEventListener('load', clear, { once: true });
|
|
1061
|
+
image.addEventListener('error', () => {
|
|
1062
|
+
clear();
|
|
1063
|
+
this.reportImageLoadFailure(source);
|
|
1064
|
+
}, { once: true });
|
|
1065
|
+
image.setAttribute('src', dataUrl);
|
|
1066
|
+
return;
|
|
1067
|
+
}
|
|
1068
|
+
clear();
|
|
1069
|
+
this.reportImageLoadFailure(source);
|
|
1070
|
+
}).catch((error) => {
|
|
1071
|
+
clear();
|
|
1072
|
+
this.options.onFailed?.(error);
|
|
1073
|
+
});
|
|
1074
|
+
return;
|
|
1075
|
+
}
|
|
1076
|
+
clear();
|
|
1077
|
+
this.reportImageLoadFailure(source);
|
|
1078
|
+
}, { once: true });
|
|
1079
|
+
}
|
|
1080
|
+
reportImageLoadFailure(source) {
|
|
1081
|
+
this.options.onFailed?.(new Error(`Failed to load archive image "${source}". When embedding the editor in a `
|
|
1082
|
+
+ 'CSP-restricted host (e.g. a VS Code webview), ensure img-src permits '
|
|
1083
|
+
+ 'blob: and data:.'));
|
|
1084
|
+
}
|
|
1085
|
+
mountPreviewExtensions(context, generation) {
|
|
1086
|
+
for (const extension of this.markdownExtensions) {
|
|
1087
|
+
if (!extension.mount) {
|
|
1088
|
+
continue;
|
|
1089
|
+
}
|
|
1090
|
+
try {
|
|
1091
|
+
const mounted = extension.mount(this.elPreviewContent, context);
|
|
1092
|
+
if (isThenable(mounted)) {
|
|
1093
|
+
void Promise.resolve(mounted).then((handle) => {
|
|
1094
|
+
if (!handle) {
|
|
1095
|
+
return;
|
|
1096
|
+
}
|
|
1097
|
+
if (generation !== this.previewGeneration) {
|
|
1098
|
+
try {
|
|
1099
|
+
handle.destroy();
|
|
1100
|
+
}
|
|
1101
|
+
catch {
|
|
1102
|
+
// Stale handle cleanup failure is not actionable.
|
|
1103
|
+
}
|
|
1104
|
+
return;
|
|
1105
|
+
}
|
|
1106
|
+
this.previewHandles.push(handle);
|
|
1107
|
+
}).catch((error) => {
|
|
1108
|
+
if (generation === this.previewGeneration) {
|
|
1109
|
+
this.options.onFailed?.(error);
|
|
1110
|
+
}
|
|
1111
|
+
});
|
|
1112
|
+
}
|
|
1113
|
+
else if (mounted) {
|
|
1114
|
+
this.previewHandles.push(mounted);
|
|
1115
|
+
}
|
|
1116
|
+
}
|
|
1117
|
+
catch (error) {
|
|
1118
|
+
this.options.onFailed?.(error);
|
|
1119
|
+
}
|
|
1120
|
+
}
|
|
1121
|
+
}
|
|
1122
|
+
firePreviewRendered(snapshot, generation) {
|
|
1123
|
+
if (generation !== this.previewGeneration) {
|
|
1124
|
+
return;
|
|
1125
|
+
}
|
|
1126
|
+
try {
|
|
1127
|
+
this.options.onPreviewRendered?.(snapshot);
|
|
1128
|
+
}
|
|
1129
|
+
catch (error) {
|
|
1130
|
+
this.options.onFailed?.(error);
|
|
1131
|
+
}
|
|
1132
|
+
}
|
|
1133
|
+
fireAssetsHydrated(snapshot, generation) {
|
|
1134
|
+
if (generation !== this.previewGeneration) {
|
|
1135
|
+
return;
|
|
1136
|
+
}
|
|
1137
|
+
this.previewHydrated = true;
|
|
1138
|
+
try {
|
|
1139
|
+
this.options.onAssetsHydrated?.(snapshot);
|
|
1140
|
+
}
|
|
1141
|
+
catch (error) {
|
|
1142
|
+
this.options.onFailed?.(error);
|
|
1143
|
+
}
|
|
1144
|
+
this.flushRenderedWaiters();
|
|
1145
|
+
}
|
|
1146
|
+
flushRenderedWaiters() {
|
|
1147
|
+
const waiters = this.renderedWaiters;
|
|
1148
|
+
this.renderedWaiters = [];
|
|
1149
|
+
for (const resolve of waiters) {
|
|
1150
|
+
resolve();
|
|
1151
|
+
}
|
|
1152
|
+
}
|
|
1153
|
+
/**
|
|
1154
|
+
* Resolves once the current preview (including any images) is mounted and
|
|
1155
|
+
* hydrated. Resolves immediately when the latest preview is already ready.
|
|
1156
|
+
* Useful for revealing or animating content without observing private DOM.
|
|
1157
|
+
*/
|
|
1158
|
+
whenRendered() {
|
|
1159
|
+
if (this.previewHydrated) {
|
|
1160
|
+
return Promise.resolve();
|
|
1161
|
+
}
|
|
1162
|
+
return new Promise((resolve) => this.renderedWaiters.push(resolve));
|
|
1163
|
+
}
|
|
1164
|
+
createMarkdownContext(snapshot, signal) {
|
|
1165
|
+
return {
|
|
1166
|
+
currentPath: snapshot.currentPath,
|
|
1167
|
+
sourceFormat: snapshot.sourceFormat,
|
|
1168
|
+
colorScheme: this.colorScheme,
|
|
1169
|
+
mode: snapshot.mode,
|
|
1170
|
+
manifest: snapshot.content.manifest,
|
|
1171
|
+
assetResolver: {
|
|
1172
|
+
resolveAssetUrl: (path) => this.assetSession?.resolveKnown(path, snapshot.currentPath)
|
|
1173
|
+
},
|
|
1174
|
+
signal
|
|
1175
|
+
};
|
|
1176
|
+
}
|
|
1177
|
+
/**
|
|
1178
|
+
* Entry renderer lifecycle. Renders are keyed by
|
|
1179
|
+
* (path, pathType, mode, sourceFormat): same key keeps the mounted handle
|
|
1180
|
+
* (calling `update()` when colorScheme or manifest changed), a changed key
|
|
1181
|
+
* destroys it and re-runs matching. Rename/move/delete of the backing entry
|
|
1182
|
+
* changes the path and is therefore handled as a selection change.
|
|
1183
|
+
*/
|
|
1184
|
+
syncEntryRenderer(snapshot) {
|
|
1185
|
+
if (this.entryRenderers.length === 0) {
|
|
1186
|
+
return false;
|
|
1187
|
+
}
|
|
1188
|
+
const matchKey = [
|
|
1189
|
+
snapshot.currentPath,
|
|
1190
|
+
snapshot.currentPathType,
|
|
1191
|
+
snapshot.mode,
|
|
1192
|
+
snapshot.sourceFormat
|
|
1193
|
+
].join('\u0000');
|
|
1194
|
+
if (this.entryState?.key === matchKey) {
|
|
1195
|
+
this.maybeUpdateEntryRenderer(snapshot);
|
|
1196
|
+
return true;
|
|
1197
|
+
}
|
|
1198
|
+
if (this.entryState) {
|
|
1199
|
+
this.teardownEntryRenderer();
|
|
1200
|
+
}
|
|
1201
|
+
if (this.entryMatchMissKey === matchKey) {
|
|
1202
|
+
return false;
|
|
1203
|
+
}
|
|
1204
|
+
const abort = new AbortController();
|
|
1205
|
+
const context = this.createEntryContext(snapshot, abort.signal);
|
|
1206
|
+
const renderer = this.matchEntryRenderer(context);
|
|
1207
|
+
if (!renderer) {
|
|
1208
|
+
this.entryMatchMissKey = matchKey;
|
|
1209
|
+
return false;
|
|
1210
|
+
}
|
|
1211
|
+
this.entryMatchMissKey = null;
|
|
1212
|
+
const generation = ++this.entryGeneration;
|
|
1213
|
+
const state = {
|
|
1214
|
+
key: matchKey,
|
|
1215
|
+
renderer,
|
|
1216
|
+
handle: null,
|
|
1217
|
+
abort,
|
|
1218
|
+
lastColorScheme: this.colorScheme,
|
|
1219
|
+
lastManifest: snapshot.content.manifest
|
|
1220
|
+
};
|
|
1221
|
+
this.entryState = state;
|
|
1222
|
+
this.elEntryPane.replaceChildren();
|
|
1223
|
+
try {
|
|
1224
|
+
const mounted = renderer.mount(this.elEntryPane, context);
|
|
1225
|
+
if (isThenable(mounted)) {
|
|
1226
|
+
void Promise.resolve(mounted).then((handle) => {
|
|
1227
|
+
if (this.entryGeneration !== generation || this.entryState !== state) {
|
|
1228
|
+
// Stale mount: the selection moved on while mounting.
|
|
1229
|
+
try {
|
|
1230
|
+
handle?.destroy();
|
|
1231
|
+
}
|
|
1232
|
+
catch {
|
|
1233
|
+
// Stale handle cleanup failure is not actionable.
|
|
1234
|
+
}
|
|
1235
|
+
return;
|
|
1236
|
+
}
|
|
1237
|
+
state.handle = handle ?? null;
|
|
1238
|
+
}).catch((error) => {
|
|
1239
|
+
if (this.entryGeneration === generation) {
|
|
1240
|
+
this.options.onFailed?.(error);
|
|
1241
|
+
}
|
|
1242
|
+
});
|
|
1243
|
+
}
|
|
1244
|
+
else {
|
|
1245
|
+
state.handle = mounted ?? null;
|
|
1246
|
+
}
|
|
1247
|
+
}
|
|
1248
|
+
catch (error) {
|
|
1249
|
+
this.options.onFailed?.(error);
|
|
1250
|
+
}
|
|
1251
|
+
return true;
|
|
1252
|
+
}
|
|
1253
|
+
maybeUpdateEntryRenderer(snapshot) {
|
|
1254
|
+
const state = this.entryState;
|
|
1255
|
+
if (!state) {
|
|
1256
|
+
return;
|
|
1257
|
+
}
|
|
1258
|
+
const manifest = snapshot.content.manifest;
|
|
1259
|
+
if (state.lastColorScheme === this.colorScheme && state.lastManifest === manifest) {
|
|
1260
|
+
return;
|
|
1261
|
+
}
|
|
1262
|
+
state.lastColorScheme = this.colorScheme;
|
|
1263
|
+
state.lastManifest = manifest;
|
|
1264
|
+
if (!state.handle?.update) {
|
|
1265
|
+
return;
|
|
1266
|
+
}
|
|
1267
|
+
const context = this.createEntryContext(snapshot, state.abort.signal);
|
|
1268
|
+
try {
|
|
1269
|
+
const result = state.handle.update(context);
|
|
1270
|
+
if (isThenable(result)) {
|
|
1271
|
+
void Promise.resolve(result).catch((error) => this.options.onFailed?.(error));
|
|
1272
|
+
}
|
|
1273
|
+
}
|
|
1274
|
+
catch (error) {
|
|
1275
|
+
this.options.onFailed?.(error);
|
|
1276
|
+
}
|
|
1277
|
+
}
|
|
1278
|
+
matchEntryRenderer(context) {
|
|
1279
|
+
const byPriority = [...this.entryRenderers]
|
|
1280
|
+
.sort((a, b) => (b.priority ?? 0) - (a.priority ?? 0));
|
|
1281
|
+
for (const renderer of byPriority) {
|
|
1282
|
+
try {
|
|
1283
|
+
if (renderer.matches(context)) {
|
|
1284
|
+
return renderer;
|
|
1285
|
+
}
|
|
1286
|
+
}
|
|
1287
|
+
catch (error) {
|
|
1288
|
+
this.options.onFailed?.(error);
|
|
1289
|
+
}
|
|
1290
|
+
}
|
|
1291
|
+
return null;
|
|
1292
|
+
}
|
|
1293
|
+
teardownEntryRenderer() {
|
|
1294
|
+
const active = this.entryState;
|
|
1295
|
+
if (!active) {
|
|
1296
|
+
return;
|
|
1297
|
+
}
|
|
1298
|
+
this.entryState = null;
|
|
1299
|
+
this.entryGeneration += 1;
|
|
1300
|
+
active.abort.abort();
|
|
1301
|
+
try {
|
|
1302
|
+
active.handle?.destroy();
|
|
1303
|
+
}
|
|
1304
|
+
catch (error) {
|
|
1305
|
+
this.options.onFailed?.(error);
|
|
1306
|
+
}
|
|
1307
|
+
this.elEntryPane.replaceChildren();
|
|
1308
|
+
}
|
|
1309
|
+
createEntryContext(snapshot, signal) {
|
|
1310
|
+
const workspace = this.workspace;
|
|
1311
|
+
const path = snapshot.currentPath;
|
|
1312
|
+
return {
|
|
1313
|
+
path,
|
|
1314
|
+
pathType: snapshot.currentPathType,
|
|
1315
|
+
mode: snapshot.mode,
|
|
1316
|
+
sourceFormat: snapshot.sourceFormat,
|
|
1317
|
+
colorScheme: this.colorScheme,
|
|
1318
|
+
manifest: snapshot.content.manifest,
|
|
1319
|
+
snapshot,
|
|
1320
|
+
signal,
|
|
1321
|
+
readBytes: async () => {
|
|
1322
|
+
const bytes = await workspace?.readPathBytes(path);
|
|
1323
|
+
if (!bytes) {
|
|
1324
|
+
throw new Error(`Cannot read bytes for "${path}".`);
|
|
1325
|
+
}
|
|
1326
|
+
return bytes;
|
|
1327
|
+
},
|
|
1328
|
+
updateManifest: async (manifest) => {
|
|
1329
|
+
if (!workspace) {
|
|
1330
|
+
throw new Error('Workspace is not open.');
|
|
1331
|
+
}
|
|
1332
|
+
await workspace.updateManifest(manifest);
|
|
1333
|
+
}
|
|
1334
|
+
};
|
|
1335
|
+
}
|
|
682
1336
|
createCmEditor(parent, initialText, mode) {
|
|
683
1337
|
// eslint-disable-next-line @typescript-eslint/no-this-alias
|
|
684
1338
|
const self = this;
|
|
@@ -754,6 +1408,15 @@ export class MdzipWorkspaceView {
|
|
|
754
1408
|
}
|
|
755
1409
|
return editor;
|
|
756
1410
|
}
|
|
1411
|
+
async ensureCmEditor(force = false) {
|
|
1412
|
+
const snapshot = this.workspace?.snapshot();
|
|
1413
|
+
if (this.cmEditor || !snapshot || (!force && this.layout === 'preview')
|
|
1414
|
+
|| !canShowSourceLayout(snapshot)) {
|
|
1415
|
+
return this.cmEditor;
|
|
1416
|
+
}
|
|
1417
|
+
this.cmEditor = this.createCmEditor(this.elEditorHost, snapshot.currentText, snapshot.mode);
|
|
1418
|
+
return this.cmEditor;
|
|
1419
|
+
}
|
|
757
1420
|
render() {
|
|
758
1421
|
const snapshot = this.workspace?.snapshot() ?? null;
|
|
759
1422
|
this.elEmptyState.hidden = snapshot !== null;
|
|
@@ -764,7 +1427,9 @@ export class MdzipWorkspaceView {
|
|
|
764
1427
|
return;
|
|
765
1428
|
}
|
|
766
1429
|
this.layout = this.validLayoutForSnapshot(this.layout, snapshot);
|
|
767
|
-
const
|
|
1430
|
+
const entryClaimed = this.syncEntryRenderer(snapshot);
|
|
1431
|
+
const canEdit = !entryClaimed
|
|
1432
|
+
&& canEditMdzipPath(snapshot.currentPathType, snapshot.currentPath, snapshot.mode);
|
|
768
1433
|
const canShowSource = canShowSourceLayout(snapshot);
|
|
769
1434
|
const showNavigationControl = this.controlPolicy.navigation;
|
|
770
1435
|
const showTitleControl = this.controlPolicy.title.visible;
|
|
@@ -877,14 +1542,16 @@ export class MdzipWorkspaceView {
|
|
|
877
1542
|
effects: this.readOnlyCompartment.reconfigure(EditorState.readOnly.of(snapshot.mode === 'read-only'))
|
|
878
1543
|
});
|
|
879
1544
|
}
|
|
880
|
-
this.
|
|
1545
|
+
this.updatePreview(snapshot, entryClaimed);
|
|
881
1546
|
const pt = snapshot.currentPathType;
|
|
882
|
-
const showEdit = (pt === 'markdown' || pt === 'text') && this.layout !== 'preview';
|
|
883
|
-
const showPreview = pt === 'image' || pt === 'binary' || pt === 'text'
|
|
884
|
-
|| (pt === 'markdown' && this.layout !== 'source');
|
|
1547
|
+
const showEdit = !entryClaimed && (pt === 'markdown' || pt === 'text') && this.layout !== 'preview';
|
|
1548
|
+
const showPreview = !entryClaimed && (pt === 'image' || pt === 'binary' || pt === 'text'
|
|
1549
|
+
|| (pt === 'markdown' && this.layout !== 'source'));
|
|
885
1550
|
this.elEditPane.classList.toggle('active', showEdit);
|
|
886
1551
|
this.elPreviewPane.classList.toggle('active', showPreview);
|
|
887
|
-
this.
|
|
1552
|
+
this.elEntryPane.classList.toggle('active', entryClaimed);
|
|
1553
|
+
this.elPaneStack.classList.toggle('split-mode', this.layout === 'split' && !entryClaimed);
|
|
1554
|
+
this.elPaneStack.classList.toggle('entry-claimed', entryClaimed);
|
|
888
1555
|
if (!showTitleControl) {
|
|
889
1556
|
this.titleDialogOpen = false;
|
|
890
1557
|
this.metadataDialogOpen = false;
|
|
@@ -1026,17 +1693,17 @@ export class MdzipWorkspaceView {
|
|
|
1026
1693
|
});
|
|
1027
1694
|
this.elPreviewBtn.addEventListener('click', () => {
|
|
1028
1695
|
if (this.controlPolicy.layout.preview) {
|
|
1029
|
-
this.setLayout('preview');
|
|
1696
|
+
void this.setLayout('preview');
|
|
1030
1697
|
}
|
|
1031
1698
|
});
|
|
1032
1699
|
this.elSplitBtn.addEventListener('click', () => {
|
|
1033
1700
|
if (this.controlPolicy.layout.split) {
|
|
1034
|
-
this.setLayout('split');
|
|
1701
|
+
void this.setLayout('split');
|
|
1035
1702
|
}
|
|
1036
1703
|
});
|
|
1037
1704
|
this.elSourceBtn.addEventListener('click', () => {
|
|
1038
1705
|
if (this.controlPolicy.layout.source) {
|
|
1039
|
-
this.setLayout('source');
|
|
1706
|
+
void this.setLayout('source');
|
|
1040
1707
|
}
|
|
1041
1708
|
});
|
|
1042
1709
|
this.elSaveBtn.addEventListener('click', () => {
|
|
@@ -1477,16 +2144,58 @@ export class MdzipWorkspaceView {
|
|
|
1477
2144
|
['Entry point', snapshot.sourceFormat === 'mdz' ? snapshot.content.entryPoint : 'Not applicable']
|
|
1478
2145
|
];
|
|
1479
2146
|
this.elMetadataList.replaceChildren(...fields.map(([label, value]) => {
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
return
|
|
2147
|
+
return this.createMetadataRow(label, value);
|
|
2148
|
+
}));
|
|
2149
|
+
const libraries = [
|
|
2150
|
+
...(this.options.libraries ?? []),
|
|
2151
|
+
...MDZIP_RUNTIME_LIBRARIES
|
|
2152
|
+
].filter((library, index, all) => all.findIndex((candidate) => candidate.name === library.name) === index);
|
|
2153
|
+
this.elLibraryList.replaceChildren(...libraries.map((library) => {
|
|
2154
|
+
return this.createLibraryRow(library);
|
|
1488
2155
|
}));
|
|
1489
2156
|
}
|
|
2157
|
+
createMetadataRow(label, value) {
|
|
2158
|
+
const row = this.elRoot.ownerDocument.createElement('div');
|
|
2159
|
+
row.className = 'metadata-row';
|
|
2160
|
+
const term = this.elRoot.ownerDocument.createElement('dt');
|
|
2161
|
+
term.textContent = label;
|
|
2162
|
+
const detail = this.elRoot.ownerDocument.createElement('dd');
|
|
2163
|
+
detail.textContent = value;
|
|
2164
|
+
row.append(term, detail);
|
|
2165
|
+
return row;
|
|
2166
|
+
}
|
|
2167
|
+
createLibraryRow(library) {
|
|
2168
|
+
const row = this.elRoot.ownerDocument.createElement('div');
|
|
2169
|
+
row.className = 'metadata-row library-row';
|
|
2170
|
+
const term = this.elRoot.ownerDocument.createElement('dt');
|
|
2171
|
+
const name = this.elRoot.ownerDocument.createElement('span');
|
|
2172
|
+
name.className = 'library-name';
|
|
2173
|
+
if (library.repositoryUrl) {
|
|
2174
|
+
const link = this.elRoot.ownerDocument.createElement('a');
|
|
2175
|
+
link.href = library.repositoryUrl;
|
|
2176
|
+
link.target = '_blank';
|
|
2177
|
+
link.rel = 'noopener noreferrer';
|
|
2178
|
+
link.textContent = library.name;
|
|
2179
|
+
link.setAttribute('aria-label', `${library.name} repository`);
|
|
2180
|
+
name.append(link);
|
|
2181
|
+
}
|
|
2182
|
+
else {
|
|
2183
|
+
name.textContent = library.name;
|
|
2184
|
+
}
|
|
2185
|
+
const version = this.elRoot.ownerDocument.createElement('span');
|
|
2186
|
+
version.className = 'library-version';
|
|
2187
|
+
version.textContent = library.version;
|
|
2188
|
+
term.append(name, version);
|
|
2189
|
+
const detail = this.elRoot.ownerDocument.createElement('dd');
|
|
2190
|
+
if (library.description) {
|
|
2191
|
+
const description = this.elRoot.ownerDocument.createElement('span');
|
|
2192
|
+
description.className = 'library-description';
|
|
2193
|
+
description.textContent = library.description;
|
|
2194
|
+
detail.append(description);
|
|
2195
|
+
}
|
|
2196
|
+
row.append(term, detail);
|
|
2197
|
+
return row;
|
|
2198
|
+
}
|
|
1490
2199
|
requestMdzConversion(action) {
|
|
1491
2200
|
const snapshot = this.workspace?.snapshot();
|
|
1492
2201
|
if (!snapshot || snapshot.mode === 'read-only' || snapshot.sourceFormat !== 'markdown') {
|
|
@@ -1501,8 +2210,9 @@ export class MdzipWorkspaceView {
|
|
|
1501
2210
|
return;
|
|
1502
2211
|
}
|
|
1503
2212
|
this.conversionHookPending = true;
|
|
2213
|
+
const context = this.createConversionContext(action);
|
|
1504
2214
|
void Promise.resolve()
|
|
1505
|
-
.then(() => hook(action))
|
|
2215
|
+
.then(() => hook(action, context))
|
|
1506
2216
|
.then((handled) => {
|
|
1507
2217
|
this.conversionHookPending = false;
|
|
1508
2218
|
if (!handled) {
|
|
@@ -1515,6 +2225,75 @@ export class MdzipWorkspaceView {
|
|
|
1515
2225
|
this.openConversionDialog(action);
|
|
1516
2226
|
});
|
|
1517
2227
|
}
|
|
2228
|
+
createConversionContext(action) {
|
|
2229
|
+
const workspace = this.workspace;
|
|
2230
|
+
const snapshot = workspace?.snapshot();
|
|
2231
|
+
const selection = this.cmEditor?.state.selection.main;
|
|
2232
|
+
const captured = workspace && snapshot ? {
|
|
2233
|
+
workspace,
|
|
2234
|
+
documentGeneration: this.conversionDocumentGeneration,
|
|
2235
|
+
path: snapshot.currentPath,
|
|
2236
|
+
text: snapshot.currentText,
|
|
2237
|
+
selectionStart: selection?.from,
|
|
2238
|
+
selectionEnd: selection?.to
|
|
2239
|
+
} : null;
|
|
2240
|
+
let consumed = false;
|
|
2241
|
+
const take = () => {
|
|
2242
|
+
if (consumed || !captured || this.workspace !== captured.workspace
|
|
2243
|
+
|| this.conversionDocumentGeneration !== captured.documentGeneration) {
|
|
2244
|
+
return null;
|
|
2245
|
+
}
|
|
2246
|
+
const current = captured.workspace.snapshot();
|
|
2247
|
+
if (current.mode === 'read-only'
|
|
2248
|
+
|| current.sourceFormat !== 'markdown'
|
|
2249
|
+
|| current.currentPath !== captured.path
|
|
2250
|
+
|| current.currentText !== captured.text) {
|
|
2251
|
+
return null;
|
|
2252
|
+
}
|
|
2253
|
+
consumed = true;
|
|
2254
|
+
return captured;
|
|
2255
|
+
};
|
|
2256
|
+
return {
|
|
2257
|
+
insertMarkdown: async (text) => {
|
|
2258
|
+
const target = take();
|
|
2259
|
+
if (!target || target.selectionStart === undefined || target.selectionEnd === undefined) {
|
|
2260
|
+
return false;
|
|
2261
|
+
}
|
|
2262
|
+
const nextText = target.text.slice(0, target.selectionStart)
|
|
2263
|
+
+ text
|
|
2264
|
+
+ target.text.slice(target.selectionEnd);
|
|
2265
|
+
target.workspace.editText(nextText);
|
|
2266
|
+
this.render();
|
|
2267
|
+
const editor = await this.ensureCmEditor();
|
|
2268
|
+
if (editor) {
|
|
2269
|
+
editor.dispatch({ selection: { anchor: target.selectionStart + text.length } });
|
|
2270
|
+
editor.focus();
|
|
2271
|
+
}
|
|
2272
|
+
return true;
|
|
2273
|
+
},
|
|
2274
|
+
convertToMdz: async () => {
|
|
2275
|
+
const target = take();
|
|
2276
|
+
if (!target || !await this.convertToMdz()) {
|
|
2277
|
+
return false;
|
|
2278
|
+
}
|
|
2279
|
+
if (action.kind === 'navigation') {
|
|
2280
|
+
this.navVisible = true;
|
|
2281
|
+
this.render();
|
|
2282
|
+
return true;
|
|
2283
|
+
}
|
|
2284
|
+
const editor = await this.ensureCmEditor();
|
|
2285
|
+
if (editor && target.selectionStart !== undefined) {
|
|
2286
|
+
editor.dispatch({ selection: { anchor: target.selectionStart } });
|
|
2287
|
+
}
|
|
2288
|
+
if (action.kind === 'image-file') {
|
|
2289
|
+
await this.insertImageFile(action.file);
|
|
2290
|
+
return true;
|
|
2291
|
+
}
|
|
2292
|
+
this.elImageInput.click();
|
|
2293
|
+
return true;
|
|
2294
|
+
}
|
|
2295
|
+
};
|
|
2296
|
+
}
|
|
1518
2297
|
openConversionDialog(action) {
|
|
1519
2298
|
this.conversionAction = action;
|
|
1520
2299
|
this.render();
|
|
@@ -1857,9 +2636,10 @@ export class MdzipWorkspaceView {
|
|
|
1857
2636
|
this.setColorScheme(colorScheme);
|
|
1858
2637
|
this.options.onColorSchemeChanged?.(colorScheme);
|
|
1859
2638
|
}
|
|
1860
|
-
setLayout(requested) {
|
|
2639
|
+
async setLayout(requested) {
|
|
1861
2640
|
const snapshot = this.workspace?.snapshot();
|
|
1862
2641
|
this.layout = snapshot ? this.validLayoutForSnapshot(requested, snapshot) : requested;
|
|
2642
|
+
await this.ensureCmEditor();
|
|
1863
2643
|
this.render();
|
|
1864
2644
|
}
|
|
1865
2645
|
navMenuTargetFromElement(element) {
|
|
@@ -2609,6 +3389,7 @@ const SHELL_HTML = `
|
|
|
2609
3389
|
<section class="pane preview-pane" data-ref="preview-pane">
|
|
2610
3390
|
<article class="preview-content" data-ref="preview-content"></article>
|
|
2611
3391
|
</section>
|
|
3392
|
+
<section class="pane entry-pane" data-ref="entry-pane"></section>
|
|
2612
3393
|
</div>
|
|
2613
3394
|
</div>
|
|
2614
3395
|
|
|
@@ -2649,6 +3430,8 @@ const SHELL_HTML = `
|
|
|
2649
3430
|
<div class="title-dialog metadata-dialog">
|
|
2650
3431
|
<h3 id="mdzip-metadata-dialog-heading">Document Information</h3>
|
|
2651
3432
|
<dl data-ref="metadata-list"></dl>
|
|
3433
|
+
<h4>Libraries</h4>
|
|
3434
|
+
<dl data-ref="library-list"></dl>
|
|
2652
3435
|
<div class="title-dialog-actions">
|
|
2653
3436
|
<button type="button" class="save-title" data-action="close-metadata">Close</button>
|
|
2654
3437
|
</div>
|