@mdzip/editor 1.4.8 → 2.0.1
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/AGENTS.md +4 -0
- package/README.md +59 -0
- package/dist/archive-utils.d.ts.map +1 -1
- package/dist/archive-utils.js +3 -3
- package/dist/archive-utils.js.map +1 -1
- package/dist/asset-cache.d.ts +1 -1
- package/dist/asset-cache.d.ts.map +1 -1
- package/dist/asset-cache.js +14 -11
- package/dist/asset-cache.js.map +1 -1
- package/dist/library-info.d.ts +2 -2
- package/dist/library-info.js +2 -2
- package/dist/mdz-archive.worker.js +738 -305
- package/dist/view.d.ts +122 -1
- package/dist/view.d.ts.map +1 -1
- package/dist/view.js +273 -18
- package/dist/view.js.map +1 -1
- package/dist/workspace-view.d.ts +21 -0
- package/dist/workspace-view.d.ts.map +1 -1
- package/dist/workspace-view.js +56 -5
- package/dist/workspace-view.js.map +1 -1
- package/dist/workspace.d.ts.map +1 -1
- package/dist/workspace.js +40 -37
- package/dist/workspace.js.map +1 -1
- package/package.json +82 -82
package/dist/view.js
CHANGED
|
@@ -6,13 +6,14 @@ import { Compartment, EditorState, RangeSetBuilder, StateEffect, StateField } fr
|
|
|
6
6
|
import { Decoration, EditorView, MatchDecorator, ViewPlugin, WidgetType, dropCursor, keymap, lineNumbers } from '@codemirror/view';
|
|
7
7
|
import { tags } from '@lezer/highlight';
|
|
8
8
|
import { Bold, Check, ChevronDown, ChevronRight, ClipboardPaste, ClipboardType, Code, Columns2, Copy, Eraser, Eye, File, FileBraces, FileImage, Folder, FolderOpen, Hash, Heading1, Heading2, Heading3, Heading4, Heading5, Heading6, Highlighter, ImagePlus, Info, Italic, Link2Off, Link, List, ListOrdered, CornerDownLeft, Moon, PackagePlus, PanelLeft, Pilcrow, Quote, Save, Scissors, Search, SpellCheck, SquareCode, SquarePen, Strikethrough, Sun, TextSelect, ZoomIn } from 'lucide';
|
|
9
|
+
import { samePath } from '@mdzip/core-js';
|
|
9
10
|
import { browserClipboardHasImage, readBrowserClipboardImage } from './browser.js';
|
|
10
11
|
import { buildPackedArchiveBytes, isMarkdownArchivePath, resolveMarkdownEntryPoint } from './archive-utils.js';
|
|
11
12
|
import { MdzipAssetSession, mdzipArchiveSourceId, sniffImageSize } from './asset-cache.js';
|
|
12
13
|
import { MD_MARKDOWN_ICON } from './icons/md-markdown.js';
|
|
13
14
|
import { MDZIP_RUNTIME_LIBRARIES } from './library-info.js';
|
|
14
15
|
import { MdzipWorkspaceService, extensionForMime, normalizeArchivePath, relativeArchivePath } from './workspace.js';
|
|
15
|
-
import { buildMdzipNavTree, canEditMdzipPath, escapeHtml, isOrphanedMdzipAsset, mdzipEntryIconKind, isMdzipManifestPath, isMdzipWorkspaceRelativeLink,
|
|
16
|
+
import { buildMdzipNavTree, canEditMdzipPath, escapeHtml, isOrphanedMdzipAsset, mdzipEntryIconKind, isMdzipManifestPath, isMdzipDefaultPolicyExternalLink, isMdzipExternalLink, isMdzipWorkspaceRelativeLink, parseMdzipLink, renderMdzipPreviewHtml } from './workspace-view.js';
|
|
16
17
|
import { escapeMarkdownImageAlt, findImageReferenceAtOffset, formatImageEditMarkdown } from './image-edit.js';
|
|
17
18
|
import { MdzipRenderingService, defaultSafeMarkdownRenderer, groupTokensIntoChunks, chunkSourceKey, collectMdzipHeadingIds, MDZIP_HEADING_ID_PREFIX, tokenEmbedsImage, tokenIsHeading } from './rendering.js';
|
|
18
19
|
import { mdzipFrontMatterExtension } from './front-matter-extension.js';
|
|
@@ -23,6 +24,14 @@ const isImageFile = (path) => IMAGE_EXTENSIONS.test(path);
|
|
|
23
24
|
function isThenable(value) {
|
|
24
25
|
return typeof value?.then === 'function';
|
|
25
26
|
}
|
|
27
|
+
function restoreAttribute(element, name, value) {
|
|
28
|
+
if (value === null) {
|
|
29
|
+
element.removeAttribute(name);
|
|
30
|
+
}
|
|
31
|
+
else {
|
|
32
|
+
element.setAttribute(name, value);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
26
35
|
const NAV_ICON_CLASS = 'nav-lucide-icon';
|
|
27
36
|
const TOOLBAR_ICON_CLASS = 'toggle-icon';
|
|
28
37
|
const MANIFEST_ICON_HTML = lucideIcon(FileBraces, NAV_ICON_CLASS);
|
|
@@ -765,7 +774,7 @@ depth = 0) {
|
|
|
765
774
|
const children = node.children
|
|
766
775
|
.map((child, index) => renderNavNode(child, state, options, childTrail, index === node.children.length - 1, depth + 1))
|
|
767
776
|
.join('');
|
|
768
|
-
const pending = options.pendingFolders.has(node.path
|
|
777
|
+
const pending = options.pendingFolders.has(node.path);
|
|
769
778
|
return `<details class="nav-directory${pending ? ' pending-folder' : ''}" open data-nav-dir="${escapeHtml(node.path)}">
|
|
770
779
|
<summary${pending ? ` title="${escapeHtml(node.path)} — not saved until it contains a file"` : ''}>
|
|
771
780
|
${guides}<span class="nav-caret" aria-hidden="true"></span>
|
|
@@ -788,7 +797,8 @@ function mergePendingFolders(nodes, pendingPaths) {
|
|
|
788
797
|
let prefix = '';
|
|
789
798
|
for (const segment of path.split('/').filter(Boolean)) {
|
|
790
799
|
prefix = prefix ? `${prefix}/${segment}` : segment;
|
|
791
|
-
|
|
800
|
+
// Folders that differ only in case are different folders (spec 1.2.0 §5.4.1).
|
|
801
|
+
let dir = children.find((n) => !n.entry && n.path === prefix);
|
|
792
802
|
if (!dir) {
|
|
793
803
|
dir = { name: segment, path: prefix, children: [] };
|
|
794
804
|
children.push(dir);
|
|
@@ -1006,6 +1016,21 @@ export class MdzipWorkspaceView {
|
|
|
1006
1016
|
// until that document's preview has mounted so there is a heading to scroll
|
|
1007
1017
|
// to. Consumed by firePreviewRendered.
|
|
1008
1018
|
this.pendingAnchorFragment = null;
|
|
1019
|
+
// The previewGeneration whose preview last mounted (or was claimed by an
|
|
1020
|
+
// entry renderer); scrollToAnchor() waits until it catches up.
|
|
1021
|
+
this.previewMountedGeneration = -1;
|
|
1022
|
+
this.previewMountWaiters = [];
|
|
1023
|
+
// True while onPreviewRendered runs — see scrollToAnchor().
|
|
1024
|
+
this.firingPreviewRendered = false;
|
|
1025
|
+
this.destroyed = false;
|
|
1026
|
+
// After an anchor scroll, content above the target can still change height
|
|
1027
|
+
// (images loading or easing open, a diagram rendering) and push it out of
|
|
1028
|
+
// view. This holds the target at the pane's top edge until the user
|
|
1029
|
+
// scrolls either pane or the preview re-renders.
|
|
1030
|
+
this.anchorPin = null;
|
|
1031
|
+
// Each link's own target/rel from before the external-link policy touched
|
|
1032
|
+
// it, so a policy change can restore them before re-applying.
|
|
1033
|
+
this.externalLinkOriginals = new WeakMap();
|
|
1009
1034
|
// Guards syncScrollToPreviewBottom against overlapping drains: set to the
|
|
1010
1035
|
// chunkedRenderState generation currently being force-drained, null when
|
|
1011
1036
|
// none is in flight. A second bottom-edge sync that arrives mid-drain
|
|
@@ -1442,6 +1467,9 @@ export class MdzipWorkspaceView {
|
|
|
1442
1467
|
this.teardownEntryRenderer();
|
|
1443
1468
|
// Release any whenRendered() waiters so their promises do not hang.
|
|
1444
1469
|
this.flushRenderedWaiters();
|
|
1470
|
+
this.destroyed = true;
|
|
1471
|
+
this.markPreviewMounted();
|
|
1472
|
+
this.releaseAnchorPin();
|
|
1445
1473
|
try {
|
|
1446
1474
|
this.workspace?.dispose();
|
|
1447
1475
|
}
|
|
@@ -1594,6 +1622,54 @@ export class MdzipWorkspaceView {
|
|
|
1594
1622
|
setImageEditOptions(options) {
|
|
1595
1623
|
this.options.imageEditHandler = options.imageEditHandler;
|
|
1596
1624
|
}
|
|
1625
|
+
/**
|
|
1626
|
+
* Replaces the external-link rendering policy and re-applies it to the
|
|
1627
|
+
* links already in the preview, without re-rendering. `undefined` restores
|
|
1628
|
+
* every link's own `target`/`rel`.
|
|
1629
|
+
*/
|
|
1630
|
+
setExternalLinks(policy) {
|
|
1631
|
+
this.options.externalLinks = policy;
|
|
1632
|
+
this.applyExternalLinkPolicy(this.elPreviewContent);
|
|
1633
|
+
}
|
|
1634
|
+
applyExternalLinkPolicy(root) {
|
|
1635
|
+
const policy = this.options.externalLinks;
|
|
1636
|
+
for (const link of Array.from(root.querySelectorAll('a[href]'))) {
|
|
1637
|
+
const original = this.externalLinkOriginals.get(link);
|
|
1638
|
+
if (original) {
|
|
1639
|
+
this.externalLinkOriginals.delete(link);
|
|
1640
|
+
restoreAttribute(link, 'target', original.target);
|
|
1641
|
+
restoreAttribute(link, 'rel', original.rel);
|
|
1642
|
+
}
|
|
1643
|
+
if (!policy) {
|
|
1644
|
+
continue;
|
|
1645
|
+
}
|
|
1646
|
+
const rel = policy.rel ?? (policy.target === '_blank' ? 'noopener noreferrer' : undefined);
|
|
1647
|
+
if (policy.target === undefined && rel === undefined) {
|
|
1648
|
+
continue;
|
|
1649
|
+
}
|
|
1650
|
+
const href = link.getAttribute('href') ?? '';
|
|
1651
|
+
if (!isMdzipExternalLink(href)) {
|
|
1652
|
+
continue;
|
|
1653
|
+
}
|
|
1654
|
+
let external = false;
|
|
1655
|
+
try {
|
|
1656
|
+
external = policy.predicate ? policy.predicate(href) : isMdzipDefaultPolicyExternalLink(href);
|
|
1657
|
+
}
|
|
1658
|
+
catch (error) {
|
|
1659
|
+
this.options.onFailed?.(error);
|
|
1660
|
+
}
|
|
1661
|
+
if (!external) {
|
|
1662
|
+
continue;
|
|
1663
|
+
}
|
|
1664
|
+
this.externalLinkOriginals.set(link, { target: link.getAttribute('target'), rel: link.getAttribute('rel') });
|
|
1665
|
+
if (policy.target !== undefined) {
|
|
1666
|
+
link.setAttribute('target', policy.target);
|
|
1667
|
+
}
|
|
1668
|
+
if (rel !== undefined) {
|
|
1669
|
+
link.setAttribute('rel', rel);
|
|
1670
|
+
}
|
|
1671
|
+
}
|
|
1672
|
+
}
|
|
1597
1673
|
applyDensityClasses() {
|
|
1598
1674
|
this.elRoot.classList.remove('toolbar-density-comfortable', 'toolbar-density-compact', 'toolbar-density-dense', 'content-density-comfortable', 'content-density-compact');
|
|
1599
1675
|
this.elRoot.classList.add(`toolbar-density-${this.toolbarDensity}`);
|
|
@@ -1681,6 +1757,7 @@ export class MdzipWorkspaceView {
|
|
|
1681
1757
|
// There is no built-in preview to wait for; release any waiters.
|
|
1682
1758
|
this.previewHydrated = true;
|
|
1683
1759
|
this.flushRenderedWaiters();
|
|
1760
|
+
this.markPreviewMounted();
|
|
1684
1761
|
return;
|
|
1685
1762
|
}
|
|
1686
1763
|
const memo = this.previewMemo;
|
|
@@ -1816,6 +1893,7 @@ export class MdzipWorkspaceView {
|
|
|
1816
1893
|
if (codeBlockHandle) {
|
|
1817
1894
|
this.previewHandles.push(codeBlockHandle);
|
|
1818
1895
|
}
|
|
1896
|
+
this.applyExternalLinkPolicy(this.elPreviewContent);
|
|
1819
1897
|
// No archive images to resolve on this path: the preview is ready now.
|
|
1820
1898
|
this.firePreviewRendered(snapshot, generation);
|
|
1821
1899
|
this.fireAssetsHydrated(snapshot, generation);
|
|
@@ -1838,6 +1916,7 @@ export class MdzipWorkspaceView {
|
|
|
1838
1916
|
if (codeBlockHandle) {
|
|
1839
1917
|
this.previewHandles.push(codeBlockHandle);
|
|
1840
1918
|
}
|
|
1919
|
+
this.applyExternalLinkPolicy(this.elPreviewContent);
|
|
1841
1920
|
this.firePreviewRendered(snapshot, generation);
|
|
1842
1921
|
this.hydrateImages(pending, context, generation, animateImageHydration, () => {
|
|
1843
1922
|
this.fireAssetsHydrated(snapshot, generation);
|
|
@@ -2466,6 +2545,7 @@ export class MdzipWorkspaceView {
|
|
|
2466
2545
|
if (codeBlockHandle) {
|
|
2467
2546
|
record.handles.push(codeBlockHandle);
|
|
2468
2547
|
}
|
|
2548
|
+
this.applyExternalLinkPolicy(record.root);
|
|
2469
2549
|
}
|
|
2470
2550
|
return { cursor, mountedRoots };
|
|
2471
2551
|
}
|
|
@@ -2813,12 +2893,26 @@ export class MdzipWorkspaceView {
|
|
|
2813
2893
|
}
|
|
2814
2894
|
this.restorePendingPreviewScroll(generation);
|
|
2815
2895
|
this.flushPendingAnchorSoon();
|
|
2896
|
+
this.markPreviewMounted();
|
|
2897
|
+
this.firingPreviewRendered = true;
|
|
2816
2898
|
try {
|
|
2817
2899
|
this.options.onPreviewRendered?.(snapshot);
|
|
2818
2900
|
}
|
|
2819
2901
|
catch (error) {
|
|
2820
2902
|
this.options.onFailed?.(error);
|
|
2821
2903
|
}
|
|
2904
|
+
finally {
|
|
2905
|
+
this.firingPreviewRendered = false;
|
|
2906
|
+
}
|
|
2907
|
+
}
|
|
2908
|
+
/** Releases {@link scrollToAnchor} calls waiting for the current preview to mount. */
|
|
2909
|
+
markPreviewMounted() {
|
|
2910
|
+
this.previewMountedGeneration = this.previewGeneration;
|
|
2911
|
+
const waiters = this.previewMountWaiters;
|
|
2912
|
+
this.previewMountWaiters = [];
|
|
2913
|
+
for (const resolve of waiters) {
|
|
2914
|
+
resolve();
|
|
2915
|
+
}
|
|
2822
2916
|
}
|
|
2823
2917
|
/** See `pendingPreviewScrollRestore`'s doc comment. */
|
|
2824
2918
|
restorePendingPreviewScroll(generation) {
|
|
@@ -3147,7 +3241,10 @@ export class MdzipWorkspaceView {
|
|
|
3147
3241
|
// scroll, on every keystroke. Wheel/touch/mousedown (which also covers
|
|
3148
3242
|
// a scrollbar-thumb drag, since that lands on this same element) are
|
|
3149
3243
|
// unambiguous and cover the vast majority of real scrolling.
|
|
3150
|
-
const markEditorGesture = () => {
|
|
3244
|
+
const markEditorGesture = () => {
|
|
3245
|
+
self.lastEditorGestureTime = performance.now();
|
|
3246
|
+
self.releaseAnchorPin();
|
|
3247
|
+
};
|
|
3151
3248
|
scroller.addEventListener('wheel', markEditorGesture, { passive: true });
|
|
3152
3249
|
scroller.addEventListener('touchstart', markEditorGesture, { passive: true });
|
|
3153
3250
|
scroller.addEventListener('mousedown', markEditorGesture);
|
|
@@ -3320,7 +3417,7 @@ export class MdzipWorkspaceView {
|
|
|
3320
3417
|
allowOrphanActions,
|
|
3321
3418
|
allowFileActions,
|
|
3322
3419
|
allowDrag: snapshot.mode !== 'read-only' && snapshot.sourceFormat === 'mdz',
|
|
3323
|
-
pendingFolders: new Set(
|
|
3420
|
+
pendingFolders: new Set(this.pendingNewFolders)
|
|
3324
3421
|
};
|
|
3325
3422
|
this.elNavTree.innerHTML = navTree
|
|
3326
3423
|
.map((n, i) => renderNavNode(n, snapshot, navRenderOptions, [], i === navTree.length - 1, 0))
|
|
@@ -3457,9 +3554,9 @@ export class MdzipWorkspaceView {
|
|
|
3457
3554
|
this.pendingNewFolders.clear();
|
|
3458
3555
|
return;
|
|
3459
3556
|
}
|
|
3460
|
-
const
|
|
3557
|
+
const paths = snapshot.content.paths.map((entry) => entry.path);
|
|
3461
3558
|
for (const folder of [...this.pendingNewFolders]) {
|
|
3462
|
-
if (
|
|
3559
|
+
if (paths.some((path) => path.startsWith(`${folder}/`))) {
|
|
3463
3560
|
this.pendingNewFolders.delete(folder);
|
|
3464
3561
|
}
|
|
3465
3562
|
}
|
|
@@ -3944,7 +4041,10 @@ export class MdzipWorkspaceView {
|
|
|
3944
4041
|
void this.replaceFileFromPicker(path, file);
|
|
3945
4042
|
}
|
|
3946
4043
|
});
|
|
3947
|
-
const markPreviewGesture = () => {
|
|
4044
|
+
const markPreviewGesture = () => {
|
|
4045
|
+
this.lastPreviewGestureTime = performance.now();
|
|
4046
|
+
this.releaseAnchorPin();
|
|
4047
|
+
};
|
|
3948
4048
|
this.elPreviewPane.addEventListener('wheel', markPreviewGesture, { passive: true });
|
|
3949
4049
|
this.elPreviewPane.addEventListener('touchstart', markPreviewGesture, { passive: true });
|
|
3950
4050
|
this.elPreviewPane.addEventListener('mousedown', markPreviewGesture);
|
|
@@ -3963,17 +4063,20 @@ export class MdzipWorkspaceView {
|
|
|
3963
4063
|
return;
|
|
3964
4064
|
}
|
|
3965
4065
|
const href = link.getAttribute('href') ?? '';
|
|
4066
|
+
const parsed = parseMdzipLink(href, snapshot.currentPath, snapshot.content.paths);
|
|
4067
|
+
if (this.options.onLinkActivated && this.dispatchLinkActivated(event, link, href, parsed, snapshot)) {
|
|
4068
|
+
return;
|
|
4069
|
+
}
|
|
3966
4070
|
if (href.startsWith('#')) {
|
|
3967
4071
|
event.preventDefault();
|
|
3968
4072
|
void this.scrollPreviewToAnchor(href.slice(1));
|
|
3969
4073
|
return;
|
|
3970
4074
|
}
|
|
3971
|
-
|
|
3972
|
-
if (targetPath) {
|
|
4075
|
+
if (parsed.targetPath) {
|
|
3973
4076
|
event.preventDefault();
|
|
3974
4077
|
const hashIndex = href.indexOf('#');
|
|
3975
4078
|
const fragment = hashIndex >= 0 ? href.slice(hashIndex + 1) : '';
|
|
3976
|
-
void this.openPathAndScrollToAnchor(targetPath, fragment);
|
|
4079
|
+
void this.openPathAndScrollToAnchor(parsed.targetPath, fragment);
|
|
3977
4080
|
return;
|
|
3978
4081
|
}
|
|
3979
4082
|
if (this.options.onUnresolvedLinkClick && isMdzipWorkspaceRelativeLink(href)) {
|
|
@@ -4025,6 +4128,132 @@ export class MdzipWorkspaceView {
|
|
|
4025
4128
|
await this.scrollPreviewToAnchor(pending);
|
|
4026
4129
|
}
|
|
4027
4130
|
}
|
|
4131
|
+
/** Returns true when the host prevented the click, ending all further handling. */
|
|
4132
|
+
dispatchLinkActivated(domEvent, link, href, parsed, snapshot) {
|
|
4133
|
+
let prevented = false;
|
|
4134
|
+
const event = {
|
|
4135
|
+
href,
|
|
4136
|
+
text: (link.textContent ?? '').trim(),
|
|
4137
|
+
kind: parsed.kind,
|
|
4138
|
+
isExternal: parsed.kind === 'external',
|
|
4139
|
+
isAnchor: parsed.kind === 'anchor',
|
|
4140
|
+
path: parsed.path,
|
|
4141
|
+
anchor: parsed.anchor,
|
|
4142
|
+
sourcePath: snapshot.currentPath,
|
|
4143
|
+
targetPath: parsed.targetPath,
|
|
4144
|
+
ctrlKey: domEvent.ctrlKey,
|
|
4145
|
+
metaKey: domEvent.metaKey,
|
|
4146
|
+
shiftKey: domEvent.shiftKey,
|
|
4147
|
+
altKey: domEvent.altKey,
|
|
4148
|
+
domEvent,
|
|
4149
|
+
snapshot,
|
|
4150
|
+
preventDefault: () => {
|
|
4151
|
+
prevented = true;
|
|
4152
|
+
},
|
|
4153
|
+
get defaultPrevented() {
|
|
4154
|
+
return prevented;
|
|
4155
|
+
}
|
|
4156
|
+
};
|
|
4157
|
+
try {
|
|
4158
|
+
this.options.onLinkActivated?.(event);
|
|
4159
|
+
}
|
|
4160
|
+
catch (error) {
|
|
4161
|
+
this.options.onFailed?.(error);
|
|
4162
|
+
}
|
|
4163
|
+
if (prevented) {
|
|
4164
|
+
domEvent.preventDefault();
|
|
4165
|
+
}
|
|
4166
|
+
return prevented;
|
|
4167
|
+
}
|
|
4168
|
+
/**
|
|
4169
|
+
* Scrolls the preview to a heading or an explicit `id`/`name` anchor.
|
|
4170
|
+
* Accepts the fragment with or without its leading `#` and matches it the
|
|
4171
|
+
* way a clicked `#fragment` link does: case-insensitive, percent-decoded,
|
|
4172
|
+
* against both bare and `user-content-`-prefixed heading ids.
|
|
4173
|
+
*
|
|
4174
|
+
* Safe to call at any point — straight after `open()`/`openWorkspace()`,
|
|
4175
|
+
* or from `onPreviewRendered`: it waits for the current document's
|
|
4176
|
+
* preview to mount, and under progressive rendering mounts the chunks up
|
|
4177
|
+
* to the target first. Resolves `true` when it scrolled to a matching
|
|
4178
|
+
* anchor (`''` and `'top'` scroll to the top and resolve `true`), and
|
|
4179
|
+
* `false` when nothing matches, an entry renderer owns the pane, or the
|
|
4180
|
+
* view is destroyed first.
|
|
4181
|
+
*/
|
|
4182
|
+
async scrollToAnchor(anchor) {
|
|
4183
|
+
const fragment = anchor.startsWith('#') ? anchor.slice(1) : anchor;
|
|
4184
|
+
// onPreviewRendered runs inside the first chunk batch's completion,
|
|
4185
|
+
// before the lazy-continuation sentinel is armed — same race as
|
|
4186
|
+
// flushPendingAnchorSoon, so a call from it (or one that had to wait for
|
|
4187
|
+
// the mount) is deferred a frame.
|
|
4188
|
+
let deferFrame = this.firingPreviewRendered;
|
|
4189
|
+
while (!this.destroyed && this.previewMountedGeneration !== this.previewGeneration) {
|
|
4190
|
+
deferFrame = true;
|
|
4191
|
+
await new Promise((resolve) => this.previewMountWaiters.push(resolve));
|
|
4192
|
+
}
|
|
4193
|
+
if (this.destroyed) {
|
|
4194
|
+
return false;
|
|
4195
|
+
}
|
|
4196
|
+
if (deferFrame) {
|
|
4197
|
+
const generation = this.previewGeneration;
|
|
4198
|
+
await new Promise((resolve) => requestAnimationFrame(() => resolve()));
|
|
4199
|
+
if (this.destroyed) {
|
|
4200
|
+
return false;
|
|
4201
|
+
}
|
|
4202
|
+
if (generation !== this.previewGeneration) {
|
|
4203
|
+
return this.scrollToAnchor(anchor);
|
|
4204
|
+
}
|
|
4205
|
+
}
|
|
4206
|
+
if (this.entryState) {
|
|
4207
|
+
return false;
|
|
4208
|
+
}
|
|
4209
|
+
const found = await this.scrollPreviewToAnchor(fragment);
|
|
4210
|
+
return found || !fragment || fragment.toLowerCase() === 'top';
|
|
4211
|
+
}
|
|
4212
|
+
/**
|
|
4213
|
+
* The anchors {@link scrollToAnchor} can reach in the current preview, in
|
|
4214
|
+
* document order: every heading id (without the `user-content-` prefix),
|
|
4215
|
+
* including headings in chunks progressive rendering hasn't mounted yet,
|
|
4216
|
+
* plus explicit `id`/`name` anchors on links in the mounted preview.
|
|
4217
|
+
* Empty before the preview first mounts.
|
|
4218
|
+
*/
|
|
4219
|
+
getAvailableAnchors() {
|
|
4220
|
+
if (this.entryState) {
|
|
4221
|
+
return [];
|
|
4222
|
+
}
|
|
4223
|
+
const anchors = [];
|
|
4224
|
+
const seen = new Set();
|
|
4225
|
+
const add = (value) => {
|
|
4226
|
+
if (!value)
|
|
4227
|
+
return;
|
|
4228
|
+
const bare = value.startsWith(MDZIP_HEADING_ID_PREFIX) ? value.slice(MDZIP_HEADING_ID_PREFIX.length) : value;
|
|
4229
|
+
if (bare && !seen.has(bare)) {
|
|
4230
|
+
seen.add(bare);
|
|
4231
|
+
anchors.push(bare);
|
|
4232
|
+
}
|
|
4233
|
+
};
|
|
4234
|
+
const fromDom = (root) => {
|
|
4235
|
+
for (const element of Array.from(root.querySelectorAll('h1[id],h2[id],h3[id],h4[id],h5[id],h6[id],a[id],a[name]'))) {
|
|
4236
|
+
add(element.getAttribute('id'));
|
|
4237
|
+
add(element.getAttribute('name'));
|
|
4238
|
+
}
|
|
4239
|
+
};
|
|
4240
|
+
const state = this.chunkedRenderState;
|
|
4241
|
+
if (state && state.generation === this.previewGeneration) {
|
|
4242
|
+
// Walk records in order so unmounted chunks' headings land in place.
|
|
4243
|
+
state.records.forEach((record) => {
|
|
4244
|
+
if (record.root) {
|
|
4245
|
+
fromDom(record.root);
|
|
4246
|
+
}
|
|
4247
|
+
else {
|
|
4248
|
+
collectMdzipHeadingIds(record.tokens).forEach(add);
|
|
4249
|
+
}
|
|
4250
|
+
});
|
|
4251
|
+
}
|
|
4252
|
+
else {
|
|
4253
|
+
fromDom(this.elPreviewContent);
|
|
4254
|
+
}
|
|
4255
|
+
return anchors;
|
|
4256
|
+
}
|
|
4028
4257
|
// Deferred a frame: firePreviewRendered runs inside the first chunk batch's
|
|
4029
4258
|
// completion callback, before the lazy-continuation sentinel for the rest is
|
|
4030
4259
|
// armed. Scrolling then (which may need to drain those chunks) would race it.
|
|
@@ -4103,9 +4332,35 @@ export class MdzipWorkspaceView {
|
|
|
4103
4332
|
}
|
|
4104
4333
|
return false;
|
|
4105
4334
|
}
|
|
4106
|
-
|
|
4335
|
+
this.scrollPaneToElement(target);
|
|
4336
|
+
this.pinPreviewAnchor(target);
|
|
4107
4337
|
return true;
|
|
4108
4338
|
}
|
|
4339
|
+
scrollPaneToElement(target) {
|
|
4340
|
+
const pane = this.elPreviewPane;
|
|
4341
|
+
pane.scrollTop += target.getBoundingClientRect().top - pane.getBoundingClientRect().top;
|
|
4342
|
+
}
|
|
4343
|
+
/** See `anchorPin`. */
|
|
4344
|
+
pinPreviewAnchor(target) {
|
|
4345
|
+
this.releaseAnchorPin();
|
|
4346
|
+
if (typeof ResizeObserver === 'undefined') {
|
|
4347
|
+
return;
|
|
4348
|
+
}
|
|
4349
|
+
const generation = this.previewGeneration;
|
|
4350
|
+
const observer = new ResizeObserver(() => {
|
|
4351
|
+
if (generation !== this.previewGeneration || !target.isConnected) {
|
|
4352
|
+
this.releaseAnchorPin();
|
|
4353
|
+
return;
|
|
4354
|
+
}
|
|
4355
|
+
this.scrollPaneToElement(target);
|
|
4356
|
+
});
|
|
4357
|
+
observer.observe(this.elPreviewContent);
|
|
4358
|
+
this.anchorPin = observer;
|
|
4359
|
+
}
|
|
4360
|
+
releaseAnchorPin() {
|
|
4361
|
+
this.anchorPin?.disconnect();
|
|
4362
|
+
this.anchorPin = null;
|
|
4363
|
+
}
|
|
4109
4364
|
async openPath(path) {
|
|
4110
4365
|
if (!this.workspace) {
|
|
4111
4366
|
return;
|
|
@@ -5907,7 +6162,7 @@ export class MdzipWorkspaceView {
|
|
|
5907
6162
|
return { kind: 'directory', path: '' };
|
|
5908
6163
|
}
|
|
5909
6164
|
fileMenuTarget(path, snapshot) {
|
|
5910
|
-
const entry = snapshot.content.paths.find((item) => item.path
|
|
6165
|
+
const entry = snapshot.content.paths.find((item) => samePath(item.path, path));
|
|
5911
6166
|
if (!entry) {
|
|
5912
6167
|
return null;
|
|
5913
6168
|
}
|
|
@@ -5915,9 +6170,9 @@ export class MdzipWorkspaceView {
|
|
|
5915
6170
|
kind: 'file',
|
|
5916
6171
|
path: entry.path,
|
|
5917
6172
|
orphaned: snapshot.content.orphanedAssetPaths
|
|
5918
|
-
.some((orphan) => orphan
|
|
6173
|
+
.some((orphan) => samePath(orphan, entry.path)),
|
|
5919
6174
|
isMarkdown: entry.isMarkdown,
|
|
5920
|
-
isEntryPoint: entry.path
|
|
6175
|
+
isEntryPoint: samePath(entry.path, snapshot.content.entryPoint),
|
|
5921
6176
|
isImage: entry.isImage,
|
|
5922
6177
|
isManifest: isMdzipManifestPath(entry.path)
|
|
5923
6178
|
};
|
|
@@ -5963,7 +6218,7 @@ export class MdzipWorkspaceView {
|
|
|
5963
6218
|
}
|
|
5964
6219
|
if (target.isImage) {
|
|
5965
6220
|
const cover = snapshot.content.manifest?.cover;
|
|
5966
|
-
stateGroup.push(cover && cover.
|
|
6221
|
+
stateGroup.push(cover && samePath(cover.replace(/^\.\//, ''), target.path)
|
|
5967
6222
|
? { action: 'remove-cover', label: 'Remove Cover Image' }
|
|
5968
6223
|
: { action: 'set-cover', label: 'Set as Cover Image' });
|
|
5969
6224
|
}
|
|
@@ -6216,7 +6471,7 @@ export class MdzipWorkspaceView {
|
|
|
6216
6471
|
if (!editor || !snapshot || snapshot.sourceFormat !== 'mdz') {
|
|
6217
6472
|
return;
|
|
6218
6473
|
}
|
|
6219
|
-
const entry = snapshot.content.paths.find((item) => item.path
|
|
6474
|
+
const entry = snapshot.content.paths.find((item) => samePath(item.path, archivePath));
|
|
6220
6475
|
if (!entry || isMdzipManifestPath(entry.path)) {
|
|
6221
6476
|
return;
|
|
6222
6477
|
}
|
|
@@ -6369,7 +6624,7 @@ export class MdzipWorkspaceView {
|
|
|
6369
6624
|
async moveFileToDirectory(path, targetDir) {
|
|
6370
6625
|
const baseName = path.slice(path.lastIndexOf('/') + 1);
|
|
6371
6626
|
const newPath = targetDir ? `${targetDir}/${baseName}` : baseName;
|
|
6372
|
-
if (newPath
|
|
6627
|
+
if (newPath === path) {
|
|
6373
6628
|
return;
|
|
6374
6629
|
}
|
|
6375
6630
|
try {
|