@mdzip/editor 1.4.7 → 1.5.0

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
@@ -12,7 +12,7 @@ import { MdzipAssetSession, mdzipArchiveSourceId, sniffImageSize } from './asset
12
12
  import { MD_MARKDOWN_ICON } from './icons/md-markdown.js';
13
13
  import { MDZIP_RUNTIME_LIBRARIES } from './library-info.js';
14
14
  import { MdzipWorkspaceService, extensionForMime, normalizeArchivePath, relativeArchivePath } from './workspace.js';
15
- import { buildMdzipNavTree, canEditMdzipPath, escapeHtml, isOrphanedMdzipAsset, mdzipEntryIconKind, isMdzipManifestPath, isMdzipWorkspaceRelativeLink, resolveMdzipArchiveLinkTarget, renderMdzipPreviewHtml } from './workspace-view.js';
15
+ import { buildMdzipNavTree, canEditMdzipPath, escapeHtml, isOrphanedMdzipAsset, mdzipEntryIconKind, isMdzipManifestPath, isMdzipDefaultPolicyExternalLink, isMdzipExternalLink, isMdzipWorkspaceRelativeLink, parseMdzipLink, renderMdzipPreviewHtml } from './workspace-view.js';
16
16
  import { escapeMarkdownImageAlt, findImageReferenceAtOffset, formatImageEditMarkdown } from './image-edit.js';
17
17
  import { MdzipRenderingService, defaultSafeMarkdownRenderer, groupTokensIntoChunks, chunkSourceKey, collectMdzipHeadingIds, MDZIP_HEADING_ID_PREFIX, tokenEmbedsImage, tokenIsHeading } from './rendering.js';
18
18
  import { mdzipFrontMatterExtension } from './front-matter-extension.js';
@@ -23,6 +23,14 @@ const isImageFile = (path) => IMAGE_EXTENSIONS.test(path);
23
23
  function isThenable(value) {
24
24
  return typeof value?.then === 'function';
25
25
  }
26
+ function restoreAttribute(element, name, value) {
27
+ if (value === null) {
28
+ element.removeAttribute(name);
29
+ }
30
+ else {
31
+ element.setAttribute(name, value);
32
+ }
33
+ }
26
34
  const NAV_ICON_CLASS = 'nav-lucide-icon';
27
35
  const TOOLBAR_ICON_CLASS = 'toggle-icon';
28
36
  const MANIFEST_ICON_HTML = lucideIcon(FileBraces, NAV_ICON_CLASS);
@@ -746,19 +754,19 @@ depth = 0) {
746
754
  : isImageFile(node.entry.path)
747
755
  ? IMAGE_ICON_HTML
748
756
  : FILE_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}
757
+ const orphanBtnHtml = isOrphaned && options.allowOrphanActions ? `
758
+ <span class="nav-orphan-button" role="button" tabindex="0"
759
+ title="Orphaned asset" aria-label="Orphaned asset actions"
760
+ data-orphan-path="${safePath}">
761
+ ${ORPHAN_ICON_HTML}
754
762
  </span>` : '';
755
763
  const draggable = options.allowDrag && !isManifest;
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>
764
+ return `<button type="button" class="${classes}" title="${title}"
765
+ data-nav-path="${safePath}" data-orphan="${isOrphaned ? 'true' : ''}"${draggable ? ' draggable="true"' : ''}>
766
+ ${guides}<span class="nav-caret"></span>
767
+ <span class="nav-file-icon ${iconKind}">${iconHtml}</span>
768
+ ${orphanBtnHtml}
769
+ <span class="nav-label">${safeName}</span>
762
770
  </button>`;
763
771
  }
764
772
  const childTrail = depth === 0 ? [] : [...trail, !isLast];
@@ -766,14 +774,14 @@ depth = 0) {
766
774
  .map((child, index) => renderNavNode(child, state, options, childTrail, index === node.children.length - 1, depth + 1))
767
775
  .join('');
768
776
  const pending = options.pendingFolders.has(node.path.toLowerCase());
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>
777
+ return `<details class="nav-directory${pending ? ' pending-folder' : ''}" open data-nav-dir="${escapeHtml(node.path)}">
778
+ <summary${pending ? ` title="${escapeHtml(node.path)} — not saved until it contains a file"` : ''}>
779
+ ${guides}<span class="nav-caret" aria-hidden="true"></span>
780
+ <span class="nav-folder-icon closed">${FOLDER_CLOSED_ICON_HTML}</span>
781
+ <span class="nav-folder-icon open">${FOLDER_OPEN_ICON_HTML}</span>
782
+ <span class="nav-label">${escapeHtml(node.name)}</span>
783
+ </summary>
784
+ <div class="nav-directory-children">${children}</div>
777
785
  </details>`;
778
786
  }
779
787
  // Inserts view-local pending (not yet saved) folders into the nav tree so they
@@ -1006,6 +1014,21 @@ export class MdzipWorkspaceView {
1006
1014
  // until that document's preview has mounted so there is a heading to scroll
1007
1015
  // to. Consumed by firePreviewRendered.
1008
1016
  this.pendingAnchorFragment = null;
1017
+ // The previewGeneration whose preview last mounted (or was claimed by an
1018
+ // entry renderer); scrollToAnchor() waits until it catches up.
1019
+ this.previewMountedGeneration = -1;
1020
+ this.previewMountWaiters = [];
1021
+ // True while onPreviewRendered runs — see scrollToAnchor().
1022
+ this.firingPreviewRendered = false;
1023
+ this.destroyed = false;
1024
+ // After an anchor scroll, content above the target can still change height
1025
+ // (images loading or easing open, a diagram rendering) and push it out of
1026
+ // view. This holds the target at the pane's top edge until the user
1027
+ // scrolls either pane or the preview re-renders.
1028
+ this.anchorPin = null;
1029
+ // Each link's own target/rel from before the external-link policy touched
1030
+ // it, so a policy change can restore them before re-applying.
1031
+ this.externalLinkOriginals = new WeakMap();
1009
1032
  // Guards syncScrollToPreviewBottom against overlapping drains: set to the
1010
1033
  // chunkedRenderState generation currently being force-drained, null when
1011
1034
  // none is in flight. A second bottom-edge sync that arrives mid-drain
@@ -1283,7 +1306,12 @@ export class MdzipWorkspaceView {
1283
1306
  void this.notifyChanged(event);
1284
1307
  });
1285
1308
  this.render();
1286
- void this.notifyChanged(this.initialWorkspaceEvent(ws.snapshot()));
1309
+ // A lazy workspace handed over without archiveBytes can only be exported
1310
+ // by rebuilding it, reading every lazy document first — and the host that
1311
+ // passed it in already holds those bytes. Skip reporting them on open.
1312
+ const initialBytesNeedRebuild = !options.archiveBytes?.length
1313
+ && workspace.documents.some((document) => document.isLazy);
1314
+ void this.notifyChanged(this.initialWorkspaceEvent(ws.snapshot()), { reportBytes: !initialBytesNeedRebuild });
1287
1315
  }
