@mdzip/editor 1.3.13 → 1.3.16

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
@@ -1,10 +1,11 @@
1
1
  import { defaultKeymap, history, historyKeymap, indentWithTab } from '@codemirror/commands';
2
2
  import { markdown } from '@codemirror/lang-markdown';
3
3
  import { HighlightStyle, syntaxHighlighting } from '@codemirror/language';
4
+ import { closeSearchPanel, openSearchPanel, search, searchKeymap } from '@codemirror/search';
4
5
  import { Compartment, EditorState } from '@codemirror/state';
5
6
  import { Decoration, EditorView, MatchDecorator, ViewPlugin, dropCursor, keymap, lineNumbers } from '@codemirror/view';
6
7
  import { tags } from '@lezer/highlight';
7
- import { Bold, 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, SquareCode, SquarePen, Strikethrough, Sun, TextSelect, ZoomIn } from 'lucide';
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, SquareCode, SquarePen, Strikethrough, Sun, TextSelect, ZoomIn } from 'lucide';
8
9
  import { browserClipboardHasImage, readBrowserClipboardImage } from './browser.js';
9
10
  import { MdzipAssetSession, mdzipArchiveSourceId, sniffImageSize } from './asset-cache.js';
10
11
  import { MD_MARKDOWN_ICON } from './icons/md-markdown.js';
@@ -35,6 +36,7 @@ const CONVERT_TO_MDZ_ICON_HTML = lucideIcon(PackagePlus, `${TOOLBAR_ICON_CLASS}
35
36
  const PREVIEW_ICON_HTML = lucideIcon(Eye, TOOLBAR_ICON_CLASS);
36
37
  const SPLIT_ICON_HTML = lucideIcon(Columns2, TOOLBAR_ICON_CLASS);
37
38
  const SAVE_ICON_HTML = lucideIcon(Save, TOOLBAR_ICON_CLASS);
39
+ const SEARCH_ICON_HTML = lucideIcon(Search, TOOLBAR_ICON_CLASS);
38
40
  const ZOOM_ICON_HTML = lucideIcon(ZoomIn, TOOLBAR_ICON_CLASS);
39
41
  const DARK_THEME_ICON_HTML = lucideIcon(Moon, TOOLBAR_ICON_CLASS);
40
42
  const LIGHT_THEME_ICON_HTML = lucideIcon(Sun, TOOLBAR_ICON_CLASS);
@@ -52,6 +54,16 @@ const LINK_ICON_HTML = lucideIcon(Link, FORMAT_ICON_CLASS);
52
54
  const IMAGE_FORMAT_ICON_HTML = lucideIcon(ImagePlus, FORMAT_ICON_CLASS);
53
55
  const CHEVRON_ICON_HTML = lucideIcon(ChevronDown, 'format-chevron');
54
56
  const INFO_ICON_HTML = lucideIcon(Info, 'document-info-icon');
57
+ const CODE_BLOCK_ICON_CLASS = 'mdzip-code-block-icon';
58
+ const CODE_BLOCK_COPY_ICON_HTML = lucideIcon(Copy, CODE_BLOCK_ICON_CLASS);
59
+ const CODE_BLOCK_COPIED_ICON_HTML = lucideIcon(Check, CODE_BLOCK_ICON_CLASS);
60
+ const CODE_BLOCK_COLLAPSE_ICON_HTML = lucideIcon(ChevronDown, CODE_BLOCK_ICON_CLASS);
61
+ // Below this, collapsing (240px / ~12 visible lines) would have no visible
62
+ // effect, so the collapse button isn't shown at all rather than appearing to
63
+ // do nothing when clicked.
64
+ const CODE_BLOCK_COLLAPSIBLE_MIN_LINES = 15;
65
+ // Blocks taller than this start collapsed; still manually expandable either way.
66
+ const CODE_BLOCK_AUTO_COLLAPSE_LINES = 25;
55
67
  // Leading icons for context-menu items (Obsidian-style icon column).
56
68
  const MENU_ICON_CLASS = 'nav-menu-icon';
57
69
  const MENU_CUT_ICON_HTML = lucideIcon(Scissors, MENU_ICON_CLASS);
@@ -179,7 +191,9 @@ const CONTROL_PRESETS = {
179
191
  zoom: false,
180
192
  colorScheme: false,
181
193
  orphanActions: false,
182
- fileActions: false
194
+ fileActions: false,
195
+ search: false,
196
+ codeBlockTools: true
183
197
  },
184
198
  viewer: {
185
199
  preset: 'viewer',
@@ -193,7 +207,9 @@ const CONTROL_PRESETS = {
193
207
  zoom: true,
194
208
  colorScheme: true,
195
209
  orphanActions: false,
196
- fileActions: false
210
+ fileActions: false,
211
+ search: true,
212
+ codeBlockTools: true
197
213
  },
198
214
  'standalone-editor': {
199
215
  preset: 'standalone-editor',
@@ -207,7 +223,9 @@ const CONTROL_PRESETS = {
207
223
  zoom: true,
208
224
  colorScheme: true,
209
225
  orphanActions: true,
210
- fileActions: true
226
+ fileActions: true,
227
+ search: true,
228
+ codeBlockTools: true
211
229
  },
212
230
  'hosted-editor': {
213
231
  preset: 'hosted-editor',
@@ -221,7 +239,9 @@ const CONTROL_PRESETS = {
221
239
  zoom: true,
222
240
  colorScheme: true,
223
241
  orphanActions: true,
224
- fileActions: true
242
+ fileActions: true,
243
+ search: true,
244
+ codeBlockTools: true
225
245
  }
226
246
  };
227
247
  export function resolveMdzipControlPolicy(controls) {
@@ -367,6 +387,73 @@ const mdzipEditorTheme = EditorView.theme({
367
387
  color: 'var(--mdzip-muted-foreground-color)',
368
388
  opacity: '0.65',
369
389
  },
390
+ '.cm-panels': {
391
+ background: 'var(--mdzip-widget-background-color)',
392
+ color: 'var(--mdzip-editor-foreground-color)',
393
+ zIndex: '2',
394
+ },
395
+ '.cm-panels.cm-panels-top': {
396
+ borderBottom: '1px solid var(--mdzip-border-color)',
397
+ },
398
+ '.cm-search': {
399
+ display: 'flex',
400
+ flexWrap: 'wrap',
401
+ alignItems: 'center',
402
+ gap: '8px',
403
+ padding: '10px 14px',
404
+ fontSize: '13px',
405
+ },
406
+ // CodeMirror separates the find and replace rows with a bare <br>; in a
407
+ // flex container that collapses to zero width instead of breaking the
408
+ // line, so force it to take the full row.
409
+ '.cm-search br': {
410
+ flexBasis: '100%',
411
+ height: '0',
412
+ },
413
+ '.cm-search label': {
414
+ display: 'inline-flex',
415
+ alignItems: 'center',
416
+ gap: '5px',
417
+ color: 'var(--mdzip-muted-foreground-color)',
418
+ },
419
+ '.cm-search input[type="checkbox"]': {
420
+ width: '14px',
421
+ height: '14px',
422
+ accentColor: 'var(--mdzip-accent-color)',
423
+ },
424
+ '.cm-textfield': {
425
+ background: 'var(--mdzip-editor-background-color)',
426
+ color: 'var(--mdzip-editor-foreground-color)',
427
+ border: '1px solid var(--mdzip-border-color)',
428
+ borderRadius: '5px',
429
+ padding: '5px 9px',
430
+ fontSize: 'inherit',
431
+ width: '220px',
432
+ },
433
+ '.cm-textfield:focus-visible': {
434
+ outline: '1px solid var(--mdzip-focus-outline-color)',
435
+ outlineOffset: '-1px',
436
+ },
437
+ '.cm-button': {
438
+ background: 'var(--mdzip-widget-background-color)',
439
+ backgroundImage: 'none',
440
+ color: 'var(--mdzip-control-foreground-color)',
441
+ border: '1px solid var(--mdzip-border-color)',
442
+ borderRadius: '5px',
443
+ padding: '5px 12px',
444
+ fontSize: 'inherit',
445
+ cursor: 'pointer',
446
+ },
447
+ '.cm-button:hover': {
448
+ background: 'var(--mdzip-control-hover-background-color)',
449
+ },
450
+ '.cm-searchMatch': {
451
+ backgroundColor: 'rgba(255, 214, 0, 0.35)',
452
+ },
453
+ '.cm-searchMatch-selected': {
454
+ backgroundColor: 'var(--mdzip-accent-color)',
455
+ color: 'var(--mdzip-accent-foreground-color)',
456
+ },
370
457
  });
371
458
  const mdzipMarkdownHighlight = HighlightStyle.define([
372
459
  { tag: [tags.heading1, tags.heading2, tags.heading3, tags.heading4, tags.heading5, tags.heading6],
@@ -376,7 +463,7 @@ const mdzipMarkdownHighlight = HighlightStyle.define([
376
463
  { tag: tags.emphasis, color: '#008b8b', fontStyle: 'italic' },
377
464
  { tag: tags.strikethrough, color: '#57606a', textDecoration: 'line-through' },
378
465
  { tag: tags.link, color: '#0969da' },
379
- { tag: tags.url, color: '#0969da' },
466
+ { tag: tags.url, color: '#6e7781' },
380
467
  { tag: tags.monospace, color: '#8a8f00' },
381
468
  { tag: tags.quote, color: '#7a5c00' },
382
469
  { tag: tags.contentSeparator, color: '#6a9955' },
@@ -398,6 +485,27 @@ const hardBreakMarkerHighlight = ViewPlugin.fromClass(class {
398
485
  }, {
399
486
  decorations: value => value.decorations
400
487
  });
488
+ // Matches raw HTML tags other than <br> (which hardBreakMarkerMatcher already
489
+ // covers) so authors can visually distinguish raw HTML from Markdown prose.
490
+ // Excludes autolinks like <https://example.com> — those have no space before
491
+ // '>' and no closing '/', so the tag-name-only branch requires the char after
492
+ // the name to be '>' directly, which a URL's ':' never satisfies.
493
+ const htmlTagMarkerMatcher = new MatchDecorator({
494
+ regexp: /<\/?(?!br\b)[a-zA-Z][a-zA-Z0-9-]*(?:\s[^<>]*)?\/?>/gi,
495
+ decoration: Decoration.mark({ class: 'mdzip-hard-break-marker' })
496
+ });
497
+ const htmlTagMarkerHighlight = ViewPlugin.fromClass(class {
498
+ constructor(view) {
499
+ this.decorations = htmlTagMarkerMatcher.createDeco(view);
500
+ }
501
+ update(update) {
502
+ if (update.docChanged || update.viewportChanged) {
503
+ this.decorations = htmlTagMarkerMatcher.updateDeco(update, this.decorations);
504
+ }
505
+ }
506
+ }, {
507
+ decorations: value => value.decorations
508
+ });
401
509
  function injectStyles(doc) {
402
510
  const existing = doc.querySelector(`style[${STYLE_ATTR}]`);
403
511
  if (existing) {
@@ -469,19 +577,19 @@ depth = 0) {
469
577
  : isImageFile(node.entry.path)
470
578
  ? IMAGE_ICON_HTML
471
579
  : FILE_ICON_HTML;
472
- const orphanBtnHtml = isOrphaned && options.allowOrphanActions ? `
473
- <span class="nav-orphan-button" role="button" tabindex="0"
474
- title="Orphaned asset" aria-label="Orphaned asset actions"
475
- data-orphan-path="${safePath}">
476
- ${ORPHAN_ICON_HTML}
580
+ const orphanBtnHtml = isOrphaned && options.allowOrphanActions ? `
581
+ <span class="nav-orphan-button" role="button" tabindex="0"
582
+ title="Orphaned asset" aria-label="Orphaned asset actions"
583
+ data-orphan-path="${safePath}">
584
+ ${ORPHAN_ICON_HTML}
477
585
  </span>` : '';
478
586
  const draggable = options.allowDrag && !isManifest;
479
- return `<button type="button" class="${classes}" title="${title}"
480
- data-nav-path="${safePath}" data-orphan="${isOrphaned ? 'true' : ''}"${draggable ? ' draggable="true"' : ''}>
481
- ${guides}<span class="nav-caret"></span>
482
- <span class="nav-file-icon ${iconKind}">${iconHtml}</span>
483
- ${orphanBtnHtml}
484
- <span class="nav-label">${safeName}</span>
587
+ return `<button type="button" class="${classes}" title="${title}"
588
+ data-nav-path="${safePath}" data-orphan="${isOrphaned ? 'true' : ''}"${draggable ? ' draggable="true"' : ''}>
589
+ ${guides}<span class="nav-caret"></span>
590
+ <span class="nav-file-icon ${iconKind}">${iconHtml}</span>
591
+ ${orphanBtnHtml}
592
+ <span class="nav-label">${safeName}</span>
485
593
  </button>`;
486
594
  }
487
595
  const childTrail = depth === 0 ? [] : [...trail, !isLast];
@@ -489,14 +597,14 @@ depth = 0) {
489
597
  .map((child, index) => renderNavNode(child, state, options, childTrail, index === node.children.length - 1, depth + 1))
490
598
  .join('');
491
599
  const pending = options.pendingFolders.has(node.path.toLowerCase());
492
- return `<details class="nav-directory${pending ? ' pending-folder' : ''}" open data-nav-dir="${escapeHtml(node.path)}">
493
- <summary${pending ? ` title="${escapeHtml(node.path)} — not saved until it contains a file"` : ''}>
494
- ${guides}<span class="nav-caret" aria-hidden="true"></span>
495
- <span class="nav-folder-icon closed">${FOLDER_CLOSED_ICON_HTML}</span>
496
- <span class="nav-folder-icon open">${FOLDER_OPEN_ICON_HTML}</span>
497
- <span class="nav-label">${escapeHtml(node.name)}</span>
498
- </summary>
499
- <div class="nav-directory-children">${children}</div>
600
+ return `<details class="nav-directory${pending ? ' pending-folder' : ''}" open data-nav-dir="${escapeHtml(node.path)}">
601
+ <summary${pending ? ` title="${escapeHtml(node.path)} — not saved until it contains a file"` : ''}>
602
+ ${guides}<span class="nav-caret" aria-hidden="true"></span>
603
+ <span class="nav-folder-icon closed">${FOLDER_CLOSED_ICON_HTML}</span>
604
+ <span class="nav-folder-icon open">${FOLDER_OPEN_ICON_HTML}</span>
605
+ <span class="nav-label">${escapeHtml(node.name)}</span>
606
+ </summary>
607
+ <div class="nav-directory-children">${children}</div>
500
608
  </details>`;
501
609
  }
