@mdzip/editor 1.3.11 → 1.3.12

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
@@ -2,11 +2,11 @@ import { defaultKeymap, history, historyKeymap, indentWithTab } from '@codemirro
2
2
  import { markdown } from '@codemirror/lang-markdown';
3
3
  import { HighlightStyle, syntaxHighlighting } from '@codemirror/language';
4
4
  import { Compartment, EditorState } from '@codemirror/state';
5
- import { EditorView, dropCursor, keymap, lineNumbers } from '@codemirror/view';
5
+ import { Decoration, EditorView, MatchDecorator, ViewPlugin, dropCursor, keymap, lineNumbers } from '@codemirror/view';
6
6
  import { tags } from '@lezer/highlight';
7
- import { Bold, ChevronDown, Code, Columns2, Eye, File, FileBraces, FileImage, Folder, FolderOpen, Hash, Heading1, ImagePlus, Info, Italic, Link2Off, Link, List, ListOrdered, Moon, PanelLeft, Quote, Save, SquarePen, Strikethrough, Sun, ZoomIn } from 'lucide';
7
+ import { Bold, ChevronDown, Code, Columns2, Eye, File, FileBraces, FileImage, Folder, FolderOpen, Hash, Heading1, ImagePlus, Info, Italic, Link2Off, Link, List, ListOrdered, CornerDownLeft, Moon, PackagePlus, PanelLeft, Quote, Save, SquarePen, Strikethrough, Sun, ZoomIn } from 'lucide';
8
8
  import { browserClipboardHasImage, readBrowserClipboardImage } from './browser.js';
9
- import { MdzipAssetSession, mdzipArchiveSourceId } from './asset-cache.js';
9
+ import { MdzipAssetSession, mdzipArchiveSourceId, sniffImageSize } from './asset-cache.js';
10
10
  import { MD_MARKDOWN_ICON } from './icons/md-markdown.js';
11
11
  import { MDZIP_RUNTIME_LIBRARIES } from './library-info.js';
12
12
  import { MdzipWorkspaceService, extensionForMime, normalizeArchivePath, relativeArchivePath } from './workspace.js';
@@ -31,6 +31,7 @@ const ORPHAN_ICON_HTML = lucideIcon(Link2Off, '');
31
31
  const SOURCE_EDIT_ICON_HTML = lucideIcon(SquarePen, TOOLBAR_ICON_CLASS);
32
32
  const SOURCE_MARKDOWN_ICON_HTML = lucideIcon(Hash, TOOLBAR_ICON_CLASS);
33
33
  const NAV_TOGGLE_ICON_HTML = lucideIcon(PanelLeft, `${TOOLBAR_ICON_CLASS} nav-toggle-icon`);
34
+ const CONVERT_TO_MDZ_ICON_HTML = lucideIcon(PackagePlus, `${TOOLBAR_ICON_CLASS} convert-mdz-icon`);
34
35
  const PREVIEW_ICON_HTML = lucideIcon(Eye, TOOLBAR_ICON_CLASS);
35
36
  const SPLIT_ICON_HTML = lucideIcon(Columns2, TOOLBAR_ICON_CLASS);
36
37
  const SAVE_ICON_HTML = lucideIcon(Save, TOOLBAR_ICON_CLASS);
@@ -46,6 +47,7 @@ const BULLET_LIST_ICON_HTML = lucideIcon(List, FORMAT_ICON_CLASS);
46
47
  const ORDERED_LIST_ICON_HTML = lucideIcon(ListOrdered, FORMAT_ICON_CLASS);
47
48
  const CODE_ICON_HTML = lucideIcon(Code, FORMAT_ICON_CLASS);
48
49
  const QUOTE_ICON_HTML = lucideIcon(Quote, FORMAT_ICON_CLASS);
50
+ const LINE_BREAK_ICON_HTML = lucideIcon(CornerDownLeft, FORMAT_ICON_CLASS);
49
51
  const LINK_ICON_HTML = lucideIcon(Link, FORMAT_ICON_CLASS);
50
52
  const IMAGE_FORMAT_ICON_HTML = lucideIcon(ImagePlus, FORMAT_ICON_CLASS);
51
53
  const CHEVRON_ICON_HTML = lucideIcon(ChevronDown, 'format-chevron');
@@ -66,6 +68,7 @@ const ALL_FORMATTING_CONTROLS = {
66
68
  inlineCode: true,
67
69
  codeBlock: true,
68
70
  blockquote: true,
71
+ lineBreak: true,
69
72
  link: true,
70
73
  image: true
71
74
  };
@@ -79,6 +82,7 @@ const NO_FORMATTING_CONTROLS = {
79
82
  inlineCode: false,
80
83
  codeBlock: false,
81
84
  blockquote: false,
85
+ lineBreak: false,
82
86
  link: false,
83
87
  image: false
84
88
  };
@@ -228,6 +232,12 @@ function resolveFormattingControls(base, override) {
228
232
  : [...new Set(headings)].filter((level) => ALL_HEADINGS.includes(level)).sort()
229
233
  };
230
234
  }