1288
1316
  catch (error) {
1289
1317
  if (generation !== this.openGeneration) {
@@ -1437,6 +1465,9 @@ export class MdzipWorkspaceView {
1437
1465
  this.teardownEntryRenderer();
1438
1466
  // Release any whenRendered() waiters so their promises do not hang.
1439
1467
  this.flushRenderedWaiters();
1468
+ this.destroyed = true;
1469
+ this.markPreviewMounted();
1470
+ this.releaseAnchorPin();
1440
1471
  try {
1441
1472
  this.workspace?.dispose();
1442
1473
  }
@@ -1589,6 +1620,54 @@ export class MdzipWorkspaceView {
1589
1620
  setImageEditOptions(options) {
1590
1621
  this.options.imageEditHandler = options.imageEditHandler;
1591
1622
  }
1623
+ /**
1624
+ * Replaces the external-link rendering policy and re-applies it to the
1625
+ * links already in the preview, without re-rendering. `undefined` restores
1626
+ * every link's own `target`/`rel`.
1627
+ */
1628
+ setExternalLinks(policy) {
1629
+ this.options.externalLinks = policy;
1630
+ this.applyExternalLinkPolicy(this.elPreviewContent);
1631
+ }
1632
+ applyExternalLinkPolicy(root) {
1633
+ const policy = this.options.externalLinks;
1634
+ for (const link of Array.from(root.querySelectorAll('a[href]'))) {
1635
+ const original = this.externalLinkOriginals.get(link);
1636
+ if (original) {
1637
+ this.externalLinkOriginals.delete(link);
1638
+ restoreAttribute(link, 'target', original.target);
1639
+ restoreAttribute(link, 'rel', original.rel);
1640
+ }
1641
+ if (!policy) {
1642
+ continue;
1643
+ }
1644
+ const rel = policy.rel ?? (policy.target === '_blank' ? 'noopener noreferrer' : undefined);
1645
+ if (policy.target === undefined && rel === undefined) {
1646
+ continue;
1647
+ }
1648
+ const href = link.getAttribute('href') ?? '';
1649
+ if (!isMdzipExternalLink(href)) {
1650
+ continue;
1651
+ }
1652
+ let external = false;
1653
+ try {
1654
+ external = policy.predicate ? policy.predicate(href) : isMdzipDefaultPolicyExternalLink(href);
1655
+ }
1656
+ catch (error) {
1657
+ this.options.onFailed?.(error);
1658
+ }
1659
+ if (!external) {
1660
+ continue;
1661
+ }
1662
+ this.externalLinkOriginals.set(link, { target: link.getAttribute('target'), rel: link.getAttribute('rel') });
1663
+ if (policy.target !== undefined) {
1664
+ link.setAttribute('target', policy.target);
1665
+ }
1666
+ if (rel !== undefined) {
1667
+ link.setAttribute('rel', rel);
1668
+ }
1669
+ }
1670
+ }
1592
1671
  applyDensityClasses() {
1593
1672
  this.elRoot.classList.remove('toolbar-density-comfortable', 'toolbar-density-compact', 'toolbar-density-dense', 'content-density-comfortable', 'content-density-compact');
1594
1673
  this.elRoot.classList.add(`toolbar-density-${this.toolbarDensity}`);
@@ -1676,6 +1755,7 @@ export class MdzipWorkspaceView {
1676
1755
  // There is no built-in preview to wait for; release any waiters.
1677
1756
  this.previewHydrated = true;
1678
1757
  this.flushRenderedWaiters();
1758
+ this.markPreviewMounted();
1679
1759
  return;
1680
1760
  }
1681
1761
  const memo = this.previewMemo;
@@ -1811,6 +1891,7 @@ export class MdzipWorkspaceView {
1811
1891
  if (codeBlockHandle) {
1812
1892
  this.previewHandles.push(codeBlockHandle);
1813
1893
  }
1894
+ this.applyExternalLinkPolicy(this.elPreviewContent);
1814
1895
  // No archive images to resolve on this path: the preview is ready now.
1815
1896
  this.firePreviewRendered(snapshot, generation);
1816
1897
  this.fireAssetsHydrated(snapshot, generation);
@@ -1833,6 +1914,7 @@ export class MdzipWorkspaceView {
1833
1914
  if (codeBlockHandle) {
1834
1915
  this.previewHandles.push(codeBlockHandle);
1835
1916
  }
1917
+ this.applyExternalLinkPolicy(this.elPreviewContent);
1836
1918
  this.firePreviewRendered(snapshot, generation);
1837
1919
  this.hydrateImages(pending, context, generation, animateImageHydration, () => {
1838
1920
  this.fireAssetsHydrated(snapshot, generation);
@@ -2461,6 +2543,7 @@ export class MdzipWorkspaceView {
2461
2543
  if (codeBlockHandle) {
2462
2544
  record.handles.push(codeBlockHandle);
2463
2545
  }
2546
+ this.applyExternalLinkPolicy(record.root);
2464
2547
  }
2465
2548
  return { cursor, mountedRoots };
2466
2549
  }
@@ -2808,12 +2891,26 @@ export class MdzipWorkspaceView {
2808
2891
  }
2809
2892
  this.restorePendingPreviewScroll(generation);
2810
2893
  this.flushPendingAnchorSoon();
2894
+ this.markPreviewMounted();
2895
+ this.firingPreviewRendered = true;
2811
2896
  try {
2812
2897
  this.options.onPreviewRendered?.(snapshot);
2813
2898
  }
2814
2899
  catch (error) {
2815
2900
  this.options.onFailed?.(error);
2816
2901
  }
2902
+ finally {
2903
+ this.firingPreviewRendered = false;
2904
+ }
2905
+ }
2906
+ /** Releases {@link scrollToAnchor} calls waiting for the current preview to mount. */
2907
+ markPreviewMounted() {
2908
+ this.previewMountedGeneration = this.previewGeneration;
2909
+ const waiters = this.previewMountWaiters;
2910
+ this.previewMountWaiters = [];
2911
+ for (const resolve of waiters) {
2912
+ resolve();
2913
+ }
2817
2914
  }
2818
2915
  /** See `pendingPreviewScrollRestore`'s doc comment. */
2819
2916
  restorePendingPreviewScroll(generation) {
@@ -3142,7 +3239,10 @@ export class MdzipWorkspaceView {
3142
3239
  // scroll, on every keystroke. Wheel/touch/mousedown (which also covers
3143
3240
  // a scrollbar-thumb drag, since that lands on this same element) are
3144
3241
  // unambiguous and cover the vast majority of real scrolling.
3145
- const markEditorGesture = () => { self.lastEditorGestureTime = performance.now(); };
3242
+ const markEditorGesture = () => {
3243
+ self.lastEditorGestureTime = performance.now();
3244
+ self.releaseAnchorPin();
3245
+ };
3146
3246
  scroller.addEventListener('wheel', markEditorGesture, { passive: true });
3147
3247
  scroller.addEventListener('touchstart', markEditorGesture, { passive: true });
3148
3248
  scroller.addEventListener('mousedown', markEditorGesture);
@@ -3939,7 +4039,10 @@ export class MdzipWorkspaceView {
3939
4039
  void this.replaceFileFromPicker(path, file);
3940
4040
  }
3941
4041
  });
3942
- const markPreviewGesture = () => { this.lastPreviewGestureTime = performance.now(); };
4042
+ const markPreviewGesture = () => {
4043
+ this.lastPreviewGestureTime = performance.now();
4044
+ this.releaseAnchorPin();
4045
+ };
3943
4046
  this.elPreviewPane.addEventListener('wheel', markPreviewGesture, { passive: true });
3944
4047
  this.elPreviewPane.addEventListener('touchstart', markPreviewGesture, { passive: true });
3945
4048
  this.elPreviewPane.addEventListener('mousedown', markPreviewGesture);
@@ -3958,17 +4061,20 @@ export class MdzipWorkspaceView {
3958
4061
  return;
3959
4062
  }
3960
4063
  const href = link.getAttribute('href') ?? '';
4064
+ const parsed = parseMdzipLink(href, snapshot.currentPath, snapshot.content.paths);
4065
+ if (this.options.onLinkActivated && this.dispatchLinkActivated(event, link, href, parsed, snapshot)) {
4066
+ return;
4067
+ }
3961
4068
  if (href.startsWith('#')) {
3962
4069
  event.preventDefault();
3963
4070
  void this.scrollPreviewToAnchor(href.slice(1));
3964
4071
  return;
3965
4072
  }
3966
- const targetPath = resolveMdzipArchiveLinkTarget(href, snapshot.currentPath, snapshot.content.paths);
3967
- if (targetPath) {
4073
+ if (parsed.targetPath) {
3968
4074
  event.preventDefault();
3969
4075
  const hashIndex = href.indexOf('#');
3970
4076
  const fragment = hashIndex >= 0 ? href.slice(hashIndex + 1) : '';
3971
- void this.openPathAndScrollToAnchor(targetPath, fragment);
4077
+ void this.openPathAndScrollToAnchor(parsed.targetPath, fragment);
3972
4078
  return;
3973
4079
  }
3974
4080
  if (this.options.onUnresolvedLinkClick && isMdzipWorkspaceRelativeLink(href)) {
@@ -4020,6 +4126,132 @@ export class MdzipWorkspaceView {
4020
4126
  await this.scrollPreviewToAnchor(pending);
4021
4127
  }
4022
4128
  }
4129
+ /** Returns true when the host prevented the click, ending all further handling. */
4130
+ dispatchLinkActivated(domEvent, link, href, parsed, snapshot) {
4131
+ let prevented = false;
4132
+ const event = {
4133
+ href,
4134
+ text: (link.textContent ?? '').trim(),
4135
+ kind: parsed.kind,
4136
+ isExternal: parsed.kind === 'external',
4137
+ isAnchor: parsed.kind === 'anchor',
4138
+ path: parsed.path,
4139
+ anchor: parsed.anchor,
4140
+ sourcePath: snapshot.currentPath,
4141
+ targetPath: parsed.targetPath,
4142
+ ctrlKey: domEvent.ctrlKey,
4143
+ metaKey: domEvent.metaKey,
4144
+ shiftKey: domEvent.shiftKey,
4145
+ altKey: domEvent.altKey,
4146
+ domEvent,
4147
+ snapshot,
4148
+ preventDefault: () => {
4149
+ prevented = true;
4150
+ },
4151
+ get defaultPrevented() {
4152
+ return prevented;
4153
+ }
4154
+ };
4155
+ try {
4156
+ this.options.onLinkActivated?.(event);
4157
+ }
4158
+ catch (error) {
4159
+ this.options.onFailed?.(error);
4160
+ }
4161
+ if (prevented) {
4162
+ domEvent.preventDefault();
4163
+ }
4164
+ return prevented;
4165
+ }
4166
+ /**
4167
+ * Scrolls the preview to a heading or an explicit `id`/`name` anchor.
4168
+ * Accepts the fragment with or without its leading `#` and matches it the
4169
+ * way a clicked `#fragment` link does: case-insensitive, percent-decoded,
4170
+ * against both bare and `user-content-`-prefixed heading ids.
4171
+ *
4172
+ * Safe to call at any point — straight after `open()`/`openWorkspace()`,
4173
+ * or from `onPreviewRendered`: it waits for the current document's
4174
+ * preview to mount, and under progressive rendering mounts the chunks up
4175
+ * to the target first. Resolves `true` when it scrolled to a matching
4176
+ * anchor (`''` and `'top'` scroll to the top and resolve `true`), and
4177
+ * `false` when nothing matches, an entry renderer owns the pane, or the
4178
+ * view is destroyed first.
4179
+ */
4180
+ async scrollToAnchor(anchor) {
4181
+ const fragment = anchor.startsWith('#') ? anchor.slice(1) : anchor;
4182
+ // onPreviewRendered runs inside the first chunk batch's completion,
4183
+ // before the lazy-continuation sentinel is armed — same race as
4184
+ // flushPendingAnchorSoon, so a call from it (or one that had to wait for
4185
+ // the mount) is deferred a frame.
4186
+ let deferFrame = this.firingPreviewRendered;
4187
+ while (!this.destroyed && this.previewMountedGeneration !== this.previewGeneration) {
4188
+ deferFrame = true;
4189
+ await new Promise((resolve) => this.previewMountWaiters.push(resolve));
4190
+ }
4191
+ if (this.destroyed) {
4192
+ return false;
4193
+ }
4194
+ if (deferFrame) {
4195
+ const generation = this.previewGeneration;
4196
+ await new Promise((resolve) => requestAnimationFrame(() => resolve()));
4197
+ if (this.destroyed) {
4198
+ return false;
4199
+ }
4200
+ if (generation !== this.previewGeneration) {
4201
+ return this.scrollToAnchor(anchor);
4202
+ }
4203
+ }
4204
+ if (this.entryState) {
4205
+ return false;
4206
+ }
4207
+ const found = await this.scrollPreviewToAnchor(fragment);
4208
+ return found || !fragment || fragment.toLowerCase() === 'top';
4209
+ }
4210
+ /**
4211
+ * The anchors {@link scrollToAnchor} can reach in the current preview, in
4212
+ * document order: every heading id (without the `user-content-` prefix),
4213
+ * including headings in chunks progressive rendering hasn't mounted yet,
4214
+ * plus explicit `id`/`name` anchors on links in the mounted preview.
4215
+ * Empty before the preview first mounts.
4216
+ */
4217
+ getAvailableAnchors() {
4218
+ if (this.entryState) {
4219
+ return [];
4220
+ }
4221
+ const anchors = [];
4222
+ const seen = new Set();
4223
+ const add = (value) => {
4224
+ if (!value)
4225
+ return;
4226
+ const bare = value.startsWith(MDZIP_HEADING_ID_PREFIX) ? value.slice(MDZIP_HEADING_ID_PREFIX.length) : value;
4227
+ if (bare && !seen.has(bare)) {
4228
+ seen.add(bare);
4229
+ anchors.push(bare);
4230
+ }
4231
+ };
4232
+ const fromDom = (root) => {
4233
+ for (const element of Array.from(root.querySelectorAll('h1[id],h2[id],h3[id],h4[id],h5[id],h6[id],a[id],a[name]'))) {
4234
+ add(element.getAttribute('id'));
4235
+ add(element.getAttribute('name'));
4236
+ }
4237
+ };
4238
+ const state = this.chunkedRenderState;
4239
+ if (state && state.generation === this.previewGeneration) {
4240
+ // Walk records in order so unmounted chunks' headings land in place.
4241
+ state.records.forEach((record) => {
4242
+ if (record.root) {
4243
+ fromDom(record.root);
4244
+ }
4245
+ else {
4246
+ collectMdzipHeadingIds(record.tokens).forEach(add);
4247
+ }
4248
+ });
4249
+ }
4250
+ else {
4251
+ fromDom(this.elPreviewContent);
4252
+ }
4253
+ return anchors;
4254
+ }
4023
4255
  // Deferred a frame: firePreviewRendered runs inside the first chunk batch's
4024
4256
  // completion callback, before the lazy-continuation sentinel for the rest is
4025
4257
  // armed. Scrolling then (which may need to drain those chunks) would race it.
@@ -4098,9 +4330,35 @@ export class MdzipWorkspaceView {
4098
4330
  }
4099
4331
  return false;
4100
4332
  }
4101
- pane.scrollTop += target.getBoundingClientRect().top - pane.getBoundingClientRect().top;
4333
+ this.scrollPaneToElement(target);
4334
+ this.pinPreviewAnchor(target);
4102
4335
  return true;
4103
4336
  }
4337
+ scrollPaneToElement(target) {
4338
+ const pane = this.elPreviewPane;
4339
+ pane.scrollTop += target.getBoundingClientRect().top - pane.getBoundingClientRect().top;
4340
+ }
4341
+ /** See `anchorPin`. */
4342
+ pinPreviewAnchor(target) {
4343
+ this.releaseAnchorPin();
4344
+ if (typeof ResizeObserver === 'undefined') {
4345
+ return;
4346
+ }
4347
+ const generation = this.previewGeneration;
4348
+ const observer = new ResizeObserver(() => {
4349
+ if (generation !== this.previewGeneration || !target.isConnected) {
4350
+ this.releaseAnchorPin();
4351
+ return;
4352
+ }
4353
+ this.scrollPaneToElement(target);
4354
+ });
4355
+ observer.observe(this.elPreviewContent);
4356
+ this.anchorPin = observer;
4357
+ }
4358
+ releaseAnchorPin() {
4359
+ this.anchorPin?.disconnect();
4360
+ this.anchorPin = null;
4361
+ }
4104
4362
  async openPath(path) {
4105
4363
  if (!this.workspace) {
4106
4364
  return;
@@ -5794,7 +6052,7 @@ export class MdzipWorkspaceView {
5794
6052
  });
5795
6053
  editor.focus();
5796
6054
  }
5797
- async notifyChanged(event) {
6055
+ async notifyChanged(event, { reportBytes = true } = {}) {
5798
6056
  if (!this.workspace) {
5799
6057
  return;
5800
6058
  }
@@ -5806,7 +6064,13 @@ export class MdzipWorkspaceView {
5806
6064
  const delegatedToManifestHandler = this.options.onManifestChanged
5807
6065
  && event.changes.length === 1
5808
6066
  && event.changes[0] === 'manifest';
5809
- if (this.options.onChanged && !delegatedToManifestHandler) {
6067
+ // Opening another path changes nothing in the archive, so there are no
6068
+ // new bytes to report. Exporting anyway rebuilt the whole archive on
6069
+ // every navigation when opened without archiveBytes — reading every
6070
+ // lazy document's text first (752 host round-trips on a 153MB archive,
6071
+ // which starved the one read the user actually asked for).
6072
+ const selectionOnly = event.changes.every((change) => change === 'selection');
6073
+ if (this.options.onChanged && reportBytes && !delegatedToManifestHandler && !selectionOnly) {
5810
6074
  const bytes = await this.workspace.exportBytes();
5811
6075
  this.options.onChanged(bytes, snapshot);
5812
6076
  }
@@ -6736,325 +7000,325 @@ function formatMetadataValue(value) {
6736
7000
  }
6737
7001
  return 'Not available';
6738
7002
  }
6739
- const SHELL_HTML = `
6740
- <section class="mdzip-root">
6741
- <div class="document-strip" data-ref="document-strip" hidden>
6742
- <button type="button" class="title-button" data-ref="title-btn"></button>
6743
- <button type="button" class="document-info-button" data-ref="document-info-btn"
6744
- title="Document information" aria-label="Document information">
6745
- ${INFO_ICON_HTML}
6746
- </button>
6747
- </div>
6748
-
6749
- <header class="toolbar" data-ref="toolbar" hidden>
6750
- <div class="toolbar-start">
6751
- <div class="toolbar-left">
6752
- <button type="button" class="icon-toggle nav-toggle" data-ref="nav-btn" title="Toggle contents" aria-label="Toggle contents">
6753
- ${NAV_TOGGLE_ICON_HTML}
6754
- </button>
6755
- </div>
6756
-
6757
- <div class="edit-toolbar" data-ref="edit-toolbar" role="toolbar" aria-label="Markdown formatting">
6758
- <div class="edit-toolbar-group">
6759
- <button type="button" data-format="bold" data-format-control="bold" title="Bold" aria-label="Bold">${BOLD_ICON_HTML}</button>
6760
- <button type="button" data-format="italic" data-format-control="italic" title="Italic" aria-label="Italic">${ITALIC_ICON_HTML}</button>
6761
- <button type="button" data-format="strike" data-format-control="strikethrough" title="Strikethrough" aria-label="Strikethrough">${STRIKE_ICON_HTML}</button>
6762
- <div class="format-menu" data-format-control="headings">
6763
- <button type="button" class="format-menu-toggle" data-format-menu-toggle
6764
- aria-label="Heading" aria-haspopup="menu" aria-expanded="false">
6765
- ${HEADING_ICON_HTML}${CHEVRON_ICON_HTML}
6766
- </button>
6767
- <div class="format-menu-popover" data-format-menu role="menu" aria-label="Heading level" hidden>
6768
- <button type="button" role="menuitem" data-format="paragraph">Paragraph</button>
6769
- <button type="button" role="menuitem" data-format="heading-1" data-heading-level="1"><strong>H1</strong> Heading 1</button>
6770
- <button type="button" role="menuitem" data-format="heading-2" data-heading-level="2"><strong>H2</strong> Heading 2</button>
6771
- <button type="button" role="menuitem" data-format="heading-3" data-heading-level="3"><strong>H3</strong> Heading 3</button>
6772
- <button type="button" role="menuitem" data-format="heading-4" data-heading-level="4"><strong>H4</strong> Heading 4</button>
6773
- <button type="button" role="menuitem" data-format="heading-5" data-heading-level="5"><strong>H5</strong> Heading 5</button>
6774
- <button type="button" role="menuitem" data-format="heading-6" data-heading-level="6"><strong>H6</strong> Heading 6</button>
6775
- </div>
6776
- </div>
6777
- </div>
6778
- <span class="edit-toolbar-divider" aria-hidden="true"></span>
6779
- <div class="edit-toolbar-group">
6780
- <button type="button" data-format="bullet-list" data-format-control="bulletList" title="Bulleted list" aria-label="Bulleted list">${BULLET_LIST_ICON_HTML}</button>
6781
- <button type="button" data-format="ordered-list" data-format-control="orderedList" title="Numbered list" aria-label="Numbered list">${ORDERED_LIST_ICON_HTML}</button>
6782
- <div class="format-menu" data-format-control="code">
6783
- <button type="button" class="format-menu-toggle" data-format-menu-toggle
6784
- aria-label="Code" aria-haspopup="menu" aria-expanded="false">
6785
- ${CODE_ICON_HTML}${CHEVRON_ICON_HTML}
6786
- </button>
6787
- <div class="format-menu-popover" data-format-menu role="menu" aria-label="Code format" hidden>
6788
- <button type="button" role="menuitem" data-format="code" data-code-kind="inline">Inline code</button>
6789
- <button type="button" role="menuitem" data-format="code-block" data-code-kind="block">Code block</button>
6790
- </div>
6791
- </div>
6792
- <button type="button" data-format="quote" data-format-control="blockquote" title="Blockquote" aria-label="Blockquote">${QUOTE_ICON_HTML}</button>
6793
- <button type="button" data-format="line-break" data-format-control="lineBreak" title="Line break" aria-label="Line break">${LINE_BREAK_ICON_HTML}</button>
6794
- </div>
6795
- <span class="edit-toolbar-divider" aria-hidden="true"></span>
6796
- <div class="edit-toolbar-group">
6797
- <button type="button" data-format="link" data-format-control="link" title="Link" aria-label="Link">${LINK_ICON_HTML}</button>
6798
- <button type="button" data-format="image" data-format-control="image" title="Image" aria-label="Image">${IMAGE_FORMAT_ICON_HTML}</button>
6799
- <input type="file" data-ref="image-input" accept="image/*" hidden />
6800
- </div>
6801
- </div>
6802
- </div>
6803
-
6804
- <div class="toolbar-buttons view-mode-toggle-group" data-ref="layout-controls" role="group" aria-label="Editor layout">
6805
- <button type="button" class="icon-toggle view-mode-toggle" data-ref="source-btn" title="Edit" aria-label="Edit" aria-pressed="false">
6806
- <span class="commandbar-flex-container" data-ref="source-icon">${SOURCE_EDIT_ICON_HTML}</span>
6807
- </button>
6808
- <button type="button" class="icon-toggle view-mode-toggle" data-ref="split-btn" title="Split" aria-label="Split" aria-pressed="false">
6809
- <span class="commandbar-flex-container">
6810
- ${SPLIT_ICON_HTML}
6811
- </span>
6812
- </button>
6813
- <button type="button" class="icon-toggle view-mode-toggle" data-ref="preview-btn" title="View" aria-label="View" aria-pressed="false">
6814
- <span class="commandbar-flex-container">
6815
- ${PREVIEW_ICON_HTML}
6816
- </span>
6817
- </button>
6818
- </div>
6819
-
6820
- <div class="toolbar-controls" data-ref="toolbar-controls">
6821
- <button type="button" class="icon-toggle" data-ref="search-btn" title="Find/replace (Mod-F)" aria-label="Find/replace">
6822
- ${SEARCH_ICON_HTML}
6823
- </button>
6824
- <button type="button" class="icon-toggle" data-ref="save-btn" title="Save" aria-label="Save">
6825
- ${SAVE_ICON_HTML}
6826
- </button>
6827
- <button type="button" class="icon-toggle zoom-toggle" data-ref="zoom-btn" title="Zoom controls" aria-label="Zoom controls">
6828
- ${ZOOM_ICON_HTML}
6829
- </button>
6830
- <div class="theme-toggle-group" data-ref="theme-controls" role="group" aria-label="Color scheme">
6831
- <button type="button" class="icon-toggle theme-toggle" data-ref="dark-theme-btn"
6832
- title="Dark mode" aria-label="Dark mode" aria-pressed="false">
6833
- ${DARK_THEME_ICON_HTML}
6834
- </button>
6835
- <button type="button" class="icon-toggle theme-toggle" data-ref="light-theme-btn"
6836
- title="Light mode" aria-label="Light mode" aria-pressed="false">
6837
- ${LIGHT_THEME_ICON_HTML}
6838
- </button>
6839
- </div>
6840
- <div class="zoom-popover" data-ref="zoom-popover" hidden role="group" aria-label="Zoom">
6841
- <span class="zoom-level" data-ref="zoom-level">100%</span>
6842
- <span class="zoom-stepper">
6843
- <button type="button" data-action="zoom-out" title="Zoom out" aria-label="Zoom out">-</button>
6844
- <button type="button" data-action="zoom-in" title="Zoom in" aria-label="Zoom in">+</button>
6845
- </span>
6846
- <button type="button" class="zoom-reset" data-action="zoom-reset" title="Reset zoom">Reset</button>
6847
- </div>
6848
- </div>
6849
- </header>
6850
-
6851
- <div class="workspace-shell" data-ref="workspace-shell" hidden>
6852
- <aside class="nav-pane" data-ref="nav-pane" aria-label="Package contents">
6853
- <div class="nav-tree" data-ref="nav-tree"></div>
6854
- </aside>
6855
- <div class="nav-resizer" data-ref="nav-resizer"
6856
- role="separator" aria-orientation="vertical" aria-label="Resize contents pane"></div>
6857
-
6858
- <div class="pane-stack" data-ref="pane-stack">
6859
- <section class="pane edit-pane" data-ref="edit-pane">
6860
- <div class="editor-host" data-ref="editor-host"></div>
6861
- </section>
6862
- <div class="split-resizer" data-ref="split-resizer"
6863
- role="separator" aria-orientation="vertical" aria-label="Resize split panes"></div>
6864
- <section class="pane preview-pane" data-ref="preview-pane" tabindex="-1">
6865
- <article class="preview-content" data-ref="preview-content"></article>
6866
- </section>
6867
- <section class="pane entry-pane" data-ref="entry-pane"></section>
6868
- </div>
6869
- </div>
6870
-
6871
- <div class="title-dialog-backdrop" data-ref="title-dialog" hidden
6872
- role="dialog" aria-modal="true" aria-labelledby="mdzip-title-dialog-heading">
6873
- <div class="title-dialog">
6874
- <h3 id="mdzip-title-dialog-heading">Set Document Title</h3>
6875
- <p>This is the package-level title stored in manifest.json.</p>
6876
- <input type="text" maxlength="120" data-ref="title-input" aria-label="Document title" />
6877
- <p class="title-dialog-validation" data-ref="title-validation" hidden>Title cannot be empty.</p>
6878
- <p>If unset, consumers may fall back to entry point, filename, or first heading.</p>
6879
- <div class="title-dialog-actions">
6880
- <button type="button" class="reset-title" data-ref="title-reset-btn">Reset</button>
6881
- <button type="button" data-action="cancel-title">Cancel</button>
6882
- <button type="button" class="save-title" data-ref="title-save-btn">Save</button>
6883
- </div>
6884
- </div>
6885
- </div>
6886
-
6887
- <div class="title-dialog-backdrop" data-ref="conversion-dialog" hidden
6888
- role="dialog" aria-modal="true" aria-labelledby="mdzip-conversion-dialog-heading">
6889
- <div class="title-dialog">
6890
- <h3 id="mdzip-conversion-dialog-heading">Convert to MDZ?</h3>
6891
- <p>
6892
- Regular Markdown files contain only text. Convert this document to an
6893
- MDZ package to add images, package navigation, and document metadata.
6894
- </p>
6895
- <p>The next save will produce an .mdz file.</p>
6896
- <div class="title-dialog-actions">
6897
- <button type="button" data-action="cancel-conversion">Cancel</button>
6898
- <button type="button" class="save-title" data-ref="conversion-confirm-btn">Convert</button>
6899
- </div>
6900
- </div>
6901
- </div>
6902
-
6903
- <div class="title-dialog-backdrop" data-ref="image-insert-dialog" hidden
6904
- role="dialog" aria-modal="true" aria-labelledby="mdzip-image-insert-dialog-heading">
6905
- <div class="title-dialog image-insert-dialog">
6906
- <h3 id="mdzip-image-insert-dialog-heading">Insert Image</h3>
6907
- <p>Choose the Markdown inserted for this image.</p>
6908
- <fieldset class="image-insert-options">
6909
- <legend>Output</legend>
6910
- <label>
6911
- <input type="radio" name="mdzip-image-insert-mode" value="markdown"
6912
- data-ref="image-insert-mode-markdown" checked />
6913
- Markdown image
6914
- </label>
6915
- <label>
6916
- <input type="radio" name="mdzip-image-insert-mode" value="html"
6917
- data-ref="image-insert-mode-html" />
6918
- HTML &lt;img&gt; with sizing
6919
- </label>
6920
- </fieldset>
6921
- <label class="image-insert-field">
6922
- <span>Alt text</span>
6923
- <input type="text" data-ref="image-insert-alt" />
6924
- </label>
6925
- <div class="image-insert-grid">
6926
- <label class="image-insert-field">
6927
- <span>Size by</span>
6928
- <select data-ref="image-insert-size-mode">
6929
- <option value="original">Original size</option>
6930
- <option value="width" selected>Width</option>
6931
- <option value="height">Height</option>
6932
- <option value="percent">Percent</option>
6933
- </select>
6934
- </label>
6935
- <label class="image-insert-field">
6936
- <span>Value</span>
6937
- <input type="number" min="1" step="1" data-ref="image-insert-size-value" />
6938
- </label>
6939
- </div>
6940
- <label class="image-insert-field">
6941
- <span>Position</span>
6942
- <select data-ref="image-insert-position">
6943
- <option value="inline">Inline/default</option>
6944
- <option value="left">Left</option>
6945
- <option value="center">Center</option>
6946
- <option value="right">Right</option>
6947
- <option value="wrap-left">Wrap left</option>
6948
- <option value="wrap-right">Wrap right</option>
6949
- </select>
6950
- </label>
6951
- <div class="title-dialog-actions">
6952
- <button type="button" data-action="cancel-image-insert">Cancel</button>
6953
- <button type="button" class="save-title" data-ref="image-insert-confirm-btn">Insert</button>
6954
- </div>
6955
- </div>
6956
- </div>
6957
-
6958
- <div class="title-dialog-backdrop" data-ref="pack-files-dialog" hidden
6959
- role="dialog" aria-modal="true" aria-labelledby="mdzip-pack-files-dialog-heading">
6960
- <div class="title-dialog pack-files-dialog">
6961
- <h3 id="mdzip-pack-files-dialog-heading">Package Markdown Files</h3>
6962
- <p>These files contain more than one Markdown document. Choose how to package them.</p>
6963
- <fieldset class="pack-files-options">
6964
- <legend>Mode</legend>
6965
- <label>
6966
- <input type="radio" name="mdzip-pack-files-mode" value="document"
6967
- data-ref="pack-files-mode-document" checked />
6968
- Document — one primary page; the rest are attached
6969
- </label>
6970
- <label>
6971
- <input type="radio" name="mdzip-pack-files-mode" value="project"
6972
- data-ref="pack-files-mode-project" />
6973
- Project — a set of related documents
6974
- </label>
6975
- </fieldset>
6976
- <label class="pack-files-field">
6977
- <span>Entry point</span>
6978
- <select data-ref="pack-files-entry"></select>
6979
- </label>
6980
- <div class="title-dialog-actions">
6981
- <button type="button" data-action="cancel-pack-files">Cancel</button>
6982
- <button type="button" class="save-title" data-ref="pack-files-confirm-btn">Package</button>
6983
- </div>
6984
- </div>
6985
- </div>
6986
-
6987
- <div class="title-dialog-backdrop" data-ref="copy-render-dialog" hidden
6988
- role="dialog" aria-modal="true" aria-labelledby="mdzip-copy-render-dialog-heading">
6989
- <div class="title-dialog copy-render-dialog">
6990
- <h3 id="mdzip-copy-render-dialog-heading" data-ref="copy-render-heading">Copying document...</h3>
6991
- <div data-ref="copy-render-progress-section">
6992
- <p data-ref="copy-render-progress-text">Rendering the full document...</p>
6993
- <div class="copy-render-progress-track">
6994
- <div class="copy-render-progress-bar" data-ref="copy-render-progress-bar"></div>
6995
- </div>
6996
- </div>
6997
- <p data-ref="copy-render-ready-text" hidden>
6998
- The document is ready. Browsers only allow a clipboard write right after a click, so this needs one more —
6999
- click Copy to finish.
7000
- </p>
7001
- <p data-ref="copy-render-done-text" hidden></p>
7002
- <div class="title-dialog-actions">
7003
- <button type="button" data-action="cancel-copy-render" data-ref="copy-render-cancel-btn">Cancel</button>
7004
- <button type="button" data-action="cancel-copy-ready" data-ref="copy-render-ready-cancel-btn" hidden>Cancel</button>
7005
- <button type="button" class="save-title" data-action="confirm-copy-ready" data-ref="copy-render-confirm-btn" hidden>Copy</button>
7006
- <button type="button" class="save-title" data-action="dismiss-copy-render" data-ref="copy-render-dismiss-btn" hidden>Dismiss</button>
7007
- </div>
7008
- </div>
7009
- </div>
7010
-
7011
- <div class="title-dialog-backdrop" data-ref="metadata-dialog" hidden
7012
- role="dialog" aria-modal="true" aria-labelledby="mdzip-metadata-dialog-heading">
7013
- <div class="title-dialog metadata-dialog">
7014
- <h3 id="mdzip-metadata-dialog-heading">Document Information</h3>
7015
- <dl data-ref="metadata-list"></dl>
7016
- <h4>Libraries</h4>
7017
- <dl data-ref="library-list"></dl>
7018
- <div class="title-dialog-actions">
7019
- <button type="button" class="save-title" data-action="close-metadata">Close</button>
7020
- </div>
7021
- </div>
7022
- </div>
7023
-
7024
- <div class="title-dialog-backdrop" data-ref="name-dialog" hidden
7025
- role="dialog" aria-modal="true" aria-labelledby="mdzip-name-dialog-heading">
7026
- <div class="title-dialog">
7027
- <h3 id="mdzip-name-dialog-heading" data-ref="name-dialog-heading">New Markdown File</h3>
7028
- <input type="text" maxlength="260" data-ref="name-input" aria-label="File name" />
7029
- <p class="title-dialog-validation" data-ref="name-validation" hidden></p>
7030
- <div class="title-dialog-actions">
7031
- <button type="button" data-action="cancel-name">Cancel</button>
7032
- <button type="button" class="save-title" data-ref="name-confirm-btn">Create</button>
7033
- </div>
7034
- </div>
7035
- </div>
7036
-
7037
- <div class="title-dialog-backdrop" data-ref="delete-dialog" hidden
7038
- role="dialog" aria-modal="true" aria-labelledby="mdzip-delete-dialog-heading">
7039
- <div class="title-dialog">
7040
- <h3 id="mdzip-delete-dialog-heading">Delete File?</h3>
7041
- <p data-ref="delete-dialog-text"></p>
7042
- <div class="title-dialog-actions">
7043
- <button type="button" data-action="cancel-delete">Cancel</button>
7044
- <button type="button" class="danger-action" data-ref="delete-confirm-btn">Delete</button>
7045
- </div>
7046
- </div>
7047
- </div>
7048
-
7049
- <input type="file" data-ref="replace-input" hidden />
7050
-
7051
- <div class="nav-context-menu" data-ref="nav-menu" hidden role="menu"></div>
7052
-
7053
- <div class="mdzip-tooltip" data-ref="tooltip" role="tooltip" hidden></div>
7054
-
7055
- <div class="mdzip-copy-toast" data-ref="copy-toast" role="status" aria-live="polite" hidden></div>
7056
-
7057
- <p class="mdzip-empty" data-ref="empty-state">No MDZip workspace loaded.</p>
7058
- </section>
7003
+ const SHELL_HTML = `
7004
+ <section class="mdzip-root">
7005
+ <div class="document-strip" data-ref="document-strip" hidden>
7006
+ <button type="button" class="title-button" data-ref="title-btn"></button>
7007
+ <button type="button" class="document-info-button" data-ref="document-info-btn"
7008
+ title="Document information" aria-label="Document information">
7009
+ ${INFO_ICON_HTML}
7010
+ </button>
7011
+ </div>
7012
+
7013
+ <header class="toolbar" data-ref="toolbar" hidden>
7014
+ <div class="toolbar-start">
7015
+ <div class="toolbar-left">
7016
+ <button type="button" class="icon-toggle nav-toggle" data-ref="nav-btn" title="Toggle contents" aria-label="Toggle contents">
7017
+ ${NAV_TOGGLE_ICON_HTML}
7018
+ </button>
7019
+ </div>
7020
+
7021
+ <div class="edit-toolbar" data-ref="edit-toolbar" role="toolbar" aria-label="Markdown formatting">
7022
+ <div class="edit-toolbar-group">
7023
+ <button type="button" data-format="bold" data-format-control="bold" title="Bold" aria-label="Bold">${BOLD_ICON_HTML}</button>
7024
+ <button type="button" data-format="italic" data-format-control="italic" title="Italic" aria-label="Italic">${ITALIC_ICON_HTML}</button>
7025
+ <button type="button" data-format="strike" data-format-control="strikethrough" title="Strikethrough" aria-label="Strikethrough">${STRIKE_ICON_HTML}</button>
7026
+ <div class="format-menu" data-format-control="headings">
7027
+ <button type="button" class="format-menu-toggle" data-format-menu-toggle
7028
+ aria-label="Heading" aria-haspopup="menu" aria-expanded="false">
7029
+ ${HEADING_ICON_HTML}${CHEVRON_ICON_HTML}
7030
+ </button>
7031
+ <div class="format-menu-popover" data-format-menu role="menu" aria-label="Heading level" hidden>
7032
+ <button type="button" role="menuitem" data-format="paragraph">Paragraph</button>
7033
+ <button type="button" role="menuitem" data-format="heading-1" data-heading-level="1"><strong>H1</strong> Heading 1</button>
7034
+ <button type="button" role="menuitem" data-format="heading-2" data-heading-level="2"><strong>H2</strong> Heading 2</button>
7035
+ <button type="button" role="menuitem" data-format="heading-3" data-heading-level="3"><strong>H3</strong> Heading 3</button>
7036
+ <button type="button" role="menuitem" data-format="heading-4" data-heading-level="4"><strong>H4</strong> Heading 4</button>
7037
+ <button type="button" role="menuitem" data-format="heading-5" data-heading-level="5"><strong>H5</strong> Heading 5</button>
7038
+ <button type="button" role="menuitem" data-format="heading-6" data-heading-level="6"><strong>H6</strong> Heading 6</button>
7039
+ </div>
7040
+ </div>
7041
+ </div>
7042
+ <span class="edit-toolbar-divider" aria-hidden="true"></span>
7043
+ <div class="edit-toolbar-group">
7044
+ <button type="button" data-format="bullet-list" data-format-control="bulletList" title="Bulleted list" aria-label="Bulleted list">${BULLET_LIST_ICON_HTML}</button>
7045
+ <button type="button" data-format="ordered-list" data-format-control="orderedList" title="Numbered list" aria-label="Numbered list">${ORDERED_LIST_ICON_HTML}</button>
7046
+ <div class="format-menu" data-format-control="code">
7047
+ <button type="button" class="format-menu-toggle" data-format-menu-toggle
7048
+ aria-label="Code" aria-haspopup="menu" aria-expanded="false">
7049
+ ${CODE_ICON_HTML}${CHEVRON_ICON_HTML}
7050
+ </button>
7051
+ <div class="format-menu-popover" data-format-menu role="menu" aria-label="Code format" hidden>
7052
+ <button type="button" role="menuitem" data-format="code" data-code-kind="inline">Inline code</button>
7053
+ <button type="button" role="menuitem" data-format="code-block" data-code-kind="block">Code block</button>
7054
+ </div>
7055
+ </div>
7056
+ <button type="button" data-format="quote" data-format-control="blockquote" title="Blockquote" aria-label="Blockquote">${QUOTE_ICON_HTML}</button>
7057
+ <button type="button" data-format="line-break" data-format-control="lineBreak" title="Line break" aria-label="Line break">${LINE_BREAK_ICON_HTML}</button>
7058
+ </div>
7059
+ <span class="edit-toolbar-divider" aria-hidden="true"></span>
7060
+ <div class="edit-toolbar-group">
7061
+ <button type="button" data-format="link" data-format-control="link" title="Link" aria-label="Link">${LINK_ICON_HTML}</button>
7062
+ <button type="button" data-format="image" data-format-control="image" title="Image" aria-label="Image">${IMAGE_FORMAT_ICON_HTML}</button>
7063
+ <input type="file" data-ref="image-input" accept="image/*" hidden />
7064
+ </div>
7065
+ </div>
7066
+ </div>
7067
+
7068
+ <div class="toolbar-buttons view-mode-toggle-group" data-ref="layout-controls" role="group" aria-label="Editor layout">
7069
+ <button type="button" class="icon-toggle view-mode-toggle" data-ref="source-btn" title="Edit" aria-label="Edit" aria-pressed="false">
7070
+ <span class="commandbar-flex-container" data-ref="source-icon">${SOURCE_EDIT_ICON_HTML}</span>
7071
+ </button>
7072
+ <button type="button" class="icon-toggle view-mode-toggle" data-ref="split-btn" title="Split" aria-label="Split" aria-pressed="false">
7073
+ <span class="commandbar-flex-container">
7074
+ ${SPLIT_ICON_HTML}
7075
+ </span>
7076
+ </button>
7077
+ <button type="button" class="icon-toggle view-mode-toggle" data-ref="preview-btn" title="View" aria-label="View" aria-pressed="false">
7078
+ <span class="commandbar-flex-container">
7079
+ ${PREVIEW_ICON_HTML}
7080
+ </span>
7081
+ </button>
7082
+ </div>
7083
+
7084
+ <div class="toolbar-controls" data-ref="toolbar-controls">
7085
+ <button type="button" class="icon-toggle" data-ref="search-btn" title="Find/replace (Mod-F)" aria-label="Find/replace">
7086
+ ${SEARCH_ICON_HTML}
7087
+ </button>
7088
+ <button type="button" class="icon-toggle" data-ref="save-btn" title="Save" aria-label="Save">
7089
+ ${SAVE_ICON_HTML}
7090
+ </button>
7091
+ <button type="button" class="icon-toggle zoom-toggle" data-ref="zoom-btn" title="Zoom controls" aria-label="Zoom controls">
7092
+ ${ZOOM_ICON_HTML}
7093
+ </button>
7094
+ <div class="theme-toggle-group" data-ref="theme-controls" role="group" aria-label="Color scheme">
7095
+ <button type="button" class="icon-toggle theme-toggle" data-ref="dark-theme-btn"
7096
+ title="Dark mode" aria-label="Dark mode" aria-pressed="false">
7097
+ ${DARK_THEME_ICON_HTML}
7098
+ </button>
7099
+ <button type="button" class="icon-toggle theme-toggle" data-ref="light-theme-btn"
7100
+ title="Light mode" aria-label="Light mode" aria-pressed="false">
7101
+ ${LIGHT_THEME_ICON_HTML}
7102
+ </button>
7103
+ </div>
7104
+ <div class="zoom-popover" data-ref="zoom-popover" hidden role="group" aria-label="Zoom">
7105
+ <span class="zoom-level" data-ref="zoom-level">100%</span>
7106
+ <span class="zoom-stepper">
7107
+ <button type="button" data-action="zoom-out" title="Zoom out" aria-label="Zoom out">-</button>
7108
+ <button type="button" data-action="zoom-in" title="Zoom in" aria-label="Zoom in">+</button>
7109
+ </span>
7110
+ <button type="button" class="zoom-reset" data-action="zoom-reset" title="Reset zoom">Reset</button>
7111
+ </div>
7112
+ </div>
7113
+ </header>
7114
+
7115
+ <div class="workspace-shell" data-ref="workspace-shell" hidden>
7116
+ <aside class="nav-pane" data-ref="nav-pane" aria-label="Package contents">
7117
+ <div class="nav-tree" data-ref="nav-tree"></div>
7118
+ </aside>
7119
+ <div class="nav-resizer" data-ref="nav-resizer"
7120
+ role="separator" aria-orientation="vertical" aria-label="Resize contents pane"></div>
7121
+
7122
+ <div class="pane-stack" data-ref="pane-stack">
7123
+ <section class="pane edit-pane" data-ref="edit-pane">
7124
+ <div class="editor-host" data-ref="editor-host"></div>
7125
+ </section>
7126
+ <div class="split-resizer" data-ref="split-resizer"
7127
+ role="separator" aria-orientation="vertical" aria-label="Resize split panes"></div>
7128
+ <section class="pane preview-pane" data-ref="preview-pane" tabindex="-1">
7129
+ <article class="preview-content" data-ref="preview-content"></article>
7130
+ </section>
7131
+ <section class="pane entry-pane" data-ref="entry-pane"></section>
7132
+ </div>
7133
+ </div>
7134
+
7135
+ <div class="title-dialog-backdrop" data-ref="title-dialog" hidden
7136
+ role="dialog" aria-modal="true" aria-labelledby="mdzip-title-dialog-heading">
7137
+ <div class="title-dialog">
7138
+ <h3 id="mdzip-title-dialog-heading">Set Document Title</h3>
7139
+ <p>This is the package-level title stored in manifest.json.</p>
7140
+ <input type="text" maxlength="120" data-ref="title-input" aria-label="Document title" />
7141
+ <p class="title-dialog-validation" data-ref="title-validation" hidden>Title cannot be empty.</p>
7142
+ <p>If unset, consumers may fall back to entry point, filename, or first heading.</p>
7143
+ <div class="title-dialog-actions">
7144
+ <button type="button" class="reset-title" data-ref="title-reset-btn">Reset</button>
7145
+ <button type="button" data-action="cancel-title">Cancel</button>
7146
+ <button type="button" class="save-title" data-ref="title-save-btn">Save</button>
7147
+ </div>
7148
+ </div>
7149
+ </div>
7150
+
7151
+ <div class="title-dialog-backdrop" data-ref="conversion-dialog" hidden
7152
+ role="dialog" aria-modal="true" aria-labelledby="mdzip-conversion-dialog-heading">
7153
+ <div class="title-dialog">
7154
+ <h3 id="mdzip-conversion-dialog-heading">Convert to MDZ?</h3>
7155
+ <p>
7156
+ Regular Markdown files contain only text. Convert this document to an
7157
+ MDZ package to add images, package navigation, and document metadata.
7158
+ </p>
7159
+ <p>The next save will produce an .mdz file.</p>
7160
+ <div class="title-dialog-actions">
7161
+ <button type="button" data-action="cancel-conversion">Cancel</button>
7162
+ <button type="button" class="save-title" data-ref="conversion-confirm-btn">Convert</button>
7163
+ </div>
7164
+ </div>
7165
+ </div>
7166
+
7167
+ <div class="title-dialog-backdrop" data-ref="image-insert-dialog" hidden
7168
+ role="dialog" aria-modal="true" aria-labelledby="mdzip-image-insert-dialog-heading">
7169
+ <div class="title-dialog image-insert-dialog">
7170
+ <h3 id="mdzip-image-insert-dialog-heading">Insert Image</h3>
7171
+ <p>Choose the Markdown inserted for this image.</p>
7172
+ <fieldset class="image-insert-options">
7173
+ <legend>Output</legend>
7174
+ <label>
7175
+ <input type="radio" name="mdzip-image-insert-mode" value="markdown"
7176
+ data-ref="image-insert-mode-markdown" checked />
7177
+ Markdown image
7178
+ </label>
7179
+ <label>
7180
+ <input type="radio" name="mdzip-image-insert-mode" value="html"
7181
+ data-ref="image-insert-mode-html" />
7182
+ HTML &lt;img&gt; with sizing
7183
+ </label>
7184
+ </fieldset>
7185
+ <label class="image-insert-field">
7186
+ <span>Alt text</span>
7187
+ <input type="text" data-ref="image-insert-alt" />
7188
+ </label>
7189
+ <div class="image-insert-grid">
7190
+ <label class="image-insert-field">
7191
+ <span>Size by</span>
7192
+ <select data-ref="image-insert-size-mode">
7193
+ <option value="original">Original size</option>
7194
+ <option value="width" selected>Width</option>
7195
+ <option value="height">Height</option>
7196
+ <option value="percent">Percent</option>
7197
+ </select>
7198
+ </label>
7199
+ <label class="image-insert-field">
7200
+ <span>Value</span>
7201
+ <input type="number" min="1" step="1" data-ref="image-insert-size-value" />
7202
+ </label>
7203
+ </div>
7204
+ <label class="image-insert-field">
7205
+ <span>Position</span>
7206
+ <select data-ref="image-insert-position">
7207
+ <option value="inline">Inline/default</option>
7208
+ <option value="left">Left</option>
7209
+ <option value="center">Center</option>
7210
+ <option value="right">Right</option>
7211
+ <option value="wrap-left">Wrap left</option>
7212
+ <option value="wrap-right">Wrap right</option>
7213
+ </select>
7214
+ </label>
7215
+ <div class="title-dialog-actions">
7216
+ <button type="button" data-action="cancel-image-insert">Cancel</button>
7217
+ <button type="button" class="save-title" data-ref="image-insert-confirm-btn">Insert</button>
7218
+ </div>
7219
+ </div>
7220
+ </div>
7221
+
7222
+ <div class="title-dialog-backdrop" data-ref="pack-files-dialog" hidden
7223
+ role="dialog" aria-modal="true" aria-labelledby="mdzip-pack-files-dialog-heading">
7224
+ <div class="title-dialog pack-files-dialog">
7225
+ <h3 id="mdzip-pack-files-dialog-heading">Package Markdown Files</h3>
7226
+ <p>These files contain more than one Markdown document. Choose how to package them.</p>
7227
+ <fieldset class="pack-files-options">
7228
+ <legend>Mode</legend>
7229
+ <label>
7230
+ <input type="radio" name="mdzip-pack-files-mode" value="document"
7231
+ data-ref="pack-files-mode-document" checked />
7232
+ Document — one primary page; the rest are attached
7233
+ </label>
7234
+ <label>
7235
+ <input type="radio" name="mdzip-pack-files-mode" value="project"
7236
+ data-ref="pack-files-mode-project" />
7237
+ Project — a set of related documents
7238
+ </label>
7239
+ </fieldset>
7240
+ <label class="pack-files-field">
7241
+ <span>Entry point</span>
7242
+ <select data-ref="pack-files-entry"></select>
7243
+ </label>
7244
+ <div class="title-dialog-actions">
7245
+ <button type="button" data-action="cancel-pack-files">Cancel</button>
7246
+ <button type="button" class="save-title" data-ref="pack-files-confirm-btn">Package</button>
7247
+ </div>
7248
+ </div>
7249
+ </div>
7250
+
7251
+ <div class="title-dialog-backdrop" data-ref="copy-render-dialog" hidden
7252
+ role="dialog" aria-modal="true" aria-labelledby="mdzip-copy-render-dialog-heading">
7253
+ <div class="title-dialog copy-render-dialog">
7254
+ <h3 id="mdzip-copy-render-dialog-heading" data-ref="copy-render-heading">Copying document...</h3>
7255
+ <div data-ref="copy-render-progress-section">
7256
+ <p data-ref="copy-render-progress-text">Rendering the full document...</p>
7257
+ <div class="copy-render-progress-track">
7258
+ <div class="copy-render-progress-bar" data-ref="copy-render-progress-bar"></div>
7259
+ </div>
7260
+ </div>
7261
+ <p data-ref="copy-render-ready-text" hidden>
7262
+ The document is ready. Browsers only allow a clipboard write right after a click, so this needs one more —
7263
+ click Copy to finish.
7264
+ </p>
7265
+ <p data-ref="copy-render-done-text" hidden></p>
7266
+ <div class="title-dialog-actions">
7267
+ <button type="button" data-action="cancel-copy-render" data-ref="copy-render-cancel-btn">Cancel</button>
7268
+ <button type="button" data-action="cancel-copy-ready" data-ref="copy-render-ready-cancel-btn" hidden>Cancel</button>
7269
+ <button type="button" class="save-title" data-action="confirm-copy-ready" data-ref="copy-render-confirm-btn" hidden>Copy</button>
7270
+ <button type="button" class="save-title" data-action="dismiss-copy-render" data-ref="copy-render-dismiss-btn" hidden>Dismiss</button>
7271
+ </div>
7272
+ </div>
7273
+ </div>
7274
+
7275
+ <div class="title-dialog-backdrop" data-ref="metadata-dialog" hidden
7276
+ role="dialog" aria-modal="true" aria-labelledby="mdzip-metadata-dialog-heading">
7277
+ <div class="title-dialog metadata-dialog">
7278
+ <h3 id="mdzip-metadata-dialog-heading">Document Information</h3>
7279
+ <dl data-ref="metadata-list"></dl>
7280
+ <h4>Libraries</h4>
7281
+ <dl data-ref="library-list"></dl>
7282
+ <div class="title-dialog-actions">
7283
+ <button type="button" class="save-title" data-action="close-metadata">Close</button>
7284
+ </div>
7285
+ </div>
7286
+ </div>
7287
+
7288
+ <div class="title-dialog-backdrop" data-ref="name-dialog" hidden
7289
+ role="dialog" aria-modal="true" aria-labelledby="mdzip-name-dialog-heading">
7290
+ <div class="title-dialog">
7291
+ <h3 id="mdzip-name-dialog-heading" data-ref="name-dialog-heading">New Markdown File</h3>
7292
+ <input type="text" maxlength="260" data-ref="name-input" aria-label="File name" />
7293
+ <p class="title-dialog-validation" data-ref="name-validation" hidden></p>
7294
+ <div class="title-dialog-actions">
7295
+ <button type="button" data-action="cancel-name">Cancel</button>
7296
+ <button type="button" class="save-title" data-ref="name-confirm-btn">Create</button>
7297
+ </div>
7298
+ </div>
7299
+ </div>
7300
+
7301
+ <div class="title-dialog-backdrop" data-ref="delete-dialog" hidden
7302
+ role="dialog" aria-modal="true" aria-labelledby="mdzip-delete-dialog-heading">
7303
+ <div class="title-dialog">
7304
+ <h3 id="mdzip-delete-dialog-heading">Delete File?</h3>
7305
+ <p data-ref="delete-dialog-text"></p>
7306
+ <div class="title-dialog-actions">
7307
+ <button type="button" data-action="cancel-delete">Cancel</button>
7308
+ <button type="button" class="danger-action" data-ref="delete-confirm-btn">Delete</button>
7309
+ </div>
7310
+ </div>
7311
+ </div>
7312
+
7313
+ <input type="file" data-ref="replace-input" hidden />
7314
+
7315
+ <div class="nav-context-menu" data-ref="nav-menu" hidden role="menu"></div>
7316
+
7317
+ <div class="mdzip-tooltip" data-ref="tooltip" role="tooltip" hidden></div>
7318
+
7319
+ <div class="mdzip-copy-toast" data-ref="copy-toast" role="status" aria-live="polite" hidden></div>
7320
+
7321
+ <p class="mdzip-empty" data-ref="empty-state">No MDZip workspace loaded.</p>
7322
+ </section>
7059
7323
  `;
7060
7324
  //# sourceMappingURL=view.js.map