@mdzip/editor 1.4.0 → 1.4.4
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 +35 -0
- package/dist/asset-cache.d.ts +2 -0
- package/dist/asset-cache.d.ts.map +1 -1
- package/dist/asset-cache.js +12 -0
- package/dist/asset-cache.js.map +1 -1
- package/dist/front-matter-extension.d.ts +45 -0
- package/dist/front-matter-extension.d.ts.map +1 -0
- package/dist/front-matter-extension.js +114 -0
- package/dist/front-matter-extension.js.map +1 -0
- package/dist/front-matter.d.ts +18 -0
- package/dist/front-matter.d.ts.map +1 -0
- package/dist/front-matter.js +48 -0
- package/dist/front-matter.js.map +1 -0
- package/dist/highlight-core.d.ts +3 -0
- package/dist/highlight-core.d.ts.map +1 -0
- package/dist/highlight-core.js +53 -0
- package/dist/highlight-core.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/library-info.d.ts +1 -1
- package/dist/library-info.js +1 -1
- package/dist/mermaid.d.ts.map +1 -1
- package/dist/mermaid.js +60 -41
- package/dist/mermaid.js.map +1 -1
- package/dist/metadata.d.ts +1 -1
- package/dist/metadata.d.ts.map +1 -1
- package/dist/metadata.js +12 -2
- package/dist/metadata.js.map +1 -1
- package/dist/preview.d.ts +1 -0
- package/dist/preview.d.ts.map +1 -1
- package/dist/preview.js +1 -0
- package/dist/preview.js.map +1 -1
- package/dist/rendering.d.ts +71 -1
- package/dist/rendering.d.ts.map +1 -1
- package/dist/rendering.js +62 -2
- package/dist/rendering.js.map +1 -1
- package/dist/view-css.d.ts.map +1 -1
- package/dist/view-css.js +67 -0
- package/dist/view-css.js.map +1 -1
- package/dist/view.d.ts +112 -10
- package/dist/view.d.ts.map +1 -1
- package/dist/view.js +940 -410
- package/dist/view.js.map +1 -1
- package/package.json +3 -1
package/dist/view.js
CHANGED
|
@@ -14,7 +14,8 @@ import { MDZIP_RUNTIME_LIBRARIES } from './library-info.js';
|
|
|
14
14
|
import { MdzipWorkspaceService, extensionForMime, normalizeArchivePath, relativeArchivePath } from './workspace.js';
|
|
15
15
|
import { buildMdzipNavTree, canEditMdzipPath, escapeHtml, isOrphanedMdzipAsset, mdzipEntryIconKind, isMdzipManifestPath, resolveMdzipArchiveLinkTarget, renderMdzipPreviewHtml } from './workspace-view.js';
|
|
16
16
|
import { escapeMarkdownImageAlt, findImageReferenceAtOffset, formatImageEditMarkdown } from './image-edit.js';
|
|
17
|
-
import { MdzipRenderingService, defaultSafeMarkdownRenderer, groupTokensIntoChunks } from './rendering.js';
|
|
17
|
+
import { MdzipRenderingService, defaultSafeMarkdownRenderer, groupTokensIntoChunks, chunkSourceKey, tokenEmbedsImage, tokenIsHeading } from './rendering.js';
|
|
18
|
+
import { mdzipFrontMatterExtension } from './front-matter-extension.js';
|
|
18
19
|
import { WORKSPACE_CSS } from './view-css.js';
|
|
19
20
|
const STYLE_ATTR = 'data-mdzip-ws-styles';
|
|
20
21
|
const IMAGE_EXTENSIONS = /\.(jpg|jpeg|png|gif|webp|svg|bmp|ico|tiff?)$/i;
|
|
@@ -745,19 +746,19 @@ depth = 0) {
|
|
|
745
746
|
: isImageFile(node.entry.path)
|
|
746
747
|
? IMAGE_ICON_HTML
|
|
747
748
|
: FILE_ICON_HTML;
|
|
748
|
-
const orphanBtnHtml = isOrphaned && options.allowOrphanActions ? `
|
|
749
|
-
<span class="nav-orphan-button" role="button" tabindex="0"
|
|
750
|
-
title="Orphaned asset" aria-label="Orphaned asset actions"
|
|
751
|
-
data-orphan-path="${safePath}">
|
|
752
|
-
${ORPHAN_ICON_HTML}
|
|
749
|
+
const orphanBtnHtml = isOrphaned && options.allowOrphanActions ? `
|
|
750
|
+
<span class="nav-orphan-button" role="button" tabindex="0"
|
|
751
|
+
title="Orphaned asset" aria-label="Orphaned asset actions"
|
|
752
|
+
data-orphan-path="${safePath}">
|
|
753
|
+
${ORPHAN_ICON_HTML}
|
|
753
754
|
</span>` : '';
|
|
754
755
|
const draggable = options.allowDrag && !isManifest;
|
|
755
|
-
return `<button type="button" class="${classes}" title="${title}"
|
|
756
|
-
data-nav-path="${safePath}" data-orphan="${isOrphaned ? 'true' : ''}"${draggable ? ' draggable="true"' : ''}>
|
|
757
|
-
${guides}<span class="nav-caret"></span>
|
|
758
|
-
<span class="nav-file-icon ${iconKind}">${iconHtml}</span>
|
|
759
|
-
${orphanBtnHtml}
|
|
760
|
-
<span class="nav-label">${safeName}</span>
|
|
756
|
+
return `<button type="button" class="${classes}" title="${title}"
|
|
757
|
+
data-nav-path="${safePath}" data-orphan="${isOrphaned ? 'true' : ''}"${draggable ? ' draggable="true"' : ''}>
|
|
758
|
+
${guides}<span class="nav-caret"></span>
|
|
759
|
+
<span class="nav-file-icon ${iconKind}">${iconHtml}</span>
|
|
760
|
+
${orphanBtnHtml}
|
|
761
|
+
<span class="nav-label">${safeName}</span>
|
|
761
762
|
</button>`;
|
|
762
763
|
}
|
|
763
764
|
const childTrail = depth === 0 ? [] : [...trail, !isLast];
|
|
@@ -765,14 +766,14 @@ depth = 0) {
|
|
|
765
766
|
.map((child, index) => renderNavNode(child, state, options, childTrail, index === node.children.length - 1, depth + 1))
|
|
766
767
|
.join('');
|
|
767
768
|
const pending = options.pendingFolders.has(node.path.toLowerCase());
|
|
768
|
-
return `<details class="nav-directory${pending ? ' pending-folder' : ''}" open data-nav-dir="${escapeHtml(node.path)}">
|
|
769
|
-
<summary${pending ? ` title="${escapeHtml(node.path)} — not saved until it contains a file"` : ''}>
|
|
770
|
-
${guides}<span class="nav-caret" aria-hidden="true"></span>
|
|
771
|
-
<span class="nav-folder-icon closed">${FOLDER_CLOSED_ICON_HTML}</span>
|
|
772
|
-
<span class="nav-folder-icon open">${FOLDER_OPEN_ICON_HTML}</span>
|
|
773
|
-
<span class="nav-label">${escapeHtml(node.name)}</span>
|
|
774
|
-
</summary>
|
|
775
|
-
<div class="nav-directory-children">${children}</div>
|
|
769
|
+
return `<details class="nav-directory${pending ? ' pending-folder' : ''}" open data-nav-dir="${escapeHtml(node.path)}">
|
|
770
|
+
<summary${pending ? ` title="${escapeHtml(node.path)} — not saved until it contains a file"` : ''}>
|
|
771
|
+
${guides}<span class="nav-caret" aria-hidden="true"></span>
|
|
772
|
+
<span class="nav-folder-icon closed">${FOLDER_CLOSED_ICON_HTML}</span>
|
|
773
|
+
<span class="nav-folder-icon open">${FOLDER_OPEN_ICON_HTML}</span>
|
|
774
|
+
<span class="nav-label">${escapeHtml(node.name)}</span>
|
|
775
|
+
</summary>
|
|
776
|
+
<div class="nav-directory-children">${children}</div>
|
|
776
777
|
</details>`;
|
|
777
778
|
}
|
|
778
779
|
// Inserts view-local pending (not yet saved) folders into the nav tree so they
|
|
@@ -838,6 +839,8 @@ function rawHtmlImageAlignClass(image) {
|
|
|
838
839
|
return null;
|
|
839
840
|
}
|
|
840
841
|
export class MdzipWorkspaceView {
|
|
842
|
+
static { this.SCROLL_SYNC_SETTLE_MS = 250; }
|
|
843
|
+
static { this.SCROLL_SYNC_GESTURE_WINDOW_MS = 500; }
|
|
841
844
|
constructor(container, options = {}) {
|
|
842
845
|
this.workspace = null;
|
|
843
846
|
this.assetSession = null;
|
|
@@ -859,12 +862,19 @@ export class MdzipWorkspaceView {
|
|
|
859
862
|
this.packFilesDialogState = null;
|
|
860
863
|
// Tracks an in-progress progressive (chunked) preview render so Copy All
|
|
861
864
|
// can force-drain whatever's left unmounted. `cursor` is how many of
|
|
862
|
-
// `
|
|
865
|
+
// `records` are mounted so far; null once everything's mounted (or when
|
|
863
866
|
// progressive rendering isn't active at all) — that's Copy All's signal
|
|
864
867
|
// to skip the dialog and copy instantly. `sentinelHandle` is the
|
|
865
868
|
// scroll-driven continuation's IntersectionObserver, if one is currently
|
|
866
869
|
// armed; Copy All tears it down before manually draining so the two don't
|
|
867
870
|
// race and double-mount the same chunk.
|
|
871
|
+
//
|
|
872
|
+
// Each record carries its own mounted DOM root and extension/code-block
|
|
873
|
+
// handles (rather than those living only in the flat `previewHandles`
|
|
874
|
+
// array below) so that a same-document text edit can reconcile against the
|
|
875
|
+
// *previous* render's records: chunks whose `chunkSourceKey` didn't change
|
|
876
|
+
// keep their existing `root`/`handles` untouched — see
|
|
877
|
+
// `applyChunkReconciliation`.
|
|
868
878
|
this.chunkedRenderState = null;
|
|
869
879
|
// Per-generation memo of renderChunk's raw HTML output, keyed by chunk
|
|
870
880
|
// index, shared between the DOM-mount path (renderAndMountChunkBatch) and
|
|
@@ -946,6 +956,52 @@ export class MdzipWorkspaceView {
|
|
|
946
956
|
// syncScrollFromPreview/syncScrollToPreview).
|
|
947
957
|
this.lastSyncedEditorScrollTop = null;
|
|
948
958
|
this.lastSyncedPreviewScrollTop = null;
|
|
959
|
+
// Timestamp of the most recent updatePreview() call (edit-driven or
|
|
960
|
+
// otherwise). A content change that alters a mounted element's rendered
|
|
961
|
+
// height (a chunk reconcile re-rendering a mermaid diagram, an image
|
|
962
|
+
// finishing decode, CodeMirror re-measuring line heights after the edit
|
|
963
|
+
// that triggered it) can cause a 'scroll' event with no explicit write
|
|
964
|
+
// behind it — the browser's own scroll anchoring, or CodeMirror's internal
|
|
965
|
+
// viewport/anchor recalculation, adjusting scrollTop on its own to keep
|
|
966
|
+
// content visually pinned. That's indistinguishable from a real user
|
|
967
|
+
// scroll to the listeners below, so within a brief window after a content
|
|
968
|
+
// change we treat scroll events as noise from settling layout rather than
|
|
969
|
+
// sync-worthy user intent — otherwise this class's own ratio-based writes
|
|
970
|
+
// (raw pixel values that don't correspond to any document position
|
|
971
|
+
// CodeMirror has actually measured) can themselves destabilize
|
|
972
|
+
// CodeMirror's virtualized viewport measurement mid-settle.
|
|
973
|
+
this.lastContentChangeTime = 0;
|
|
974
|
+
// Timestamp of the most recent genuine user-input gesture (wheel, touch,
|
|
975
|
+
// mousedown — including a scrollbar-thumb drag, which lands on the same
|
|
976
|
+
// element — or a key press) on each pane. Content-change-adjacent 'scroll'
|
|
977
|
+
// events (see lastContentChangeTime above) turned out to be one of several
|
|
978
|
+
// distinct ways a non-user-driven scroll can fire: native CSS scroll
|
|
979
|
+
// anchoring reacting to an unrelated element's height changing — a chunk
|
|
980
|
+
// reconcile, an image decode, or (confirmed live) an image/mermaid reveal
|
|
981
|
+
// transition finishing several seconds after the edit that triggered it —
|
|
982
|
+
// can smoothly drive scrollTop, frame by frame, all the way to a pane's
|
|
983
|
+
// absolute end, entirely decoupled from any edit and well outside any
|
|
984
|
+
// settle window. Rather than continuing to special-case each newly
|
|
985
|
+
// discovered non-user cause after the fact, sync is now gated on positive
|
|
986
|
+
// evidence of real user intent: a 'scroll' event is only treated as
|
|
987
|
+
// sync-worthy if a genuine input gesture on that same pane happened very
|
|
988
|
+
// recently. Everything else — anchoring, our own reconcile restores,
|
|
989
|
+
// CodeMirror's internal viewport correction — is left alone by definition.
|
|
990
|
+
this.lastEditorGestureTime = 0;
|
|
991
|
+
this.lastPreviewGestureTime = 0;
|
|
992
|
+
// Every preview re-render — whether reconciled or a full cold-start reset
|
|
993
|
+
// (mountChunkedPreview unconditionally does `elPreviewContent.
|
|
994
|
+
// replaceChildren()`) — has a real window where the preview is shorter
|
|
995
|
+
// than before (old DOM torn down, replacement mounted asynchronously: a
|
|
996
|
+
// chunk reconcile, a full reset, mermaid's async completion). If the
|
|
997
|
+
// current scroll position no longer fits that transient shrink, the
|
|
998
|
+
// browser clamps scrollTop to what does fit and never un-clamps once the
|
|
999
|
+
// replacement's DOM restores the true height. Captured once per
|
|
1000
|
+
// updatePreview() call (before anything moves), restored in
|
|
1001
|
+
// firePreviewRendered — the one point every rendering path (reconcile,
|
|
1002
|
+
// cold-start, progressive, eager, mermaid-triggered or not) already calls
|
|
1003
|
+
// once its content is back in the DOM.
|
|
1004
|
+
this.pendingPreviewScrollRestore = null;
|
|
949
1005
|
// Guards syncScrollToPreviewBottom against overlapping drains: set to the
|
|
950
1006
|
// chunkedRenderState generation currently being force-drained, null when
|
|
951
1007
|
// none is in flight. A second bottom-edge sync that arrives mid-drain
|
|
@@ -957,6 +1013,7 @@ export class MdzipWorkspaceView {
|
|
|
957
1013
|
// debounce) — see syncScrollToPreviewBottom.
|
|
958
1014
|
this.scrollCatchUpState = null;
|
|
959
1015
|
this.markdownExtensions = [];
|
|
1016
|
+
this.frontMatterOptions = {};
|
|
960
1017
|
this.entryRenderers = [];
|
|
961
1018
|
this.renderingService = new MdzipRenderingService();
|
|
962
1019
|
// Preview render memo: the preview pipeline only re-runs when one of these
|
|
@@ -991,8 +1048,9 @@ export class MdzipWorkspaceView {
|
|
|
991
1048
|
this.navVisible = options.navigationButtonActive ?? this.navVisible;
|
|
992
1049
|
this.markdownRenderer = options.markdownRenderer;
|
|
993
1050
|
this.markdownExtensions = options.markdownExtensions ?? [];
|
|
1051
|
+
this.frontMatterOptions = options.frontMatter ?? {};
|
|
994
1052
|
this.entryRenderers = options.entryRenderers ?? [];
|
|
995
|
-
this.renderingService = new MdzipRenderingService(this.markdownRenderer ?? defaultSafeMarkdownRenderer, this.
|
|
1053
|
+
this.renderingService = new MdzipRenderingService(this.markdownRenderer ?? defaultSafeMarkdownRenderer, this.pipelineMarkdownExtensions());
|
|
996
1054
|
injectStyles(container.ownerDocument);
|
|
997
1055
|
container.replaceChildren();
|
|
998
1056
|
container.innerHTML = SHELL_HTML;
|
|
@@ -1407,15 +1465,46 @@ export class MdzipWorkspaceView {
|
|
|
1407
1465
|
if (options.markdownExtensions) {
|
|
1408
1466
|
this.markdownExtensions = options.markdownExtensions;
|
|
1409
1467
|
}
|
|
1468
|
+
if (options.frontMatter) {
|
|
1469
|
+
this.frontMatterOptions = options.frontMatter;
|
|
1470
|
+
}
|
|
1410
1471
|
if (options.entryRenderers) {
|
|
1411
1472
|
this.entryRenderers = options.entryRenderers;
|
|
1412
1473
|
}
|
|
1413
|
-
this.renderingService = new MdzipRenderingService(this.markdownRenderer ?? defaultSafeMarkdownRenderer, this.
|
|
1474
|
+
this.renderingService = new MdzipRenderingService(this.markdownRenderer ?? defaultSafeMarkdownRenderer, this.pipelineMarkdownExtensions());
|
|
1414
1475
|
this.resetPreviewState();
|
|
1415
1476
|
this.teardownEntryRenderer();
|
|
1416
1477
|
this.entryMatchMissKey = null;
|
|
1417
1478
|
this.render();
|
|
1418
1479
|
}
|
|
1480
|
+
/**
|
|
1481
|
+
* The extensions actually handed to `MdzipRenderingService`: the front
|
|
1482
|
+
* matter extension first (always on — see `frontMatterOptions`), then
|
|
1483
|
+
* whatever extensions the host passed via `markdownExtensions`. A fresh
|
|
1484
|
+
* front matter extension instance is created each time this runs (view
|
|
1485
|
+
* construction, `setRenderingOptions`), which is fine — its per-render
|
|
1486
|
+
* state lives in a `WeakMap` keyed by the render context, not on the
|
|
1487
|
+
* extension instance across renders.
|
|
1488
|
+
*/
|
|
1489
|
+
pipelineMarkdownExtensions() {
|
|
1490
|
+
return [mdzipFrontMatterExtension(this.frontMatterOptions), ...this.markdownExtensions];
|
|
1491
|
+
}
|
|
1492
|
+
/**
|
|
1493
|
+
* Chunking options combining every registered extension's
|
|
1494
|
+
* {@link MdzipMarkdownRenderExtension.shouldIsolateChunk} — see that doc
|
|
1495
|
+
* comment for why a token an extension owns needs its own chunk regardless
|
|
1496
|
+
* of the size budget — with the same treatment built in for any token that
|
|
1497
|
+
* embeds an image ({@link tokenEmbedsImage}'s doc comment), plus a forced
|
|
1498
|
+
* chunk boundary at every heading ({@link tokenIsHeading}'s doc comment)
|
|
1499
|
+
* so unrelated sections never share a chunk.
|
|
1500
|
+
*/
|
|
1501
|
+
chunkOptions() {
|
|
1502
|
+
const extensions = this.pipelineMarkdownExtensions();
|
|
1503
|
+
return {
|
|
1504
|
+
shouldIsolate: (token) => tokenEmbedsImage(token) || extensions.some((ext) => ext.shouldIsolateChunk?.(token)),
|
|
1505
|
+
shouldStartChunk: tokenIsHeading
|
|
1506
|
+
};
|
|
1507
|
+
}
|
|
1419
1508
|
/**
|
|
1420
1509
|
* Replaces the view control policy without recreating the workspace or
|
|
1421
1510
|
* CodeMirror editor. In particular, `lineNumbers` is reconfigured through a
|
|
@@ -1534,6 +1623,19 @@ export class MdzipWorkspaceView {
|
|
|
1534
1623
|
this.options.onFailed?.(error);
|
|
1535
1624
|
}
|
|
1536
1625
|
}
|
|
1626
|
+
// Per-chunk handles live on their own records (see MdzipChunkRecord), not
|
|
1627
|
+
// in the flat array above — a full reset must still destroy every one of
|
|
1628
|
+
// them, the same as it always destroyed everything in previewHandles.
|
|
1629
|
+
for (const record of this.chunkedRenderState?.records ?? []) {
|
|
1630
|
+
for (const handle of record.handles) {
|
|
1631
|
+
try {
|
|
1632
|
+
handle.destroy();
|
|
1633
|
+
}
|
|
1634
|
+
catch (error) {
|
|
1635
|
+
this.options.onFailed?.(error);
|
|
1636
|
+
}
|
|
1637
|
+
}
|
|
1638
|
+
}
|
|
1537
1639
|
// A new render generation invalidates any in-progress chunk draining —
|
|
1538
1640
|
// Copy All's own abort check unwinds it and hides the dialog. It also
|
|
1539
1641
|
// invalidates a "ready to copy" or "done" dialog left over from a
|
|
@@ -1556,6 +1658,10 @@ export class MdzipWorkspaceView {
|
|
|
1556
1658
|
&& memo.colorScheme === this.colorScheme;
|
|
1557
1659
|
}
|
|
1558
1660
|
updatePreview(snapshot, entryClaimed) {
|
|
1661
|
+
this.lastContentChangeTime = performance.now();
|
|
1662
|
+
// Captured before anything below can move it — see
|
|
1663
|
+
// `pendingPreviewScrollRestore`'s doc comment.
|
|
1664
|
+
const previewScrollTopBeforeUpdate = this.elPreviewPane.scrollTop;
|
|
1559
1665
|
if (entryClaimed) {
|
|
1560
1666
|
// The entry renderer owns the pane stack; release preview resources so
|
|
1561
1667
|
// a later fallback re-renders from scratch.
|
|
@@ -1575,10 +1681,33 @@ export class MdzipWorkspaceView {
|
|
|
1575
1681
|
return;
|
|
1576
1682
|
}
|
|
1577
1683
|
const animateImageHydration = this.shouldAnimateImageHydration(memo, snapshot);
|
|
1684
|
+
// A same-document, text-only edit with an existing chunked structure can
|
|
1685
|
+
// be reconciled against (only the chunks whose source actually changed
|
|
1686
|
+
// get touched) instead of torn down and rebuilt from scratch. Every
|
|
1687
|
+
// other case — first render, a path/pathType/colorScheme change, a
|
|
1688
|
+
// non-default renderer, a prior explicit reset that already cleared
|
|
1689
|
+
// chunkedRenderState, or an edit-triggered batch mount still in flight
|
|
1690
|
+
// for the current generation (`mounting` — see its doc comment: its
|
|
1691
|
+
// `cursor` doesn't yet reflect where that mount actually is, so
|
|
1692
|
+
// reconciling against it risks orphaned duplicate DOM) — falls through
|
|
1693
|
+
// to today's full reset + cold start, unchanged.
|
|
1694
|
+
const reconcileEligible = !!memo
|
|
1695
|
+
&& memo.path === snapshot.currentPath
|
|
1696
|
+
&& memo.pathType === snapshot.currentPathType
|
|
1697
|
+
&& memo.colorScheme === this.colorScheme
|
|
1698
|
+
&& snapshot.currentPathType === 'markdown'
|
|
1699
|
+
&& this.renderingService.supportsChunking
|
|
1700
|
+
&& !!this.chunkedRenderState
|
|
1701
|
+
&& this.chunkedRenderState.generation === this.previewGeneration
|
|
1702
|
+
&& !this.chunkedRenderState.mounting;
|
|
1703
|
+
const priorChunkedState = reconcileEligible ? this.chunkedRenderState : null;
|
|
1578
1704
|
this.previewAbort?.abort();
|
|
1579
1705
|
this.previewAbort = null;
|
|
1580
|
-
|
|
1706
|
+
if (!priorChunkedState) {
|
|
1707
|
+
this.destroyPreviewHandles();
|
|
1708
|
+
}
|
|
1581
1709
|
const generation = ++this.previewGeneration;
|
|
1710
|
+
this.pendingPreviewScrollRestore = { scrollTop: previewScrollTopBeforeUpdate, generation };
|
|
1582
1711
|
this.previewHydrated = false;
|
|
1583
1712
|
this.previewMemo = {
|
|
1584
1713
|
path: snapshot.currentPath,
|
|
@@ -1610,8 +1739,13 @@ export class MdzipWorkspaceView {
|
|
|
1610
1739
|
const abort = new AbortController();
|
|
1611
1740
|
this.previewAbort = abort;
|
|
1612
1741
|
const context = this.createMarkdownContext(snapshot, abort.signal);
|
|
1613
|
-
if (this.
|
|
1614
|
-
|
|
1742
|
+
if (this.renderingService.supportsChunking) {
|
|
1743
|
+
if (priorChunkedState) {
|
|
1744
|
+
this.reconcileChunkedPreview(snapshot, context, generation, animateImageHydration, priorChunkedState);
|
|
1745
|
+
}
|
|
1746
|
+
else {
|
|
1747
|
+
this.renderChunkedPreview(snapshot, context, generation, animateImageHydration);
|
|
1748
|
+
}
|
|
1615
1749
|
return;
|
|
1616
1750
|
}
|
|
1617
1751
|
let result;
|
|
@@ -1689,7 +1823,7 @@ export class MdzipWorkspaceView {
|
|
|
1689
1823
|
// controls mount, same relative order as always — see
|
|
1690
1824
|
// collectPendingImages for why it's split from the (expensive) slot
|
|
1691
1825
|
// creation that follows.
|
|
1692
|
-
const pending = this.collectPendingImages([this.elPreviewContent], animateImageHydration);
|
|
1826
|
+
const pending = this.collectPendingImages([this.elPreviewContent], context, generation, animateImageHydration);
|
|
1693
1827
|
this.mountPreviewExtensions(context, generation);
|
|
1694
1828
|
const codeBlockHandle = this.mountCodeBlockControls();
|
|
1695
1829
|
if (codeBlockHandle) {
|
|
@@ -1709,8 +1843,19 @@ export class MdzipWorkspaceView {
|
|
|
1709
1843
|
* class, if any — they never get a slot). Returns `[]` without touching
|
|
1710
1844
|
* anything when there's no asset session to resolve archive images
|
|
1711
1845
|
* against, matching `mountPreviewHtml`'s no-op image handling.
|
|
1846
|
+
*
|
|
1847
|
+
* An image this session has *already* resolved once (e.g. it was already
|
|
1848
|
+
* on screen before an edit elsewhere caused its chunk to remount) is
|
|
1849
|
+
* applied immediately here via `resolveKnownImage` — no slot, no loading
|
|
1850
|
+
* class, no `hydrateImages`/`IntersectionObserver` round trip — instead of
|
|
1851
|
+
* being handed to the async placeholder-then-swap path meant for an image
|
|
1852
|
+
* that's genuinely being resolved for the first time. Skipping that path
|
|
1853
|
+
* for a re-mount of an already-known image is what actually avoids the
|
|
1854
|
+
* visible flash; still going through it (even instantly) briefly blanks
|
|
1855
|
+
* the image, since a fresh `<img>` element never carries over its
|
|
1856
|
+
* predecessor's already-resolved `src`.
|
|
1712
1857
|
*/
|
|
1713
|
-
collectPendingImages(roots, animateImageHydration) {
|
|
1858
|
+
collectPendingImages(roots, context, generation, animateImageHydration) {
|
|
1714
1859
|
if (!this.assetSession) {
|
|
1715
1860
|
return [];
|
|
1716
1861
|
}
|
|
@@ -1727,6 +1872,22 @@ export class MdzipWorkspaceView {
|
|
|
1727
1872
|
}
|
|
1728
1873
|
continue;
|
|
1729
1874
|
}
|
|
1875
|
+
const known = this.assetSession.resolveKnownImage(source, context.currentPath);
|
|
1876
|
+
if (known) {
|
|
1877
|
+
if (known.width && known.height && !image.hasAttribute('width') && !image.hasAttribute('height')) {
|
|
1878
|
+
image.setAttribute('width', String(known.width));
|
|
1879
|
+
image.setAttribute('height', String(known.height));
|
|
1880
|
+
}
|
|
1881
|
+
// No slot wrapper on this path — same direct-on-image alignment as
|
|
1882
|
+
// the external-URL branch above, not the slot's own align classes.
|
|
1883
|
+
const alignClass = rawHtmlImageAlignClass(image);
|
|
1884
|
+
if (alignClass) {
|
|
1885
|
+
image.classList.add(alignClass);
|
|
1886
|
+
}
|
|
1887
|
+
this.attachImageLoadHandlers(image, source, known.url, context, generation);
|
|
1888
|
+
image.setAttribute('src', known.url);
|
|
1889
|
+
continue;
|
|
1890
|
+
}
|
|
1730
1891
|
image.removeAttribute('src');
|
|
1731
1892
|
if (animateImageHydration) {
|
|
1732
1893
|
image.classList.add('mdzip-image-loading');
|
|
@@ -1896,6 +2057,201 @@ export class MdzipWorkspaceView {
|
|
|
1896
2057
|
}
|
|
1897
2058
|
});
|
|
1898
2059
|
}
|
|
2060
|
+
/**
|
|
2061
|
+
* Same-document, text-only-edit counterpart to {@link renderChunkedPreview}:
|
|
2062
|
+
* tokenizes the new text, then reconciles against the *previous* render's
|
|
2063
|
+
* records (`priorState`) instead of tearing everything down. Only reachable
|
|
2064
|
+
* when `updatePreview` determined the prior chunked structure is still
|
|
2065
|
+
* reuse-eligible (same path/pathType/colorScheme); the caller already
|
|
2066
|
+
* checked that.
|
|
2067
|
+
*/
|
|
2068
|
+
reconcileChunkedPreview(snapshot, context, generation, animateImageHydration, priorState) {
|
|
2069
|
+
let tokens;
|
|
2070
|
+
try {
|
|
2071
|
+
tokens = this.renderingService.tokenizeMarkdown(snapshot.currentText, context);
|
|
2072
|
+
}
|
|
2073
|
+
catch (error) {
|
|
2074
|
+
this.options.onFailed?.(error);
|
|
2075
|
+
// Can't reconcile without tokens — fall back to a hard reset.
|
|
2076
|
+
this.destroyPreviewHandles();
|
|
2077
|
+
this.elPreviewContent.innerHTML = renderMdzipPreviewHtml(snapshot);
|
|
2078
|
+
this.firePreviewRendered(snapshot, generation);
|
|
2079
|
+
this.fireAssetsHydrated(snapshot, generation);
|
|
2080
|
+
return;
|
|
2081
|
+
}
|
|
2082
|
+
const proceed = (resolvedTokens) => {
|
|
2083
|
+
if (generation !== this.previewGeneration || context.signal.aborted)
|
|
2084
|
+
return;
|
|
2085
|
+
this.applyChunkReconciliation(resolvedTokens, snapshot, context, generation, animateImageHydration, priorState);
|
|
2086
|
+
};
|
|
2087
|
+
if (Array.isArray(tokens)) {
|
|
2088
|
+
proceed(tokens);
|
|
2089
|
+
return;
|
|
2090
|
+
}
|
|
2091
|
+
void tokens.then(proceed).catch((error) => {
|
|
2092
|
+
if (generation !== this.previewGeneration || context.signal.aborted)
|
|
2093
|
+
return;
|
|
2094
|
+
if (error?.name !== 'AbortError') {
|
|
2095
|
+
this.options.onFailed?.(error);
|
|
2096
|
+
}
|
|
2097
|
+
});
|
|
2098
|
+
}
|
|
2099
|
+
/** Destroys one chunk record's mounted DOM and handles (but not the record object itself). */
|
|
2100
|
+
teardownChunkRecord(record) {
|
|
2101
|
+
for (const handle of record.handles) {
|
|
2102
|
+
try {
|
|
2103
|
+
handle.destroy();
|
|
2104
|
+
}
|
|
2105
|
+
catch (error) {
|
|
2106
|
+
this.options.onFailed?.(error);
|
|
2107
|
+
}
|
|
2108
|
+
}
|
|
2109
|
+
record.root?.remove();
|
|
2110
|
+
}
|
|
2111
|
+
/**
|
|
2112
|
+
* Diffs the new tokenize pass's chunks against `priorState.records` by
|
|
2113
|
+
* source-text identity (`chunkSourceKey`) and reconciles: chunks in the
|
|
2114
|
+
* matched leading/trailing run keep their existing DOM and handles
|
|
2115
|
+
* untouched (never re-rendered, never re-mounted — see
|
|
2116
|
+
* `chunkSourceKey`'s doc comment for why that matters for extensions like
|
|
2117
|
+
* mermaid); only the "middle" range that actually changed is torn down and
|
|
2118
|
+
* remounted. Uses prefix/suffix matching rather than a general LCS — a
|
|
2119
|
+
* text editor's edits are localized, so this is both simpler and correct
|
|
2120
|
+
* for the real-world case.
|
|
2121
|
+
*/
|
|
2122
|
+
applyChunkReconciliation(tokens, snapshot, context, generation, animateImageHydration, priorState) {
|
|
2123
|
+
// Tear down any live sentinel from the prior state before touching DOM —
|
|
2124
|
+
// same reasoning as drainRemainingChunks: stops it from racing this
|
|
2125
|
+
// reconciliation and double-mounting a chunk.
|
|
2126
|
+
priorState.sentinelHandle?.destroy();
|
|
2127
|
+
this.previewHandles = this.previewHandles.filter((h) => h !== priorState.sentinelHandle);
|
|
2128
|
+
const newChunks = groupTokensIntoChunks(tokens, this.chunkOptions());
|
|
2129
|
+
const oldRecords = priorState.records;
|
|
2130
|
+
const oldCursor = priorState.cursor;
|
|
2131
|
+
if (newChunks.length === 0) {
|
|
2132
|
+
// Whole document collapsed to nothing renderable — nothing to reuse.
|
|
2133
|
+
for (const record of oldRecords) {
|
|
2134
|
+
this.teardownChunkRecord(record);
|
|
2135
|
+
}
|
|
2136
|
+
this.elPreviewContent.replaceChildren();
|
|
2137
|
+
this.chunkedRenderState = null;
|
|
2138
|
+
this.firePreviewRendered(snapshot, generation);
|
|
2139
|
+
this.fireAssetsHydrated(snapshot, generation);
|
|
2140
|
+
return;
|
|
2141
|
+
}
|
|
2142
|
+
const oldKeys = oldRecords.map((r) => r.key);
|
|
2143
|
+
const newKeys = newChunks.map((c) => chunkSourceKey(c));
|
|
2144
|
+
const maxMatch = Math.min(oldKeys.length, newKeys.length);
|
|
2145
|
+
let prefixLen = 0;
|
|
2146
|
+
while (prefixLen < maxMatch && oldKeys[prefixLen] === newKeys[prefixLen])
|
|
2147
|
+
prefixLen += 1;
|
|
2148
|
+
const maxSuffix = maxMatch - prefixLen;
|
|
2149
|
+
let suffixLen = 0;
|
|
2150
|
+
while (suffixLen < maxSuffix
|
|
2151
|
+
&& oldKeys[oldKeys.length - 1 - suffixLen] === newKeys[newKeys.length - 1 - suffixLen])
|
|
2152
|
+
suffixLen += 1;
|
|
2153
|
+
const oldMiddleStart = prefixLen;
|
|
2154
|
+
const oldMiddleEnd = oldRecords.length - suffixLen;
|
|
2155
|
+
const newMiddleStart = prefixLen;
|
|
2156
|
+
const newMiddleEnd = newChunks.length - suffixLen;
|
|
2157
|
+
const newRecords = [];
|
|
2158
|
+
for (let i = 0; i < prefixLen; i += 1) {
|
|
2159
|
+
newRecords.push({ ...oldRecords[i], key: newKeys[i], tokens: newChunks[i] });
|
|
2160
|
+
}
|
|
2161
|
+
for (let i = newMiddleStart; i < newMiddleEnd; i += 1) {
|
|
2162
|
+
newRecords.push({ key: newKeys[i], tokens: newChunks[i], root: null, html: null, handles: [] });
|
|
2163
|
+
}
|
|
2164
|
+
for (let i = 0; i < suffixLen; i += 1) {
|
|
2165
|
+
const oi = oldRecords.length - suffixLen + i;
|
|
2166
|
+
const ni = newChunks.length - suffixLen + i;
|
|
2167
|
+
newRecords.push({ ...oldRecords[oi], key: newKeys[ni], tokens: newChunks[ni] });
|
|
2168
|
+
}
|
|
2169
|
+
// Destroy DOM+handles for old-middle records that were actually mounted
|
|
2170
|
+
// (bounded by the old cursor — everything from there on was still
|
|
2171
|
+
// waiting in the lazy-load tail, never mounted, nothing to destroy).
|
|
2172
|
+
const oldMountedMiddleEnd = Math.min(oldCursor, oldMiddleEnd);
|
|
2173
|
+
for (let i = oldMiddleStart; i < oldMountedMiddleEnd; i += 1) {
|
|
2174
|
+
this.teardownChunkRecord(oldRecords[i]);
|
|
2175
|
+
}
|
|
2176
|
+
const oldMountedMiddleCount = Math.max(0, oldMountedMiddleEnd - oldMiddleStart);
|
|
2177
|
+
// Reference node for splicing in the new middle's DOM: the first
|
|
2178
|
+
// surviving *mounted* suffix record (mounted suffix records are
|
|
2179
|
+
// contiguous from newMiddleEnd by construction), else null (append).
|
|
2180
|
+
let insertBeforeNode = null;
|
|
2181
|
+
for (let i = newMiddleEnd; i < newRecords.length; i += 1) {
|
|
2182
|
+
if (newRecords[i].root) {
|
|
2183
|
+
insertBeforeNode = newRecords[i].root;
|
|
2184
|
+
break;
|
|
2185
|
+
}
|
|
2186
|
+
}
|
|
2187
|
+
this.chunkedRenderState = {
|
|
2188
|
+
records: newRecords,
|
|
2189
|
+
cursor: oldCursor,
|
|
2190
|
+
context,
|
|
2191
|
+
generation,
|
|
2192
|
+
animateImageHydration,
|
|
2193
|
+
sentinelHandle: null,
|
|
2194
|
+
mounting: oldMountedMiddleCount > 0
|
|
2195
|
+
};
|
|
2196
|
+
if (oldMountedMiddleCount === 0) {
|
|
2197
|
+
// The edit is entirely beyond what was mounted (progressive mode,
|
|
2198
|
+
// still in the lazy-load tail) — nothing was destroyed, nothing needs
|
|
2199
|
+
// mounting now. Cursor is unaffected: every index below it is an
|
|
2200
|
+
// untouched prefix survivor, valid at the same index in newRecords. The
|
|
2201
|
+
// old sentinel was already torn down above, so a fresh one has to be
|
|
2202
|
+
// armed to keep the lazy-load tail continuing.
|
|
2203
|
+
if (oldCursor < newRecords.length) {
|
|
2204
|
+
this.armChunkSentinel(newRecords, oldCursor, context, generation, animateImageHydration);
|
|
2205
|
+
}
|
|
2206
|
+
this.firePreviewRendered(snapshot, generation);
|
|
2207
|
+
this.fireAssetsHydrated(snapshot, generation);
|
|
2208
|
+
return;
|
|
2209
|
+
}
|
|
2210
|
+
void this.mountReconciledMiddle(newRecords, newMiddleStart, newMiddleEnd, insertBeforeNode, context, generation, animateImageHydration, snapshot, oldCursor, oldMiddleEnd);
|
|
2211
|
+
}
|
|
2212
|
+
/**
|
|
2213
|
+
* Mounts a reconciliation's replacement "middle" range in full (uncapped —
|
|
2214
|
+
* a single edit only ever touches 1-2 chunks in practice; see the plan's
|
|
2215
|
+
* disclosed v1 risk for the pathological large-paste case), then carries
|
|
2216
|
+
* forward however much of the surviving suffix was already mounted before
|
|
2217
|
+
* reconciling, and re-arms the sentinel for whatever's still left.
|
|
2218
|
+
*/
|
|
2219
|
+
async mountReconciledMiddle(records, middleStart, middleEnd, insertBeforeNode, context, generation, animateImageHydration, snapshot, oldCursor, oldMiddleEnd) {
|
|
2220
|
+
let cursor = middleStart;
|
|
2221
|
+
let mountedRoots = [];
|
|
2222
|
+
while (cursor < middleEnd) {
|
|
2223
|
+
const batch = await this.renderAndMountChunkBatch(records, cursor, context, generation, insertBeforeNode);
|
|
2224
|
+
if (generation !== this.previewGeneration || context.signal.aborted)
|
|
2225
|
+
return;
|
|
2226
|
+
if (batch.cursor === cursor)
|
|
2227
|
+
break; // Aborted/superseded mid-loop with no progress.
|
|
2228
|
+
cursor = batch.cursor;
|
|
2229
|
+
mountedRoots = mountedRoots.concat(batch.mountedRoots);
|
|
2230
|
+
}
|
|
2231
|
+
const carriedSuffixMounted = Math.max(0, oldCursor - oldMiddleEnd);
|
|
2232
|
+
const finalCursor = Math.min(records.length, Math.max(cursor, middleEnd) + carriedSuffixMounted);
|
|
2233
|
+
// See armChunkSentinel's mountNext for why this is recorded before the
|
|
2234
|
+
// staleness check: finalCursor already correctly folds in the carried-
|
|
2235
|
+
// forward suffix (chunks beyond the reconciled middle that were already
|
|
2236
|
+
// mounted), so discarding it here just because a newer edit has since
|
|
2237
|
+
// landed would regress chunkedRenderState.cursor back down — causing the
|
|
2238
|
+
// next reconcile to re-arm a sentinel from that stale, lower cursor and
|
|
2239
|
+
// re-mount (duplicate) chunks that are already correctly in the DOM.
|
|
2240
|
+
this.recordChunkProgress(generation, records, finalCursor);
|
|
2241
|
+
if (generation !== this.previewGeneration || context.signal.aborted)
|
|
2242
|
+
return;
|
|
2243
|
+
if (this.chunkedRenderState?.generation === generation) {
|
|
2244
|
+
this.chunkedRenderState.mounting = false;
|
|
2245
|
+
}
|
|
2246
|
+
const pending = this.collectPendingImages(mountedRoots, context, generation, animateImageHydration);
|
|
2247
|
+
this.hydrateImages(pending, context, generation, animateImageHydration, () => {
|
|
2248
|
+
this.fireAssetsHydrated(snapshot, generation);
|
|
2249
|
+
});
|
|
2250
|
+
this.firePreviewRendered(snapshot, generation);
|
|
2251
|
+
if (this.chunkedRenderState?.generation === generation && finalCursor < records.length) {
|
|
2252
|
+
this.armChunkSentinel(records, finalCursor, context, generation, animateImageHydration);
|
|
2253
|
+
}
|
|
2254
|
+
}
|
|
1899
2255
|
/**
|
|
1900
2256
|
* Groups tokens into chunks and mounts them: an initial batch synchronously
|
|
1901
2257
|
* (enough for a small document to behave exactly like the non-chunked
|
|
@@ -1911,41 +2267,111 @@ export class MdzipWorkspaceView {
|
|
|
1911
2267
|
this.elPreviewContent.replaceChildren();
|
|
1912
2268
|
// Also called with no options in renderFullDocumentHtml — see the
|
|
1913
2269
|
// comment there; keep both call sites' chunking in sync.
|
|
1914
|
-
const chunks = groupTokensIntoChunks(tokens);
|
|
2270
|
+
const chunks = groupTokensIntoChunks(tokens, this.chunkOptions());
|
|
1915
2271
|
if (chunks.length === 0) {
|
|
1916
2272
|
this.chunkedRenderState = null;
|
|
1917
2273
|
this.firePreviewRendered(snapshot, generation);
|
|
1918
2274
|
this.fireAssetsHydrated(snapshot, generation);
|
|
1919
2275
|
return;
|
|
1920
2276
|
}
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
2277
|
+
const records = chunks.map((chunkTokens) => ({
|
|
2278
|
+
key: chunkSourceKey(chunkTokens),
|
|
2279
|
+
tokens: chunkTokens,
|
|
2280
|
+
root: null,
|
|
2281
|
+
html: null,
|
|
2282
|
+
handles: []
|
|
2283
|
+
}));
|
|
2284
|
+
this.chunkedRenderState = {
|
|
2285
|
+
records, cursor: 0, context, generation, animateImageHydration, sentinelHandle: null,
|
|
2286
|
+
// Set for the whole initial batch/eager mount below — see the field's
|
|
2287
|
+
// doc comment: an edit landing before this settles must not reconcile
|
|
2288
|
+
// against a `cursor` that doesn't yet reflect this in-flight mount.
|
|
2289
|
+
mounting: true
|
|
2290
|
+
};
|
|
2291
|
+
if (this.progressiveTextRendering) {
|
|
2292
|
+
// Mount an initial batch synchronously (enough for a small document to
|
|
2293
|
+
// behave exactly like a non-progressive mount), then lazily continue
|
|
2294
|
+
// the rest as the user scrolls near a trailing sentinel.
|
|
2295
|
+
void this.mountChunkBatch(records, 0, context, generation, animateImageHydration, () => {
|
|
2296
|
+
this.fireAssetsHydrated(snapshot, generation);
|
|
2297
|
+
}).then((cursor) => {
|
|
2298
|
+
// See armChunkSentinel's mountNext for why this is recorded before
|
|
2299
|
+
// the staleness check below.
|
|
2300
|
+
this.recordChunkProgress(generation, records, cursor);
|
|
2301
|
+
if (generation !== this.previewGeneration || context.signal.aborted)
|
|
2302
|
+
return;
|
|
2303
|
+
if (this.chunkedRenderState?.generation === generation) {
|
|
2304
|
+
this.chunkedRenderState.mounting = false;
|
|
2305
|
+
}
|
|
2306
|
+
this.firePreviewRendered(snapshot, generation);
|
|
2307
|
+
if (cursor < records.length) {
|
|
2308
|
+
this.armChunkSentinel(records, cursor, context, generation, animateImageHydration);
|
|
2309
|
+
}
|
|
2310
|
+
});
|
|
2311
|
+
return;
|
|
2312
|
+
}
|
|
2313
|
+
// Not opted into lazy-load-on-scroll: mount every chunk up front, same
|
|
2314
|
+
// visible end state as the old monolithic non-chunked path, but yielding
|
|
2315
|
+
// a frame between batches (mirroring drainRemainingChunks) instead of
|
|
2316
|
+
// blocking the main thread for one giant synchronous render — see the
|
|
2317
|
+
// plan's disclosed cold-start tradeoff.
|
|
2318
|
+
void this.mountAllChunksEagerly(records, context, generation, animateImageHydration, snapshot);
|
|
2319
|
+
}
|
|
2320
|
+
/**
|
|
2321
|
+
* Mounts every chunk of a non-progressive (`progressiveTextRendering:
|
|
2322
|
+
* false`) document up front — same end state as the old monolithic
|
|
2323
|
+
* non-chunked path (the whole document mounted before `onPreviewRendered`
|
|
2324
|
+
* fires), but yielding a frame between batches (mirroring
|
|
2325
|
+
* `drainRemainingChunks`) instead of blocking the main thread for one
|
|
2326
|
+
* giant synchronous render.
|
|
2327
|
+
*/
|
|
2328
|
+
async mountAllChunksEagerly(records, context, generation, animateImageHydration, snapshot) {
|
|
2329
|
+
const allPending = [];
|
|
2330
|
+
while (generation === this.previewGeneration && !context.signal.aborted) {
|
|
2331
|
+
const cursorBefore = this.chunkedRenderState?.generation === generation
|
|
2332
|
+
? this.chunkedRenderState.cursor
|
|
2333
|
+
: records.length;
|
|
2334
|
+
if (cursorBefore >= records.length)
|
|
2335
|
+
break;
|
|
2336
|
+
const { cursor, mountedRoots } = await this.renderAndMountChunkBatch(records, cursorBefore, context, generation);
|
|
2337
|
+
// See armChunkSentinel's mountNext for why this is recorded before the
|
|
2338
|
+
// staleness check: the chunks this batch appended are physically in
|
|
2339
|
+
// the DOM regardless of whether a newer edit has since landed, and
|
|
2340
|
+
// recordChunkProgress's own generation check keeps this a safe no-op
|
|
2341
|
+
// once chunkedRenderState has moved on.
|
|
2342
|
+
this.recordChunkProgress(generation, records, cursor);
|
|
1925
2343
|
if (generation !== this.previewGeneration || context.signal.aborted)
|
|
1926
2344
|
return;
|
|
1927
|
-
this.
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
2345
|
+
allPending.push(...this.collectPendingImages(mountedRoots, context, generation, animateImageHydration));
|
|
2346
|
+
if (cursor >= records.length)
|
|
2347
|
+
break;
|
|
2348
|
+
await new Promise((resolve) => requestAnimationFrame(() => resolve()));
|
|
2349
|
+
}
|
|
2350
|
+
if (generation === this.previewGeneration && !context.signal.aborted) {
|
|
2351
|
+
if (this.chunkedRenderState?.generation === generation) {
|
|
2352
|
+
this.chunkedRenderState.mounting = false;
|
|
1931
2353
|
}
|
|
1932
|
-
|
|
2354
|
+
this.firePreviewRendered(snapshot, generation);
|
|
2355
|
+
this.hydrateImages(allPending, context, generation, animateImageHydration, () => {
|
|
2356
|
+
this.fireAssetsHydrated(snapshot, generation);
|
|
2357
|
+
});
|
|
2358
|
+
}
|
|
1933
2359
|
}
|
|
1934
2360
|
/**
|
|
1935
|
-
* Updates `chunkedRenderState.cursor` after a batch mounts
|
|
1936
|
-
*
|
|
1937
|
-
*
|
|
1938
|
-
*
|
|
2361
|
+
* Updates `chunkedRenderState.cursor` after a batch mounts. A no-op if a
|
|
2362
|
+
* newer render generation has already superseded this one. Deliberately
|
|
2363
|
+
* never nulls `chunkedRenderState` out just because `cursor` reached
|
|
2364
|
+
* `records.length` — the records stay alive for the whole generation so a
|
|
2365
|
+
* later same-document edit can reconcile against them (see
|
|
2366
|
+
* `applyChunkReconciliation`). Callers that need "is there still an
|
|
2367
|
+
* unmounted lazy tail" (Copy All's instant path, the scroll-to-bottom fast
|
|
2368
|
+
* path) check `cursor < records.length` directly instead of this field's
|
|
2369
|
+
* truthiness.
|
|
1939
2370
|
*/
|
|
1940
|
-
recordChunkProgress(generation,
|
|
2371
|
+
recordChunkProgress(generation, records, cursor) {
|
|
1941
2372
|
if (this.chunkedRenderState?.generation !== generation)
|
|
1942
2373
|
return;
|
|
1943
|
-
|
|
1944
|
-
this.chunkedRenderState = null;
|
|
1945
|
-
}
|
|
1946
|
-
else {
|
|
1947
|
-
this.chunkedRenderState.cursor = cursor;
|
|
1948
|
-
}
|
|
2374
|
+
this.chunkedRenderState.cursor = cursor;
|
|
1949
2375
|
}
|
|
1950
2376
|
/**
|
|
1951
2377
|
* Renders+appends chunks starting at `startCursor` up to a char budget
|
|
@@ -1964,7 +2390,7 @@ export class MdzipWorkspaceView {
|
|
|
1964
2390
|
* included here; see {@link mountChunkBatch} and
|
|
1965
2391
|
* {@link drainRemainingChunks} for the two different ways callers pace it.
|
|
1966
2392
|
*/
|
|
1967
|
-
async renderAndMountChunkBatch(
|
|
2393
|
+
async renderAndMountChunkBatch(records, startCursor, context, generation, insertBeforeNode = null) {
|
|
1968
2394
|
const BATCH_CHAR_BUDGET = 4000;
|
|
1969
2395
|
const BATCH_TIME_BUDGET_MS = 10;
|
|
1970
2396
|
const clock = this.elPreviewContent.ownerDocument.defaultView?.performance ?? performance;
|
|
@@ -1972,12 +2398,13 @@ export class MdzipWorkspaceView {
|
|
|
1972
2398
|
let cursor = startCursor;
|
|
1973
2399
|
let renderedChars = 0;
|
|
1974
2400
|
const mountedRoots = [];
|
|
1975
|
-
|
|
2401
|
+
const batchStartCursor = startCursor;
|
|
2402
|
+
while (cursor < records.length
|
|
1976
2403
|
&& renderedChars < BATCH_CHAR_BUDGET
|
|
1977
2404
|
&& (mountedRoots.length === 0 || clock.now() - batchStart < BATCH_TIME_BUDGET_MS)) {
|
|
1978
2405
|
if (generation !== this.previewGeneration || context.signal.aborted)
|
|
1979
2406
|
return { cursor, mountedRoots };
|
|
1980
|
-
const chunkTokens =
|
|
2407
|
+
const chunkTokens = records[cursor].tokens;
|
|
1981
2408
|
const chunkIndex = cursor;
|
|
1982
2409
|
cursor += 1;
|
|
1983
2410
|
let html;
|
|
@@ -2011,15 +2438,19 @@ export class MdzipWorkspaceView {
|
|
|
2011
2438
|
}
|
|
2012
2439
|
this.chunkHtmlCache.html.set(chunkIndex, html);
|
|
2013
2440
|
}
|
|
2014
|
-
const root = this.appendChunkHtml(html);
|
|
2441
|
+
const root = this.appendChunkHtml(html, insertBeforeNode);
|
|
2442
|
+
records[chunkIndex].root = root;
|
|
2443
|
+
records[chunkIndex].html = html;
|
|
2015
2444
|
mountedRoots.push(root);
|
|
2016
2445
|
renderedChars += html.length;
|
|
2017
2446
|
}
|
|
2018
|
-
for (const
|
|
2019
|
-
|
|
2020
|
-
|
|
2447
|
+
for (const record of records.slice(batchStartCursor, cursor)) {
|
|
2448
|
+
if (!record.root)
|
|
2449
|
+
continue;
|
|
2450
|
+
this.mountPreviewExtensions(context, generation, record.root, record.handles);
|
|
2451
|
+
const codeBlockHandle = this.mountCodeBlockControls(record.root);
|
|
2021
2452
|
if (codeBlockHandle) {
|
|
2022
|
-
|
|
2453
|
+
record.handles.push(codeBlockHandle);
|
|
2023
2454
|
}
|
|
2024
2455
|
}
|
|
2025
2456
|
return { cursor, mountedRoots };
|
|
@@ -2033,21 +2464,31 @@ export class MdzipWorkspaceView {
|
|
|
2033
2464
|
* by how fast the user scrolls, so one independent hydration loop per
|
|
2034
2465
|
* batch never has a chance to pile up against another.
|
|
2035
2466
|
*/
|
|
2036
|
-
async mountChunkBatch(
|
|
2037
|
-
const { cursor, mountedRoots } = await this.renderAndMountChunkBatch(
|
|
2467
|
+
async mountChunkBatch(records, startCursor, context, generation, animateImageHydration, onImagesSettled) {
|
|
2468
|
+
const { cursor, mountedRoots } = await this.renderAndMountChunkBatch(records, startCursor, context, generation);
|
|
2038
2469
|
// Same relative order as the non-chunked path: cheap image pass, then
|
|
2039
2470
|
// the (expensive) slot/observe pass.
|
|
2040
|
-
const pending = this.collectPendingImages(mountedRoots, animateImageHydration);
|
|
2471
|
+
const pending = this.collectPendingImages(mountedRoots, context, generation, animateImageHydration);
|
|
2041
2472
|
this.hydrateImages(pending, context, generation, animateImageHydration, onImagesSettled);
|
|
2042
2473
|
return cursor;
|
|
2043
2474
|
}
|
|
2044
|
-
/**
|
|
2045
|
-
|
|
2475
|
+
/**
|
|
2476
|
+
* Wraps one chunk's rendered HTML in a mount boundary and appends it (or,
|
|
2477
|
+
* given `beforeNode`, splices it in ahead of that still-attached node —
|
|
2478
|
+
* used by reconciliation to insert a replacement chunk ahead of a
|
|
2479
|
+
* surviving mounted suffix). See the `.mdzip-chunk` CSS rules for why.
|
|
2480
|
+
*/
|
|
2481
|
+
appendChunkHtml(html, beforeNode = null) {
|
|
2046
2482
|
const doc = this.elPreviewContent.ownerDocument;
|
|
2047
2483
|
const wrapper = doc.createElement('div');
|
|
2048
2484
|
wrapper.className = 'mdzip-chunk';
|
|
2049
2485
|
wrapper.innerHTML = html;
|
|
2050
|
-
this.elPreviewContent
|
|
2486
|
+
if (beforeNode && beforeNode.parentNode === this.elPreviewContent) {
|
|
2487
|
+
this.elPreviewContent.insertBefore(wrapper, beforeNode);
|
|
2488
|
+
}
|
|
2489
|
+
else {
|
|
2490
|
+
this.elPreviewContent.appendChild(wrapper);
|
|
2491
|
+
}
|
|
2051
2492
|
return wrapper;
|
|
2052
2493
|
}
|
|
2053
2494
|
/**
|
|
@@ -2058,17 +2499,36 @@ export class MdzipWorkspaceView {
|
|
|
2058
2499
|
* every chunk is mounted. Falls back to mounting everything immediately
|
|
2059
2500
|
* when IntersectionObserver isn't available, matching `hydrateImages`.
|
|
2060
2501
|
*/
|
|
2061
|
-
armChunkSentinel(
|
|
2502
|
+
armChunkSentinel(records, cursor, context, generation, animateImageHydration) {
|
|
2062
2503
|
const doc = this.elPreviewContent.ownerDocument;
|
|
2063
2504
|
const observerWindow = doc.defaultView;
|
|
2064
2505
|
const mountNext = (nextCursor) => {
|
|
2065
|
-
|
|
2506
|
+
// Same race this field guards against everywhere else: an edit landing
|
|
2507
|
+
// while this scroll-triggered continuation is mid-flight must not
|
|
2508
|
+
// reconcile against a cursor that doesn't yet reflect it.
|
|
2509
|
+
if (this.chunkedRenderState?.generation === generation) {
|
|
2510
|
+
this.chunkedRenderState.mounting = true;
|
|
2511
|
+
}
|
|
2512
|
+
void this.mountChunkBatch(records, nextCursor, context, generation, animateImageHydration, () => { })
|
|
2066
2513
|
.then((newCursor) => {
|
|
2514
|
+
// Record whatever this batch actually mounted regardless of
|
|
2515
|
+
// whether a newer edit has since landed — recordChunkProgress's
|
|
2516
|
+
// own generation check already makes this a safe no-op once
|
|
2517
|
+
// chunkedRenderState has moved on. Skipping this call when stale
|
|
2518
|
+
// was a real bug: the chunks this batch appended are physically in
|
|
2519
|
+
// the DOM either way, but a newer edit's reconcile still saw the
|
|
2520
|
+
// *old*, un-advanced cursor and re-armed a sentinel from the same
|
|
2521
|
+
// starting point — re-rendering and re-appending (duplicating)
|
|
2522
|
+
// chunks that were already mounted, visible as an already-settled
|
|
2523
|
+
// image flashing back to broken as a fresh duplicate replaced it.
|
|
2524
|
+
this.recordChunkProgress(generation, records, newCursor);
|
|
2067
2525
|
if (generation !== this.previewGeneration || context.signal.aborted)
|
|
2068
2526
|
return;
|
|
2069
|
-
this.
|
|
2070
|
-
|
|
2071
|
-
|
|
2527
|
+
if (this.chunkedRenderState?.generation === generation) {
|
|
2528
|
+
this.chunkedRenderState.mounting = false;
|
|
2529
|
+
}
|
|
2530
|
+
if (newCursor < records.length) {
|
|
2531
|
+
this.armChunkSentinel(records, newCursor, context, generation, animateImageHydration);
|
|
2072
2532
|
}
|
|
2073
2533
|
});
|
|
2074
2534
|
};
|
|
@@ -2169,7 +2629,7 @@ export class MdzipWorkspaceView {
|
|
|
2169
2629
|
* appended chunk rather than once over the whole document. Defaults to
|
|
2170
2630
|
* the whole preview content, matching the non-chunked path exactly.
|
|
2171
2631
|
*/
|
|
2172
|
-
mountPreviewExtensions(context, generation, root = this.elPreviewContent) {
|
|
2632
|
+
mountPreviewExtensions(context, generation, root = this.elPreviewContent, collector = this.previewHandles) {
|
|
2173
2633
|
for (const extension of this.markdownExtensions) {
|
|
2174
2634
|
if (!extension.mount) {
|
|
2175
2635
|
continue;
|
|
@@ -2190,7 +2650,7 @@ export class MdzipWorkspaceView {
|
|
|
2190
2650
|
}
|
|
2191
2651
|
return;
|
|
2192
2652
|
}
|
|
2193
|
-
|
|
2653
|
+
collector.push(handle);
|
|
2194
2654
|
}).catch((error) => {
|
|
2195
2655
|
if (generation === this.previewGeneration) {
|
|
2196
2656
|
this.options.onFailed?.(error);
|
|
@@ -2198,7 +2658,7 @@ export class MdzipWorkspaceView {
|
|
|
2198
2658
|
});
|
|
2199
2659
|
}
|
|
2200
2660
|
else if (mounted) {
|
|
2201
|
-
|
|
2661
|
+
collector.push(mounted);
|
|
2202
2662
|
}
|
|
2203
2663
|
}
|
|
2204
2664
|
catch (error) {
|
|
@@ -2337,6 +2797,7 @@ export class MdzipWorkspaceView {
|
|
|
2337
2797
|
if (generation !== this.previewGeneration) {
|
|
2338
2798
|
return;
|
|
2339
2799
|
}
|
|
2800
|
+
this.restorePendingPreviewScroll(generation);
|
|
2340
2801
|
try {
|
|
2341
2802
|
this.options.onPreviewRendered?.(snapshot);
|
|
2342
2803
|
}
|
|
@@ -2344,6 +2805,20 @@ export class MdzipWorkspaceView {
|
|
|
2344
2805
|
this.options.onFailed?.(error);
|
|
2345
2806
|
}
|
|
2346
2807
|
}
|
|
2808
|
+
/** See `pendingPreviewScrollRestore`'s doc comment. */
|
|
2809
|
+
restorePendingPreviewScroll(generation) {
|
|
2810
|
+
const pending = this.pendingPreviewScrollRestore;
|
|
2811
|
+
if (!pending || pending.generation !== generation) {
|
|
2812
|
+
return;
|
|
2813
|
+
}
|
|
2814
|
+
this.pendingPreviewScrollRestore = null;
|
|
2815
|
+
if (this.elPreviewPane.scrollTop !== pending.scrollTop) {
|
|
2816
|
+
this.syncing = true;
|
|
2817
|
+
this.lastSyncedPreviewScrollTop = pending.scrollTop;
|
|
2818
|
+
this.elPreviewPane.scrollTop = pending.scrollTop;
|
|
2819
|
+
this.syncing = false;
|
|
2820
|
+
}
|
|
2821
|
+
}
|
|
2347
2822
|
fireAssetsHydrated(snapshot, generation) {
|
|
2348
2823
|
if (generation !== this.previewGeneration) {
|
|
2349
2824
|
return;
|
|
@@ -2648,6 +3123,19 @@ export class MdzipWorkspaceView {
|
|
|
2648
3123
|
// Attach scroll listener to sync with preview
|
|
2649
3124
|
const scroller = editor.dom.querySelector('.cm-scroller');
|
|
2650
3125
|
if (scroller) {
|
|
3126
|
+
// No 'keydown' here, deliberately: .cm-scroller receives every keystroke
|
|
3127
|
+
// typed into the editor, not just navigation keys, so it can't tell
|
|
3128
|
+
// "user pressed PageDown to scroll" apart from "user typed a letter" —
|
|
3129
|
+
// confirmed live, where ordinary typing was arming this gesture window
|
|
3130
|
+
// and letting CodeMirror's own small post-edit viewport-correction
|
|
3131
|
+
// scroll (a real native behavior) through as if it were a deliberate
|
|
3132
|
+
// scroll, on every keystroke. Wheel/touch/mousedown (which also covers
|
|
3133
|
+
// a scrollbar-thumb drag, since that lands on this same element) are
|
|
3134
|
+
// unambiguous and cover the vast majority of real scrolling.
|
|
3135
|
+
const markEditorGesture = () => { self.lastEditorGestureTime = performance.now(); };
|
|
3136
|
+
scroller.addEventListener('wheel', markEditorGesture, { passive: true });
|
|
3137
|
+
scroller.addEventListener('touchstart', markEditorGesture, { passive: true });
|
|
3138
|
+
scroller.addEventListener('mousedown', markEditorGesture);
|
|
2651
3139
|
scroller.addEventListener('scroll', () => self.syncScrollToPreview());
|
|
2652
3140
|
}
|
|
2653
3141
|
editor.dom.addEventListener(IMAGE_EDIT_AFFORDANCE_CLICK_EVENT, (event) => {
|
|
@@ -3421,6 +3909,11 @@ export class MdzipWorkspaceView {
|
|
|
3421
3909
|
void this.replaceFileFromPicker(path, file);
|
|
3422
3910
|
}
|
|
3423
3911
|
});
|
|
3912
|
+
const markPreviewGesture = () => { this.lastPreviewGestureTime = performance.now(); };
|
|
3913
|
+
this.elPreviewPane.addEventListener('wheel', markPreviewGesture, { passive: true });
|
|
3914
|
+
this.elPreviewPane.addEventListener('touchstart', markPreviewGesture, { passive: true });
|
|
3915
|
+
this.elPreviewPane.addEventListener('mousedown', markPreviewGesture);
|
|
3916
|
+
this.elPreviewPane.addEventListener('keydown', markPreviewGesture);
|
|
3424
3917
|
this.elPreviewPane.addEventListener('scroll', () => this.syncScrollFromPreview());
|
|
3425
3918
|
// Gives the pane logical focus on click so a subsequent Ctrl/Cmd+A (below)
|
|
3426
3919
|
// can be scoped to it. tabindex="-1" keeps it out of Tab order; this is
|
|
@@ -4460,20 +4953,22 @@ export class MdzipWorkspaceView {
|
|
|
4460
4953
|
* Copies the entire rendered document as plain text (same fidelity as
|
|
4461
4954
|
* `copyPreviewSelection` — no HTML, no `ClipboardItem`), regardless of how
|
|
4462
4955
|
* much of it is currently mounted under progressive rendering. If
|
|
4463
|
-
* everything's already mounted (small doc, non-
|
|
4464
|
-
* already scrolled through it) this is instant —
|
|
4465
|
-
*
|
|
4466
|
-
*
|
|
4467
|
-
*
|
|
4956
|
+
* everything's already mounted (small doc, eager/non-progressive mount, or
|
|
4957
|
+
* the user already scrolled through it) this is instant —
|
|
4958
|
+
* `chunkedRenderState` is either null (no markdown open, or a non-default
|
|
4959
|
+
* renderer) or has nothing left to mount (`cursor >= records.length`) in
|
|
4960
|
+
* exactly that case. Otherwise it force-drains the rest first, showing a
|
|
4961
|
+
* cancelable progress dialog once the wait clears a short debounce so fast
|
|
4962
|
+
* documents never flicker it into view.
|
|
4468
4963
|
*/
|
|
4469
4964
|
async copyAllPreviewContent() {
|
|
4470
4965
|
const pending = this.chunkedRenderState;
|
|
4471
|
-
if (!pending) {
|
|
4966
|
+
if (!pending || pending.cursor >= pending.records.length) {
|
|
4472
4967
|
const outcome = await this.copyPreviewSelection(this.elPreviewContent.textContent ?? '');
|
|
4473
4968
|
this.finishCopyNotification(false, outcome);
|
|
4474
4969
|
return;
|
|
4475
4970
|
}
|
|
4476
|
-
const total = pending.
|
|
4971
|
+
const total = pending.records.length;
|
|
4477
4972
|
const abort = new AbortController();
|
|
4478
4973
|
let dialogShown = false;
|
|
4479
4974
|
const label = 'Rendering the full document';
|
|
@@ -4503,8 +4998,9 @@ export class MdzipWorkspaceView {
|
|
|
4503
4998
|
// Cancelled partway through — whatever's left stays unmounted, so
|
|
4504
4999
|
// re-arm the usual scroll-driven continuation for it.
|
|
4505
5000
|
const remaining = this.chunkedRenderState;
|
|
4506
|
-
if (remaining && remaining.generation === pending.generation && !remaining.sentinelHandle
|
|
4507
|
-
|
|
5001
|
+
if (remaining && remaining.generation === pending.generation && !remaining.sentinelHandle
|
|
5002
|
+
&& remaining.cursor < remaining.records.length) {
|
|
5003
|
+
this.armChunkSentinel(remaining.records, remaining.cursor, remaining.context, remaining.generation, remaining.animateImageHydration);
|
|
4508
5004
|
}
|
|
4509
5005
|
return;
|
|
4510
5006
|
}
|
|
@@ -4546,7 +5042,7 @@ export class MdzipWorkspaceView {
|
|
|
4546
5042
|
// renderAndMountChunkBatch's caller (mountChunkedPreview) — keep both
|
|
4547
5043
|
// call sites' chunking in sync, since chunk index is chunkHtmlCache's
|
|
4548
5044
|
// only key.
|
|
4549
|
-
const chunks = groupTokensIntoChunks(tokens);
|
|
5045
|
+
const chunks = groupTokensIntoChunks(tokens, this.chunkOptions());
|
|
4550
5046
|
const view = this.elPreviewContent.ownerDocument.defaultView;
|
|
4551
5047
|
const clock = view?.performance ?? performance;
|
|
4552
5048
|
const BATCH_TIME_BUDGET_MS = 10;
|
|
@@ -4761,32 +5257,39 @@ export class MdzipWorkspaceView {
|
|
|
4761
5257
|
* making Copy All wait on it.
|
|
4762
5258
|
*/
|
|
4763
5259
|
async drainRemainingChunks(state, onProgress, signal) {
|
|
4764
|
-
const {
|
|
5260
|
+
const { records, context, generation, animateImageHydration } = state;
|
|
4765
5261
|
// Draining is instead of the scroll-driven continuation, not alongside
|
|
4766
5262
|
// it — tearing down any armed sentinel first stops the two from racing
|
|
4767
5263
|
// and double-mounting the same chunk.
|
|
4768
5264
|
state.sentinelHandle?.destroy();
|
|
4769
5265
|
if (this.chunkedRenderState?.generation === generation) {
|
|
4770
5266
|
this.chunkedRenderState.sentinelHandle = null;
|
|
5267
|
+
// Same race the `mounting` field guards against elsewhere: an edit
|
|
5268
|
+
// landing mid-drain must not reconcile against a cursor this loop is
|
|
5269
|
+
// actively advancing out from under it.
|
|
5270
|
+
this.chunkedRenderState.mounting = true;
|
|
4771
5271
|
}
|
|
4772
5272
|
const allPending = [];
|
|
4773
5273
|
try {
|
|
4774
|
-
while (this.chunkedRenderState?.generation === generation && this.chunkedRenderState.cursor <
|
|
5274
|
+
while (this.chunkedRenderState?.generation === generation && this.chunkedRenderState.cursor < records.length) {
|
|
4775
5275
|
if (signal.aborted || generation !== this.previewGeneration || context.signal.aborted)
|
|
4776
5276
|
return;
|
|
4777
5277
|
const cursor = this.chunkedRenderState.cursor;
|
|
4778
|
-
const { cursor: newCursor, mountedRoots } = await this.renderAndMountChunkBatch(
|
|
5278
|
+
const { cursor: newCursor, mountedRoots } = await this.renderAndMountChunkBatch(records, cursor, context, generation);
|
|
4779
5279
|
if (generation !== this.previewGeneration || context.signal.aborted)
|
|
4780
5280
|
return;
|
|
4781
|
-
allPending.push(...this.collectPendingImages(mountedRoots, animateImageHydration));
|
|
4782
|
-
this.recordChunkProgress(generation,
|
|
4783
|
-
onProgress(newCursor,
|
|
5281
|
+
allPending.push(...this.collectPendingImages(mountedRoots, context, generation, animateImageHydration));
|
|
5282
|
+
this.recordChunkProgress(generation, records, newCursor);
|
|
5283
|
+
onProgress(newCursor, records.length);
|
|
4784
5284
|
if (signal.aborted)
|
|
4785
5285
|
return;
|
|
4786
5286
|
await new Promise((resolve) => requestAnimationFrame(() => resolve()));
|
|
4787
5287
|
}
|
|
4788
5288
|
}
|
|
4789
5289
|
finally {
|
|
5290
|
+
if (this.chunkedRenderState?.generation === generation) {
|
|
5291
|
+
this.chunkedRenderState.mounting = false;
|
|
5292
|
+
}
|
|
4790
5293
|
if (generation === this.previewGeneration && !context.signal.aborted) {
|
|
4791
5294
|
this.hydrateImages(allPending, context, generation, animateImageHydration, () => { });
|
|
4792
5295
|
}
|
|
@@ -5757,6 +6260,12 @@ export class MdzipWorkspaceView {
|
|
|
5757
6260
|
if (this.syncing || !this.cmEditor || this.layout !== 'split') {
|
|
5758
6261
|
return;
|
|
5759
6262
|
}
|
|
6263
|
+
if (performance.now() - this.lastPreviewGestureTime > MdzipWorkspaceView.SCROLL_SYNC_GESTURE_WINDOW_MS) {
|
|
6264
|
+
return;
|
|
6265
|
+
}
|
|
6266
|
+
if (performance.now() - this.lastContentChangeTime < MdzipWorkspaceView.SCROLL_SYNC_SETTLE_MS) {
|
|
6267
|
+
return;
|
|
6268
|
+
}
|
|
5760
6269
|
const currentTop = this.elPreviewPane.scrollTop;
|
|
5761
6270
|
// Recognize this event as the echo of our own prior write (from
|
|
5762
6271
|
// syncScrollToPreview) by comparing values instead of racing timing.
|
|
@@ -5787,6 +6296,12 @@ export class MdzipWorkspaceView {
|
|
|
5787
6296
|
if (this.syncing || !this.cmEditor || this.layout !== 'split') {
|
|
5788
6297
|
return;
|
|
5789
6298
|
}
|
|
6299
|
+
if (performance.now() - this.lastEditorGestureTime > MdzipWorkspaceView.SCROLL_SYNC_GESTURE_WINDOW_MS) {
|
|
6300
|
+
return;
|
|
6301
|
+
}
|
|
6302
|
+
if (performance.now() - this.lastContentChangeTime < MdzipWorkspaceView.SCROLL_SYNC_SETTLE_MS) {
|
|
6303
|
+
return;
|
|
6304
|
+
}
|
|
5790
6305
|
const cmScroller = this.cmEditor.dom.querySelector('.cm-scroller');
|
|
5791
6306
|
if (!cmScroller) {
|
|
5792
6307
|
return;
|
|
@@ -5813,8 +6328,23 @@ export class MdzipWorkspaceView {
|
|
|
5813
6328
|
// CodeMirror has actually decided to draw for the current scroll
|
|
5814
6329
|
// position, so comparing it to the document length is exact regardless
|
|
5815
6330
|
// of any height estimation drift.
|
|
5816
|
-
|
|
5817
|
-
|
|
6331
|
+
//
|
|
6332
|
+
// `viewport.to >= doc.length` alone is a false positive whenever the
|
|
6333
|
+
// whole document already fits within the editor's visible area — there
|
|
6334
|
+
// is no scrolling at all in that case, so `viewport.to` trivially covers
|
|
6335
|
+
// the full document from position 0 regardless of scroll intent. Without
|
|
6336
|
+
// the overflow check below, a short-to-medium document (especially one
|
|
6337
|
+
// with images, which slow down per-chunk mounting enough to leave a real
|
|
6338
|
+
// window where `cursor < records.length`) could hit this branch off of
|
|
6339
|
+
// an incidental scroll event fired mid-edit — e.g. focus or cursor
|
|
6340
|
+
// movement nudging `scrollTop` by a sub-pixel amount — and force-drain
|
|
6341
|
+
// plus jump the preview to its bottom for no reason the user asked for,
|
|
6342
|
+
// which (via the echo-breaking reflow that follows, since late-arriving
|
|
6343
|
+
// image layout shifts the "bottom" after the jump) can drag the editor's
|
|
6344
|
+
// own scroll down to match through `syncScrollFromPreview`'s echo path.
|
|
6345
|
+
const hasScrollableOverflow = editorHeight > 0;
|
|
6346
|
+
const atDocEnd = hasScrollableOverflow && this.cmEditor.viewport.to >= this.cmEditor.state.doc.length;
|
|
6347
|
+
if (atDocEnd && this.chunkedRenderState && this.chunkedRenderState.cursor < this.chunkedRenderState.records.length) {
|
|
5818
6348
|
void this.syncScrollToPreviewBottom();
|
|
5819
6349
|
return;
|
|
5820
6350
|
}
|
|
@@ -5857,7 +6387,7 @@ export class MdzipWorkspaceView {
|
|
|
5857
6387
|
}
|
|
5858
6388
|
let showToastTimer = setTimeout(() => {
|
|
5859
6389
|
showToastTimer = null;
|
|
5860
|
-
this.scrollCatchUpState = { done: state.cursor, total: state.
|
|
6390
|
+
this.scrollCatchUpState = { done: state.cursor, total: state.records.length };
|
|
5861
6391
|
this.updateScrollCatchUpToast();
|
|
5862
6392
|
}, 200);
|
|
5863
6393
|
try {
|
|
@@ -6057,325 +6587,325 @@ function formatMetadataValue(value) {
|
|
|
6057
6587
|
}
|
|
6058
6588
|
return 'Not available';
|
|
6059
6589
|
}
|
|
6060
|
-
const SHELL_HTML = `
|
|
6061
|
-
<section class="mdzip-root">
|
|
6062
|
-
<div class="document-strip" data-ref="document-strip" hidden>
|
|
6063
|
-
<button type="button" class="title-button" data-ref="title-btn"></button>
|
|
6064
|
-
<button type="button" class="document-info-button" data-ref="document-info-btn"
|
|
6065
|
-
title="Document information" aria-label="Document information">
|
|
6066
|
-
${INFO_ICON_HTML}
|
|
6067
|
-
</button>
|
|
6068
|
-
</div>
|
|
6069
|
-
|
|
6070
|
-
<header class="toolbar" data-ref="toolbar" hidden>
|
|
6071
|
-
<div class="toolbar-start">
|
|
6072
|
-
<div class="toolbar-left">
|
|
6073
|
-
<button type="button" class="icon-toggle nav-toggle" data-ref="nav-btn" title="Toggle contents" aria-label="Toggle contents">
|
|
6074
|
-
${NAV_TOGGLE_ICON_HTML}
|
|
6075
|
-
</button>
|
|
6076
|
-
</div>
|
|
6077
|
-
|
|
6078
|
-
<div class="edit-toolbar" data-ref="edit-toolbar" role="toolbar" aria-label="Markdown formatting">
|
|
6079
|
-
<div class="edit-toolbar-group">
|
|
6080
|
-
<button type="button" data-format="bold" data-format-control="bold" title="Bold" aria-label="Bold">${BOLD_ICON_HTML}</button>
|
|
6081
|
-
<button type="button" data-format="italic" data-format-control="italic" title="Italic" aria-label="Italic">${ITALIC_ICON_HTML}</button>
|
|
6082
|
-
<button type="button" data-format="strike" data-format-control="strikethrough" title="Strikethrough" aria-label="Strikethrough">${STRIKE_ICON_HTML}</button>
|
|
6083
|
-
<div class="format-menu" data-format-control="headings">
|
|
6084
|
-
<button type="button" class="format-menu-toggle" data-format-menu-toggle
|
|
6085
|
-
aria-label="Heading" aria-haspopup="menu" aria-expanded="false">
|
|
6086
|
-
${HEADING_ICON_HTML}${CHEVRON_ICON_HTML}
|
|
6087
|
-
</button>
|
|
6088
|
-
<div class="format-menu-popover" data-format-menu role="menu" aria-label="Heading level" hidden>
|
|
6089
|
-
<button type="button" role="menuitem" data-format="paragraph">Paragraph</button>
|
|
6090
|
-
<button type="button" role="menuitem" data-format="heading-1" data-heading-level="1"><strong>H1</strong> Heading 1</button>
|
|
6091
|
-
<button type="button" role="menuitem" data-format="heading-2" data-heading-level="2"><strong>H2</strong> Heading 2</button>
|
|
6092
|
-
<button type="button" role="menuitem" data-format="heading-3" data-heading-level="3"><strong>H3</strong> Heading 3</button>
|
|
6093
|
-
<button type="button" role="menuitem" data-format="heading-4" data-heading-level="4"><strong>H4</strong> Heading 4</button>
|
|
6094
|
-
<button type="button" role="menuitem" data-format="heading-5" data-heading-level="5"><strong>H5</strong> Heading 5</button>
|
|
6095
|
-
<button type="button" role="menuitem" data-format="heading-6" data-heading-level="6"><strong>H6</strong> Heading 6</button>
|
|
6096
|
-
</div>
|
|
6097
|
-
</div>
|
|
6098
|
-
</div>
|
|
6099
|
-
<span class="edit-toolbar-divider" aria-hidden="true"></span>
|
|
6100
|
-
<div class="edit-toolbar-group">
|
|
6101
|
-
<button type="button" data-format="bullet-list" data-format-control="bulletList" title="Bulleted list" aria-label="Bulleted list">${BULLET_LIST_ICON_HTML}</button>
|
|
6102
|
-
<button type="button" data-format="ordered-list" data-format-control="orderedList" title="Numbered list" aria-label="Numbered list">${ORDERED_LIST_ICON_HTML}</button>
|
|
6103
|
-
<div class="format-menu" data-format-control="code">
|
|
6104
|
-
<button type="button" class="format-menu-toggle" data-format-menu-toggle
|
|
6105
|
-
aria-label="Code" aria-haspopup="menu" aria-expanded="false">
|
|
6106
|
-
${CODE_ICON_HTML}${CHEVRON_ICON_HTML}
|
|
6107
|
-
</button>
|
|
6108
|
-
<div class="format-menu-popover" data-format-menu role="menu" aria-label="Code format" hidden>
|
|
6109
|
-
<button type="button" role="menuitem" data-format="code" data-code-kind="inline">Inline code</button>
|
|
6110
|
-
<button type="button" role="menuitem" data-format="code-block" data-code-kind="block">Code block</button>
|
|
6111
|
-
</div>
|
|
6112
|
-
</div>
|
|
6113
|
-
<button type="button" data-format="quote" data-format-control="blockquote" title="Blockquote" aria-label="Blockquote">${QUOTE_ICON_HTML}</button>
|
|
6114
|
-
<button type="button" data-format="line-break" data-format-control="lineBreak" title="Line break" aria-label="Line break">${LINE_BREAK_ICON_HTML}</button>
|
|
6115
|
-
</div>
|
|
6116
|
-
<span class="edit-toolbar-divider" aria-hidden="true"></span>
|
|
6117
|
-
<div class="edit-toolbar-group">
|
|
6118
|
-
<button type="button" data-format="link" data-format-control="link" title="Link" aria-label="Link">${LINK_ICON_HTML}</button>
|
|
6119
|
-
<button type="button" data-format="image" data-format-control="image" title="Image" aria-label="Image">${IMAGE_FORMAT_ICON_HTML}</button>
|
|
6120
|
-
<input type="file" data-ref="image-input" accept="image/*" hidden />
|
|
6121
|
-
</div>
|
|
6122
|
-
</div>
|
|
6123
|
-
</div>
|
|
6124
|
-
|
|
6125
|
-
<div class="toolbar-buttons view-mode-toggle-group" data-ref="layout-controls" role="group" aria-label="Editor layout">
|
|
6126
|
-
<button type="button" class="icon-toggle view-mode-toggle" data-ref="source-btn" title="Edit" aria-label="Edit" aria-pressed="false">
|
|
6127
|
-
<span class="commandbar-flex-container" data-ref="source-icon">${SOURCE_EDIT_ICON_HTML}</span>
|
|
6128
|
-
</button>
|
|
6129
|
-
<button type="button" class="icon-toggle view-mode-toggle" data-ref="split-btn" title="Split" aria-label="Split" aria-pressed="false">
|
|
6130
|
-
<span class="commandbar-flex-container">
|
|
6131
|
-
${SPLIT_ICON_HTML}
|
|
6132
|
-
</span>
|
|
6133
|
-
</button>
|
|
6134
|
-
<button type="button" class="icon-toggle view-mode-toggle" data-ref="preview-btn" title="View" aria-label="View" aria-pressed="false">
|
|
6135
|
-
<span class="commandbar-flex-container">
|
|
6136
|
-
${PREVIEW_ICON_HTML}
|
|
6137
|
-
</span>
|
|
6138
|
-
</button>
|
|
6139
|
-
</div>
|
|
6140
|
-
|
|
6141
|
-
<div class="toolbar-controls" data-ref="toolbar-controls">
|
|
6142
|
-
<button type="button" class="icon-toggle" data-ref="search-btn" title="Find/replace (Mod-F)" aria-label="Find/replace">
|
|
6143
|
-
${SEARCH_ICON_HTML}
|
|
6144
|
-
</button>
|
|
6145
|
-
<button type="button" class="icon-toggle" data-ref="save-btn" title="Save" aria-label="Save">
|
|
6146
|
-
${SAVE_ICON_HTML}
|
|
6147
|
-
</button>
|
|
6148
|
-
<button type="button" class="icon-toggle zoom-toggle" data-ref="zoom-btn" title="Zoom controls" aria-label="Zoom controls">
|
|
6149
|
-
${ZOOM_ICON_HTML}
|
|
6150
|
-
</button>
|
|
6151
|
-
<div class="theme-toggle-group" data-ref="theme-controls" role="group" aria-label="Color scheme">
|
|
6152
|
-
<button type="button" class="icon-toggle theme-toggle" data-ref="dark-theme-btn"
|
|
6153
|
-
title="Dark mode" aria-label="Dark mode" aria-pressed="false">
|
|
6154
|
-
${DARK_THEME_ICON_HTML}
|
|
6155
|
-
</button>
|
|
6156
|
-
<button type="button" class="icon-toggle theme-toggle" data-ref="light-theme-btn"
|
|
6157
|
-
title="Light mode" aria-label="Light mode" aria-pressed="false">
|
|
6158
|
-
${LIGHT_THEME_ICON_HTML}
|
|
6159
|
-
</button>
|
|
6160
|
-
</div>
|
|
6161
|
-
<div class="zoom-popover" data-ref="zoom-popover" hidden role="group" aria-label="Zoom">
|
|
6162
|
-
<span class="zoom-level" data-ref="zoom-level">100%</span>
|
|
6163
|
-
<span class="zoom-stepper">
|
|
6164
|
-
<button type="button" data-action="zoom-out" title="Zoom out" aria-label="Zoom out">-</button>
|
|
6165
|
-
<button type="button" data-action="zoom-in" title="Zoom in" aria-label="Zoom in">+</button>
|
|
6166
|
-
</span>
|
|
6167
|
-
<button type="button" class="zoom-reset" data-action="zoom-reset" title="Reset zoom">Reset</button>
|
|
6168
|
-
</div>
|
|
6169
|
-
</div>
|
|
6170
|
-
</header>
|
|
6171
|
-
|
|
6172
|
-
<div class="workspace-shell" data-ref="workspace-shell" hidden>
|
|
6173
|
-
<aside class="nav-pane" data-ref="nav-pane" aria-label="Package contents">
|
|
6174
|
-
<div class="nav-tree" data-ref="nav-tree"></div>
|
|
6175
|
-
</aside>
|
|
6176
|
-
<div class="nav-resizer" data-ref="nav-resizer"
|
|
6177
|
-
role="separator" aria-orientation="vertical" aria-label="Resize contents pane"></div>
|
|
6178
|
-
|
|
6179
|
-
<div class="pane-stack" data-ref="pane-stack">
|
|
6180
|
-
<section class="pane edit-pane" data-ref="edit-pane">
|
|
6181
|
-
<div class="editor-host" data-ref="editor-host"></div>
|
|
6182
|
-
</section>
|
|
6183
|
-
<div class="split-resizer" data-ref="split-resizer"
|
|
6184
|
-
role="separator" aria-orientation="vertical" aria-label="Resize split panes"></div>
|
|
6185
|
-
<section class="pane preview-pane" data-ref="preview-pane" tabindex="-1">
|
|
6186
|
-
<article class="preview-content" data-ref="preview-content"></article>
|
|
6187
|
-
</section>
|
|
6188
|
-
<section class="pane entry-pane" data-ref="entry-pane"></section>
|
|
6189
|
-
</div>
|
|
6190
|
-
</div>
|
|
6191
|
-
|
|
6192
|
-
<div class="title-dialog-backdrop" data-ref="title-dialog" hidden
|
|
6193
|
-
role="dialog" aria-modal="true" aria-labelledby="mdzip-title-dialog-heading">
|
|
6194
|
-
<div class="title-dialog">
|
|
6195
|
-
<h3 id="mdzip-title-dialog-heading">Set Document Title</h3>
|
|
6196
|
-
<p>This is the package-level title stored in manifest.json.</p>
|
|
6197
|
-
<input type="text" maxlength="120" data-ref="title-input" aria-label="Document title" />
|
|
6198
|
-
<p class="title-dialog-validation" data-ref="title-validation" hidden>Title cannot be empty.</p>
|
|
6199
|
-
<p>If unset, consumers may fall back to entry point, filename, or first heading.</p>
|
|
6200
|
-
<div class="title-dialog-actions">
|
|
6201
|
-
<button type="button" class="reset-title" data-ref="title-reset-btn">Reset</button>
|
|
6202
|
-
<button type="button" data-action="cancel-title">Cancel</button>
|
|
6203
|
-
<button type="button" class="save-title" data-ref="title-save-btn">Save</button>
|
|
6204
|
-
</div>
|
|
6205
|
-
</div>
|
|
6206
|
-
</div>
|
|
6207
|
-
|
|
6208
|
-
<div class="title-dialog-backdrop" data-ref="conversion-dialog" hidden
|
|
6209
|
-
role="dialog" aria-modal="true" aria-labelledby="mdzip-conversion-dialog-heading">
|
|
6210
|
-
<div class="title-dialog">
|
|
6211
|
-
<h3 id="mdzip-conversion-dialog-heading">Convert to MDZ?</h3>
|
|
6212
|
-
<p>
|
|
6213
|
-
Regular Markdown files contain only text. Convert this document to an
|
|
6214
|
-
MDZ package to add images, package navigation, and document metadata.
|
|
6215
|
-
</p>
|
|
6216
|
-
<p>The next save will produce an .mdz file.</p>
|
|
6217
|
-
<div class="title-dialog-actions">
|
|
6218
|
-
<button type="button" data-action="cancel-conversion">Cancel</button>
|
|
6219
|
-
<button type="button" class="save-title" data-ref="conversion-confirm-btn">Convert</button>
|
|
6220
|
-
</div>
|
|
6221
|
-
</div>
|
|
6222
|
-
</div>
|
|
6223
|
-
|
|
6224
|
-
<div class="title-dialog-backdrop" data-ref="image-insert-dialog" hidden
|
|
6225
|
-
role="dialog" aria-modal="true" aria-labelledby="mdzip-image-insert-dialog-heading">
|
|
6226
|
-
<div class="title-dialog image-insert-dialog">
|
|
6227
|
-
<h3 id="mdzip-image-insert-dialog-heading">Insert Image</h3>
|
|
6228
|
-
<p>Choose the Markdown inserted for this image.</p>
|
|
6229
|
-
<fieldset class="image-insert-options">
|
|
6230
|
-
<legend>Output</legend>
|
|
6231
|
-
<label>
|
|
6232
|
-
<input type="radio" name="mdzip-image-insert-mode" value="markdown"
|
|
6233
|
-
data-ref="image-insert-mode-markdown" checked />
|
|
6234
|
-
Markdown image
|
|
6235
|
-
</label>
|
|
6236
|
-
<label>
|
|
6237
|
-
<input type="radio" name="mdzip-image-insert-mode" value="html"
|
|
6238
|
-
data-ref="image-insert-mode-html" />
|
|
6239
|
-
HTML <img> with sizing
|
|
6240
|
-
</label>
|
|
6241
|
-
</fieldset>
|
|
6242
|
-
<label class="image-insert-field">
|
|
6243
|
-
<span>Alt text</span>
|
|
6244
|
-
<input type="text" data-ref="image-insert-alt" />
|
|
6245
|
-
</label>
|
|
6246
|
-
<div class="image-insert-grid">
|
|
6247
|
-
<label class="image-insert-field">
|
|
6248
|
-
<span>Size by</span>
|
|
6249
|
-
<select data-ref="image-insert-size-mode">
|
|
6250
|
-
<option value="original">Original size</option>
|
|
6251
|
-
<option value="width" selected>Width</option>
|
|
6252
|
-
<option value="height">Height</option>
|
|
6253
|
-
<option value="percent">Percent</option>
|
|
6254
|
-
</select>
|
|
6255
|
-
</label>
|
|
6256
|
-
<label class="image-insert-field">
|
|
6257
|
-
<span>Value</span>
|
|
6258
|
-
<input type="number" min="1" step="1" data-ref="image-insert-size-value" />
|
|
6259
|
-
</label>
|
|
6260
|
-
</div>
|
|
6261
|
-
<label class="image-insert-field">
|
|
6262
|
-
<span>Position</span>
|
|
6263
|
-
<select data-ref="image-insert-position">
|
|
6264
|
-
<option value="inline">Inline/default</option>
|
|
6265
|
-
<option value="left">Left</option>
|
|
6266
|
-
<option value="center">Center</option>
|
|
6267
|
-
<option value="right">Right</option>
|
|
6268
|
-
<option value="wrap-left">Wrap left</option>
|
|
6269
|
-
<option value="wrap-right">Wrap right</option>
|
|
6270
|
-
</select>
|
|
6271
|
-
</label>
|
|
6272
|
-
<div class="title-dialog-actions">
|
|
6273
|
-
<button type="button" data-action="cancel-image-insert">Cancel</button>
|
|
6274
|
-
<button type="button" class="save-title" data-ref="image-insert-confirm-btn">Insert</button>
|
|
6275
|
-
</div>
|
|
6276
|
-
</div>
|
|
6277
|
-
</div>
|
|
6278
|
-
|
|
6279
|
-
<div class="title-dialog-backdrop" data-ref="pack-files-dialog" hidden
|
|
6280
|
-
role="dialog" aria-modal="true" aria-labelledby="mdzip-pack-files-dialog-heading">
|
|
6281
|
-
<div class="title-dialog pack-files-dialog">
|
|
6282
|
-
<h3 id="mdzip-pack-files-dialog-heading">Package Markdown Files</h3>
|
|
6283
|
-
<p>These files contain more than one Markdown document. Choose how to package them.</p>
|
|
6284
|
-
<fieldset class="pack-files-options">
|
|
6285
|
-
<legend>Mode</legend>
|
|
6286
|
-
<label>
|
|
6287
|
-
<input type="radio" name="mdzip-pack-files-mode" value="document"
|
|
6288
|
-
data-ref="pack-files-mode-document" checked />
|
|
6289
|
-
Document — one primary page; the rest are attached
|
|
6290
|
-
</label>
|
|
6291
|
-
<label>
|
|
6292
|
-
<input type="radio" name="mdzip-pack-files-mode" value="project"
|
|
6293
|
-
data-ref="pack-files-mode-project" />
|
|
6294
|
-
Project — a set of related documents
|
|
6295
|
-
</label>
|
|
6296
|
-
</fieldset>
|
|
6297
|
-
<label class="pack-files-field">
|
|
6298
|
-
<span>Entry point</span>
|
|
6299
|
-
<select data-ref="pack-files-entry"></select>
|
|
6300
|
-
</label>
|
|
6301
|
-
<div class="title-dialog-actions">
|
|
6302
|
-
<button type="button" data-action="cancel-pack-files">Cancel</button>
|
|
6303
|
-
<button type="button" class="save-title" data-ref="pack-files-confirm-btn">Package</button>
|
|
6304
|
-
</div>
|
|
6305
|
-
</div>
|
|
6306
|
-
</div>
|
|
6307
|
-
|
|
6308
|
-
<div class="title-dialog-backdrop" data-ref="copy-render-dialog" hidden
|
|
6309
|
-
role="dialog" aria-modal="true" aria-labelledby="mdzip-copy-render-dialog-heading">
|
|
6310
|
-
<div class="title-dialog copy-render-dialog">
|
|
6311
|
-
<h3 id="mdzip-copy-render-dialog-heading" data-ref="copy-render-heading">Copying document...</h3>
|
|
6312
|
-
<div data-ref="copy-render-progress-section">
|
|
6313
|
-
<p data-ref="copy-render-progress-text">Rendering the full document...</p>
|
|
6314
|
-
<div class="copy-render-progress-track">
|
|
6315
|
-
<div class="copy-render-progress-bar" data-ref="copy-render-progress-bar"></div>
|
|
6316
|
-
</div>
|
|
6317
|
-
</div>
|
|
6318
|
-
<p data-ref="copy-render-ready-text" hidden>
|
|
6319
|
-
The document is ready. Browsers only allow a clipboard write right after a click, so this needs one more —
|
|
6320
|
-
click Copy to finish.
|
|
6321
|
-
</p>
|
|
6322
|
-
<p data-ref="copy-render-done-text" hidden></p>
|
|
6323
|
-
<div class="title-dialog-actions">
|
|
6324
|
-
<button type="button" data-action="cancel-copy-render" data-ref="copy-render-cancel-btn">Cancel</button>
|
|
6325
|
-
<button type="button" data-action="cancel-copy-ready" data-ref="copy-render-ready-cancel-btn" hidden>Cancel</button>
|
|
6326
|
-
<button type="button" class="save-title" data-action="confirm-copy-ready" data-ref="copy-render-confirm-btn" hidden>Copy</button>
|
|
6327
|
-
<button type="button" class="save-title" data-action="dismiss-copy-render" data-ref="copy-render-dismiss-btn" hidden>Dismiss</button>
|
|
6328
|
-
</div>
|
|
6329
|
-
</div>
|
|
6330
|
-
</div>
|
|
6331
|
-
|
|
6332
|
-
<div class="title-dialog-backdrop" data-ref="metadata-dialog" hidden
|
|
6333
|
-
role="dialog" aria-modal="true" aria-labelledby="mdzip-metadata-dialog-heading">
|
|
6334
|
-
<div class="title-dialog metadata-dialog">
|
|
6335
|
-
<h3 id="mdzip-metadata-dialog-heading">Document Information</h3>
|
|
6336
|
-
<dl data-ref="metadata-list"></dl>
|
|
6337
|
-
<h4>Libraries</h4>
|
|
6338
|
-
<dl data-ref="library-list"></dl>
|
|
6339
|
-
<div class="title-dialog-actions">
|
|
6340
|
-
<button type="button" class="save-title" data-action="close-metadata">Close</button>
|
|
6341
|
-
</div>
|
|
6342
|
-
</div>
|
|
6343
|
-
</div>
|
|
6344
|
-
|
|
6345
|
-
<div class="title-dialog-backdrop" data-ref="name-dialog" hidden
|
|
6346
|
-
role="dialog" aria-modal="true" aria-labelledby="mdzip-name-dialog-heading">
|
|
6347
|
-
<div class="title-dialog">
|
|
6348
|
-
<h3 id="mdzip-name-dialog-heading" data-ref="name-dialog-heading">New Markdown File</h3>
|
|
6349
|
-
<input type="text" maxlength="260" data-ref="name-input" aria-label="File name" />
|
|
6350
|
-
<p class="title-dialog-validation" data-ref="name-validation" hidden></p>
|
|
6351
|
-
<div class="title-dialog-actions">
|
|
6352
|
-
<button type="button" data-action="cancel-name">Cancel</button>
|
|
6353
|
-
<button type="button" class="save-title" data-ref="name-confirm-btn">Create</button>
|
|
6354
|
-
</div>
|
|
6355
|
-
</div>
|
|
6356
|
-
</div>
|
|
6357
|
-
|
|
6358
|
-
<div class="title-dialog-backdrop" data-ref="delete-dialog" hidden
|
|
6359
|
-
role="dialog" aria-modal="true" aria-labelledby="mdzip-delete-dialog-heading">
|
|
6360
|
-
<div class="title-dialog">
|
|
6361
|
-
<h3 id="mdzip-delete-dialog-heading">Delete File?</h3>
|
|
6362
|
-
<p data-ref="delete-dialog-text"></p>
|
|
6363
|
-
<div class="title-dialog-actions">
|
|
6364
|
-
<button type="button" data-action="cancel-delete">Cancel</button>
|
|
6365
|
-
<button type="button" class="danger-action" data-ref="delete-confirm-btn">Delete</button>
|
|
6366
|
-
</div>
|
|
6367
|
-
</div>
|
|
6368
|
-
</div>
|
|
6369
|
-
|
|
6370
|
-
<input type="file" data-ref="replace-input" hidden />
|
|
6371
|
-
|
|
6372
|
-
<div class="nav-context-menu" data-ref="nav-menu" hidden role="menu"></div>
|
|
6373
|
-
|
|
6374
|
-
<div class="mdzip-tooltip" data-ref="tooltip" role="tooltip" hidden></div>
|
|
6375
|
-
|
|
6376
|
-
<div class="mdzip-copy-toast" data-ref="copy-toast" role="status" aria-live="polite" hidden></div>
|
|
6377
|
-
|
|
6378
|
-
<p class="mdzip-empty" data-ref="empty-state">No MDZip workspace loaded.</p>
|
|
6379
|
-
</section>
|
|
6590
|
+
const SHELL_HTML = `
|
|
6591
|
+
<section class="mdzip-root">
|
|
6592
|
+
<div class="document-strip" data-ref="document-strip" hidden>
|
|
6593
|
+
<button type="button" class="title-button" data-ref="title-btn"></button>
|
|
6594
|
+
<button type="button" class="document-info-button" data-ref="document-info-btn"
|
|
6595
|
+
title="Document information" aria-label="Document information">
|
|
6596
|
+
${INFO_ICON_HTML}
|
|
6597
|
+
</button>
|
|
6598
|
+
</div>
|
|
6599
|
+
|
|
6600
|
+
<header class="toolbar" data-ref="toolbar" hidden>
|
|
6601
|
+
<div class="toolbar-start">
|
|
6602
|
+
<div class="toolbar-left">
|
|
6603
|
+
<button type="button" class="icon-toggle nav-toggle" data-ref="nav-btn" title="Toggle contents" aria-label="Toggle contents">
|
|
6604
|
+
${NAV_TOGGLE_ICON_HTML}
|
|
6605
|
+
</button>
|
|
6606
|
+
</div>
|
|
6607
|
+
|
|
6608
|
+
<div class="edit-toolbar" data-ref="edit-toolbar" role="toolbar" aria-label="Markdown formatting">
|
|
6609
|
+
<div class="edit-toolbar-group">
|
|
6610
|
+
<button type="button" data-format="bold" data-format-control="bold" title="Bold" aria-label="Bold">${BOLD_ICON_HTML}</button>
|
|
6611
|
+
<button type="button" data-format="italic" data-format-control="italic" title="Italic" aria-label="Italic">${ITALIC_ICON_HTML}</button>
|
|
6612
|
+
<button type="button" data-format="strike" data-format-control="strikethrough" title="Strikethrough" aria-label="Strikethrough">${STRIKE_ICON_HTML}</button>
|
|
6613
|
+
<div class="format-menu" data-format-control="headings">
|
|
6614
|
+
<button type="button" class="format-menu-toggle" data-format-menu-toggle
|
|
6615
|
+
aria-label="Heading" aria-haspopup="menu" aria-expanded="false">
|
|
6616
|
+
${HEADING_ICON_HTML}${CHEVRON_ICON_HTML}
|
|
6617
|
+
</button>
|
|
6618
|
+
<div class="format-menu-popover" data-format-menu role="menu" aria-label="Heading level" hidden>
|
|
6619
|
+
<button type="button" role="menuitem" data-format="paragraph">Paragraph</button>
|
|
6620
|
+
<button type="button" role="menuitem" data-format="heading-1" data-heading-level="1"><strong>H1</strong> Heading 1</button>
|
|
6621
|
+
<button type="button" role="menuitem" data-format="heading-2" data-heading-level="2"><strong>H2</strong> Heading 2</button>
|
|
6622
|
+
<button type="button" role="menuitem" data-format="heading-3" data-heading-level="3"><strong>H3</strong> Heading 3</button>
|
|
6623
|
+
<button type="button" role="menuitem" data-format="heading-4" data-heading-level="4"><strong>H4</strong> Heading 4</button>
|
|
6624
|
+
<button type="button" role="menuitem" data-format="heading-5" data-heading-level="5"><strong>H5</strong> Heading 5</button>
|
|
6625
|
+
<button type="button" role="menuitem" data-format="heading-6" data-heading-level="6"><strong>H6</strong> Heading 6</button>
|
|
6626
|
+
</div>
|
|
6627
|
+
</div>
|
|
6628
|
+
</div>
|
|
6629
|
+
<span class="edit-toolbar-divider" aria-hidden="true"></span>
|
|
6630
|
+
<div class="edit-toolbar-group">
|
|
6631
|
+
<button type="button" data-format="bullet-list" data-format-control="bulletList" title="Bulleted list" aria-label="Bulleted list">${BULLET_LIST_ICON_HTML}</button>
|
|
6632
|
+
<button type="button" data-format="ordered-list" data-format-control="orderedList" title="Numbered list" aria-label="Numbered list">${ORDERED_LIST_ICON_HTML}</button>
|
|
6633
|
+
<div class="format-menu" data-format-control="code">
|
|
6634
|
+
<button type="button" class="format-menu-toggle" data-format-menu-toggle
|
|
6635
|
+
aria-label="Code" aria-haspopup="menu" aria-expanded="false">
|
|
6636
|
+
${CODE_ICON_HTML}${CHEVRON_ICON_HTML}
|
|
6637
|
+
</button>
|
|
6638
|
+
<div class="format-menu-popover" data-format-menu role="menu" aria-label="Code format" hidden>
|
|
6639
|
+
<button type="button" role="menuitem" data-format="code" data-code-kind="inline">Inline code</button>
|
|
6640
|
+
<button type="button" role="menuitem" data-format="code-block" data-code-kind="block">Code block</button>
|
|
6641
|
+
</div>
|
|
6642
|
+
</div>
|
|
6643
|
+
<button type="button" data-format="quote" data-format-control="blockquote" title="Blockquote" aria-label="Blockquote">${QUOTE_ICON_HTML}</button>
|
|
6644
|
+
<button type="button" data-format="line-break" data-format-control="lineBreak" title="Line break" aria-label="Line break">${LINE_BREAK_ICON_HTML}</button>
|
|
6645
|
+
</div>
|
|
6646
|
+
<span class="edit-toolbar-divider" aria-hidden="true"></span>
|
|
6647
|
+
<div class="edit-toolbar-group">
|
|
6648
|
+
<button type="button" data-format="link" data-format-control="link" title="Link" aria-label="Link">${LINK_ICON_HTML}</button>
|
|
6649
|
+
<button type="button" data-format="image" data-format-control="image" title="Image" aria-label="Image">${IMAGE_FORMAT_ICON_HTML}</button>
|
|
6650
|
+
<input type="file" data-ref="image-input" accept="image/*" hidden />
|
|
6651
|
+
</div>
|
|
6652
|
+
</div>
|
|
6653
|
+
</div>
|
|
6654
|
+
|
|
6655
|
+
<div class="toolbar-buttons view-mode-toggle-group" data-ref="layout-controls" role="group" aria-label="Editor layout">
|
|
6656
|
+
<button type="button" class="icon-toggle view-mode-toggle" data-ref="source-btn" title="Edit" aria-label="Edit" aria-pressed="false">
|
|
6657
|
+
<span class="commandbar-flex-container" data-ref="source-icon">${SOURCE_EDIT_ICON_HTML}</span>
|
|
6658
|
+
</button>
|
|
6659
|
+
<button type="button" class="icon-toggle view-mode-toggle" data-ref="split-btn" title="Split" aria-label="Split" aria-pressed="false">
|
|
6660
|
+
<span class="commandbar-flex-container">
|
|
6661
|
+
${SPLIT_ICON_HTML}
|
|
6662
|
+
</span>
|
|
6663
|
+
</button>
|
|
6664
|
+
<button type="button" class="icon-toggle view-mode-toggle" data-ref="preview-btn" title="View" aria-label="View" aria-pressed="false">
|
|
6665
|
+
<span class="commandbar-flex-container">
|
|
6666
|
+
${PREVIEW_ICON_HTML}
|
|
6667
|
+
</span>
|
|
6668
|
+
</button>
|
|
6669
|
+
</div>
|
|
6670
|
+
|
|
6671
|
+
<div class="toolbar-controls" data-ref="toolbar-controls">
|
|
6672
|
+
<button type="button" class="icon-toggle" data-ref="search-btn" title="Find/replace (Mod-F)" aria-label="Find/replace">
|
|
6673
|
+
${SEARCH_ICON_HTML}
|
|
6674
|
+
</button>
|
|
6675
|
+
<button type="button" class="icon-toggle" data-ref="save-btn" title="Save" aria-label="Save">
|
|
6676
|
+
${SAVE_ICON_HTML}
|
|
6677
|
+
</button>
|
|
6678
|
+
<button type="button" class="icon-toggle zoom-toggle" data-ref="zoom-btn" title="Zoom controls" aria-label="Zoom controls">
|
|
6679
|
+
${ZOOM_ICON_HTML}
|
|
6680
|
+
</button>
|
|
6681
|
+
<div class="theme-toggle-group" data-ref="theme-controls" role="group" aria-label="Color scheme">
|
|
6682
|
+
<button type="button" class="icon-toggle theme-toggle" data-ref="dark-theme-btn"
|
|
6683
|
+
title="Dark mode" aria-label="Dark mode" aria-pressed="false">
|
|
6684
|
+
${DARK_THEME_ICON_HTML}
|
|
6685
|
+
</button>
|
|
6686
|
+
<button type="button" class="icon-toggle theme-toggle" data-ref="light-theme-btn"
|
|
6687
|
+
title="Light mode" aria-label="Light mode" aria-pressed="false">
|
|
6688
|
+
${LIGHT_THEME_ICON_HTML}
|
|
6689
|
+
</button>
|
|
6690
|
+
</div>
|
|
6691
|
+
<div class="zoom-popover" data-ref="zoom-popover" hidden role="group" aria-label="Zoom">
|
|
6692
|
+
<span class="zoom-level" data-ref="zoom-level">100%</span>
|
|
6693
|
+
<span class="zoom-stepper">
|
|
6694
|
+
<button type="button" data-action="zoom-out" title="Zoom out" aria-label="Zoom out">-</button>
|
|
6695
|
+
<button type="button" data-action="zoom-in" title="Zoom in" aria-label="Zoom in">+</button>
|
|
6696
|
+
</span>
|
|
6697
|
+
<button type="button" class="zoom-reset" data-action="zoom-reset" title="Reset zoom">Reset</button>
|
|
6698
|
+
</div>
|
|
6699
|
+
</div>
|
|
6700
|
+
</header>
|
|
6701
|
+
|
|
6702
|
+
<div class="workspace-shell" data-ref="workspace-shell" hidden>
|
|
6703
|
+
<aside class="nav-pane" data-ref="nav-pane" aria-label="Package contents">
|
|
6704
|
+
<div class="nav-tree" data-ref="nav-tree"></div>
|
|
6705
|
+
</aside>
|
|
6706
|
+
<div class="nav-resizer" data-ref="nav-resizer"
|
|
6707
|
+
role="separator" aria-orientation="vertical" aria-label="Resize contents pane"></div>
|
|
6708
|
+
|
|
6709
|
+
<div class="pane-stack" data-ref="pane-stack">
|
|
6710
|
+
<section class="pane edit-pane" data-ref="edit-pane">
|
|
6711
|
+
<div class="editor-host" data-ref="editor-host"></div>
|
|
6712
|
+
</section>
|
|
6713
|
+
<div class="split-resizer" data-ref="split-resizer"
|
|
6714
|
+
role="separator" aria-orientation="vertical" aria-label="Resize split panes"></div>
|
|
6715
|
+
<section class="pane preview-pane" data-ref="preview-pane" tabindex="-1">
|
|
6716
|
+
<article class="preview-content" data-ref="preview-content"></article>
|
|
6717
|
+
</section>
|
|
6718
|
+
<section class="pane entry-pane" data-ref="entry-pane"></section>
|
|
6719
|
+
</div>
|
|
6720
|
+
</div>
|
|
6721
|
+
|
|
6722
|
+
<div class="title-dialog-backdrop" data-ref="title-dialog" hidden
|
|
6723
|
+
role="dialog" aria-modal="true" aria-labelledby="mdzip-title-dialog-heading">
|
|
6724
|
+
<div class="title-dialog">
|
|
6725
|
+
<h3 id="mdzip-title-dialog-heading">Set Document Title</h3>
|
|
6726
|
+
<p>This is the package-level title stored in manifest.json.</p>
|
|
6727
|
+
<input type="text" maxlength="120" data-ref="title-input" aria-label="Document title" />
|
|
6728
|
+
<p class="title-dialog-validation" data-ref="title-validation" hidden>Title cannot be empty.</p>
|
|
6729
|
+
<p>If unset, consumers may fall back to entry point, filename, or first heading.</p>
|
|
6730
|
+
<div class="title-dialog-actions">
|
|
6731
|
+
<button type="button" class="reset-title" data-ref="title-reset-btn">Reset</button>
|
|
6732
|
+
<button type="button" data-action="cancel-title">Cancel</button>
|
|
6733
|
+
<button type="button" class="save-title" data-ref="title-save-btn">Save</button>
|
|
6734
|
+
</div>
|
|
6735
|
+
</div>
|
|
6736
|
+
</div>
|
|
6737
|
+
|
|
6738
|
+
<div class="title-dialog-backdrop" data-ref="conversion-dialog" hidden
|
|
6739
|
+
role="dialog" aria-modal="true" aria-labelledby="mdzip-conversion-dialog-heading">
|
|
6740
|
+
<div class="title-dialog">
|
|
6741
|
+
<h3 id="mdzip-conversion-dialog-heading">Convert to MDZ?</h3>
|
|
6742
|
+
<p>
|
|
6743
|
+
Regular Markdown files contain only text. Convert this document to an
|
|
6744
|
+
MDZ package to add images, package navigation, and document metadata.
|
|
6745
|
+
</p>
|
|
6746
|
+
<p>The next save will produce an .mdz file.</p>
|
|
6747
|
+
<div class="title-dialog-actions">
|
|
6748
|
+
<button type="button" data-action="cancel-conversion">Cancel</button>
|
|
6749
|
+
<button type="button" class="save-title" data-ref="conversion-confirm-btn">Convert</button>
|
|
6750
|
+
</div>
|
|
6751
|
+
</div>
|
|
6752
|
+
</div>
|
|
6753
|
+
|
|
6754
|
+
<div class="title-dialog-backdrop" data-ref="image-insert-dialog" hidden
|
|
6755
|
+
role="dialog" aria-modal="true" aria-labelledby="mdzip-image-insert-dialog-heading">
|
|
6756
|
+
<div class="title-dialog image-insert-dialog">
|
|
6757
|
+
<h3 id="mdzip-image-insert-dialog-heading">Insert Image</h3>
|
|
6758
|
+
<p>Choose the Markdown inserted for this image.</p>
|
|
6759
|
+
<fieldset class="image-insert-options">
|
|
6760
|
+
<legend>Output</legend>
|
|
6761
|
+
<label>
|
|
6762
|
+
<input type="radio" name="mdzip-image-insert-mode" value="markdown"
|
|
6763
|
+
data-ref="image-insert-mode-markdown" checked />
|
|
6764
|
+
Markdown image
|
|
6765
|
+
</label>
|
|
6766
|
+
<label>
|
|
6767
|
+
<input type="radio" name="mdzip-image-insert-mode" value="html"
|
|
6768
|
+
data-ref="image-insert-mode-html" />
|
|
6769
|
+
HTML <img> with sizing
|
|
6770
|
+
</label>
|
|
6771
|
+
</fieldset>
|
|
6772
|
+
<label class="image-insert-field">
|
|
6773
|
+
<span>Alt text</span>
|
|
6774
|
+
<input type="text" data-ref="image-insert-alt" />
|
|
6775
|
+
</label>
|
|
6776
|
+
<div class="image-insert-grid">
|
|
6777
|
+
<label class="image-insert-field">
|
|
6778
|
+
<span>Size by</span>
|
|
6779
|
+
<select data-ref="image-insert-size-mode">
|
|
6780
|
+
<option value="original">Original size</option>
|
|
6781
|
+
<option value="width" selected>Width</option>
|
|
6782
|
+
<option value="height">Height</option>
|
|
6783
|
+
<option value="percent">Percent</option>
|
|
6784
|
+
</select>
|
|
6785
|
+
</label>
|
|
6786
|
+
<label class="image-insert-field">
|
|
6787
|
+
<span>Value</span>
|
|
6788
|
+
<input type="number" min="1" step="1" data-ref="image-insert-size-value" />
|
|
6789
|
+
</label>
|
|
6790
|
+
</div>
|
|
6791
|
+
<label class="image-insert-field">
|
|
6792
|
+
<span>Position</span>
|
|
6793
|
+
<select data-ref="image-insert-position">
|
|
6794
|
+
<option value="inline">Inline/default</option>
|
|
6795
|
+
<option value="left">Left</option>
|
|
6796
|
+
<option value="center">Center</option>
|
|
6797
|
+
<option value="right">Right</option>
|
|
6798
|
+
<option value="wrap-left">Wrap left</option>
|
|
6799
|
+
<option value="wrap-right">Wrap right</option>
|
|
6800
|
+
</select>
|
|
6801
|
+
</label>
|
|
6802
|
+
<div class="title-dialog-actions">
|
|
6803
|
+
<button type="button" data-action="cancel-image-insert">Cancel</button>
|
|
6804
|
+
<button type="button" class="save-title" data-ref="image-insert-confirm-btn">Insert</button>
|
|
6805
|
+
</div>
|
|
6806
|
+
</div>
|
|
6807
|
+
</div>
|
|
6808
|
+
|
|
6809
|
+
<div class="title-dialog-backdrop" data-ref="pack-files-dialog" hidden
|
|
6810
|
+
role="dialog" aria-modal="true" aria-labelledby="mdzip-pack-files-dialog-heading">
|
|
6811
|
+
<div class="title-dialog pack-files-dialog">
|
|
6812
|
+
<h3 id="mdzip-pack-files-dialog-heading">Package Markdown Files</h3>
|
|
6813
|
+
<p>These files contain more than one Markdown document. Choose how to package them.</p>
|
|
6814
|
+
<fieldset class="pack-files-options">
|
|
6815
|
+
<legend>Mode</legend>
|
|
6816
|
+
<label>
|
|
6817
|
+
<input type="radio" name="mdzip-pack-files-mode" value="document"
|
|
6818
|
+
data-ref="pack-files-mode-document" checked />
|
|
6819
|
+
Document — one primary page; the rest are attached
|
|
6820
|
+
</label>
|
|
6821
|
+
<label>
|
|
6822
|
+
<input type="radio" name="mdzip-pack-files-mode" value="project"
|
|
6823
|
+
data-ref="pack-files-mode-project" />
|
|
6824
|
+
Project — a set of related documents
|
|
6825
|
+
</label>
|
|
6826
|
+
</fieldset>
|
|
6827
|
+
<label class="pack-files-field">
|
|
6828
|
+
<span>Entry point</span>
|
|
6829
|
+
<select data-ref="pack-files-entry"></select>
|
|
6830
|
+
</label>
|
|
6831
|
+
<div class="title-dialog-actions">
|
|
6832
|
+
<button type="button" data-action="cancel-pack-files">Cancel</button>
|
|
6833
|
+
<button type="button" class="save-title" data-ref="pack-files-confirm-btn">Package</button>
|
|
6834
|
+
</div>
|
|
6835
|
+
</div>
|
|
6836
|
+
</div>
|
|
6837
|
+
|
|
6838
|
+
<div class="title-dialog-backdrop" data-ref="copy-render-dialog" hidden
|
|
6839
|
+
role="dialog" aria-modal="true" aria-labelledby="mdzip-copy-render-dialog-heading">
|
|
6840
|
+
<div class="title-dialog copy-render-dialog">
|
|
6841
|
+
<h3 id="mdzip-copy-render-dialog-heading" data-ref="copy-render-heading">Copying document...</h3>
|
|
6842
|
+
<div data-ref="copy-render-progress-section">
|
|
6843
|
+
<p data-ref="copy-render-progress-text">Rendering the full document...</p>
|
|
6844
|
+
<div class="copy-render-progress-track">
|
|
6845
|
+
<div class="copy-render-progress-bar" data-ref="copy-render-progress-bar"></div>
|
|
6846
|
+
</div>
|
|
6847
|
+
</div>
|
|
6848
|
+
<p data-ref="copy-render-ready-text" hidden>
|
|
6849
|
+
The document is ready. Browsers only allow a clipboard write right after a click, so this needs one more —
|
|
6850
|
+
click Copy to finish.
|
|
6851
|
+
</p>
|
|
6852
|
+
<p data-ref="copy-render-done-text" hidden></p>
|
|
6853
|
+
<div class="title-dialog-actions">
|
|
6854
|
+
<button type="button" data-action="cancel-copy-render" data-ref="copy-render-cancel-btn">Cancel</button>
|
|
6855
|
+
<button type="button" data-action="cancel-copy-ready" data-ref="copy-render-ready-cancel-btn" hidden>Cancel</button>
|
|
6856
|
+
<button type="button" class="save-title" data-action="confirm-copy-ready" data-ref="copy-render-confirm-btn" hidden>Copy</button>
|
|
6857
|
+
<button type="button" class="save-title" data-action="dismiss-copy-render" data-ref="copy-render-dismiss-btn" hidden>Dismiss</button>
|
|
6858
|
+
</div>
|
|
6859
|
+
</div>
|
|
6860
|
+
</div>
|
|
6861
|
+
|
|
6862
|
+
<div class="title-dialog-backdrop" data-ref="metadata-dialog" hidden
|
|
6863
|
+
role="dialog" aria-modal="true" aria-labelledby="mdzip-metadata-dialog-heading">
|
|
6864
|
+
<div class="title-dialog metadata-dialog">
|
|
6865
|
+
<h3 id="mdzip-metadata-dialog-heading">Document Information</h3>
|
|
6866
|
+
<dl data-ref="metadata-list"></dl>
|
|
6867
|
+
<h4>Libraries</h4>
|
|
6868
|
+
<dl data-ref="library-list"></dl>
|
|
6869
|
+
<div class="title-dialog-actions">
|
|
6870
|
+
<button type="button" class="save-title" data-action="close-metadata">Close</button>
|
|
6871
|
+
</div>
|
|
6872
|
+
</div>
|
|
6873
|
+
</div>
|
|
6874
|
+
|
|
6875
|
+
<div class="title-dialog-backdrop" data-ref="name-dialog" hidden
|
|
6876
|
+
role="dialog" aria-modal="true" aria-labelledby="mdzip-name-dialog-heading">
|
|
6877
|
+
<div class="title-dialog">
|
|
6878
|
+
<h3 id="mdzip-name-dialog-heading" data-ref="name-dialog-heading">New Markdown File</h3>
|
|
6879
|
+
<input type="text" maxlength="260" data-ref="name-input" aria-label="File name" />
|
|
6880
|
+
<p class="title-dialog-validation" data-ref="name-validation" hidden></p>
|
|
6881
|
+
<div class="title-dialog-actions">
|
|
6882
|
+
<button type="button" data-action="cancel-name">Cancel</button>
|
|
6883
|
+
<button type="button" class="save-title" data-ref="name-confirm-btn">Create</button>
|
|
6884
|
+
</div>
|
|
6885
|
+
</div>
|
|
6886
|
+
</div>
|
|
6887
|
+
|
|
6888
|
+
<div class="title-dialog-backdrop" data-ref="delete-dialog" hidden
|
|
6889
|
+
role="dialog" aria-modal="true" aria-labelledby="mdzip-delete-dialog-heading">
|
|
6890
|
+
<div class="title-dialog">
|
|
6891
|
+
<h3 id="mdzip-delete-dialog-heading">Delete File?</h3>
|
|
6892
|
+
<p data-ref="delete-dialog-text"></p>
|
|
6893
|
+
<div class="title-dialog-actions">
|
|
6894
|
+
<button type="button" data-action="cancel-delete">Cancel</button>
|
|
6895
|
+
<button type="button" class="danger-action" data-ref="delete-confirm-btn">Delete</button>
|
|
6896
|
+
</div>
|
|
6897
|
+
</div>
|
|
6898
|
+
</div>
|
|
6899
|
+
|
|
6900
|
+
<input type="file" data-ref="replace-input" hidden />
|
|
6901
|
+
|
|
6902
|
+
<div class="nav-context-menu" data-ref="nav-menu" hidden role="menu"></div>
|
|
6903
|
+
|
|
6904
|
+
<div class="mdzip-tooltip" data-ref="tooltip" role="tooltip" hidden></div>
|
|
6905
|
+
|
|
6906
|
+
<div class="mdzip-copy-toast" data-ref="copy-toast" role="status" aria-live="polite" hidden></div>
|
|
6907
|
+
|
|
6908
|
+
<p class="mdzip-empty" data-ref="empty-state">No MDZip workspace loaded.</p>
|
|
6909
|
+
</section>
|
|
6380
6910
|
`;
|
|
6381
6911
|
//# sourceMappingURL=view.js.map
|