@mdzip/editor 1.3.1 → 1.3.8
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 +141 -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 +81 -0
- package/dist/asset-cache.d.ts.map +1 -0
- package/dist/asset-cache.js +365 -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/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 +138 -2
- package/dist/rendering.d.ts.map +1 -1
- package/dist/rendering.js +108 -13
- package/dist/rendering.js.map +1 -1
- package/dist/view-css.d.ts.map +1 -1
- package/dist/view-css.js +109 -1
- package/dist/view-css.js.map +1 -1
- package/dist/view.d.ts +136 -1
- package/dist/view.d.ts.map +1 -1
- package/dist/view.js +733 -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 +28 -0
- package/dist/workspace.js.map +1 -1
- package/package.json +14 -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,7 @@ export class MdzipWorkspaceView {
|
|
|
414
421
|
this.pendingNewFolders = new Set();
|
|
415
422
|
this.pendingReplacePath = null;
|
|
416
423
|
this.conversionHookPending = false;
|
|
424
|
+
this.conversionDocumentGeneration = 0;
|
|
417
425
|
this.dragSourcePath = null;
|
|
418
426
|
this.dragOverElement = null;
|
|
419
427
|
this.tooltipState = null;
|
|
@@ -423,6 +431,25 @@ export class MdzipWorkspaceView {
|
|
|
423
431
|
this.readOnlyCompartment = new Compartment();
|
|
424
432
|
this.updatingCm = false;
|
|
425
433
|
this.syncing = false;
|
|
434
|
+
this.markdownExtensions = [];
|
|
435
|
+
this.entryRenderers = [];
|
|
436
|
+
this.renderingService = new MdzipRenderingService();
|
|
437
|
+
// Preview render memo: the preview pipeline only re-runs when one of these
|
|
438
|
+
// inputs actually changed, so unrelated snapshot renders (dialogs, nav,
|
|
439
|
+
// layout toggles) never reset preview DOM or re-run extension mounts.
|
|
440
|
+
this.previewMemo = null;
|
|
441
|
+
this.previewGeneration = 0;
|
|
442
|
+
this.previewAbort = null;
|
|
443
|
+
this.previewHandles = [];
|
|
444
|
+
// Whether the latest preview generation has finished mounting and hydrating
|
|
445
|
+
// its images; drives `whenRendered()` and gates the hydration callback.
|
|
446
|
+
this.previewHydrated = false;
|
|
447
|
+
this.renderedWaiters = [];
|
|
448
|
+
this.entryState = null;
|
|
449
|
+
// Negative match cache: with only non-matching renderers registered,
|
|
450
|
+
// matches() does not re-run on every snapshot render of the same entry.
|
|
451
|
+
this.entryMatchMissKey = null;
|
|
452
|
+
this.entryGeneration = 0;
|
|
426
453
|
this.options = options;
|
|
427
454
|
this.controlPolicy = resolveMdzipControlPolicy(options.controls);
|
|
428
455
|
this.navigationMode = options.navigationMode ?? 'editor';
|
|
@@ -432,6 +459,10 @@ export class MdzipWorkspaceView {
|
|
|
432
459
|
? 'dark'
|
|
433
460
|
: 'light');
|
|
434
461
|
this.navVisible = options.navigationButtonActive ?? this.navVisible;
|
|
462
|
+
this.markdownRenderer = options.markdownRenderer;
|
|
463
|
+
this.markdownExtensions = options.markdownExtensions ?? [];
|
|
464
|
+
this.entryRenderers = options.entryRenderers ?? [];
|
|
465
|
+
this.renderingService = new MdzipRenderingService(this.markdownRenderer ?? defaultSafeMarkdownRenderer, this.markdownExtensions);
|
|
435
466
|
injectStyles(container.ownerDocument);
|
|
436
467
|
container.replaceChildren();
|
|
437
468
|
container.innerHTML = SHELL_HTML;
|
|
@@ -468,6 +499,7 @@ export class MdzipWorkspaceView {
|
|
|
468
499
|
this.elSplitResizer = q('[data-ref="split-resizer"]');
|
|
469
500
|
this.elPreviewPane = q('[data-ref="preview-pane"]');
|
|
470
501
|
this.elPreviewContent = q('[data-ref="preview-content"]');
|
|
502
|
+
this.elEntryPane = q('[data-ref="entry-pane"]');
|
|
471
503
|
this.elTitleDialog = q('[data-ref="title-dialog"]');
|
|
472
504
|
this.elTitleInput = q('[data-ref="title-input"]');
|
|
473
505
|
this.elTitleValidation = q('[data-ref="title-validation"]');
|
|
@@ -475,6 +507,7 @@ export class MdzipWorkspaceView {
|
|
|
475
507
|
this.elTitleResetBtn = q('[data-ref="title-reset-btn"]');
|
|
476
508
|
this.elMetadataDialog = q('[data-ref="metadata-dialog"]');
|
|
477
509
|
this.elMetadataList = q('[data-ref="metadata-list"]');
|
|
510
|
+
this.elLibraryList = q('[data-ref="library-list"]');
|
|
478
511
|
this.elConversionDialog = q('[data-ref="conversion-dialog"]');
|
|
479
512
|
this.elConversionConfirmBtn = q('[data-ref="conversion-confirm-btn"]');
|
|
480
513
|
this.elNavMenu = q('[data-ref="nav-menu"]');
|
|
@@ -505,19 +538,37 @@ export class MdzipWorkspaceView {
|
|
|
505
538
|
}
|
|
506
539
|
async openArchive(bytes, options = {}) {
|
|
507
540
|
this.unsub?.();
|
|
541
|
+
this.resetRenderingState();
|
|
508
542
|
this.cmEditor?.destroy();
|
|
509
543
|
this.cmEditor = null;
|
|
510
544
|
this.workspace = null;
|
|
545
|
+
this.replaceAssetSession(null);
|
|
546
|
+
this.conversionDocumentGeneration += 1;
|
|
511
547
|
try {
|
|
512
548
|
const ws = await MdzipWorkspaceService.open(bytes, options);
|
|
513
549
|
this.workspace = ws;
|
|
550
|
+
this.replaceAssetSession(new MdzipAssetSession(ws, ws.snapshot().workspace.assets, this.elRoot.ownerDocument, {
|
|
551
|
+
cache: this.options.assetCache,
|
|
552
|
+
sourceId: () => mdzipArchiveSourceId(bytes),
|
|
553
|
+
onFailed: this.options.onFailed
|
|
554
|
+
}));
|
|
514
555
|
const snap = ws.snapshot();
|
|
515
556
|
if (snap.sourceFormat === 'markdown') {
|
|
516
557
|
this.navVisible = false;
|
|
517
558
|
}
|
|
518
559
|
this.layout = this.validLayoutForSnapshot(this.options.initialLayout ?? defaultLayoutForPolicy(this.controlPolicy), snap);
|
|
519
|
-
|
|
560
|
+
await this.ensureCmEditor();
|
|
520
561
|
this.unsub = ws.subscribe((event) => {
|
|
562
|
+
if (event.changes.includes('asset')) {
|
|
563
|
+
this.replaceAssetSession(new MdzipAssetSession(ws, event.snapshot.workspace.assets, this.elRoot.ownerDocument, {
|
|
564
|
+
cache: this.options.assetCache,
|
|
565
|
+
sourceId: () => mdzipArchiveSourceId(event.snapshot.archiveBytes),
|
|
566
|
+
onFailed: this.options.onFailed
|
|
567
|
+
}));
|
|
568
|
+
}
|
|
569
|
+
if (event.changes.includes('workspace') || event.changes.includes('document')) {
|
|
570
|
+
this.conversionDocumentGeneration += 1;
|
|
571
|
+
}
|
|
521
572
|
this.render();
|
|
522
573
|
void this.notifyChanged(event);
|
|
523
574
|
});
|
|
@@ -556,16 +607,37 @@ export class MdzipWorkspaceView {
|
|
|
556
607
|
*/
|
|
557
608
|
async openWorkspace(workspace, options = {}) {
|
|
558
609
|
this.unsub?.();
|
|
610
|
+
this.resetRenderingState();
|
|
559
611
|
this.cmEditor?.destroy();
|
|
560
612
|
this.cmEditor = null;
|
|
561
613
|
this.workspace = null;
|
|
614
|
+
this.replaceAssetSession(null);
|
|
615
|
+
this.conversionDocumentGeneration += 1;
|
|
562
616
|
try {
|
|
563
617
|
const ws = await MdzipWorkspaceService.openWorkspace(workspace, options);
|
|
564
618
|
this.workspace = ws;
|
|
619
|
+
this.replaceAssetSession(new MdzipAssetSession(ws, ws.snapshot().workspace.assets, this.elRoot.ownerDocument, {
|
|
620
|
+
cache: this.options.assetCache,
|
|
621
|
+
sourceId: options.assetSourceId
|
|
622
|
+
?? (options.archiveBytes ? () => mdzipArchiveSourceId(options.archiveBytes) : undefined),
|
|
623
|
+
onFailed: this.options.onFailed
|
|
624
|
+
}));
|
|
565
625
|
const snap = ws.snapshot();
|
|
566
626
|
this.layout = this.validLayoutForSnapshot(this.options.initialLayout ?? defaultLayoutForPolicy(this.controlPolicy), snap);
|
|
567
|
-
|
|
627
|
+
await this.ensureCmEditor();
|
|
568
628
|
this.unsub = ws.subscribe((event) => {
|
|
629
|
+
if (event.changes.includes('asset')) {
|
|
630
|
+
this.replaceAssetSession(new MdzipAssetSession(ws, event.snapshot.workspace.assets, this.elRoot.ownerDocument, {
|
|
631
|
+
cache: this.options.assetCache,
|
|
632
|
+
sourceId: event.snapshot.archiveBytes.length
|
|
633
|
+
? () => mdzipArchiveSourceId(event.snapshot.archiveBytes)
|
|
634
|
+
: undefined,
|
|
635
|
+
onFailed: this.options.onFailed
|
|
636
|
+
}));
|
|
637
|
+
}
|
|
638
|
+
if (event.changes.includes('workspace') || event.changes.includes('document')) {
|
|
639
|
+
this.conversionDocumentGeneration += 1;
|
|
640
|
+
}
|
|
569
641
|
this.render();
|
|
570
642
|
void this.notifyChanged(event);
|
|
571
643
|
});
|
|
@@ -613,14 +685,18 @@ export class MdzipWorkspaceView {
|
|
|
613
685
|
return this.workspace?.listAssets() ?? [];
|
|
614
686
|
}
|
|
615
687
|
canExecuteCommand(command) {
|
|
688
|
+
// Availability is currently uniform across commands; the parameter is
|
|
689
|
+
// kept so per-command policies stay a non-breaking change.
|
|
690
|
+
void command;
|
|
616
691
|
const snapshot = this.workspace?.snapshot();
|
|
617
|
-
if (!snapshot ||
|
|
692
|
+
if (!snapshot || snapshot.mode === 'read-only'
|
|
618
693
|
|| snapshot.currentPathType !== 'markdown') {
|
|
619
694
|
return false;
|
|
620
695
|
}
|
|
621
696
|
return true;
|
|
622
697
|
}
|
|
623
698
|
async executeCommand(command, file) {
|
|
699
|
+
await this.ensureCmEditor(true);
|
|
624
700
|
if (!this.canExecuteCommand(command)) {
|
|
625
701
|
return false;
|
|
626
702
|
}
|
|
@@ -660,12 +736,18 @@ export class MdzipWorkspaceView {
|
|
|
660
736
|
this.cmEditor?.focus();
|
|
661
737
|
}
|
|
662
738
|
destroy() {
|
|
739
|
+
this.conversionDocumentGeneration += 1;
|
|
663
740
|
try {
|
|
664
741
|
this.unsub?.();
|
|
665
742
|
}
|
|
666
743
|
catch {
|
|
667
744
|
// Ignore subscription cleanup errors
|
|
668
745
|
}
|
|
746
|
+
this.resetPreviewState();
|
|
747
|
+
this.replaceAssetSession(null);
|
|
748
|
+
this.teardownEntryRenderer();
|
|
749
|
+
// Release any whenRendered() waiters so their promises do not hang.
|
|
750
|
+
this.flushRenderedWaiters();
|
|
669
751
|
try {
|
|
670
752
|
this.cmEditor?.destroy();
|
|
671
753
|
}
|
|
@@ -679,6 +761,506 @@ export class MdzipWorkspaceView {
|
|
|
679
761
|
// Ignore DOM cleanup errors
|
|
680
762
|
}
|
|
681
763
|
}
|
|
764
|
+
/**
|
|
765
|
+
* Replaces the rendering configuration without recreating the view (and
|
|
766
|
+
* therefore without re-opening the workspace). Wrappers call this when
|
|
767
|
+
* renderer props change; the cost is one preview re-render and entry
|
|
768
|
+
* renderer re-match.
|
|
769
|
+
*/
|
|
770
|
+
setRenderingOptions(options) {
|
|
771
|
+
if ('markdownRenderer' in options) {
|
|
772
|
+
this.markdownRenderer = options.markdownRenderer ?? undefined;
|
|
773
|
+
}
|
|
774
|
+
if (options.markdownExtensions) {
|
|
775
|
+
this.markdownExtensions = options.markdownExtensions;
|
|
776
|
+
}
|
|
777
|
+
if (options.entryRenderers) {
|
|
778
|
+
this.entryRenderers = options.entryRenderers;
|
|
779
|
+
}
|
|
780
|
+
this.renderingService = new MdzipRenderingService(this.markdownRenderer ?? defaultSafeMarkdownRenderer, this.markdownExtensions);
|
|
781
|
+
this.resetPreviewState();
|
|
782
|
+
this.teardownEntryRenderer();
|
|
783
|
+
this.entryMatchMissKey = null;
|
|
784
|
+
this.render();
|
|
785
|
+
}
|
|
786
|
+
/**
|
|
787
|
+
* Tears down all custom rendering state: aborts in-flight renders, destroys
|
|
788
|
+
* mounted extension and entry renderer handles, and clears memo caches.
|
|
789
|
+
* Used when the workspace is replaced or the view is destroyed.
|
|
790
|
+
*/
|
|
791
|
+
resetRenderingState() {
|
|
792
|
+
this.resetPreviewState();
|
|
793
|
+
this.teardownEntryRenderer();
|
|
794
|
+
this.entryMatchMissKey = null;
|
|
795
|
+
}
|
|
796
|
+
replaceAssetSession(session) {
|
|
797
|
+
this.assetSession?.destroy();
|
|
798
|
+
this.assetSession = session;
|
|
799
|
+
this.resetPreviewState();
|
|
800
|
+
}
|
|
801
|
+
resetPreviewState() {
|
|
802
|
+
this.previewAbort?.abort();
|
|
803
|
+
this.previewAbort = null;
|
|
804
|
+
this.previewGeneration += 1;
|
|
805
|
+
this.destroyPreviewHandles();
|
|
806
|
+
this.previewMemo = null;
|
|
807
|
+
}
|
|
808
|
+
destroyPreviewHandles() {
|
|
809
|
+
const handles = this.previewHandles;
|
|
810
|
+
this.previewHandles = [];
|
|
811
|
+
for (const handle of handles) {
|
|
812
|
+
try {
|
|
813
|
+
handle.destroy();
|
|
814
|
+
}
|
|
815
|
+
catch (error) {
|
|
816
|
+
this.options.onFailed?.(error);
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
updatePreview(snapshot, entryClaimed) {
|
|
821
|
+
if (entryClaimed) {
|
|
822
|
+
// The entry renderer owns the pane stack; release preview resources so
|
|
823
|
+
// a later fallback re-renders from scratch.
|
|
824
|
+
if (this.previewMemo || this.previewHandles.length > 0 || this.previewAbort) {
|
|
825
|
+
this.resetPreviewState();
|
|
826
|
+
this.elPreviewContent.replaceChildren();
|
|
827
|
+
}
|
|
828
|
+
// There is no built-in preview to wait for; release any waiters.
|
|
829
|
+
this.previewHydrated = true;
|
|
830
|
+
this.flushRenderedWaiters();
|
|
831
|
+
return;
|
|
832
|
+
}
|
|
833
|
+
const memo = this.previewMemo;
|
|
834
|
+
if (memo
|
|
835
|
+
&& memo.path === snapshot.currentPath
|
|
836
|
+
&& memo.pathType === snapshot.currentPathType
|
|
837
|
+
&& memo.text === snapshot.currentText
|
|
838
|
+
&& memo.colorScheme === this.colorScheme) {
|
|
839
|
+
// Nothing that feeds the preview changed; keep the existing DOM and any
|
|
840
|
+
// mounted extension handles.
|
|
841
|
+
return;
|
|
842
|
+
}
|
|
843
|
+
this.previewAbort?.abort();
|
|
844
|
+
this.previewAbort = null;
|
|
845
|
+
this.destroyPreviewHandles();
|
|
846
|
+
const generation = ++this.previewGeneration;
|
|
847
|
+
this.previewHydrated = false;
|
|
848
|
+
this.previewMemo = {
|
|
849
|
+
path: snapshot.currentPath,
|
|
850
|
+
pathType: snapshot.currentPathType,
|
|
851
|
+
text: snapshot.currentText,
|
|
852
|
+
colorScheme: this.colorScheme
|
|
853
|
+
};
|
|
854
|
+
if (snapshot.currentPathType !== 'markdown') {
|
|
855
|
+
if (snapshot.currentPathType === 'image' && this.assetSession) {
|
|
856
|
+
const abort = new AbortController();
|
|
857
|
+
this.previewAbort = abort;
|
|
858
|
+
void this.assetSession.resolve(snapshot.currentPath, snapshot.currentPath).then((src) => {
|
|
859
|
+
if (generation !== this.previewGeneration || abort.signal.aborted)
|
|
860
|
+
return;
|
|
861
|
+
this.elPreviewContent.innerHTML = src
|
|
862
|
+
? `<div class="asset-preview-wrap"><img class="asset-preview-image" src="${escapeHtml(src)}" alt="${escapeHtml(snapshot.currentPath)}"></div>`
|
|
863
|
+
: renderMdzipPreviewHtml(snapshot);
|
|
864
|
+
this.firePreviewRendered(snapshot, generation);
|
|
865
|
+
this.fireAssetsHydrated(snapshot, generation);
|
|
866
|
+
}).catch((error) => this.options.onFailed?.(error));
|
|
867
|
+
}
|
|
868
|
+
else {
|
|
869
|
+
this.elPreviewContent.innerHTML = renderMdzipPreviewHtml(snapshot);
|
|
870
|
+
this.firePreviewRendered(snapshot, generation);
|
|
871
|
+
this.fireAssetsHydrated(snapshot, generation);
|
|
872
|
+
}
|
|
873
|
+
return;
|
|
874
|
+
}
|
|
875
|
+
const abort = new AbortController();
|
|
876
|
+
this.previewAbort = abort;
|
|
877
|
+
const context = this.createMarkdownContext(snapshot, abort.signal);
|
|
878
|
+
let result;
|
|
879
|
+
try {
|
|
880
|
+
result = this.renderingService.renderMarkdown(snapshot.currentText, context);
|
|
881
|
+
}
|
|
882
|
+
catch (error) {
|
|
883
|
+
this.options.onFailed?.(error);
|
|
884
|
+
this.elPreviewContent.innerHTML = renderMdzipPreviewHtml(snapshot);
|
|
885
|
+
this.firePreviewRendered(snapshot, generation);
|
|
886
|
+
this.fireAssetsHydrated(snapshot, generation);
|
|
887
|
+
return;
|
|
888
|
+
}
|
|
889
|
+
if (typeof result === 'string') {
|
|
890
|
+
// Sync fast path: no microtask hop when every pipeline stage is sync.
|
|
891
|
+
this.applyPreviewHtml(result, snapshot, context, generation);
|
|
892
|
+
return;
|
|
893
|
+
}
|
|
894
|
+
void result.then((html) => {
|
|
895
|
+
if (generation !== this.previewGeneration || abort.signal.aborted) {
|
|
896
|
+
return; // Stale: the selection or content moved on while rendering.
|
|
897
|
+
}
|
|
898
|
+
this.applyPreviewHtml(html, snapshot, context, generation);
|
|
899
|
+
}).catch((error) => {
|
|
900
|
+
if (generation !== this.previewGeneration || abort.signal.aborted) {
|
|
901
|
+
return;
|
|
902
|
+
}
|
|
903
|
+
if (error?.name !== 'AbortError') {
|
|
904
|
+
this.options.onFailed?.(error);
|
|
905
|
+
}
|
|
906
|
+
});
|
|
907
|
+
}
|
|
908
|
+
applyPreviewHtml(html, snapshot, context, generation) {
|
|
909
|
+
// When the preview references archive images, mount the text immediately
|
|
910
|
+
// and hydrate each image progressively (reserving layout space from its
|
|
911
|
+
// sniffed intrinsic size), rather than blocking the whole preview on image
|
|
912
|
+
// resolution. Other markdown mounts synchronously.
|
|
913
|
+
if (this.assetSession && /<img\b/i.test(html)) {
|
|
914
|
+
this.mountProgressivePreview(html, snapshot, context, generation);
|
|
915
|
+
return;
|
|
916
|
+
}
|
|
917
|
+
this.mountPreviewHtml(html, snapshot, context, generation);
|
|
918
|
+
}
|
|
919
|
+
mountPreviewHtml(html, snapshot, context, generation) {
|
|
920
|
+
this.elPreviewContent.innerHTML = html;
|
|
921
|
+
this.mountPreviewExtensions(context, generation);
|
|
922
|
+
// No archive images to resolve on this path: the preview is ready now.
|
|
923
|
+
this.firePreviewRendered(snapshot, generation);
|
|
924
|
+
this.fireAssetsHydrated(snapshot, generation);
|
|
925
|
+
}
|
|
926
|
+
/**
|
|
927
|
+
* Mounts the rendered text immediately with image placeholders, then swaps
|
|
928
|
+
* each archive image in as it resolves. `onPreviewRendered` fires once the
|
|
929
|
+
* text is in the DOM; `onAssetsHydrated` fires once every referenced image
|
|
930
|
+
* has resolved and had its final `src` assigned (or there are none).
|
|
931
|
+
*/
|
|
932
|
+
mountProgressivePreview(html, snapshot, context, generation) {
|
|
933
|
+
this.elPreviewContent.innerHTML = html;
|
|
934
|
+
const session = this.assetSession;
|
|
935
|
+
const document = this.elPreviewContent.ownerDocument;
|
|
936
|
+
const pending = [];
|
|
937
|
+
for (const image of Array.from(this.elPreviewContent.querySelectorAll('img'))) {
|
|
938
|
+
const source = image.getAttribute('src');
|
|
939
|
+
// Leave external, protocol-relative, data, and fragment URLs untouched.
|
|
940
|
+
if (!source || /^(?:[a-z][a-z\d+.-]*:|\/\/|#)/i.test(source)) {
|
|
941
|
+
continue;
|
|
942
|
+
}
|
|
943
|
+
// Drop the archive-relative src so the browser does not fetch the bad
|
|
944
|
+
// path. Wrap the image in a collapsed slot so the text stays compact and
|
|
945
|
+
// immediately readable; the slot eases open to the reserved height once
|
|
946
|
+
// the image resolves.
|
|
947
|
+
image.removeAttribute('src');
|
|
948
|
+
image.classList.add('mdzip-image-loading');
|
|
949
|
+
const slot = document.createElement('span');
|
|
950
|
+
slot.className = 'mdzip-image-slot';
|
|
951
|
+
image.parentNode?.insertBefore(slot, image);
|
|
952
|
+
slot.appendChild(image);
|
|
953
|
+
pending.push({ image, slot, source });
|
|
954
|
+
}
|
|
955
|
+
this.mountPreviewExtensions(context, generation);
|
|
956
|
+
this.firePreviewRendered(snapshot, generation);
|
|
957
|
+
if (!session || pending.length === 0) {
|
|
958
|
+
this.fireAssetsHydrated(snapshot, generation);
|
|
959
|
+
return;
|
|
960
|
+
}
|
|
961
|
+
let remaining = pending.length;
|
|
962
|
+
const settle = () => {
|
|
963
|
+
remaining -= 1;
|
|
964
|
+
if (remaining === 0) {
|
|
965
|
+
this.fireAssetsHydrated(snapshot, generation);
|
|
966
|
+
}
|
|
967
|
+
};
|
|
968
|
+
for (const { image, slot, source } of pending) {
|
|
969
|
+
void session.resolveImage(source, context.currentPath).then((resolved) => {
|
|
970
|
+
if (generation !== this.previewGeneration || context.signal.aborted) {
|
|
971
|
+
settle();
|
|
972
|
+
return;
|
|
973
|
+
}
|
|
974
|
+
if (!resolved) {
|
|
975
|
+
image.classList.remove('mdzip-image-loading');
|
|
976
|
+
this.openImageSlot(slot);
|
|
977
|
+
settle();
|
|
978
|
+
return;
|
|
979
|
+
}
|
|
980
|
+
// Size the reserved box from the sniffed dimensions so the slot eases
|
|
981
|
+
// open to the image's exact height in a single slide — and the pixels
|
|
982
|
+
// drop into an already-correct box with no further reflow.
|
|
983
|
+
if (resolved.width && resolved.height) {
|
|
984
|
+
image.setAttribute('width', String(resolved.width));
|
|
985
|
+
image.setAttribute('height', String(resolved.height));
|
|
986
|
+
}
|
|
987
|
+
const clear = () => image.classList.remove('mdzip-image-loading');
|
|
988
|
+
image.addEventListener('load', clear, { once: true });
|
|
989
|
+
image.addEventListener('error', clear, { once: true });
|
|
990
|
+
image.setAttribute('src', resolved.url);
|
|
991
|
+
this.openImageSlot(slot);
|
|
992
|
+
settle();
|
|
993
|
+
}).catch((error) => {
|
|
994
|
+
if (error?.name !== 'AbortError') {
|
|
995
|
+
this.options.onFailed?.(error);
|
|
996
|
+
}
|
|
997
|
+
image.classList.remove('mdzip-image-loading');
|
|
998
|
+
this.openImageSlot(slot);
|
|
999
|
+
settle();
|
|
1000
|
+
});
|
|
1001
|
+
}
|
|
1002
|
+
}
|
|
1003
|
+
/**
|
|
1004
|
+
* Reveals a collapsed image slot. Flushing layout before toggling the class
|
|
1005
|
+
* lets the `0fr -> 1fr` grid transition run from the collapsed state instead
|
|
1006
|
+
* of being coalesced into the initial mount; CSS snaps it open instantly
|
|
1007
|
+
* under `prefers-reduced-motion`.
|
|
1008
|
+
*/
|
|
1009
|
+
openImageSlot(slot) {
|
|
1010
|
+
void slot.offsetHeight;
|
|
1011
|
+
slot.classList.add('mdzip-image-open');
|
|
1012
|
+
}
|
|
1013
|
+
mountPreviewExtensions(context, generation) {
|
|
1014
|
+
for (const extension of this.markdownExtensions) {
|
|
1015
|
+
if (!extension.mount) {
|
|
1016
|
+
continue;
|
|
1017
|
+
}
|
|
1018
|
+
try {
|
|
1019
|
+
const mounted = extension.mount(this.elPreviewContent, context);
|
|
1020
|
+
if (isThenable(mounted)) {
|
|
1021
|
+
void Promise.resolve(mounted).then((handle) => {
|
|
1022
|
+
if (!handle) {
|
|
1023
|
+
return;
|
|
1024
|
+
}
|
|
1025
|
+
if (generation !== this.previewGeneration) {
|
|
1026
|
+
try {
|
|
1027
|
+
handle.destroy();
|
|
1028
|
+
}
|
|
1029
|
+
catch {
|
|
1030
|
+
// Stale handle cleanup failure is not actionable.
|
|
1031
|
+
}
|
|
1032
|
+
return;
|
|
1033
|
+
}
|
|
1034
|
+
this.previewHandles.push(handle);
|
|
1035
|
+
}).catch((error) => {
|
|
1036
|
+
if (generation === this.previewGeneration) {
|
|
1037
|
+
this.options.onFailed?.(error);
|
|
1038
|
+
}
|
|
1039
|
+
});
|
|
1040
|
+
}
|
|
1041
|
+
else if (mounted) {
|
|
1042
|
+
this.previewHandles.push(mounted);
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
1045
|
+
catch (error) {
|
|
1046
|
+
this.options.onFailed?.(error);
|
|
1047
|
+
}
|
|
1048
|
+
}
|
|
1049
|
+
}
|
|
1050
|
+
firePreviewRendered(snapshot, generation) {
|
|
1051
|
+
if (generation !== this.previewGeneration) {
|
|
1052
|
+
return;
|
|
1053
|
+
}
|
|
1054
|
+
try {
|
|
1055
|
+
this.options.onPreviewRendered?.(snapshot);
|
|
1056
|
+
}
|
|
1057
|
+
catch (error) {
|
|
1058
|
+
this.options.onFailed?.(error);
|
|
1059
|
+
}
|
|
1060
|
+
}
|
|
1061
|
+
fireAssetsHydrated(snapshot, generation) {
|
|
1062
|
+
if (generation !== this.previewGeneration) {
|
|
1063
|
+
return;
|
|
1064
|
+
}
|
|
1065
|
+
this.previewHydrated = true;
|
|
1066
|
+
try {
|
|
1067
|
+
this.options.onAssetsHydrated?.(snapshot);
|
|
1068
|
+
}
|
|
1069
|
+
catch (error) {
|
|
1070
|
+
this.options.onFailed?.(error);
|
|
1071
|
+
}
|
|
1072
|
+
this.flushRenderedWaiters();
|
|
1073
|
+
}
|
|
1074
|
+
flushRenderedWaiters() {
|
|
1075
|
+
const waiters = this.renderedWaiters;
|
|
1076
|
+
this.renderedWaiters = [];
|
|
1077
|
+
for (const resolve of waiters) {
|
|
1078
|
+
resolve();
|
|
1079
|
+
}
|
|
1080
|
+
}
|
|
1081
|
+
/**
|
|
1082
|
+
* Resolves once the current preview (including any images) is mounted and
|
|
1083
|
+
* hydrated. Resolves immediately when the latest preview is already ready.
|
|
1084
|
+
* Useful for revealing or animating content without observing private DOM.
|
|
1085
|
+
*/
|
|
1086
|
+
whenRendered() {
|
|
1087
|
+
if (this.previewHydrated) {
|
|
1088
|
+
return Promise.resolve();
|
|
1089
|
+
}
|
|
1090
|
+
return new Promise((resolve) => this.renderedWaiters.push(resolve));
|
|
1091
|
+
}
|
|
1092
|
+
createMarkdownContext(snapshot, signal) {
|
|
1093
|
+
return {
|
|
1094
|
+
currentPath: snapshot.currentPath,
|
|
1095
|
+
sourceFormat: snapshot.sourceFormat,
|
|
1096
|
+
colorScheme: this.colorScheme,
|
|
1097
|
+
mode: snapshot.mode,
|
|
1098
|
+
manifest: snapshot.content.manifest,
|
|
1099
|
+
assetResolver: {
|
|
1100
|
+
resolveAssetUrl: (path) => this.assetSession?.resolveKnown(path, snapshot.currentPath)
|
|
1101
|
+
},
|
|
1102
|
+
signal
|
|
1103
|
+
};
|
|
1104
|
+
}
|
|
1105
|
+
/**
|
|
1106
|
+
* Entry renderer lifecycle. Renders are keyed by
|
|
1107
|
+
* (path, pathType, mode, sourceFormat): same key keeps the mounted handle
|
|
1108
|
+
* (calling `update()` when colorScheme or manifest changed), a changed key
|
|
1109
|
+
* destroys it and re-runs matching. Rename/move/delete of the backing entry
|
|
1110
|
+
* changes the path and is therefore handled as a selection change.
|
|
1111
|
+
*/
|
|
1112
|
+
syncEntryRenderer(snapshot) {
|
|
1113
|
+
if (this.entryRenderers.length === 0) {
|
|
1114
|
+
return false;
|
|
1115
|
+
}
|
|
1116
|
+
const matchKey = [
|
|
1117
|
+
snapshot.currentPath,
|
|
1118
|
+
snapshot.currentPathType,
|
|
1119
|
+
snapshot.mode,
|
|
1120
|
+
snapshot.sourceFormat
|
|
1121
|
+
].join('\u0000');
|
|
1122
|
+
if (this.entryState?.key === matchKey) {
|
|
1123
|
+
this.maybeUpdateEntryRenderer(snapshot);
|
|
1124
|
+
return true;
|
|
1125
|
+
}
|
|
1126
|
+
if (this.entryState) {
|
|
1127
|
+
this.teardownEntryRenderer();
|
|
1128
|
+
}
|
|
1129
|
+
if (this.entryMatchMissKey === matchKey) {
|
|
1130
|
+
return false;
|
|
1131
|
+
}
|
|
1132
|
+
const abort = new AbortController();
|
|
1133
|
+
const context = this.createEntryContext(snapshot, abort.signal);
|
|
1134
|
+
const renderer = this.matchEntryRenderer(context);
|
|
1135
|
+
if (!renderer) {
|
|
1136
|
+
this.entryMatchMissKey = matchKey;
|
|
1137
|
+
return false;
|
|
1138
|
+
}
|
|
1139
|
+
this.entryMatchMissKey = null;
|
|
1140
|
+
const generation = ++this.entryGeneration;
|
|
1141
|
+
const state = {
|
|
1142
|
+
key: matchKey,
|
|
1143
|
+
renderer,
|
|
1144
|
+
handle: null,
|
|
1145
|
+
abort,
|
|
1146
|
+
lastColorScheme: this.colorScheme,
|
|
1147
|
+
lastManifest: snapshot.content.manifest
|
|
1148
|
+
};
|
|
1149
|
+
this.entryState = state;
|
|
1150
|
+
this.elEntryPane.replaceChildren();
|
|
1151
|
+
try {
|
|
1152
|
+
const mounted = renderer.mount(this.elEntryPane, context);
|
|
1153
|
+
if (isThenable(mounted)) {
|
|
1154
|
+
void Promise.resolve(mounted).then((handle) => {
|
|
1155
|
+
if (this.entryGeneration !== generation || this.entryState !== state) {
|
|
1156
|
+
// Stale mount: the selection moved on while mounting.
|
|
1157
|
+
try {
|
|
1158
|
+
handle?.destroy();
|
|
1159
|
+
}
|
|
1160
|
+
catch {
|
|
1161
|
+
// Stale handle cleanup failure is not actionable.
|
|
1162
|
+
}
|
|
1163
|
+
return;
|
|
1164
|
+
}
|
|
1165
|
+
state.handle = handle ?? null;
|
|
1166
|
+
}).catch((error) => {
|
|
1167
|
+
if (this.entryGeneration === generation) {
|
|
1168
|
+
this.options.onFailed?.(error);
|
|
1169
|
+
}
|
|
1170
|
+
});
|
|
1171
|
+
}
|
|
1172
|
+
else {
|
|
1173
|
+
state.handle = mounted ?? null;
|
|
1174
|
+
}
|
|
1175
|
+
}
|
|
1176
|
+
catch (error) {
|
|
1177
|
+
this.options.onFailed?.(error);
|
|
1178
|
+
}
|
|
1179
|
+
return true;
|
|
1180
|
+
}
|
|
1181
|
+
maybeUpdateEntryRenderer(snapshot) {
|
|
1182
|
+
const state = this.entryState;
|
|
1183
|
+
if (!state) {
|
|
1184
|
+
return;
|
|
1185
|
+
}
|
|
1186
|
+
const manifest = snapshot.content.manifest;
|
|
1187
|
+
if (state.lastColorScheme === this.colorScheme && state.lastManifest === manifest) {
|
|
1188
|
+
return;
|
|
1189
|
+
}
|
|
1190
|
+
state.lastColorScheme = this.colorScheme;
|
|
1191
|
+
state.lastManifest = manifest;
|
|
1192
|
+
if (!state.handle?.update) {
|
|
1193
|
+
return;
|
|
1194
|
+
}
|
|
1195
|
+
const context = this.createEntryContext(snapshot, state.abort.signal);
|
|
1196
|
+
try {
|
|
1197
|
+
const result = state.handle.update(context);
|
|
1198
|
+
if (isThenable(result)) {
|
|
1199
|
+
void Promise.resolve(result).catch((error) => this.options.onFailed?.(error));
|
|
1200
|
+
}
|
|
1201
|
+
}
|
|
1202
|
+
catch (error) {
|
|
1203
|
+
this.options.onFailed?.(error);
|
|
1204
|
+
}
|
|
1205
|
+
}
|
|
1206
|
+
matchEntryRenderer(context) {
|
|
1207
|
+
const byPriority = [...this.entryRenderers]
|
|
1208
|
+
.sort((a, b) => (b.priority ?? 0) - (a.priority ?? 0));
|
|
1209
|
+
for (const renderer of byPriority) {
|
|
1210
|
+
try {
|
|
1211
|
+
if (renderer.matches(context)) {
|
|
1212
|
+
return renderer;
|
|
1213
|
+
}
|
|
1214
|
+
}
|
|
1215
|
+
catch (error) {
|
|
1216
|
+
this.options.onFailed?.(error);
|
|
1217
|
+
}
|
|
1218
|
+
}
|
|
1219
|
+
return null;
|
|
1220
|
+
}
|
|
1221
|
+
teardownEntryRenderer() {
|
|
1222
|
+
const active = this.entryState;
|
|
1223
|
+
if (!active) {
|
|
1224
|
+
return;
|
|
1225
|
+
}
|
|
1226
|
+
this.entryState = null;
|
|
1227
|
+
this.entryGeneration += 1;
|
|
1228
|
+
active.abort.abort();
|
|
1229
|
+
try {
|
|
1230
|
+
active.handle?.destroy();
|
|
1231
|
+
}
|
|
1232
|
+
catch (error) {
|
|
1233
|
+
this.options.onFailed?.(error);
|
|
1234
|
+
}
|
|
1235
|
+
this.elEntryPane.replaceChildren();
|
|
1236
|
+
}
|
|
1237
|
+
createEntryContext(snapshot, signal) {
|
|
1238
|
+
const workspace = this.workspace;
|
|
1239
|
+
const path = snapshot.currentPath;
|
|
1240
|
+
return {
|
|
1241
|
+
path,
|
|
1242
|
+
pathType: snapshot.currentPathType,
|
|
1243
|
+
mode: snapshot.mode,
|
|
1244
|
+
sourceFormat: snapshot.sourceFormat,
|
|
1245
|
+
colorScheme: this.colorScheme,
|
|
1246
|
+
manifest: snapshot.content.manifest,
|
|
1247
|
+
snapshot,
|
|
1248
|
+
signal,
|
|
1249
|
+
readBytes: async () => {
|
|
1250
|
+
const bytes = await workspace?.readPathBytes(path);
|
|
1251
|
+
if (!bytes) {
|
|
1252
|
+
throw new Error(`Cannot read bytes for "${path}".`);
|
|
1253
|
+
}
|
|
1254
|
+
return bytes;
|
|
1255
|
+
},
|
|
1256
|
+
updateManifest: async (manifest) => {
|
|
1257
|
+
if (!workspace) {
|
|
1258
|
+
throw new Error('Workspace is not open.');
|
|
1259
|
+
}
|
|
1260
|
+
await workspace.updateManifest(manifest);
|
|
1261
|
+
}
|
|
1262
|
+
};
|
|
1263
|
+
}
|
|
682
1264
|
createCmEditor(parent, initialText, mode) {
|
|
683
1265
|
// eslint-disable-next-line @typescript-eslint/no-this-alias
|
|
684
1266
|
const self = this;
|
|
@@ -754,6 +1336,15 @@ export class MdzipWorkspaceView {
|
|
|
754
1336
|
}
|
|
755
1337
|
return editor;
|
|
756
1338
|
}
|
|
1339
|
+
async ensureCmEditor(force = false) {
|
|
1340
|
+
const snapshot = this.workspace?.snapshot();
|
|
1341
|
+
if (this.cmEditor || !snapshot || (!force && this.layout === 'preview')
|
|
1342
|
+
|| !canShowSourceLayout(snapshot)) {
|
|
1343
|
+
return this.cmEditor;
|
|
1344
|
+
}
|
|
1345
|
+
this.cmEditor = this.createCmEditor(this.elEditorHost, snapshot.currentText, snapshot.mode);
|
|
1346
|
+
return this.cmEditor;
|
|
1347
|
+
}
|
|
757
1348
|
render() {
|
|
758
1349
|
const snapshot = this.workspace?.snapshot() ?? null;
|
|
759
1350
|
this.elEmptyState.hidden = snapshot !== null;
|
|
@@ -764,7 +1355,9 @@ export class MdzipWorkspaceView {
|
|
|
764
1355
|
return;
|
|
765
1356
|
}
|
|
766
1357
|
this.layout = this.validLayoutForSnapshot(this.layout, snapshot);
|
|
767
|
-
const
|
|
1358
|
+
const entryClaimed = this.syncEntryRenderer(snapshot);
|
|
1359
|
+
const canEdit = !entryClaimed
|
|
1360
|
+
&& canEditMdzipPath(snapshot.currentPathType, snapshot.currentPath, snapshot.mode);
|
|
768
1361
|
const canShowSource = canShowSourceLayout(snapshot);
|
|
769
1362
|
const showNavigationControl = this.controlPolicy.navigation;
|
|
770
1363
|
const showTitleControl = this.controlPolicy.title.visible;
|
|
@@ -877,14 +1470,16 @@ export class MdzipWorkspaceView {
|
|
|
877
1470
|
effects: this.readOnlyCompartment.reconfigure(EditorState.readOnly.of(snapshot.mode === 'read-only'))
|
|
878
1471
|
});
|
|
879
1472
|
}
|
|
880
|
-
this.
|
|
1473
|
+
this.updatePreview(snapshot, entryClaimed);
|
|
881
1474
|
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');
|
|
1475
|
+
const showEdit = !entryClaimed && (pt === 'markdown' || pt === 'text') && this.layout !== 'preview';
|
|
1476
|
+
const showPreview = !entryClaimed && (pt === 'image' || pt === 'binary' || pt === 'text'
|
|
1477
|
+
|| (pt === 'markdown' && this.layout !== 'source'));
|
|
885
1478
|
this.elEditPane.classList.toggle('active', showEdit);
|
|
886
1479
|
this.elPreviewPane.classList.toggle('active', showPreview);
|
|
887
|
-
this.
|
|
1480
|
+
this.elEntryPane.classList.toggle('active', entryClaimed);
|
|
1481
|
+
this.elPaneStack.classList.toggle('split-mode', this.layout === 'split' && !entryClaimed);
|
|
1482
|
+
this.elPaneStack.classList.toggle('entry-claimed', entryClaimed);
|
|
888
1483
|
if (!showTitleControl) {
|
|
889
1484
|
this.titleDialogOpen = false;
|
|
890
1485
|
this.metadataDialogOpen = false;
|
|
@@ -1026,17 +1621,17 @@ export class MdzipWorkspaceView {
|
|
|
1026
1621
|
});
|
|
1027
1622
|
this.elPreviewBtn.addEventListener('click', () => {
|
|
1028
1623
|
if (this.controlPolicy.layout.preview) {
|
|
1029
|
-
this.setLayout('preview');
|
|
1624
|
+
void this.setLayout('preview');
|
|
1030
1625
|
}
|
|
1031
1626
|
});
|
|
1032
1627
|
this.elSplitBtn.addEventListener('click', () => {
|
|
1033
1628
|
if (this.controlPolicy.layout.split) {
|
|
1034
|
-
this.setLayout('split');
|
|
1629
|
+
void this.setLayout('split');
|
|
1035
1630
|
}
|
|
1036
1631
|
});
|
|
1037
1632
|
this.elSourceBtn.addEventListener('click', () => {
|
|
1038
1633
|
if (this.controlPolicy.layout.source) {
|
|
1039
|
-
this.setLayout('source');
|
|
1634
|
+
void this.setLayout('source');
|
|
1040
1635
|
}
|
|
1041
1636
|
});
|
|
1042
1637
|
this.elSaveBtn.addEventListener('click', () => {
|
|
@@ -1477,16 +2072,58 @@ export class MdzipWorkspaceView {
|
|
|
1477
2072
|
['Entry point', snapshot.sourceFormat === 'mdz' ? snapshot.content.entryPoint : 'Not applicable']
|
|
1478
2073
|
];
|
|
1479
2074
|
this.elMetadataList.replaceChildren(...fields.map(([label, value]) => {
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
return
|
|
2075
|
+
return this.createMetadataRow(label, value);
|
|
2076
|
+
}));
|
|
2077
|
+
const libraries = [
|
|
2078
|
+
...(this.options.libraries ?? []),
|
|
2079
|
+
...MDZIP_RUNTIME_LIBRARIES
|
|
2080
|
+
].filter((library, index, all) => all.findIndex((candidate) => candidate.name === library.name) === index);
|
|
2081
|
+
this.elLibraryList.replaceChildren(...libraries.map((library) => {
|
|
2082
|
+
return this.createLibraryRow(library);
|
|
1488
2083
|
}));
|
|
1489
2084
|
}
|
|
2085
|
+
createMetadataRow(label, value) {
|
|
2086
|
+
const row = this.elRoot.ownerDocument.createElement('div');
|
|
2087
|
+
row.className = 'metadata-row';
|
|
2088
|
+
const term = this.elRoot.ownerDocument.createElement('dt');
|
|
2089
|
+
term.textContent = label;
|
|
2090
|
+
const detail = this.elRoot.ownerDocument.createElement('dd');
|
|
2091
|
+
detail.textContent = value;
|
|
2092
|
+
row.append(term, detail);
|
|
2093
|
+
return row;
|
|
2094
|
+
}
|
|
2095
|
+
createLibraryRow(library) {
|
|
2096
|
+
const row = this.elRoot.ownerDocument.createElement('div');
|
|
2097
|
+
row.className = 'metadata-row library-row';
|
|
2098
|
+
const term = this.elRoot.ownerDocument.createElement('dt');
|
|
2099
|
+
const name = this.elRoot.ownerDocument.createElement('span');
|
|
2100
|
+
name.className = 'library-name';
|
|
2101
|
+
if (library.repositoryUrl) {
|
|
2102
|
+
const link = this.elRoot.ownerDocument.createElement('a');
|
|
2103
|
+
link.href = library.repositoryUrl;
|
|
2104
|
+
link.target = '_blank';
|
|
2105
|
+
link.rel = 'noopener noreferrer';
|
|
2106
|
+
link.textContent = library.name;
|
|
2107
|
+
link.setAttribute('aria-label', `${library.name} repository`);
|
|
2108
|
+
name.append(link);
|
|
2109
|
+
}
|
|
2110
|
+
else {
|
|
2111
|
+
name.textContent = library.name;
|
|
2112
|
+
}
|
|
2113
|
+
const version = this.elRoot.ownerDocument.createElement('span');
|
|
2114
|
+
version.className = 'library-version';
|
|
2115
|
+
version.textContent = library.version;
|
|
2116
|
+
term.append(name, version);
|
|
2117
|
+
const detail = this.elRoot.ownerDocument.createElement('dd');
|
|
2118
|
+
if (library.description) {
|
|
2119
|
+
const description = this.elRoot.ownerDocument.createElement('span');
|
|
2120
|
+
description.className = 'library-description';
|
|
2121
|
+
description.textContent = library.description;
|
|
2122
|
+
detail.append(description);
|
|
2123
|
+
}
|
|
2124
|
+
row.append(term, detail);
|
|
2125
|
+
return row;
|
|
2126
|
+
}
|
|
1490
2127
|
requestMdzConversion(action) {
|
|
1491
2128
|
const snapshot = this.workspace?.snapshot();
|
|
1492
2129
|
if (!snapshot || snapshot.mode === 'read-only' || snapshot.sourceFormat !== 'markdown') {
|
|
@@ -1501,8 +2138,9 @@ export class MdzipWorkspaceView {
|
|
|
1501
2138
|
return;
|
|
1502
2139
|
}
|
|
1503
2140
|
this.conversionHookPending = true;
|
|
2141
|
+
const context = this.createConversionContext(action);
|
|
1504
2142
|
void Promise.resolve()
|
|
1505
|
-
.then(() => hook(action))
|
|
2143
|
+
.then(() => hook(action, context))
|
|
1506
2144
|
.then((handled) => {
|
|
1507
2145
|
this.conversionHookPending = false;
|
|
1508
2146
|
if (!handled) {
|
|
@@ -1515,6 +2153,75 @@ export class MdzipWorkspaceView {
|
|
|
1515
2153
|
this.openConversionDialog(action);
|
|
1516
2154
|
});
|
|
1517
2155
|
}
|
|
2156
|
+
createConversionContext(action) {
|
|
2157
|
+
const workspace = this.workspace;
|
|
2158
|
+
const snapshot = workspace?.snapshot();
|
|
2159
|
+
const selection = this.cmEditor?.state.selection.main;
|
|
2160
|
+
const captured = workspace && snapshot ? {
|
|
2161
|
+
workspace,
|
|
2162
|
+
documentGeneration: this.conversionDocumentGeneration,
|
|
2163
|
+
path: snapshot.currentPath,
|
|
2164
|
+
text: snapshot.currentText,
|
|
2165
|
+
selectionStart: selection?.from,
|
|
2166
|
+
selectionEnd: selection?.to
|
|
2167
|
+
} : null;
|
|
2168
|
+
let consumed = false;
|
|
2169
|
+
const take = () => {
|
|
2170
|
+
if (consumed || !captured || this.workspace !== captured.workspace
|
|
2171
|
+
|| this.conversionDocumentGeneration !== captured.documentGeneration) {
|
|
2172
|
+
return null;
|
|
2173
|
+
}
|
|
2174
|
+
const current = captured.workspace.snapshot();
|
|
2175
|
+
if (current.mode === 'read-only'
|
|
2176
|
+
|| current.sourceFormat !== 'markdown'
|
|
2177
|
+
|| current.currentPath !== captured.path
|
|
2178
|
+
|| current.currentText !== captured.text) {
|
|
2179
|
+
return null;
|
|
2180
|
+
}
|
|
2181
|
+
consumed = true;
|
|
2182
|
+
return captured;
|
|
2183
|
+
};
|
|
2184
|
+
return {
|
|
2185
|
+
insertMarkdown: async (text) => {
|
|
2186
|
+
const target = take();
|
|
2187
|
+
if (!target || target.selectionStart === undefined || target.selectionEnd === undefined) {
|
|
2188
|
+
return false;
|
|
2189
|
+
}
|
|
2190
|
+
const nextText = target.text.slice(0, target.selectionStart)
|
|
2191
|
+
+ text
|
|
2192
|
+
+ target.text.slice(target.selectionEnd);
|
|
2193
|
+
target.workspace.editText(nextText);
|
|
2194
|
+
this.render();
|
|
2195
|
+
const editor = await this.ensureCmEditor();
|
|
2196
|
+
if (editor) {
|
|
2197
|
+
editor.dispatch({ selection: { anchor: target.selectionStart + text.length } });
|
|
2198
|
+
editor.focus();
|
|
2199
|
+
}
|
|
2200
|
+
return true;
|
|
2201
|
+
},
|
|
2202
|
+
convertToMdz: async () => {
|
|
2203
|
+
const target = take();
|
|
2204
|
+
if (!target || !await this.convertToMdz()) {
|
|
2205
|
+
return false;
|
|
2206
|
+
}
|
|
2207
|
+
if (action.kind === 'navigation') {
|
|
2208
|
+
this.navVisible = true;
|
|
2209
|
+
this.render();
|
|
2210
|
+
return true;
|
|
2211
|
+
}
|
|
2212
|
+
const editor = await this.ensureCmEditor();
|
|
2213
|
+
if (editor && target.selectionStart !== undefined) {
|
|
2214
|
+
editor.dispatch({ selection: { anchor: target.selectionStart } });
|
|
2215
|
+
}
|
|
2216
|
+
if (action.kind === 'image-file') {
|
|
2217
|
+
await this.insertImageFile(action.file);
|
|
2218
|
+
return true;
|
|
2219
|
+
}
|
|
2220
|
+
this.elImageInput.click();
|
|
2221
|
+
return true;
|
|
2222
|
+
}
|
|
2223
|
+
};
|
|
2224
|
+
}
|
|
1518
2225
|
openConversionDialog(action) {
|
|
1519
2226
|
this.conversionAction = action;
|
|
1520
2227
|
this.render();
|
|
@@ -1857,9 +2564,10 @@ export class MdzipWorkspaceView {
|
|
|
1857
2564
|
this.setColorScheme(colorScheme);
|
|
1858
2565
|
this.options.onColorSchemeChanged?.(colorScheme);
|
|
1859
2566
|
}
|
|
1860
|
-
setLayout(requested) {
|
|
2567
|
+
async setLayout(requested) {
|
|
1861
2568
|
const snapshot = this.workspace?.snapshot();
|
|
1862
2569
|
this.layout = snapshot ? this.validLayoutForSnapshot(requested, snapshot) : requested;
|
|
2570
|
+
await this.ensureCmEditor();
|
|
1863
2571
|
this.render();
|
|
1864
2572
|
}
|
|
1865
2573
|
navMenuTargetFromElement(element) {
|
|
@@ -2609,6 +3317,7 @@ const SHELL_HTML = `
|
|
|
2609
3317
|
<section class="pane preview-pane" data-ref="preview-pane">
|
|
2610
3318
|
<article class="preview-content" data-ref="preview-content"></article>
|
|
2611
3319
|
</section>
|
|
3320
|
+
<section class="pane entry-pane" data-ref="entry-pane"></section>
|
|
2612
3321
|
</div>
|
|
2613
3322
|
</div>
|
|
2614
3323
|
|
|
@@ -2649,6 +3358,8 @@ const SHELL_HTML = `
|
|
|
2649
3358
|
<div class="title-dialog metadata-dialog">
|
|
2650
3359
|
<h3 id="mdzip-metadata-dialog-heading">Document Information</h3>
|
|
2651
3360
|
<dl data-ref="metadata-list"></dl>
|
|
3361
|
+
<h4>Libraries</h4>
|
|
3362
|
+
<dl data-ref="library-list"></dl>
|
|
2652
3363
|
<div class="title-dialog-actions">
|
|
2653
3364
|
<button type="button" class="save-title" data-action="close-metadata">Close</button>
|
|
2654
3365
|
</div>
|