235
+ function normalizeToolbarDensity(value) {
236
+ return value === 'compact' || value === 'dense' ? value : 'comfortable';
237
+ }
238
+ function normalizeContentDensity(value) {
239
+ return value === 'compact' ? value : 'comfortable';
240
+ }
231
241
  const mdzipEditorTheme = EditorView.theme({
232
242
  '&': {
233
243
  height: '100%',
@@ -242,7 +252,7 @@ const mdzipEditorTheme = EditorView.theme({
242
252
  overflow: 'auto',
243
253
  },
244
254
  '.cm-content': {
245
- padding: '36px 48px',
255
+ padding: 'var(--mdzip-editor-content-padding, var(--mdzip-density-editor-content-padding, 36px 48px))',
246
256
  caretColor: 'var(--mdzip-editor-cursor-color)',
247
257
  overflowWrap: 'anywhere',
248
258
  wordBreak: 'normal',
@@ -273,6 +283,10 @@ const mdzipEditorTheme = EditorView.theme({
273
283
  '.cm-activeLineGutter': {
274
284
  background: 'transparent',
275
285
  },
286
+ '.mdzip-hard-break-marker': {
287
+ color: 'var(--mdzip-muted-foreground-color)',
288
+ opacity: '0.65',
289
+ },
276
290
  });
277
291
  const mdzipMarkdownHighlight = HighlightStyle.define([
278
292
  { tag: [tags.heading1, tags.heading2, tags.heading3, tags.heading4, tags.heading5, tags.heading6],
@@ -288,6 +302,22 @@ const mdzipMarkdownHighlight = HighlightStyle.define([
288
302
  { tag: tags.contentSeparator, color: '#6a9955' },
289
303
  { tag: tags.atom, color: '#d100d1' },
290
304
  ]);
305
+ const hardBreakMarkerMatcher = new MatchDecorator({
306
+ regexp: /<br\s*\/?>/gi,
307
+ decoration: Decoration.mark({ class: 'mdzip-hard-break-marker' })
308
+ });
309
+ const hardBreakMarkerHighlight = ViewPlugin.fromClass(class {
310
+ constructor(view) {
311
+ this.decorations = hardBreakMarkerMatcher.createDeco(view);
312
+ }
313
+ update(update) {
314
+ if (update.docChanged || update.viewportChanged) {
315
+ this.decorations = hardBreakMarkerMatcher.updateDeco(update, this.decorations);
316
+ }
317
+ }
318
+ }, {
319
+ decorations: value => value.decorations
320
+ });
291
321
  function injectStyles(doc) {
292
322
  const existing = doc.querySelector(`style[${STYLE_ATTR}]`);
293
323
  if (existing) {
@@ -312,7 +342,27 @@ function attributesToHtml(attrs) {
312
342
  .map(([key, value]) => ` ${key}="${escapeHtml(String(value))}"`)
313
343
  .join('');
314
344
  }
315
- function renderNavNode(node, state, options) {
345
+ // Renders the per-row indent/guide cells: one fixed-width cell per ancestor
346
+ // depth. An ancestor column draws a continuous vertical rail when that folder
347
+ // still has siblings below it (so the spine keeps going), otherwise it is
348
+ // blank. The final cell connects to this row with an elbow (└, last child) or a
349
+ // tee (├). Painting per row at the row's own height means the guides never
350
+ // overshoot an expanded last subfolder the way a single container rail did.
351
+ function renderNavGuides(trail, isLast) {
352
+ const cells = trail.map((continues) => `<span class="nav-indent${continues ? ' nav-indent-rail' : ''}"></span>`);
353
+ cells.push(`<span class="nav-indent nav-indent-connector${isLast ? '' : ' nav-indent-continues'}"></span>`);
354
+ return cells.join('');
355
+ }
356
+ function renderNavNode(node, state, options,
357
+ // Per-ancestor flags: whether each ancestor column still has a sibling below
358
+ // it (so its rail continues through this row). One entry per guide column
359
+ // above the immediate connector.
360
+ trail = [],
361
+ // Whether this node is the last among its siblings (elbow vs tee connector).
362
+ isLast = true,
363
+ // Nesting depth; depth 0 (root entries) draws no guides.
364
+ depth = 0) {
365
+ const guides = depth === 0 ? '' : renderNavGuides(trail, isLast);
316
366
  if (node.entry) {
317
367
  const isCurrent = node.entry.path === state.currentPath;
318
368
  const isOrphaned = isOrphanedMdzipAsset(node.entry, state);
@@ -348,17 +398,20 @@ function renderNavNode(node, state, options) {
348
398
  const draggable = options.allowDrag && !isManifest;
349
399
  return `<button type="button" class="${classes}" title="${title}"
350
400
  data-nav-path="${safePath}" data-orphan="${isOrphaned ? 'true' : ''}"${draggable ? ' draggable="true"' : ''}>
351
- <span class="nav-caret"></span>
401
+ ${guides}<span class="nav-caret"></span>
352
402
  <span class="nav-file-icon ${iconKind}">${iconHtml}</span>
353
403
  ${orphanBtnHtml}
354
404
  <span class="nav-label">${safeName}</span>
355
405
  </button>`;
356
406
  }
357
- const children = node.children.map(c => renderNavNode(c, state, options)).join('');
407
+ const childTrail = depth === 0 ? [] : [...trail, !isLast];
408
+ const children = node.children
409
+ .map((child, index) => renderNavNode(child, state, options, childTrail, index === node.children.length - 1, depth + 1))
410
+ .join('');
358
411
  const pending = options.pendingFolders.has(node.path.toLowerCase());
359
412
  return `<details class="nav-directory${pending ? ' pending-folder' : ''}" open data-nav-dir="${escapeHtml(node.path)}">
360
413
  <summary${pending ? ` title="${escapeHtml(node.path)} — not saved until it contains a file"` : ''}>
361
- <span class="nav-caret" aria-hidden="true"></span>
414
+ ${guides}<span class="nav-caret" aria-hidden="true"></span>
362
415
  <span class="nav-folder-icon closed">${FOLDER_CLOSED_ICON_HTML}</span>
363
416
  <span class="nav-folder-icon open">${FOLDER_OPEN_ICON_HTML}</span>
364
417
  <span class="nav-label">${escapeHtml(node.name)}</span>
@@ -412,6 +465,7 @@ export class MdzipWorkspaceView {
412
465
  this.metadataDialogOpen = false;
413
466
  this.titleDraft = '';
414
467
  this.conversionAction = null;
468
+ this.imageInsertDialogState = null;
415
469
  this.navPaneWidth = 280;
416
470
  this.splitRatio = 0.5;
417
471
  this.resizing = false;
@@ -434,6 +488,7 @@ export class MdzipWorkspaceView {
434
488
  this.tooltipHideTimer = null;
435
489
  this.cmEditor = null;
436
490
  this.readOnlyCompartment = new Compartment();
491
+ this.lineNumbersCompartment = new Compartment();
437
492
  this.updatingCm = false;
438
493
  this.syncing = false;
439
494
  this.markdownExtensions = [];
@@ -458,6 +513,9 @@ export class MdzipWorkspaceView {
458
513
  this.options = options;
459
514
  this.controlPolicy = resolveMdzipControlPolicy(options.controls);
460
515
  this.navigationMode = options.navigationMode ?? 'editor';
516
+ this.imageHydrationAnimation = options.imageHydrationAnimation ?? 'auto';
517
+ this.toolbarDensity = options.toolbarDensity ?? 'comfortable';
518
+ this.contentDensity = options.contentDensity ?? 'comfortable';
461
519
  this.layout = options.initialLayout ?? defaultLayoutForPolicy(this.controlPolicy);
462
520
  this.colorScheme = options.initialColorScheme
463
521
  ?? (container.ownerDocument.defaultView?.matchMedia('(prefers-color-scheme: dark)').matches
@@ -473,6 +531,7 @@ export class MdzipWorkspaceView {
473
531
  container.innerHTML = SHELL_HTML;
474
532
  const q = (sel) => container.querySelector(sel);
475
533
  this.elRoot = q('.mdzip-root');
534
+ this.applyDensityClasses();
476
535
  this.elDocumentStrip = q('[data-ref="document-strip"]');
477
536
  this.elToolbar = q('[data-ref="toolbar"]');
478
537
  this.elToolbarLeft = q('.toolbar-left');
@@ -515,6 +574,14 @@ export class MdzipWorkspaceView {
515
574
  this.elLibraryList = q('[data-ref="library-list"]');
516
575
  this.elConversionDialog = q('[data-ref="conversion-dialog"]');
517
576
  this.elConversionConfirmBtn = q('[data-ref="conversion-confirm-btn"]');
577
+ this.elImageInsertDialog = q('[data-ref="image-insert-dialog"]');
578
+ this.elImageInsertModeMarkdown = q('[data-ref="image-insert-mode-markdown"]');
579
+ this.elImageInsertModeHtml = q('[data-ref="image-insert-mode-html"]');
580
+ this.elImageInsertAltInput = q('[data-ref="image-insert-alt"]');
581
+ this.elImageInsertSizeModeSelect = q('[data-ref="image-insert-size-mode"]');
582
+ this.elImageInsertSizeValueInput = q('[data-ref="image-insert-size-value"]');
583
+ this.elImageInsertPositionSelect = q('[data-ref="image-insert-position"]');
584
+ this.elImageInsertConfirmBtn = q('[data-ref="image-insert-confirm-btn"]');
518
585
  this.elNavMenu = q('[data-ref="nav-menu"]');
519
586
  this.elNameDialog = q('[data-ref="name-dialog"]');
520
587
  this.elNameDialogHeading = q('[data-ref="name-dialog-heading"]');
@@ -731,12 +798,12 @@ export class MdzipWorkspaceView {
731
798
  if (command === 'insert-image') {
732
799
  if (this.workspace?.sourceFormat === 'markdown') {
733
800
  this.requestMdzConversion(file
734
- ? { kind: 'image-file', file }
801
+ ? { kind: 'image-file', file, source: 'picker' }
735
802
  : { kind: 'image-picker' });
736
803
  return true;
737
804
  }
738
805
  if (file) {
739
- await this.insertImageFile(file);
806
+ await this.insertImageFile(file, 'picker');
740
807
  }
741
808
  else {
742
809
  this.elImageInput.click();
@@ -765,6 +832,7 @@ export class MdzipWorkspaceView {
765
832
  }
766
833
  destroy() {
767
834
  this.conversionDocumentGeneration += 1;
835
+ this.resolveImageInsertDialog(null);
768
836
  try {
769
837
  this.unsub?.();
770
838
  }
@@ -811,6 +879,55 @@ export class MdzipWorkspaceView {
811
879
  this.entryMatchMissKey = null;
812
880
  this.render();
813
881
  }
882
+ /**
883
+ * Replaces the view control policy without recreating the workspace or
884
+ * CodeMirror editor. In particular, `lineNumbers` is reconfigured through a
885
+ * CodeMirror compartment so text, selection, focus, scroll, and undo history
886
+ * stay intact.
887
+ */
888
+ setControls(controls) {
889
+ const next = resolveMdzipControlPolicy(controls);
890
+ const lineNumbersChanged = next.lineNumbers !== this.controlPolicy.lineNumbers;
891
+ this.controlPolicy = next;
892
+ if (lineNumbersChanged && this.cmEditor) {
893
+ this.cmEditor.dispatch({
894
+ effects: this.lineNumbersCompartment.reconfigure(next.lineNumbers ? lineNumbers() : [])
895
+ });
896
+ }
897
+ const snapshot = this.workspace?.snapshot();
898
+ if (snapshot) {
899
+ this.layout = this.validLayoutForSnapshot(this.layout, snapshot);
900
+ }
901
+ this.render();
902
+ }
903
+ setImageHydrationAnimation(animation) {
904
+ const next = animation ?? 'auto';
905
+ if (next === this.imageHydrationAnimation) {
906
+ return;
907
+ }
908
+ this.imageHydrationAnimation = next;
909
+ this.resetPreviewState();
910
+ this.render();
911
+ }
912
+ setDensityOptions(options) {
913
+ const nextToolbarDensity = normalizeToolbarDensity(options.toolbarDensity);
914
+ const nextContentDensity = normalizeContentDensity(options.contentDensity);
915
+ if (nextToolbarDensity === this.toolbarDensity && nextContentDensity === this.contentDensity) {
916
+ return;
917
+ }
918
+ this.toolbarDensity = nextToolbarDensity;
919
+ this.contentDensity = nextContentDensity;
920
+ this.applyDensityClasses();
921
+ }
922
+ setImageInsertOptions(options) {
923
+ this.options.imageInsertMode = options.imageInsertMode;
924
+ this.options.imageInsertHandler = options.imageInsertHandler;
925
+ }
926
+ applyDensityClasses() {
927
+ this.elRoot.classList.remove('toolbar-density-comfortable', 'toolbar-density-compact', 'toolbar-density-dense', 'content-density-comfortable', 'content-density-compact');
928
+ this.elRoot.classList.add(`toolbar-density-${this.toolbarDensity}`);
929
+ this.elRoot.classList.add(`content-density-${this.contentDensity}`);
930
+ }
814
931
  /**
815
932
  * Tears down all custom rendering state: aborts in-flight renders, destroys
816
933
  * mounted extension and entry renderer handles, and clears memo caches.
@@ -868,6 +985,7 @@ export class MdzipWorkspaceView {
868
985
  // mounted extension handles.
869
986
  return;
870
987
  }
988
+ const animateImageHydration = this.shouldAnimateImageHydration(memo, snapshot);
871
989
  this.previewAbort?.abort();
872
990
  this.previewAbort = null;
873
991
  this.destroyPreviewHandles();
@@ -916,14 +1034,14 @@ export class MdzipWorkspaceView {
916
1034
  }
917
1035
  if (typeof result === 'string') {
918
1036
  // Sync fast path: no microtask hop when every pipeline stage is sync.
919
- this.applyPreviewHtml(result, snapshot, context, generation);
1037
+ this.applyPreviewHtml(result, snapshot, context, generation, animateImageHydration);
920
1038
  return;
921
1039
  }
922
1040
  void result.then((html) => {
923
1041
  if (generation !== this.previewGeneration || abort.signal.aborted) {
924
1042
  return; // Stale: the selection or content moved on while rendering.
925
1043
  }
926
- this.applyPreviewHtml(html, snapshot, context, generation);
1044
+ this.applyPreviewHtml(html, snapshot, context, generation, animateImageHydration);
927
1045
  }).catch((error) => {
928
1046
  if (generation !== this.previewGeneration || abort.signal.aborted) {
929
1047
  return;
@@ -933,17 +1051,28 @@ export class MdzipWorkspaceView {
933
1051
  }
934
1052
  });
935
1053
  }
936
- applyPreviewHtml(html, snapshot, context, generation) {
1054
+ applyPreviewHtml(html, snapshot, context, generation, animateImageHydration) {
937
1055
  // When the preview references archive images, mount the text immediately
938
1056
  // and hydrate each image progressively (reserving layout space from its
939
1057
  // sniffed intrinsic size), rather than blocking the whole preview on image
940
1058
  // resolution. Other markdown mounts synchronously.
941
1059
  if (this.assetSession && /<img\b/i.test(html)) {
942
- this.mountProgressivePreview(html, snapshot, context, generation);
1060
+ this.mountProgressivePreview(html, snapshot, context, generation, animateImageHydration);
943
1061
  return;
944
1062
  }
945
1063
  this.mountPreviewHtml(html, snapshot, context, generation);
946
1064
  }
1065
+ shouldAnimateImageHydration(previousMemo, snapshot) {
1066
+ if (this.imageHydrationAnimation === 'off') {
1067
+ return false;
1068
+ }
1069
+ if (this.imageHydrationAnimation === 'auto') {
1070
+ return true;
1071
+ }
1072
+ return !previousMemo
1073
+ || previousMemo.path !== snapshot.currentPath
1074
+ || previousMemo.pathType !== snapshot.currentPathType;
1075
+ }
947
1076
  mountPreviewHtml(html, snapshot, context, generation) {
948
1077
  this.elPreviewContent.innerHTML = html;
949
1078
  this.mountPreviewExtensions(context, generation);
@@ -957,7 +1086,7 @@ export class MdzipWorkspaceView {
957
1086
  * text is in the DOM; `onAssetsHydrated` fires once every referenced image
958
1087
  * has resolved and had its final `src` assigned (or there are none).
959
1088
  */
960
- mountProgressivePreview(html, snapshot, context, generation) {
1089
+ mountProgressivePreview(html, snapshot, context, generation, animateImageHydration) {
961
1090
  this.elPreviewContent.innerHTML = html;
962
1091
  const session = this.assetSession;
963
1092
  const document = this.elPreviewContent.ownerDocument;
@@ -973,9 +1102,20 @@ export class MdzipWorkspaceView {
973
1102
  // immediately readable; the slot eases open to the reserved height once
974
1103
  // the image resolves.
975
1104
  image.removeAttribute('src');
976
- image.classList.add('mdzip-image-loading');
1105
+ if (animateImageHydration) {
1106
+ image.classList.add('mdzip-image-loading');
1107
+ }
977
1108
  const slot = document.createElement('span');
978
- slot.className = 'mdzip-image-slot';
1109
+ slot.className = animateImageHydration
1110
+ ? 'mdzip-image-slot'
1111
+ : 'mdzip-image-slot mdzip-image-open mdzip-image-animation-off';
1112
+ const align = image.getAttribute('align')?.toLowerCase();
1113
+ if (align === 'left' || image.classList.contains('mdzip-image-wrap-left')) {
1114
+ slot.classList.add('mdzip-image-align-left');
1115
+ }
1116
+ else if (align === 'right' || image.classList.contains('mdzip-image-wrap-right')) {
1117
+ slot.classList.add('mdzip-image-align-right');
1118
+ }
979
1119
  image.parentNode?.insertBefore(slot, image);
980
1120
  slot.appendChild(image);
981
1121
  pending.push({ image, slot, source });
@@ -1008,7 +1148,8 @@ export class MdzipWorkspaceView {
1008
1148
  // Size the reserved box from the sniffed dimensions so the slot eases
1009
1149
  // open to the image's exact height in a single slide — and the pixels
1010
1150
  // drop into an already-correct box with no further reflow.
1011
- if (resolved.width && resolved.height) {
1151
+ if (resolved.width && resolved.height
1152
+ && !image.hasAttribute('width') && !image.hasAttribute('height')) {
1012
1153
  image.setAttribute('width', String(resolved.width));
1013
1154
  image.setAttribute('height', String(resolved.height));
1014
1155
  }
@@ -1033,6 +1174,10 @@ export class MdzipWorkspaceView {
1033
1174
  * under `prefers-reduced-motion`.
1034
1175
  */
1035
1176
  openImageSlot(slot) {
1177
+ if (this.imageHydrationAnimation === 'off' || slot.classList.contains('mdzip-image-animation-off')) {
1178
+ slot.classList.add('mdzip-image-open');
1179
+ return;
1180
+ }
1036
1181
  void slot.offsetHeight;
1037
1182
  slot.classList.add('mdzip-image-open');
1038
1183
  }
@@ -1339,11 +1484,12 @@ export class MdzipWorkspaceView {
1339
1484
  const state = EditorState.create({
1340
1485
  doc: initialText,
1341
1486
  extensions: [
1342
- ...(this.controlPolicy.lineNumbers ? [lineNumbers()] : []),
1487
+ this.lineNumbersCompartment.of(this.controlPolicy.lineNumbers ? lineNumbers() : []),
1343
1488
  history(),
1344
1489
  keymap.of([...defaultKeymap, ...historyKeymap, indentWithTab]),
1345
1490
  markdown(),
1346
1491
  syntaxHighlighting(mdzipMarkdownHighlight),
1492
+ hardBreakMarkerHighlight,
1347
1493
  EditorView.lineWrapping,
1348
1494
  dropCursor(),
1349
1495
  mdzipEditorTheme,
@@ -1499,8 +1645,24 @@ export class MdzipWorkspaceView {
1499
1645
  this.elPreviewBtn.setAttribute('aria-pressed', String(this.layout === 'preview'));
1500
1646
  this.elSplitBtn.setAttribute('aria-pressed', String(this.layout === 'split'));
1501
1647
  this.elSourceBtn.setAttribute('aria-pressed', String(this.layout === 'source'));
1502
- this.elNavBtn.classList.toggle('active', this.navVisible);
1503
- this.elNavBtn.setAttribute('aria-pressed', String(this.navVisible));
1648
+ if (snapshot.sourceFormat === 'markdown') {
1649
+ this.elNavBtn.innerHTML = CONVERT_TO_MDZ_ICON_HTML;
1650
+ this.elNavBtn.classList.remove('active');
1651
+ this.elNavBtn.classList.add('convert-mdz-toggle');
1652
+ this.elNavBtn.setAttribute('title', 'Convert to MDZ');
1653
+ this.elNavBtn.setAttribute('aria-label', 'Convert to MDZ');
1654
+ this.elNavBtn.dataset['tooltip'] = 'Convert to MDZ';
1655
+ this.elNavBtn.removeAttribute('aria-pressed');
1656
+ }
1657
+ else {
1658
+ this.elNavBtn.innerHTML = NAV_TOGGLE_ICON_HTML;
1659
+ this.elNavBtn.classList.remove('convert-mdz-toggle');
1660
+ this.elNavBtn.classList.toggle('active', this.navVisible);
1661
+ this.elNavBtn.setAttribute('title', 'Toggle contents');
1662
+ this.elNavBtn.setAttribute('aria-label', 'Toggle contents');
1663
+ this.elNavBtn.dataset['tooltip'] = 'Toggle contents';
1664
+ this.elNavBtn.setAttribute('aria-pressed', String(this.navVisible));
1665
+ }
1504
1666
  this.elZoomBtn.classList.toggle('active', this.zoomOpen);
1505
1667
  this.elDarkThemeBtn.classList.toggle('active', this.colorScheme === 'dark');
1506
1668
  this.elLightThemeBtn.classList.toggle('active', this.colorScheme === 'light');
@@ -1517,7 +1679,7 @@ export class MdzipWorkspaceView {
1517
1679
  this.elNavResizer.classList.toggle('hidden', !showNavigationPane);
1518
1680
  this.prunePendingFolders(snapshot);
1519
1681
  const navTree = snapshot.sourceFormat === 'mdz'
1520
- ? mergePendingFolders(buildMdzipNavTree(snapshot.content.paths), this.pendingNewFolders)
1682
+ ? mergePendingFolders(buildMdzipNavTree(snapshot.content.paths, snapshot.content.entryPoint), this.pendingNewFolders)
1521
1683
  : [];
1522
1684
  const allowOrphanActions = this.controlPolicy.orphanActions && snapshot.mode !== 'read-only';
1523
1685
  const allowFileActions = this.allowFileActions(snapshot);
@@ -1527,7 +1689,9 @@ export class MdzipWorkspaceView {
1527
1689
  allowDrag: snapshot.mode !== 'read-only' && snapshot.sourceFormat === 'mdz',
1528
1690
  pendingFolders: new Set([...this.pendingNewFolders].map((path) => path.toLowerCase()))
1529
1691
  };
1530
- this.elNavTree.innerHTML = navTree.map(n => renderNavNode(n, snapshot, navRenderOptions)).join('');
1692
+ this.elNavTree.innerHTML = navTree
1693
+ .map((n, i) => renderNavNode(n, snapshot, navRenderOptions, [], i === navTree.length - 1, 0))
1694
+ .join('');
1531
1695
  this.prepareTooltips();
1532
1696
  if (this.cmEditor) {
1533
1697
  this.updatingCm = true;
@@ -1564,6 +1728,18 @@ export class MdzipWorkspaceView {
1564
1728
  this.elTitleSaveBtn.disabled = !valid;
1565
1729
  }
1566
1730
  this.elConversionDialog.hidden = this.conversionAction === null;
1731
+ this.elImageInsertDialog.hidden = this.imageInsertDialogState === null;
1732
+ if (this.imageInsertDialogState) {
1733
+ this.elImageInsertAltInput.value = this.imageInsertDialogState.request.defaultAltText;
1734
+ this.elImageInsertModeMarkdown.checked = true;
1735
+ this.elImageInsertModeHtml.checked = false;
1736
+ this.elImageInsertSizeModeSelect.value = 'width';
1737
+ this.elImageInsertSizeValueInput.value = this.imageInsertDialogState.request.intrinsicWidth
1738
+ ? String(this.imageInsertDialogState.request.intrinsicWidth)
1739
+ : '';
1740
+ this.elImageInsertPositionSelect.value = 'inline';
1741
+ this.updateImageInsertOptionControls();
1742
+ }
1567
1743
  this.elMetadataDialog.hidden = !this.metadataDialogOpen;
1568
1744
  if (this.navMenuState) {
1569
1745
  const items = this.navMenuItems(this.navMenuState.target, snapshot);
@@ -1758,7 +1934,7 @@ export class MdzipWorkspaceView {
1758
1934
  const file = this.elImageInput.files?.[0];
1759
1935
  this.elImageInput.value = '';
1760
1936
  if (file) {
1761
- void this.insertImageFile(file);
1937
+ void this.insertImageFile(file, 'picker');
1762
1938
  }
1763
1939
  });
1764
1940
  this.elEditToolbar.addEventListener('keydown', (event) => {
@@ -1901,6 +2077,19 @@ export class MdzipWorkspaceView {
1901
2077
  this.elConversionConfirmBtn.addEventListener('click', () => {
1902
2078
  void this.confirmMdzConversion();
1903
2079
  });
2080
+ this.elImageInsertDialog.querySelector('[data-action="cancel-image-insert"]')
2081
+ .addEventListener('click', () => {
2082
+ this.resolveImageInsertDialog(null);
2083
+ });
2084
+ this.elImageInsertConfirmBtn.addEventListener('click', () => {
2085
+ this.resolveImageInsertDialog(this.readImageInsertDialogDecision());
2086
+ });
2087
+ this.elImageInsertModeMarkdown.addEventListener('change', () => this.updateImageInsertOptionControls());
2088
+ this.elImageInsertModeHtml.addEventListener('change', () => this.updateImageInsertOptionControls());
2089
+ this.elImageInsertSizeModeSelect.addEventListener('change', () => {
2090
+ this.resetImageInsertSizeValue();
2091
+ this.updateImageInsertOptionControls();
2092
+ });
1904
2093
  this.elNavMenu.addEventListener('click', (e) => {
1905
2094
  e.stopPropagation();
1906
2095
  const item = e.target.closest('[data-menu-action]');
@@ -2286,7 +2475,7 @@ export class MdzipWorkspaceView {
2286
2475
  editor.dispatch({ selection: { anchor: target.selectionStart } });
2287
2476
  }
2288
2477
  if (action.kind === 'image-file') {
2289
- await this.insertImageFile(action.file);
2478
+ await this.insertImageFile(action.file, action.source ?? 'picker');
2290
2479
  return true;
2291
2480
  }
2292
2481
  this.elImageInput.click();
@@ -2312,7 +2501,7 @@ export class MdzipWorkspaceView {
2312
2501
  return;
2313
2502
  }
2314
2503
  if (action.kind === 'image-file') {
2315
- await this.insertImageFile(action.file);
2504
+ await this.insertImageFile(action.file, action.source ?? 'picker');
2316
2505
  return;
2317
2506
  }
2318
2507
  this.elImageInput.click();
@@ -2332,37 +2521,45 @@ export class MdzipWorkspaceView {
2332
2521
  if (this.workspace?.sourceFormat === 'markdown') {
2333
2522
  const extension = extensionForMime(image.mimeType);
2334
2523
  const pastedFile = new window.File([new Blob([image.bytes], { type: image.mimeType })], `pasted.${extension}`, { type: image.mimeType });
2335
- this.requestMdzConversion({ kind: 'image-file', file: pastedFile });
2524
+ this.requestMdzConversion({ kind: 'image-file', file: pastedFile, source: 'paste' });
2336
2525
  return;
2337
2526
  }
2338
- await this.insertImageBytes(image.bytes, image.mimeType);
2527
+ await this.insertImageBytes(image.bytes, image.mimeType, {
2528
+ fileName: `pasted.${extensionForMime(image.mimeType)}`,
2529
+ source: 'paste'
2530
+ });
2339
2531
  }
2340
2532
  catch (error) {
2341
2533
  this.options.onFailed?.(error);
2342
2534
  }
2343
2535
  }
2344
- async insertImageFile(file) {
2536
+ async insertImageFile(file, source = 'picker') {
2345
2537
  if (!file.type.startsWith('image/') && !/\.(png|jpe?g|gif|webp|svg)$/i.test(file.name)) {
2346
2538
  return;
2347
2539
  }
2348
2540
  try {
2349
- await this.insertImageBytes(new Uint8Array(await file.arrayBuffer()), file.type || imageMimeTypeFromFileName(file.name));
2541
+ await this.insertImageBytes(new Uint8Array(await file.arrayBuffer()), file.type || imageMimeTypeFromFileName(file.name), { fileName: file.name, source });
2350
2542
  }
2351
2543
  catch (error) {
2352
2544
  this.options.onFailed?.(error);
2353
2545
  }
2354
2546
  }
2355
- async insertImageBytes(bytes, mimeType) {
2547
+ async insertImageBytes(bytes, mimeType, options) {
2356
2548
  const editor = this.cmEditor;
2357
2549
  if (!editor) {
2358
2550
  return;
2359
2551
  }
2360
2552
  const selection = editor.state.selection.main;
2553
+ const decision = await this.resolveImageInsertDecision(bytes, mimeType, options);
2554
+ if (!decision) {
2555
+ return;
2556
+ }
2361
2557
  const result = await this.workspace?.pasteImage({
2362
2558
  bytes,
2363
2559
  mimeType,
2364
2560
  selectionStart: selection.from,
2365
- selectionEnd: selection.to
2561
+ selectionEnd: selection.to,
2562
+ markdownImage: (markdownPath) => formatImageInsertMarkdown(markdownPath, decision, editor.state.doc.toString(), selection.from, selection.to)
2366
2563
  });
2367
2564
  if (result && this.cmEditor) {
2368
2565
  this.render();
@@ -2370,6 +2567,115 @@ export class MdzipWorkspaceView {
2370
2567
  this.cmEditor.focus();
2371
2568
  }
2372
2569
  }
2570
+ async resolveImageInsertDecision(bytes, mimeType, options) {
2571
+ const size = sniffImageSize(bytes, mimeType);
2572
+ const request = {
2573
+ fileName: options.fileName || `pasted.${extensionForMime(mimeType)}`,
2574
+ mimeType,
2575
+ intrinsicWidth: size?.width,
2576
+ intrinsicHeight: size?.height,
2577
+ defaultAltText: 'Pasted image',
2578
+ source: options.source
2579
+ };
2580
+ const handler = this.options.imageInsertHandler;
2581
+ if (handler) {
2582
+ try {
2583
+ const handled = await handler(request);
2584
+ if (handled !== undefined) {
2585
+ return normalizeImageInsertDecision(handled);
2586
+ }
2587
+ }
2588
+ catch (error) {
2589
+ this.options.onFailed?.(error);
2590
+ return null;
2591
+ }
2592
+ }
2593
+ const mode = this.options.imageInsertMode ?? 'markdown';
2594
+ if (mode === 'ask') {
2595
+ return this.openImageInsertDialog(request);
2596
+ }
2597
+ return normalizeImageInsertDecision({
2598
+ mode: mode === 'html' ? 'html' : 'markdown',
2599
+ altText: request.defaultAltText,
2600
+ width: mode === 'html' ? request.intrinsicWidth : undefined,
2601
+ height: mode === 'html' ? request.intrinsicHeight : undefined,
2602
+ position: 'inline'
2603
+ });
2604
+ }
2605
+ openImageInsertDialog(request) {
2606
+ return new Promise((resolve) => {
2607
+ this.resolveImageInsertDialog(null);
2608
+ this.imageInsertDialogState = { request, resolve };
2609
+ this.render();
2610
+ requestAnimationFrame(() => this.elImageInsertAltInput.focus());
2611
+ });
2612
+ }
2613
+ resolveImageInsertDialog(decision) {
2614
+ const state = this.imageInsertDialogState;
2615
+ if (!state) {
2616
+ return;
2617
+ }
2618
+ this.imageInsertDialogState = null;
2619
+ this.render();
2620
+ state.resolve(normalizeImageInsertDecision(decision));
2621
+ }
2622
+ readImageInsertDialogDecision() {
2623
+ const parseDimension = (value) => {
2624
+ const parsed = Number(value);
2625
+ return Number.isFinite(parsed) && parsed > 0 ? Math.round(parsed) : undefined;
2626
+ };
2627
+ const request = this.imageInsertDialogState?.request;
2628
+ const sizeMode = this.elImageInsertSizeModeSelect.value;
2629
+ const sizeValue = parseDimension(this.elImageInsertSizeValueInput.value);
2630
+ let width;
2631
+ let height;
2632
+ if (this.elImageInsertModeHtml.checked && sizeValue) {
2633
+ if (sizeMode === 'width') {
2634
+ width = sizeValue;
2635
+ }
2636
+ else if (sizeMode === 'height') {
2637
+ height = sizeValue;
2638
+ }
2639
+ else if (sizeMode === 'percent' && request?.intrinsicWidth && request.intrinsicHeight) {
2640
+ width = Math.max(1, Math.round(request.intrinsicWidth * sizeValue / 100));
2641
+ height = Math.max(1, Math.round(request.intrinsicHeight * sizeValue / 100));
2642
+ }
2643
+ }
2644
+ return {
2645
+ mode: this.elImageInsertModeHtml.checked ? 'html' : 'markdown',
2646
+ altText: this.elImageInsertAltInput.value,
2647
+ width,
2648
+ height,
2649
+ position: this.elImageInsertPositionSelect.value
2650
+ };
2651
+ }
2652
+ updateImageInsertOptionControls() {
2653
+ const htmlMode = this.elImageInsertModeHtml.checked;
2654
+ const sizeDisabled = !htmlMode || this.elImageInsertSizeModeSelect.value === 'original';
2655
+ this.elImageInsertSizeModeSelect.disabled = !htmlMode;
2656
+ this.elImageInsertSizeValueInput.disabled = sizeDisabled;
2657
+ this.elImageInsertPositionSelect.disabled = !htmlMode;
2658
+ this.elImageInsertSizeModeSelect.closest('.image-insert-field')?.classList.toggle('field-disabled', !htmlMode);
2659
+ this.elImageInsertSizeValueInput.closest('.image-insert-field')?.classList.toggle('field-disabled', sizeDisabled);
2660
+ this.elImageInsertPositionSelect.closest('.image-insert-field')?.classList.toggle('field-disabled', !htmlMode);
2661
+ }
2662
+ resetImageInsertSizeValue() {
2663
+ const request = this.imageInsertDialogState?.request;
2664
+ switch (this.elImageInsertSizeModeSelect.value) {
2665
+ case 'width':
2666
+ this.elImageInsertSizeValueInput.value = request?.intrinsicWidth ? String(request.intrinsicWidth) : '';
2667
+ break;
2668
+ case 'height':
2669
+ this.elImageInsertSizeValueInput.value = request?.intrinsicHeight ? String(request.intrinsicHeight) : '';
2670
+ break;
2671
+ case 'percent':
2672
+ this.elImageInsertSizeValueInput.value = '100';
2673
+ break;
2674
+ default:
2675
+ this.elImageInsertSizeValueInput.value = '';
2676
+ break;
2677
+ }
2678
+ }
2373
2679
  applyMarkdownFormat(format) {
2374
2680
  const editor = this.cmEditor;
2375
2681
  const snapshot = this.workspace?.snapshot();
@@ -2412,8 +2718,11 @@ export class MdzipWorkspaceView {
2412
2718
  case 'blockquote':
2413
2719
  this.prefixSelectedLines('> ', /^(\s*)>\s?/);
2414
2720
  break;
2721
+ case 'insert-line-break':
2722
+ this.insertLineBreak();
2723
+ break;
2415
2724
  case 'link':
2416
- this.wrapSelection('[', '](url)', 'link text');
2725
+ this.insertMarkdownLink();
2417
2726
  break;
2418
2727
  default:
2419
2728
  return;
@@ -2430,6 +2739,7 @@ export class MdzipWorkspaceView {
2430
2739
  orderedList: formatting.orderedList,
2431
2740
  code: formatting.inlineCode || formatting.codeBlock,
2432
2741
  blockquote: formatting.blockquote,
2742
+ lineBreak: formatting.lineBreak,
2433
2743
  link: formatting.link,
2434
2744
  image: formatting.image
2435
2745
  };
@@ -2490,6 +2800,38 @@ export class MdzipWorkspaceView {
2490
2800
  });
2491
2801
  editor.focus();
2492
2802
  }
2803
+ insertMarkdownLink() {
2804
+ const editor = this.cmEditor;
2805
+ if (!editor) {
2806
+ return;
2807
+ }
2808
+ const selection = editor.state.selection.main;
2809
+ const selectedText = editor.state.sliceDoc(selection.from, selection.to);
2810
+ const linkText = selectedText || 'link text';
2811
+ const urlPlaceholder = 'url';
2812
+ const insert = `[${linkText}](${urlPlaceholder})`;
2813
+ const anchor = selection.from + linkText.length + 3;
2814
+ editor.dispatch({
2815
+ changes: { from: selection.from, to: selection.to, insert },
2816
+ selection: { anchor, head: anchor + urlPlaceholder.length },
2817
+ scrollIntoView: true
2818
+ });
2819
+ editor.focus();
2820
+ }
2821
+ insertLineBreak() {
2822
+ const editor = this.cmEditor;
2823
+ if (!editor) {
2824
+ return;
2825
+ }
2826
+ const selection = editor.state.selection.main;
2827
+ const insert = '<br>\n';
2828
+ editor.dispatch({
2829
+ changes: { from: selection.from, to: selection.to, insert },
2830
+ selection: { anchor: selection.from + insert.length },
2831
+ scrollIntoView: true
2832
+ });
2833
+ editor.focus();
2834
+ }
2493
2835
  prefixSelectedLines(prefix, existingPrefix) {
2494
2836
  const editor = this.cmEditor;
2495
2837
  if (!editor) {
@@ -2985,7 +3327,7 @@ export class MdzipWorkspaceView {
2985
3327
  async handleEditorImageDrop(file, x, y) {
2986
3328
  try {
2987
3329
  if (this.workspace?.sourceFormat === 'markdown') {
2988
- this.requestMdzConversion({ kind: 'image-file', file });
3330
+ this.requestMdzConversion({ kind: 'image-file', file, source: 'drop' });
2989
3331
  return;
2990
3332
  }
2991
3333
  const editor = this.cmEditor;
@@ -2995,7 +3337,7 @@ export class MdzipWorkspaceView {
2995
3337
  editor.dispatch({ selection: { anchor: pos } });
2996
3338
  }
2997
3339
  }
2998
- await this.insertImageFile(file);
3340
+ await this.insertImageFile(file, 'drop');
2999
3341
  }
3000
3342
  catch (error) {
3001
3343
  this.options.onFailed?.(error);
@@ -3209,6 +3551,7 @@ function hasFormattingControls(policy) {
3209
3551
  || policy.inlineCode
3210
3552
  || policy.codeBlock
3211
3553
  || policy.blockquote
3554
+ || policy.lineBreak
3212
3555
  || policy.link
3213
3556
  || policy.image;
3214
3557
  }
@@ -3228,6 +3571,8 @@ function markdownCommandFromToolbarFormat(format) {
3228
3571
  case 'code-block':
3229
3572
  case 'link':
3230
3573
  return format;
3574
+ case 'line-break':
3575
+ return 'insert-line-break';
3231
3576
  case 'strike':
3232
3577
  return 'strikethrough';
3233
3578
  case 'code':
@@ -3255,6 +3600,70 @@ function imageMimeTypeFromFileName(fileName) {
3255
3600
  return 'image/png';
3256
3601
  }
3257
3602
  }
3603
+ function normalizeImageInsertDecision(decision) {
3604
+ if (!decision) {
3605
+ return null;
3606
+ }
3607
+ const normalizeDimension = (value) => value !== undefined && Number.isFinite(value) && value > 0
3608
+ ? Math.round(value)
3609
+ : undefined;
3610
+ return {
3611
+ mode: decision.mode === 'html' ? 'html' : 'markdown',
3612
+ altText: decision.altText ?? '',
3613
+ width: normalizeDimension(decision.width),
3614
+ height: normalizeDimension(decision.height),
3615
+ position: normalizeImagePosition(decision.position)
3616
+ };
3617
+ }
3618
+ function normalizeImagePosition(position) {
3619
+ switch (position) {
3620
+ case 'left':
3621
+ case 'center':
3622
+ case 'right':
3623
+ case 'wrap-left':
3624
+ case 'wrap-right':
3625
+ return position;
3626
+ default:
3627
+ return 'inline';
3628
+ }
3629
+ }
3630
+ function formatImageInsertMarkdown(markdownPath, decision, currentText = '', selectionStart = 0, selectionEnd = selectionStart) {
3631
+ if (decision.mode === 'html') {
3632
+ const attrs = [
3633
+ `src="${escapeHtml(markdownPath)}"`,
3634
+ `alt="${escapeHtml(decision.altText)}"`
3635
+ ];
3636
+ if (decision.width) {
3637
+ attrs.push(`width="${decision.width}"`);
3638
+ }
3639
+ if (decision.height) {
3640
+ attrs.push(`height="${decision.height}"`);
3641
+ }
3642
+ if (decision.position === 'wrap-left' || decision.position === 'wrap-right') {
3643
+ attrs.push(`align="${decision.position === 'wrap-left' ? 'left' : 'right'}"`);
3644
+ }
3645
+ const image = `<img ${attrs.join(' ')}>`;
3646
+ if (decision.position === 'left' || decision.position === 'center' || decision.position === 'right') {
3647
+ return padHtmlImageBlock(`<p align="${decision.position}">${image}</p>`, currentText, selectionStart, selectionEnd);
3648
+ }
3649
+ return padHtmlImageBlock(image, currentText, selectionStart, selectionEnd);
3650
+ }
3651
+ return `![${escapeMarkdownImageAlt(decision.altText)}](${markdownPath})`;
3652
+ }
3653
+ function padHtmlImageBlock(html, currentText, selectionStart, selectionEnd) {
3654
+ const before = currentText.slice(0, Math.max(0, selectionStart));
3655
+ const after = currentText.slice(Math.max(selectionStart, selectionEnd));
3656
+ const prefix = before.length === 0 || before.endsWith('\n\n')
3657
+ ? ''
3658
+ : before.endsWith('\n') ? '\n' : '\n\n';
3659
+ const suffix = after.startsWith('\n\n')
3660
+ ? ''
3661
+ : after.startsWith('\n') ? '\n' : '\n\n';
3662
+ return `${prefix}${html}${suffix}`;
3663
+ }
3664
+ function escapeMarkdownImageAlt(value) {
3665
+ return value.replace(/\\/g, '\\\\').replace(/]/g, '\\]');
3666
+ }
3258
3667
  function formatMetadataValue(value) {
3259
3668
  if (typeof value === 'string' && value.trim()) {
3260
3669
  return value;
@@ -3319,6 +3728,7 @@ const SHELL_HTML = `
3319
3728
  </div>
3320
3729
  </div>
3321
3730
  <button type="button" data-format="quote" data-format-control="blockquote" title="Blockquote" aria-label="Blockquote">${QUOTE_ICON_HTML}</button>
3731
+ <button type="button" data-format="line-break" data-format-control="lineBreak" title="Line break" aria-label="Line break">${LINE_BREAK_ICON_HTML}</button>
3322
3732
  </div>
3323
3733
  <span class="edit-toolbar-divider" aria-hidden="true"></span>
3324
3734
  <div class="edit-toolbar-group">
@@ -3425,6 +3835,61 @@ const SHELL_HTML = `
3425
3835
  </div>
3426
3836
  </div>
3427
3837
 
3838
+ <div class="title-dialog-backdrop" data-ref="image-insert-dialog" hidden
3839
+ role="dialog" aria-modal="true" aria-labelledby="mdzip-image-insert-dialog-heading">
3840
+ <div class="title-dialog image-insert-dialog">
3841
+ <h3 id="mdzip-image-insert-dialog-heading">Insert Image</h3>
3842
+ <p>Choose the Markdown inserted for this image.</p>
3843
+ <fieldset class="image-insert-options">
3844
+ <legend>Output</legend>
3845
+ <label>
3846
+ <input type="radio" name="mdzip-image-insert-mode" value="markdown"
3847
+ data-ref="image-insert-mode-markdown" checked />
3848
+ Markdown image
3849
+ </label>
3850
+ <label>
3851
+ <input type="radio" name="mdzip-image-insert-mode" value="html"
3852
+ data-ref="image-insert-mode-html" />
3853
+ HTML &lt;img&gt; with sizing
3854
+ </label>
3855
+ </fieldset>
3856
+ <label class="image-insert-field">
3857
+ <span>Alt text</span>
3858
+ <input type="text" data-ref="image-insert-alt" />
3859
+ </label>
3860
+ <div class="image-insert-grid">
3861
+ <label class="image-insert-field">
3862
+ <span>Size by</span>
3863
+ <select data-ref="image-insert-size-mode">
3864
+ <option value="original">Original size</option>
3865
+ <option value="width" selected>Width</option>
3866
+ <option value="height">Height</option>
3867
+ <option value="percent">Percent</option>
3868
+ </select>
3869
+ </label>
3870
+ <label class="image-insert-field">
3871
+ <span>Value</span>
3872
+ <input type="number" min="1" step="1" data-ref="image-insert-size-value" />
3873
+ </label>
3874
+ </div>
3875
+ <label class="image-insert-field">
3876
+ <span>Position</span>
3877
+ <select data-ref="image-insert-position">
3878
+ <option value="inline">Inline/default</option>
3879
+ <option value="left">Left</option>
3880
+ <option value="center">Center</option>
3881
+ <option value="right">Right</option>
3882
+ <option value="wrap-left">Wrap left</option>
3883
+ <option value="wrap-right">Wrap right</option>
3884
+ </select>
3885
+ </label>
3886
+ <div class="title-dialog-actions">
3887
+ <button type="button" data-action="cancel-image-insert">Cancel</button>
3888
+ <button type="button" class="save-title" data-ref="image-insert-confirm-btn">Insert</button>
3889
+ </div>
3890
+ </div>
3891
+ </div>
3892
+
3428
3893
  <div class="title-dialog-backdrop" data-ref="metadata-dialog" hidden
3429
3894
  role="dialog" aria-modal="true" aria-labelledby="mdzip-metadata-dialog-heading">
3430
3895
  <div class="title-dialog metadata-dialog">