@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/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, resolveMdzipArchiveLinkTarget, renderMdzipPreviewHtml } from './workspace-view.js';
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.toLowerCase());
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
- let dir = children.find((n) => !n.entry && n.path.toLowerCase() === prefix.toLowerCase());
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 = () => { self.lastEditorGestureTime = performance.now(); };
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([...this.pendingNewFolders].map((path) => path.toLowerCase()))
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 lowerPaths = snapshot.content.paths.map((entry) => entry.path.toLowerCase());
3557
+ const paths = snapshot.content.paths.map((entry) => entry.path);
3461
3558
  for (const folder of [...this.pendingNewFolders]) {
3462
- if (lowerPaths.some((path) => path.startsWith(`${folder.toLowerCase()}/`))) {
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 = () => { this.lastPreviewGestureTime = performance.now(); };
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
- const targetPath = resolveMdzipArchiveLinkTarget(href, snapshot.currentPath, snapshot.content.paths);
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
- pane.scrollTop += target.getBoundingClientRect().top - pane.getBoundingClientRect().top;
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.toLowerCase() === path.toLowerCase());
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.toLowerCase() === entry.path.toLowerCase()),
6173
+ .some((orphan) => samePath(orphan, entry.path)),
5919
6174
  isMarkdown: entry.isMarkdown,
5920
- isEntryPoint: entry.path.toLowerCase() === snapshot.content.entryPoint.toLowerCase(),
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.toLowerCase() === target.path.toLowerCase()
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.toLowerCase() === archivePath.toLowerCase());
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.toLowerCase() === path.toLowerCase()) {
6627
+ if (newPath === path) {
6373
6628
  return;
6374
6629
  }
6375
6630
  try {