502
610
  // Inserts view-local pending (not yet saved) folders into the nav tree so they
@@ -532,6 +640,35 @@ function navNodeOrder(a, b) {
532
640
  return 1;
533
641
  return a.name.localeCompare(b.name);
534
642
  }
643
+ // Browsers apply raw HTML width/height attributes as presentational sizing
644
+ // hints, but the preview's `img { height: auto }` rule (for responsive
645
+ // scaling) overrides that hint via the normal CSS cascade — so an author's
646
+ // `<img height="300">` was silently ignored. Re-applying numeric, unitless
647
+ // width/height attributes as inline pixel styles restores that sizing
648
+ // without touching an author-supplied inline style.
649
+ function applyRawHtmlImageSizeAttributes(image) {
650
+ const width = image.getAttribute('width');
651
+ if (width && /^\d+$/.test(width) && !image.style.width) {
652
+ image.style.width = `${width}px`;
653
+ }
654
+ const height = image.getAttribute('height');
655
+ if (height && /^\d+$/.test(height) && !image.style.height) {
656
+ image.style.height = `${height}px`;
657
+ }
658
+ }
659
+ // Maps the legacy raw HTML `align="left"|"right"` attribute (or the editor's
660
+ // own wrap classes) to a layout direction, for images that skip archive
661
+ // hydration — external, data, and fragment sources — and so never get a slot.
662
+ function rawHtmlImageAlignClass(image) {
663
+ const align = image.getAttribute('align')?.toLowerCase();
664
+ if (align === 'left' || image.classList.contains('mdzip-image-wrap-left')) {
665
+ return 'mdzip-image-left';
666
+ }
667
+ if (align === 'right' || image.classList.contains('mdzip-image-wrap-right')) {
668
+ return 'mdzip-image-right';
669
+ }
670
+ return null;
671
+ }
535
672
  export class MdzipWorkspaceView {
536
673
  constructor(container, options = {}) {
537
674
  this.workspace = null;
@@ -574,6 +711,7 @@ export class MdzipWorkspaceView {
574
711
  this.cmEditor = null;
575
712
  this.readOnlyCompartment = new Compartment();
576
713
  this.lineNumbersCompartment = new Compartment();
714
+ this.searchCompartment = new Compartment();
577
715
  this.updatingCm = false;
578
716
  this.syncing = false;
579
717
  this.markdownExtensions = [];
@@ -630,6 +768,7 @@ export class MdzipWorkspaceView {
630
768
  this.elSourceBtn = q('[data-ref="source-btn"]');
631
769
  this.elSourceIcon = q('[data-ref="source-icon"]');
632
770
  this.elSaveBtn = q('[data-ref="save-btn"]');
771
+ this.elSearchBtn = q('[data-ref="search-btn"]');
633
772
  this.elZoomBtn = q('[data-ref="zoom-btn"]');
634
773
  this.elThemeControls = q('[data-ref="theme-controls"]');
635
774
  this.elDarkThemeBtn = q('[data-ref="dark-theme-btn"]');
@@ -907,6 +1046,40 @@ export class MdzipWorkspaceView {
907
1046
  this.applyMarkdownFormat(command);
908
1047
  return true;
909
1048
  }
1049
+ /**
1050
+ * Opens CodeMirror's find/replace panel for the current document. Unlike
1051
+ * {@link executeCommand}, this works in read-only (Viewer) hosts too —
1052
+ * searching doesn't require edit access, only a visible source pane. If
1053
+ * the current layout is preview-only, switches to split/source first so
1054
+ * the panel has somewhere to render.
1055
+ */
1056
+ async openSearch() {
1057
+ if (!this.controlPolicy.search) {
1058
+ return false;
1059
+ }
1060
+ const snapshot = this.workspace?.snapshot();
1061
+ if (!snapshot || snapshot.currentPathType !== 'markdown') {
1062
+ return false;
1063
+ }
1064
+ if (this.layout === 'preview') {
1065
+ const nextLayout = this.controlPolicy.layout.split
1066
+ ? 'split'
1067
+ : this.controlPolicy.layout.source ? 'source' : null;
1068
+ if (!nextLayout) {
1069
+ return false;
1070
+ }
1071
+ await this.setLayout(nextLayout);
1072
+ }
1073
+ const editor = await this.ensureCmEditor(true);
1074
+ if (!editor) {
1075
+ return false;
1076
+ }
1077
+ editor.focus();
1078
+ return openSearchPanel(editor);
1079
+ }
1080
+ closeSearch() {
1081
+ return this.cmEditor ? closeSearchPanel(this.cmEditor) : false;
1082
+ }
910
1083
  async convertToMdz() {
911
1084
  if (!this.workspace || this.workspace.mode === 'read-only') {
912
1085
  return false;
@@ -982,12 +1155,21 @@ export class MdzipWorkspaceView {
982
1155
  setControls(controls) {
983
1156
  const next = resolveMdzipControlPolicy(controls);
984
1157
  const lineNumbersChanged = next.lineNumbers !== this.controlPolicy.lineNumbers;
1158
+ const searchChanged = next.search !== this.controlPolicy.search;
985
1159
  this.controlPolicy = next;
986
1160
  if (lineNumbersChanged && this.cmEditor) {
987
1161
  this.cmEditor.dispatch({
988
1162
  effects: this.lineNumbersCompartment.reconfigure(next.lineNumbers ? lineNumbers() : [])
989
1163
  });
990
1164
  }
1165
+ if (searchChanged && this.cmEditor) {
1166
+ if (!next.search) {
1167
+ closeSearchPanel(this.cmEditor);
1168
+ }
1169
+ this.cmEditor.dispatch({
1170
+ effects: this.searchCompartment.reconfigure(next.search ? [search({ top: true }), keymap.of(searchKeymap)] : [])
1171
+ });
1172
+ }
991
1173
  const snapshot = this.workspace?.snapshot();
992
1174
  if (snapshot) {
993
1175
  this.layout = this.validLayoutForSnapshot(this.layout, snapshot);
@@ -1170,6 +1352,10 @@ export class MdzipWorkspaceView {
1170
1352
  mountPreviewHtml(html, snapshot, context, generation) {
1171
1353
  this.elPreviewContent.innerHTML = html;
1172
1354
  this.mountPreviewExtensions(context, generation);
1355
+ const codeBlockHandle = this.mountCodeBlockControls();
1356
+ if (codeBlockHandle) {
1357
+ this.previewHandles.push(codeBlockHandle);
1358
+ }
1173
1359
  // No archive images to resolve on this path: the preview is ready now.
1174
1360
  this.firePreviewRendered(snapshot, generation);
1175
1361
  this.fireAssetsHydrated(snapshot, generation);
@@ -1186,9 +1372,14 @@ export class MdzipWorkspaceView {
1186
1372
  const document = this.elPreviewContent.ownerDocument;
1187
1373
  const pending = [];
1188
1374
  for (const image of Array.from(this.elPreviewContent.querySelectorAll('img'))) {
1375
+ applyRawHtmlImageSizeAttributes(image);
1189
1376
  const source = image.getAttribute('src');
1190
1377
  // Leave external, protocol-relative, data, and fragment URLs untouched.
1191
1378
  if (!source || /^(?:[a-z][a-z\d+.-]*:|\/\/|#)/i.test(source)) {
1379
+ const alignClass = rawHtmlImageAlignClass(image);
1380
+ if (alignClass) {
1381
+ image.classList.add(alignClass);
1382
+ }
1192
1383
  continue;
1193
1384
  }
1194
1385
  // Drop the archive-relative src so the browser does not fetch the bad
@@ -1203,11 +1394,11 @@ export class MdzipWorkspaceView {
1203
1394
  slot.className = animateImageHydration
1204
1395
  ? 'mdzip-image-slot'
1205
1396
  : 'mdzip-image-slot mdzip-image-open mdzip-image-animation-off';
1206
- const align = image.getAttribute('align')?.toLowerCase();
1207
- if (align === 'left' || image.classList.contains('mdzip-image-wrap-left')) {
1397
+ const alignClass = rawHtmlImageAlignClass(image);
1398
+ if (alignClass === 'mdzip-image-left') {
1208
1399
  slot.classList.add('mdzip-image-align-left');
1209
1400
  }
1210
- else if (align === 'right' || image.classList.contains('mdzip-image-wrap-right')) {
1401
+ else if (alignClass === 'mdzip-image-right') {
1211
1402
  slot.classList.add('mdzip-image-align-right');
1212
1403
  }
1213
1404
  image.parentNode?.insertBefore(slot, image);
@@ -1215,6 +1406,10 @@ export class MdzipWorkspaceView {
1215
1406
  pending.push({ image, slot, source });
1216
1407
  }
1217
1408
  this.mountPreviewExtensions(context, generation);
1409
+ const codeBlockHandle = this.mountCodeBlockControls();
1410
+ if (codeBlockHandle) {
1411
+ this.previewHandles.push(codeBlockHandle);
1412
+ }
1218
1413
  this.firePreviewRendered(snapshot, generation);
1219
1414
  if (!session || pending.length === 0) {
1220
1415
  this.fireAssetsHydrated(snapshot, generation);
@@ -1358,6 +1553,130 @@ export class MdzipWorkspaceView {
1358
1553
  }
1359
1554
  }
1360
1555
  }
1556
+ /**
1557
+ * Built-in preview affordances for rendered code blocks: a language-name
1558
+ * header, a copy-to-clipboard button, and (on long enough blocks) a
1559
+ * collapse/expand toggle. Gated by `controlPolicy.codeBlockTools`.
1560
+ *
1561
+ * Deliberately not a `markdownExtensions` entry (unlike the Mermaid
1562
+ * extension) — every consumer gets this automatically, gated by policy
1563
+ * rather than opt-in wiring. Runs after `mountPreviewExtensions` so any
1564
+ * extension-provided `pre > code` blocks already exist in the DOM. See #29.
1565
+ */
1566
+ mountCodeBlockControls() {
1567
+ if (!this.controlPolicy.codeBlockTools) {
1568
+ return null;
1569
+ }
1570
+ const codeEls = Array.from(this.elPreviewContent.querySelectorAll('pre > code'));
1571
+ if (codeEls.length === 0) {
1572
+ return null;
1573
+ }
1574
+ const doc = this.elPreviewContent.ownerDocument;
1575
+ // Resolve AbortController from the document's own realm, not the ambient
1576
+ // global: addEventListener's `signal` option is validated against the
1577
+ // listener target's realm, so a controller from a different realm (e.g.
1578
+ // Node's global AbortController against a jsdom-hosted element in a
1579
+ // server-side/test environment) fails that check even though both are
1580
+ // spec-compliant AbortControllers.
1581
+ const AbortControllerCtor = doc.defaultView?.AbortController ?? AbortController;
1582
+ const controller = new AbortControllerCtor();
1583
+ for (const code of codeEls) {
1584
+ const pre = code.parentElement;
1585
+ if (pre) {
1586
+ this.enhanceCodeBlock(pre, code, doc, controller.signal);
1587
+ }
1588
+ }
1589
+ return { destroy: () => controller.abort() };
1590
+ }
1591
+ /**
1592
+ * Wraps one `pre > code` block with a header (language label + action
1593
+ * buttons) and a collapsible body, in place. `pre` itself is relocated
1594
+ * (not cloned) into the new structure, so existing references to it and
1595
+ * its `code` child stay valid.
1596
+ */
1597
+ enhanceCodeBlock(pre, code, doc, signal) {
1598
+ const language = /language-([\w-]+)/.exec(code.className)?.[1] ?? '';
1599
+ const wrapper = doc.createElement('div');
1600
+ wrapper.className = 'mdzip-code-block';
1601
+ const header = doc.createElement('div');
1602
+ header.className = 'mdzip-code-block-header';
1603
+ const label = doc.createElement('span');
1604
+ label.className = 'mdzip-code-block-lang';
1605
+ label.textContent = language || 'text';
1606
+ header.appendChild(label);
1607
+ const actions = doc.createElement('div');
1608
+ actions.className = 'mdzip-code-block-actions';
1609
+ header.appendChild(actions);
1610
+ const body = doc.createElement('div');
1611
+ body.className = 'mdzip-code-block-body';
1612
+ pre.replaceWith(wrapper);
1613
+ wrapper.append(header, body);
1614
+ body.appendChild(pre);
1615
+ // Collapse toggle: only shown when the block has enough lines that
1616
+ // collapsing is actually visible — the collapsed body still shows ~12
1617
+ // lines (240px, see .mdzip-code-block-collapsed in view-css.ts), so a
1618
+ // button that collapses a block shorter than that would visibly do
1619
+ // nothing, which is more confusing than not offering it at all. Blocks
1620
+ // past the longer auto-collapse threshold start pre-collapsed; anything
1621
+ // between the two thresholds is collapsible but starts open.
1622
+ const lineCount = (code.textContent ?? '').split('\n').length;
1623
+ if (lineCount > CODE_BLOCK_COLLAPSIBLE_MIN_LINES) {
1624
+ if (lineCount > CODE_BLOCK_AUTO_COLLAPSE_LINES) {
1625
+ wrapper.classList.add('mdzip-code-block-collapsed');
1626
+ }
1627
+ const collapseBtn = doc.createElement('button');
1628
+ collapseBtn.type = 'button';
1629
+ collapseBtn.className = 'mdzip-code-block-btn';
1630
+ collapseBtn.innerHTML = CODE_BLOCK_COLLAPSE_ICON_HTML;
1631
+ const syncCollapseLabel = () => {
1632
+ const collapsed = wrapper.classList.contains('mdzip-code-block-collapsed');
1633
+ collapseBtn.setAttribute('aria-expanded', collapsed ? 'false' : 'true');
1634
+ collapseBtn.setAttribute('aria-label', collapsed ? 'Expand code block' : 'Collapse code block');
1635
+ };
1636
+ syncCollapseLabel();
1637
+ collapseBtn.title = 'Collapse/expand';
1638
+ collapseBtn.addEventListener('click', () => {
1639
+ wrapper.classList.toggle('mdzip-code-block-collapsed');
1640
+ syncCollapseLabel();
1641
+ }, { signal });
1642
+ actions.appendChild(collapseBtn);
1643
+ }
1644
+ // Copy button: icon swaps to a confirmation state for a beat, then reverts.
1645
+ // The revert timer is cleared on unmount so it never touches a stale button
1646
+ // after the preview re-renders.
1647
+ const copyBtn = doc.createElement('button');
1648
+ copyBtn.type = 'button';
1649
+ copyBtn.className = 'mdzip-code-block-btn';
1650
+ copyBtn.innerHTML = CODE_BLOCK_COPY_ICON_HTML;
1651
+ copyBtn.setAttribute('aria-label', 'Copy code');
1652
+ copyBtn.title = 'Copy code';
1653
+ // Re-check clipboard availability at click time rather than capturing it
1654
+ // once at mount: a host can grant/attach clipboard access after the
1655
+ // preview first renders, and this way there is nothing to keep in sync.
1656
+ let copiedTimer;
1657
+ copyBtn.addEventListener('click', () => {
1658
+ const clip = doc.defaultView?.navigator.clipboard;
1659
+ if (!clip) {
1660
+ return;
1661
+ }
1662
+ void clip.writeText(code.textContent ?? '').then(() => {
1663
+ copyBtn.innerHTML = CODE_BLOCK_COPIED_ICON_HTML;
1664
+ copyBtn.classList.add('mdzip-code-block-btn-copied');
1665
+ copyBtn.setAttribute('aria-label', 'Copied');
1666
+ clearTimeout(copiedTimer);
1667
+ copiedTimer = setTimeout(() => {
1668
+ copyBtn.innerHTML = CODE_BLOCK_COPY_ICON_HTML;
1669
+ copyBtn.classList.remove('mdzip-code-block-btn-copied');
1670
+ copyBtn.setAttribute('aria-label', 'Copy code');
1671
+ }, 1500);
1672
+ }).catch(() => {
1673
+ // Write can fail (permissions, insecure context); leave the button
1674
+ // as-is rather than showing a false confirmation.
1675
+ });
1676
+ }, { signal });
1677
+ signal.addEventListener('abort', () => clearTimeout(copiedTimer), { once: true });
1678
+ actions.appendChild(copyBtn);
1679
+ }
1361
1680
  firePreviewRendered(snapshot, generation) {
1362
1681
  if (generation !== this.previewGeneration) {
1363
1682
  return;
@@ -1579,6 +1898,7 @@ export class MdzipWorkspaceView {
1579
1898
  doc: initialText,
1580
1899
  extensions: [
1581
1900
  this.lineNumbersCompartment.of(this.controlPolicy.lineNumbers ? lineNumbers() : []),
1901
+ this.searchCompartment.of(this.controlPolicy.search ? [search({ top: true }), keymap.of(searchKeymap)] : []),
1582
1902
  history(),
1583
1903
  keymap.of([
1584
1904
  { key: 'Mod-b', run: () => self.runFormatShortcut('bold') },
@@ -1589,6 +1909,7 @@ export class MdzipWorkspaceView {
1589
1909
  markdown(),
1590
1910
  syntaxHighlighting(mdzipMarkdownHighlight),
1591
1911
  hardBreakMarkerHighlight,
1912
+ htmlTagMarkerHighlight,
1592
1913
  EditorView.lineWrapping,
1593
1914
  dropCursor(),
1594
1915
  mdzipEditorTheme,
@@ -1682,24 +2003,28 @@ export class MdzipWorkspaceView {
1682
2003
  const showSaveControl = this.controlPolicy.save && snapshot.mode !== 'read-only';
1683
2004
  const showZoomControl = this.controlPolicy.zoom;
1684
2005
  const showColorSchemeControl = this.controlPolicy.colorScheme;
2006
+ const showSearchControl = this.controlPolicy.search
2007
+ && canShowSource && snapshot.currentPathType === 'markdown';
1685
2008
  const showEditControls = canEdit
1686
2009
  && snapshot.currentPathType === 'markdown'
1687
2010
  && this.layout !== 'preview'
1688
2011
  && hasFormattingControls(this.controlPolicy.formatting);
1689
2012
  const showToolbar = this.controlPolicy.toolbar
1690
- && (showNavigationControl || showLayoutControls
1691
- || showSaveControl || showZoomControl || showColorSchemeControl || showEditControls);
2013
+ && (showNavigationControl || showLayoutControls || showSaveControl
2014
+ || showZoomControl || showColorSchemeControl || showSearchControl || showEditControls);
1692
2015
  this.elDocumentStrip.hidden = !showTitleControl;
1693
2016
  this.elToolbar.hidden = !showToolbar;
1694
2017
  this.elToolbarLeft.hidden = !showNavigationControl;
1695
2018
  this.elEditToolbar.hidden = !showEditControls;
1696
2019
  this.elLayoutControls.hidden = !showLayoutControls;
1697
- this.elToolbarControls.hidden = !showSaveControl && !showZoomControl && !showColorSchemeControl;
2020
+ this.elToolbarControls.hidden = !showSaveControl && !showZoomControl
2021
+ && !showColorSchemeControl && !showSearchControl;
1698
2022
  this.elNavBtn.hidden = !showNavigationControl;
1699
2023
  this.elPreviewBtn.hidden = !this.controlPolicy.layout.preview;
1700
2024
  this.elSplitBtn.hidden = !this.controlPolicy.layout.split;
1701
2025
  this.elSourceBtn.hidden = !this.controlPolicy.layout.source;
1702
2026
  this.elSaveBtn.hidden = !showSaveControl;
2027
+ this.elSearchBtn.hidden = !showSearchControl;
1703
2028
  this.elZoomBtn.hidden = !showZoomControl;
1704
2029
  this.elThemeControls.hidden = !showColorSchemeControl;
1705
2030
  this.elRoot.style.setProperty('--mdz-zoom', String(this.zoom));
@@ -1984,6 +2309,9 @@ export class MdzipWorkspaceView {
1984
2309
  void this.save();
1985
2310
  }
1986
2311
  });
2312
+ this.elSearchBtn.addEventListener('click', () => {
2313
+ void this.openSearch();
2314
+ });
1987
2315
  this.elZoomBtn.addEventListener('click', (e) => {
1988
2316
  if (!this.controlPolicy.zoom) {
1989
2317
  return;
@@ -4117,267 +4445,270 @@ function formatMetadataValue(value) {
4117
4445
  }
4118
4446
  return 'Not available';
4119
4447
  }
4120
- const SHELL_HTML = `
4121
- <section class="mdzip-root">
4122
- <div class="document-strip" data-ref="document-strip" hidden>
4123
- <button type="button" class="title-button" data-ref="title-btn"></button>
4124
- <button type="button" class="document-info-button" data-ref="document-info-btn"
4125
- title="Document information" aria-label="Document information">
4126
- ${INFO_ICON_HTML}
4127
- </button>
4128
- </div>
4129
-
4130
- <header class="toolbar" data-ref="toolbar" hidden>
4131
- <div class="toolbar-start">
4132
- <div class="toolbar-left">
4133
- <button type="button" class="icon-toggle nav-toggle" data-ref="nav-btn" title="Toggle contents" aria-label="Toggle contents">
4134
- ${NAV_TOGGLE_ICON_HTML}
4135
- </button>
4136
- </div>
4137
-
4138
- <div class="edit-toolbar" data-ref="edit-toolbar" role="toolbar" aria-label="Markdown formatting">
4139
- <div class="edit-toolbar-group">
4140
- <button type="button" data-format="bold" data-format-control="bold" title="Bold" aria-label="Bold">${BOLD_ICON_HTML}</button>
4141
- <button type="button" data-format="italic" data-format-control="italic" title="Italic" aria-label="Italic">${ITALIC_ICON_HTML}</button>
4142
- <button type="button" data-format="strike" data-format-control="strikethrough" title="Strikethrough" aria-label="Strikethrough">${STRIKE_ICON_HTML}</button>
4143
- <div class="format-menu" data-format-control="headings">
4144
- <button type="button" class="format-menu-toggle" data-format-menu-toggle
4145
- aria-label="Heading" aria-haspopup="menu" aria-expanded="false">
4146
- ${HEADING_ICON_HTML}${CHEVRON_ICON_HTML}
4147
- </button>
4148
- <div class="format-menu-popover" data-format-menu role="menu" aria-label="Heading level" hidden>
4149
- <button type="button" role="menuitem" data-format="paragraph">Paragraph</button>
4150
- <button type="button" role="menuitem" data-format="heading-1" data-heading-level="1"><strong>H1</strong> Heading 1</button>
4151
- <button type="button" role="menuitem" data-format="heading-2" data-heading-level="2"><strong>H2</strong> Heading 2</button>
4152
- <button type="button" role="menuitem" data-format="heading-3" data-heading-level="3"><strong>H3</strong> Heading 3</button>
4153
- <button type="button" role="menuitem" data-format="heading-4" data-heading-level="4"><strong>H4</strong> Heading 4</button>
4154
- <button type="button" role="menuitem" data-format="heading-5" data-heading-level="5"><strong>H5</strong> Heading 5</button>
4155
- <button type="button" role="menuitem" data-format="heading-6" data-heading-level="6"><strong>H6</strong> Heading 6</button>
4156
- </div>
4157
- </div>
4158
- </div>
4159
- <span class="edit-toolbar-divider" aria-hidden="true"></span>
4160
- <div class="edit-toolbar-group">
4161
- <button type="button" data-format="bullet-list" data-format-control="bulletList" title="Bulleted list" aria-label="Bulleted list">${BULLET_LIST_ICON_HTML}</button>
4162
- <button type="button" data-format="ordered-list" data-format-control="orderedList" title="Numbered list" aria-label="Numbered list">${ORDERED_LIST_ICON_HTML}</button>
4163
- <div class="format-menu" data-format-control="code">
4164
- <button type="button" class="format-menu-toggle" data-format-menu-toggle
4165
- aria-label="Code" aria-haspopup="menu" aria-expanded="false">
4166
- ${CODE_ICON_HTML}${CHEVRON_ICON_HTML}
4167
- </button>
4168
- <div class="format-menu-popover" data-format-menu role="menu" aria-label="Code format" hidden>
4169
- <button type="button" role="menuitem" data-format="code" data-code-kind="inline">Inline code</button>
4170
- <button type="button" role="menuitem" data-format="code-block" data-code-kind="block">Code block</button>
4171
- </div>
4172
- </div>
4173
- <button type="button" data-format="quote" data-format-control="blockquote" title="Blockquote" aria-label="Blockquote">${QUOTE_ICON_HTML}</button>
4174
- <button type="button" data-format="line-break" data-format-control="lineBreak" title="Line break" aria-label="Line break">${LINE_BREAK_ICON_HTML}</button>
4175
- </div>
4176
- <span class="edit-toolbar-divider" aria-hidden="true"></span>
4177
- <div class="edit-toolbar-group">
4178
- <button type="button" data-format="link" data-format-control="link" title="Link" aria-label="Link">${LINK_ICON_HTML}</button>
4179
- <button type="button" data-format="image" data-format-control="image" title="Image" aria-label="Image">${IMAGE_FORMAT_ICON_HTML}</button>
4180
- <input type="file" data-ref="image-input" accept="image/*" hidden />
4181
- </div>
4182
- </div>
4183
- </div>
4184
-
4185
- <div class="toolbar-buttons view-mode-toggle-group" data-ref="layout-controls" role="group" aria-label="Editor layout">
4186
- <button type="button" class="icon-toggle view-mode-toggle" data-ref="source-btn" title="Edit" aria-label="Edit" aria-pressed="false">
4187
- <span class="commandbar-flex-container" data-ref="source-icon">${SOURCE_EDIT_ICON_HTML}</span>
4188
- </button>
4189
- <button type="button" class="icon-toggle view-mode-toggle" data-ref="split-btn" title="Split" aria-label="Split" aria-pressed="false">
4190
- <span class="commandbar-flex-container">
4191
- ${SPLIT_ICON_HTML}
4192
- </span>
4193
- </button>
4194
- <button type="button" class="icon-toggle view-mode-toggle" data-ref="preview-btn" title="View" aria-label="View" aria-pressed="false">
4195
- <span class="commandbar-flex-container">
4196
- ${PREVIEW_ICON_HTML}
4197
- </span>
4198
- </button>
4199
- </div>
4200
-
4201
- <div class="toolbar-controls" data-ref="toolbar-controls">
4202
- <button type="button" class="icon-toggle" data-ref="save-btn" title="Save" aria-label="Save">
4203
- ${SAVE_ICON_HTML}
4204
- </button>
4205
- <button type="button" class="icon-toggle zoom-toggle" data-ref="zoom-btn" title="Zoom controls" aria-label="Zoom controls">
4206
- ${ZOOM_ICON_HTML}
4207
- </button>
4208
- <div class="theme-toggle-group" data-ref="theme-controls" role="group" aria-label="Color scheme">
4209
- <button type="button" class="icon-toggle theme-toggle" data-ref="dark-theme-btn"
4210
- title="Dark mode" aria-label="Dark mode" aria-pressed="false">
4211
- ${DARK_THEME_ICON_HTML}
4212
- </button>
4213
- <button type="button" class="icon-toggle theme-toggle" data-ref="light-theme-btn"
4214
- title="Light mode" aria-label="Light mode" aria-pressed="false">
4215
- ${LIGHT_THEME_ICON_HTML}
4216
- </button>
4217
- </div>
4218
- <div class="zoom-popover" data-ref="zoom-popover" hidden role="group" aria-label="Zoom">
4219
- <span class="zoom-level" data-ref="zoom-level">100%</span>
4220
- <span class="zoom-stepper">
4221
- <button type="button" data-action="zoom-out" title="Zoom out" aria-label="Zoom out">-</button>
4222
- <button type="button" data-action="zoom-in" title="Zoom in" aria-label="Zoom in">+</button>
4223
- </span>
4224
- <button type="button" class="zoom-reset" data-action="zoom-reset" title="Reset zoom">Reset</button>
4225
- </div>
4226
- </div>
4227
- </header>
4228
-
4229
- <div class="workspace-shell" data-ref="workspace-shell" hidden>
4230
- <aside class="nav-pane" data-ref="nav-pane" aria-label="Package contents">
4231
- <div class="nav-tree" data-ref="nav-tree"></div>
4232
- </aside>
4233
- <div class="nav-resizer" data-ref="nav-resizer"
4234
- role="separator" aria-orientation="vertical" aria-label="Resize contents pane"></div>
4235
-
4236
- <div class="pane-stack" data-ref="pane-stack">
4237
- <section class="pane edit-pane" data-ref="edit-pane">
4238
- <div class="editor-host" data-ref="editor-host"></div>
4239
- </section>
4240
- <div class="split-resizer" data-ref="split-resizer"
4241
- role="separator" aria-orientation="vertical" aria-label="Resize split panes"></div>
4242
- <section class="pane preview-pane" data-ref="preview-pane">
4243
- <article class="preview-content" data-ref="preview-content"></article>
4244
- </section>
4245
- <section class="pane entry-pane" data-ref="entry-pane"></section>
4246
- </div>
4247
- </div>
4248
-
4249
- <div class="title-dialog-backdrop" data-ref="title-dialog" hidden
4250
- role="dialog" aria-modal="true" aria-labelledby="mdzip-title-dialog-heading">
4251
- <div class="title-dialog">
4252
- <h3 id="mdzip-title-dialog-heading">Set Document Title</h3>
4253
- <p>This is the package-level title stored in manifest.json.</p>
4254
- <input type="text" maxlength="120" data-ref="title-input" aria-label="Document title" />
4255
- <p class="title-dialog-validation" data-ref="title-validation" hidden>Title cannot be empty.</p>
4256
- <p>If unset, consumers may fall back to entry point, filename, or first heading.</p>
4257
- <div class="title-dialog-actions">
4258
- <button type="button" class="reset-title" data-ref="title-reset-btn">Reset</button>
4259
- <button type="button" data-action="cancel-title">Cancel</button>
4260
- <button type="button" class="save-title" data-ref="title-save-btn">Save</button>
4261
- </div>
4262
- </div>
4263
- </div>
4264
-
4265
- <div class="title-dialog-backdrop" data-ref="conversion-dialog" hidden
4266
- role="dialog" aria-modal="true" aria-labelledby="mdzip-conversion-dialog-heading">
4267
- <div class="title-dialog">
4268
- <h3 id="mdzip-conversion-dialog-heading">Convert to MDZ?</h3>
4269
- <p>
4270
- Regular Markdown files contain only text. Convert this document to an
4271
- MDZ package to add images, package navigation, and document metadata.
4272
- </p>
4273
- <p>The next save will produce an .mdz file.</p>
4274
- <div class="title-dialog-actions">
4275
- <button type="button" data-action="cancel-conversion">Cancel</button>
4276
- <button type="button" class="save-title" data-ref="conversion-confirm-btn">Convert</button>
4277
- </div>
4278
- </div>
4279
- </div>
4280
-
4281
- <div class="title-dialog-backdrop" data-ref="image-insert-dialog" hidden
4282
- role="dialog" aria-modal="true" aria-labelledby="mdzip-image-insert-dialog-heading">
4283
- <div class="title-dialog image-insert-dialog">
4284
- <h3 id="mdzip-image-insert-dialog-heading">Insert Image</h3>
4285
- <p>Choose the Markdown inserted for this image.</p>
4286
- <fieldset class="image-insert-options">
4287
- <legend>Output</legend>
4288
- <label>
4289
- <input type="radio" name="mdzip-image-insert-mode" value="markdown"
4290
- data-ref="image-insert-mode-markdown" checked />
4291
- Markdown image
4292
- </label>
4293
- <label>
4294
- <input type="radio" name="mdzip-image-insert-mode" value="html"
4295
- data-ref="image-insert-mode-html" />
4296
- HTML &lt;img&gt; with sizing
4297
- </label>
4298
- </fieldset>
4299
- <label class="image-insert-field">
4300
- <span>Alt text</span>
4301
- <input type="text" data-ref="image-insert-alt" />
4302
- </label>
4303
- <div class="image-insert-grid">
4304
- <label class="image-insert-field">
4305
- <span>Size by</span>
4306
- <select data-ref="image-insert-size-mode">
4307
- <option value="original">Original size</option>
4308
- <option value="width" selected>Width</option>
4309
- <option value="height">Height</option>
4310
- <option value="percent">Percent</option>
4311
- </select>
4312
- </label>
4313
- <label class="image-insert-field">
4314
- <span>Value</span>
4315
- <input type="number" min="1" step="1" data-ref="image-insert-size-value" />
4316
- </label>
4317
- </div>
4318
- <label class="image-insert-field">
4319
- <span>Position</span>
4320
- <select data-ref="image-insert-position">
4321
- <option value="inline">Inline/default</option>
4322
- <option value="left">Left</option>
4323
- <option value="center">Center</option>
4324
- <option value="right">Right</option>
4325
- <option value="wrap-left">Wrap left</option>
4326
- <option value="wrap-right">Wrap right</option>
4327
- </select>
4328
- </label>
4329
- <div class="title-dialog-actions">
4330
- <button type="button" data-action="cancel-image-insert">Cancel</button>
4331
- <button type="button" class="save-title" data-ref="image-insert-confirm-btn">Insert</button>
4332
- </div>
4333
- </div>
4334
- </div>
4335
-
4336
- <div class="title-dialog-backdrop" data-ref="metadata-dialog" hidden
4337
- role="dialog" aria-modal="true" aria-labelledby="mdzip-metadata-dialog-heading">
4338
- <div class="title-dialog metadata-dialog">
4339
- <h3 id="mdzip-metadata-dialog-heading">Document Information</h3>
4340
- <dl data-ref="metadata-list"></dl>
4341
- <h4>Libraries</h4>
4342
- <dl data-ref="library-list"></dl>
4343
- <div class="title-dialog-actions">
4344
- <button type="button" class="save-title" data-action="close-metadata">Close</button>
4345
- </div>
4346
- </div>
4347
- </div>
4348
-
4349
- <div class="title-dialog-backdrop" data-ref="name-dialog" hidden
4350
- role="dialog" aria-modal="true" aria-labelledby="mdzip-name-dialog-heading">
4351
- <div class="title-dialog">
4352
- <h3 id="mdzip-name-dialog-heading" data-ref="name-dialog-heading">New Markdown File</h3>
4353
- <input type="text" maxlength="260" data-ref="name-input" aria-label="File name" />
4354
- <p class="title-dialog-validation" data-ref="name-validation" hidden></p>
4355
- <div class="title-dialog-actions">
4356
- <button type="button" data-action="cancel-name">Cancel</button>
4357
- <button type="button" class="save-title" data-ref="name-confirm-btn">Create</button>
4358
- </div>
4359
- </div>
4360
- </div>
4361
-
4362
- <div class="title-dialog-backdrop" data-ref="delete-dialog" hidden
4363
- role="dialog" aria-modal="true" aria-labelledby="mdzip-delete-dialog-heading">
4364
- <div class="title-dialog">
4365
- <h3 id="mdzip-delete-dialog-heading">Delete File?</h3>
4366
- <p data-ref="delete-dialog-text"></p>
4367
- <div class="title-dialog-actions">
4368
- <button type="button" data-action="cancel-delete">Cancel</button>
4369
- <button type="button" class="danger-action" data-ref="delete-confirm-btn">Delete</button>
4370
- </div>
4371
- </div>
4372
- </div>
4373
-
4374
- <input type="file" data-ref="replace-input" hidden />
4375
-
4376
- <div class="nav-context-menu" data-ref="nav-menu" hidden role="menu"></div>
4377
-
4378
- <div class="mdzip-tooltip" data-ref="tooltip" role="tooltip" hidden></div>
4379
-
4380
- <p class="mdzip-empty" data-ref="empty-state">No MDZip workspace loaded.</p>
4381
- </section>
4448
+ const SHELL_HTML = `
4449
+ <section class="mdzip-root">
4450
+ <div class="document-strip" data-ref="document-strip" hidden>
4451
+ <button type="button" class="title-button" data-ref="title-btn"></button>
4452
+ <button type="button" class="document-info-button" data-ref="document-info-btn"
4453
+ title="Document information" aria-label="Document information">
4454
+ ${INFO_ICON_HTML}
4455
+ </button>
4456
+ </div>
4457
+
4458
+ <header class="toolbar" data-ref="toolbar" hidden>
4459
+ <div class="toolbar-start">
4460
+ <div class="toolbar-left">
4461
+ <button type="button" class="icon-toggle nav-toggle" data-ref="nav-btn" title="Toggle contents" aria-label="Toggle contents">
4462
+ ${NAV_TOGGLE_ICON_HTML}
4463
+ </button>
4464
+ </div>
4465
+
4466
+ <div class="edit-toolbar" data-ref="edit-toolbar" role="toolbar" aria-label="Markdown formatting">
4467
+ <div class="edit-toolbar-group">
4468
+ <button type="button" data-format="bold" data-format-control="bold" title="Bold" aria-label="Bold">${BOLD_ICON_HTML}</button>
4469
+ <button type="button" data-format="italic" data-format-control="italic" title="Italic" aria-label="Italic">${ITALIC_ICON_HTML}</button>
4470
+ <button type="button" data-format="strike" data-format-control="strikethrough" title="Strikethrough" aria-label="Strikethrough">${STRIKE_ICON_HTML}</button>
4471
+ <div class="format-menu" data-format-control="headings">
4472
+ <button type="button" class="format-menu-toggle" data-format-menu-toggle
4473
+ aria-label="Heading" aria-haspopup="menu" aria-expanded="false">
4474
+ ${HEADING_ICON_HTML}${CHEVRON_ICON_HTML}
4475
+ </button>
4476
+ <div class="format-menu-popover" data-format-menu role="menu" aria-label="Heading level" hidden>
4477
+ <button type="button" role="menuitem" data-format="paragraph">Paragraph</button>
4478
+ <button type="button" role="menuitem" data-format="heading-1" data-heading-level="1"><strong>H1</strong> Heading 1</button>
4479
+ <button type="button" role="menuitem" data-format="heading-2" data-heading-level="2"><strong>H2</strong> Heading 2</button>
4480
+ <button type="button" role="menuitem" data-format="heading-3" data-heading-level="3"><strong>H3</strong> Heading 3</button>
4481
+ <button type="button" role="menuitem" data-format="heading-4" data-heading-level="4"><strong>H4</strong> Heading 4</button>
4482
+ <button type="button" role="menuitem" data-format="heading-5" data-heading-level="5"><strong>H5</strong> Heading 5</button>
4483
+ <button type="button" role="menuitem" data-format="heading-6" data-heading-level="6"><strong>H6</strong> Heading 6</button>
4484
+ </div>
4485
+ </div>
4486
+ </div>
4487
+ <span class="edit-toolbar-divider" aria-hidden="true"></span>
4488
+ <div class="edit-toolbar-group">
4489
+ <button type="button" data-format="bullet-list" data-format-control="bulletList" title="Bulleted list" aria-label="Bulleted list">${BULLET_LIST_ICON_HTML}</button>
4490
+ <button type="button" data-format="ordered-list" data-format-control="orderedList" title="Numbered list" aria-label="Numbered list">${ORDERED_LIST_ICON_HTML}</button>
4491
+ <div class="format-menu" data-format-control="code">
4492
+ <button type="button" class="format-menu-toggle" data-format-menu-toggle
4493
+ aria-label="Code" aria-haspopup="menu" aria-expanded="false">
4494
+ ${CODE_ICON_HTML}${CHEVRON_ICON_HTML}
4495
+ </button>
4496
+ <div class="format-menu-popover" data-format-menu role="menu" aria-label="Code format" hidden>
4497
+ <button type="button" role="menuitem" data-format="code" data-code-kind="inline">Inline code</button>
4498
+ <button type="button" role="menuitem" data-format="code-block" data-code-kind="block">Code block</button>
4499
+ </div>
4500
+ </div>
4501
+ <button type="button" data-format="quote" data-format-control="blockquote" title="Blockquote" aria-label="Blockquote">${QUOTE_ICON_HTML}</button>
4502
+ <button type="button" data-format="line-break" data-format-control="lineBreak" title="Line break" aria-label="Line break">${LINE_BREAK_ICON_HTML}</button>
4503
+ </div>
4504
+ <span class="edit-toolbar-divider" aria-hidden="true"></span>
4505
+ <div class="edit-toolbar-group">
4506
+ <button type="button" data-format="link" data-format-control="link" title="Link" aria-label="Link">${LINK_ICON_HTML}</button>
4507
+ <button type="button" data-format="image" data-format-control="image" title="Image" aria-label="Image">${IMAGE_FORMAT_ICON_HTML}</button>
4508
+ <input type="file" data-ref="image-input" accept="image/*" hidden />
4509
+ </div>
4510
+ </div>
4511
+ </div>
4512
+
4513
+ <div class="toolbar-buttons view-mode-toggle-group" data-ref="layout-controls" role="group" aria-label="Editor layout">
4514
+ <button type="button" class="icon-toggle view-mode-toggle" data-ref="source-btn" title="Edit" aria-label="Edit" aria-pressed="false">
4515
+ <span class="commandbar-flex-container" data-ref="source-icon">${SOURCE_EDIT_ICON_HTML}</span>
4516
+ </button>
4517
+ <button type="button" class="icon-toggle view-mode-toggle" data-ref="split-btn" title="Split" aria-label="Split" aria-pressed="false">
4518
+ <span class="commandbar-flex-container">
4519
+ ${SPLIT_ICON_HTML}
4520
+ </span>
4521
+ </button>
4522
+ <button type="button" class="icon-toggle view-mode-toggle" data-ref="preview-btn" title="View" aria-label="View" aria-pressed="false">
4523
+ <span class="commandbar-flex-container">
4524
+ ${PREVIEW_ICON_HTML}
4525
+ </span>
4526
+ </button>
4527
+ </div>
4528
+
4529
+ <div class="toolbar-controls" data-ref="toolbar-controls">
4530
+ <button type="button" class="icon-toggle" data-ref="search-btn" title="Find/replace (Mod-F)" aria-label="Find/replace">
4531
+ ${SEARCH_ICON_HTML}
4532
+ </button>
4533
+ <button type="button" class="icon-toggle" data-ref="save-btn" title="Save" aria-label="Save">
4534
+ ${SAVE_ICON_HTML}
4535
+ </button>
4536
+ <button type="button" class="icon-toggle zoom-toggle" data-ref="zoom-btn" title="Zoom controls" aria-label="Zoom controls">
4537
+ ${ZOOM_ICON_HTML}
4538
+ </button>
4539
+ <div class="theme-toggle-group" data-ref="theme-controls" role="group" aria-label="Color scheme">
4540
+ <button type="button" class="icon-toggle theme-toggle" data-ref="dark-theme-btn"
4541
+ title="Dark mode" aria-label="Dark mode" aria-pressed="false">
4542
+ ${DARK_THEME_ICON_HTML}
4543
+ </button>
4544
+ <button type="button" class="icon-toggle theme-toggle" data-ref="light-theme-btn"
4545
+ title="Light mode" aria-label="Light mode" aria-pressed="false">
4546
+ ${LIGHT_THEME_ICON_HTML}
4547
+ </button>
4548
+ </div>
4549
+ <div class="zoom-popover" data-ref="zoom-popover" hidden role="group" aria-label="Zoom">
4550
+ <span class="zoom-level" data-ref="zoom-level">100%</span>
4551
+ <span class="zoom-stepper">
4552
+ <button type="button" data-action="zoom-out" title="Zoom out" aria-label="Zoom out">-</button>
4553
+ <button type="button" data-action="zoom-in" title="Zoom in" aria-label="Zoom in">+</button>
4554
+ </span>
4555
+ <button type="button" class="zoom-reset" data-action="zoom-reset" title="Reset zoom">Reset</button>
4556
+ </div>
4557
+ </div>
4558
+ </header>
4559
+
4560
+ <div class="workspace-shell" data-ref="workspace-shell" hidden>
4561
+ <aside class="nav-pane" data-ref="nav-pane" aria-label="Package contents">
4562
+ <div class="nav-tree" data-ref="nav-tree"></div>
4563
+ </aside>
4564
+ <div class="nav-resizer" data-ref="nav-resizer"
4565
+ role="separator" aria-orientation="vertical" aria-label="Resize contents pane"></div>
4566
+
4567
+ <div class="pane-stack" data-ref="pane-stack">
4568
+ <section class="pane edit-pane" data-ref="edit-pane">
4569
+ <div class="editor-host" data-ref="editor-host"></div>
4570
+ </section>
4571
+ <div class="split-resizer" data-ref="split-resizer"
4572
+ role="separator" aria-orientation="vertical" aria-label="Resize split panes"></div>
4573
+ <section class="pane preview-pane" data-ref="preview-pane">
4574
+ <article class="preview-content" data-ref="preview-content"></article>
4575
+ </section>
4576
+ <section class="pane entry-pane" data-ref="entry-pane"></section>
4577
+ </div>
4578
+ </div>
4579
+
4580
+ <div class="title-dialog-backdrop" data-ref="title-dialog" hidden
4581
+ role="dialog" aria-modal="true" aria-labelledby="mdzip-title-dialog-heading">
4582
+ <div class="title-dialog">
4583
+ <h3 id="mdzip-title-dialog-heading">Set Document Title</h3>
4584
+ <p>This is the package-level title stored in manifest.json.</p>
4585
+ <input type="text" maxlength="120" data-ref="title-input" aria-label="Document title" />
4586
+ <p class="title-dialog-validation" data-ref="title-validation" hidden>Title cannot be empty.</p>
4587
+ <p>If unset, consumers may fall back to entry point, filename, or first heading.</p>
4588
+ <div class="title-dialog-actions">
4589
+ <button type="button" class="reset-title" data-ref="title-reset-btn">Reset</button>
4590
+ <button type="button" data-action="cancel-title">Cancel</button>
4591
+ <button type="button" class="save-title" data-ref="title-save-btn">Save</button>
4592
+ </div>
4593
+ </div>
4594
+ </div>
4595
+
4596
+ <div class="title-dialog-backdrop" data-ref="conversion-dialog" hidden
4597
+ role="dialog" aria-modal="true" aria-labelledby="mdzip-conversion-dialog-heading">
4598
+ <div class="title-dialog">
4599
+ <h3 id="mdzip-conversion-dialog-heading">Convert to MDZ?</h3>
4600
+ <p>
4601
+ Regular Markdown files contain only text. Convert this document to an
4602
+ MDZ package to add images, package navigation, and document metadata.
4603
+ </p>
4604
+ <p>The next save will produce an .mdz file.</p>
4605
+ <div class="title-dialog-actions">
4606
+ <button type="button" data-action="cancel-conversion">Cancel</button>
4607
+ <button type="button" class="save-title" data-ref="conversion-confirm-btn">Convert</button>
4608
+ </div>
4609
+ </div>
4610
+ </div>
4611
+
4612
+ <div class="title-dialog-backdrop" data-ref="image-insert-dialog" hidden
4613
+ role="dialog" aria-modal="true" aria-labelledby="mdzip-image-insert-dialog-heading">
4614
+ <div class="title-dialog image-insert-dialog">
4615
+ <h3 id="mdzip-image-insert-dialog-heading">Insert Image</h3>
4616
+ <p>Choose the Markdown inserted for this image.</p>
4617
+ <fieldset class="image-insert-options">
4618
+ <legend>Output</legend>
4619
+ <label>
4620
+ <input type="radio" name="mdzip-image-insert-mode" value="markdown"
4621
+ data-ref="image-insert-mode-markdown" checked />
4622
+ Markdown image
4623
+ </label>
4624
+ <label>
4625
+ <input type="radio" name="mdzip-image-insert-mode" value="html"
4626
+ data-ref="image-insert-mode-html" />
4627
+ HTML &lt;img&gt; with sizing
4628
+ </label>
4629
+ </fieldset>
4630
+ <label class="image-insert-field">
4631
+ <span>Alt text</span>
4632
+ <input type="text" data-ref="image-insert-alt" />
4633
+ </label>
4634
+ <div class="image-insert-grid">
4635
+ <label class="image-insert-field">
4636
+ <span>Size by</span>
4637
+ <select data-ref="image-insert-size-mode">
4638
+ <option value="original">Original size</option>
4639
+ <option value="width" selected>Width</option>
4640
+ <option value="height">Height</option>
4641
+ <option value="percent">Percent</option>
4642
+ </select>
4643
+ </label>
4644
+ <label class="image-insert-field">
4645
+ <span>Value</span>
4646
+ <input type="number" min="1" step="1" data-ref="image-insert-size-value" />
4647
+ </label>
4648
+ </div>
4649
+ <label class="image-insert-field">
4650
+ <span>Position</span>
4651
+ <select data-ref="image-insert-position">
4652
+ <option value="inline">Inline/default</option>
4653
+ <option value="left">Left</option>
4654
+ <option value="center">Center</option>
4655
+ <option value="right">Right</option>
4656
+ <option value="wrap-left">Wrap left</option>
4657
+ <option value="wrap-right">Wrap right</option>
4658
+ </select>
4659
+ </label>
4660
+ <div class="title-dialog-actions">
4661
+ <button type="button" data-action="cancel-image-insert">Cancel</button>
4662
+ <button type="button" class="save-title" data-ref="image-insert-confirm-btn">Insert</button>
4663
+ </div>
4664
+ </div>
4665
+ </div>
4666
+
4667
+ <div class="title-dialog-backdrop" data-ref="metadata-dialog" hidden
4668
+ role="dialog" aria-modal="true" aria-labelledby="mdzip-metadata-dialog-heading">
4669
+ <div class="title-dialog metadata-dialog">
4670
+ <h3 id="mdzip-metadata-dialog-heading">Document Information</h3>
4671
+ <dl data-ref="metadata-list"></dl>
4672
+ <h4>Libraries</h4>
4673
+ <dl data-ref="library-list"></dl>
4674
+ <div class="title-dialog-actions">
4675
+ <button type="button" class="save-title" data-action="close-metadata">Close</button>
4676
+ </div>
4677
+ </div>
4678
+ </div>
4679
+
4680
+ <div class="title-dialog-backdrop" data-ref="name-dialog" hidden
4681
+ role="dialog" aria-modal="true" aria-labelledby="mdzip-name-dialog-heading">
4682
+ <div class="title-dialog">
4683
+ <h3 id="mdzip-name-dialog-heading" data-ref="name-dialog-heading">New Markdown File</h3>
4684
+ <input type="text" maxlength="260" data-ref="name-input" aria-label="File name" />
4685
+ <p class="title-dialog-validation" data-ref="name-validation" hidden></p>
4686
+ <div class="title-dialog-actions">
4687
+ <button type="button" data-action="cancel-name">Cancel</button>
4688
+ <button type="button" class="save-title" data-ref="name-confirm-btn">Create</button>
4689
+ </div>
4690
+ </div>
4691
+ </div>
4692
+
4693
+ <div class="title-dialog-backdrop" data-ref="delete-dialog" hidden
4694
+ role="dialog" aria-modal="true" aria-labelledby="mdzip-delete-dialog-heading">
4695
+ <div class="title-dialog">
4696
+ <h3 id="mdzip-delete-dialog-heading">Delete File?</h3>
4697
+ <p data-ref="delete-dialog-text"></p>
4698
+ <div class="title-dialog-actions">
4699
+ <button type="button" data-action="cancel-delete">Cancel</button>
4700
+ <button type="button" class="danger-action" data-ref="delete-confirm-btn">Delete</button>
4701
+ </div>
4702
+ </div>
4703
+ </div>
4704
+
4705
+ <input type="file" data-ref="replace-input" hidden />
4706
+
4707
+ <div class="nav-context-menu" data-ref="nav-menu" hidden role="menu"></div>
4708
+
4709
+ <div class="mdzip-tooltip" data-ref="tooltip" role="tooltip" hidden></div>
4710
+
4711
+ <p class="mdzip-empty" data-ref="empty-state">No MDZip workspace loaded.</p>
4712
+ </section>
4382
4713
  `;
4383
4714
  //# sourceMappingURL=view.js.map