@mdzip/editor 1.3.13 → 1.3.15

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, 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);
@@ -179,7 +181,8 @@ const CONTROL_PRESETS = {
179
181
  zoom: false,
180
182
  colorScheme: false,
181
183
  orphanActions: false,
182
- fileActions: false
184
+ fileActions: false,
185
+ search: false
183
186
  },
184
187
  viewer: {
185
188
  preset: 'viewer',
@@ -193,7 +196,8 @@ const CONTROL_PRESETS = {
193
196
  zoom: true,
194
197
  colorScheme: true,
195
198
  orphanActions: false,
196
- fileActions: false
199
+ fileActions: false,
200
+ search: true
197
201
  },
198
202
  'standalone-editor': {
199
203
  preset: 'standalone-editor',
@@ -207,7 +211,8 @@ const CONTROL_PRESETS = {
207
211
  zoom: true,
208
212
  colorScheme: true,
209
213
  orphanActions: true,
210
- fileActions: true
214
+ fileActions: true,
215
+ search: true
211
216
  },
212
217
  'hosted-editor': {
213
218
  preset: 'hosted-editor',
@@ -221,7 +226,8 @@ const CONTROL_PRESETS = {
221
226
  zoom: true,
222
227
  colorScheme: true,
223
228
  orphanActions: true,
224
- fileActions: true
229
+ fileActions: true,
230
+ search: true
225
231
  }
226
232
  };
227
233
  export function resolveMdzipControlPolicy(controls) {
@@ -367,6 +373,73 @@ const mdzipEditorTheme = EditorView.theme({
367
373
  color: 'var(--mdzip-muted-foreground-color)',
368
374
  opacity: '0.65',
369
375
  },
376
+ '.cm-panels': {
377
+ background: 'var(--mdzip-widget-background-color)',
378
+ color: 'var(--mdzip-editor-foreground-color)',
379
+ zIndex: '2',
380
+ },
381
+ '.cm-panels.cm-panels-top': {
382
+ borderBottom: '1px solid var(--mdzip-border-color)',
383
+ },
384
+ '.cm-search': {
385
+ display: 'flex',
386
+ flexWrap: 'wrap',
387
+ alignItems: 'center',
388
+ gap: '8px',
389
+ padding: '10px 14px',
390
+ fontSize: '13px',
391
+ },
392
+ // CodeMirror separates the find and replace rows with a bare <br>; in a
393
+ // flex container that collapses to zero width instead of breaking the
394
+ // line, so force it to take the full row.
395
+ '.cm-search br': {
396
+ flexBasis: '100%',
397
+ height: '0',
398
+ },
399
+ '.cm-search label': {
400
+ display: 'inline-flex',
401
+ alignItems: 'center',
402
+ gap: '5px',
403
+ color: 'var(--mdzip-muted-foreground-color)',
404
+ },
405
+ '.cm-search input[type="checkbox"]': {
406
+ width: '14px',
407
+ height: '14px',
408
+ accentColor: 'var(--mdzip-accent-color)',
409
+ },
410
+ '.cm-textfield': {
411
+ background: 'var(--mdzip-editor-background-color)',
412
+ color: 'var(--mdzip-editor-foreground-color)',
413
+ border: '1px solid var(--mdzip-border-color)',
414
+ borderRadius: '5px',
415
+ padding: '5px 9px',
416
+ fontSize: 'inherit',
417
+ width: '220px',
418
+ },
419
+ '.cm-textfield:focus-visible': {
420
+ outline: '1px solid var(--mdzip-focus-outline-color)',
421
+ outlineOffset: '-1px',
422
+ },
423
+ '.cm-button': {
424
+ background: 'var(--mdzip-widget-background-color)',
425
+ backgroundImage: 'none',
426
+ color: 'var(--mdzip-control-foreground-color)',
427
+ border: '1px solid var(--mdzip-border-color)',
428
+ borderRadius: '5px',
429
+ padding: '5px 12px',
430
+ fontSize: 'inherit',
431
+ cursor: 'pointer',
432
+ },
433
+ '.cm-button:hover': {
434
+ background: 'var(--mdzip-control-hover-background-color)',
435
+ },
436
+ '.cm-searchMatch': {
437
+ backgroundColor: 'rgba(255, 214, 0, 0.35)',
438
+ },
439
+ '.cm-searchMatch-selected': {
440
+ backgroundColor: 'var(--mdzip-accent-color)',
441
+ color: 'var(--mdzip-accent-foreground-color)',
442
+ },
370
443
  });
371
444
  const mdzipMarkdownHighlight = HighlightStyle.define([
372
445
  { tag: [tags.heading1, tags.heading2, tags.heading3, tags.heading4, tags.heading5, tags.heading6],
@@ -398,6 +471,27 @@ const hardBreakMarkerHighlight = ViewPlugin.fromClass(class {
398
471
  }, {
399
472
  decorations: value => value.decorations
400
473
  });
474
+ // Matches raw HTML tags other than <br> (which hardBreakMarkerMatcher already
475
+ // covers) so authors can visually distinguish raw HTML from Markdown prose.
476
+ // Excludes autolinks like <https://example.com> — those have no space before
477
+ // '>' and no closing '/', so the tag-name-only branch requires the char after
478
+ // the name to be '>' directly, which a URL's ':' never satisfies.
479
+ const htmlTagMarkerMatcher = new MatchDecorator({
480
+ regexp: /<\/?(?!br\b)[a-zA-Z][a-zA-Z0-9-]*(?:\s[^<>]*)?\/?>/gi,
481
+ decoration: Decoration.mark({ class: 'mdzip-hard-break-marker' })
482
+ });
483
+ const htmlTagMarkerHighlight = ViewPlugin.fromClass(class {
484
+ constructor(view) {
485
+ this.decorations = htmlTagMarkerMatcher.createDeco(view);
486
+ }
487
+ update(update) {
488
+ if (update.docChanged || update.viewportChanged) {
489
+ this.decorations = htmlTagMarkerMatcher.updateDeco(update, this.decorations);
490
+ }
491
+ }
492
+ }, {
493
+ decorations: value => value.decorations
494
+ });
401
495
  function injectStyles(doc) {
402
496
  const existing = doc.querySelector(`style[${STYLE_ATTR}]`);
403
497
  if (existing) {
@@ -532,6 +626,35 @@ function navNodeOrder(a, b) {
532
626
  return 1;
533
627
  return a.name.localeCompare(b.name);
534
628
  }
629
+ // Browsers apply raw HTML width/height attributes as presentational sizing
630
+ // hints, but the preview's `img { height: auto }` rule (for responsive
631
+ // scaling) overrides that hint via the normal CSS cascade — so an author's
632
+ // `<img height="300">` was silently ignored. Re-applying numeric, unitless
633
+ // width/height attributes as inline pixel styles restores that sizing
634
+ // without touching an author-supplied inline style.
635
+ function applyRawHtmlImageSizeAttributes(image) {
636
+ const width = image.getAttribute('width');
637
+ if (width && /^\d+$/.test(width) && !image.style.width) {
638
+ image.style.width = `${width}px`;
639
+ }
640
+ const height = image.getAttribute('height');
641
+ if (height && /^\d+$/.test(height) && !image.style.height) {
642
+ image.style.height = `${height}px`;
643
+ }
644
+ }
645
+ // Maps the legacy raw HTML `align="left"|"right"` attribute (or the editor's
646
+ // own wrap classes) to a layout direction, for images that skip archive
647
+ // hydration — external, data, and fragment sources — and so never get a slot.
648
+ function rawHtmlImageAlignClass(image) {
649
+ const align = image.getAttribute('align')?.toLowerCase();
650
+ if (align === 'left' || image.classList.contains('mdzip-image-wrap-left')) {
651
+ return 'mdzip-image-left';
652
+ }
653
+ if (align === 'right' || image.classList.contains('mdzip-image-wrap-right')) {
654
+ return 'mdzip-image-right';
655
+ }
656
+ return null;
657
+ }
535
658
  export class MdzipWorkspaceView {
536
659
  constructor(container, options = {}) {
537
660
  this.workspace = null;
@@ -574,6 +697,7 @@ export class MdzipWorkspaceView {
574
697
  this.cmEditor = null;
575
698
  this.readOnlyCompartment = new Compartment();
576
699
  this.lineNumbersCompartment = new Compartment();
700
+ this.searchCompartment = new Compartment();
577
701
  this.updatingCm = false;
578
702
  this.syncing = false;
579
703
  this.markdownExtensions = [];
@@ -630,6 +754,7 @@ export class MdzipWorkspaceView {
630
754
  this.elSourceBtn = q('[data-ref="source-btn"]');
631
755
  this.elSourceIcon = q('[data-ref="source-icon"]');
632
756
  this.elSaveBtn = q('[data-ref="save-btn"]');
757
+ this.elSearchBtn = q('[data-ref="search-btn"]');
633
758
  this.elZoomBtn = q('[data-ref="zoom-btn"]');
634
759
  this.elThemeControls = q('[data-ref="theme-controls"]');
635
760
  this.elDarkThemeBtn = q('[data-ref="dark-theme-btn"]');
@@ -907,6 +1032,40 @@ export class MdzipWorkspaceView {
907
1032
  this.applyMarkdownFormat(command);
908
1033
  return true;
909
1034
  }
1035
+ /**
1036
+ * Opens CodeMirror's find/replace panel for the current document. Unlike
1037
+ * {@link executeCommand}, this works in read-only (Viewer) hosts too —
1038
+ * searching doesn't require edit access, only a visible source pane. If
1039
+ * the current layout is preview-only, switches to split/source first so
1040
+ * the panel has somewhere to render.
1041
+ */
1042
+ async openSearch() {
1043
+ if (!this.controlPolicy.search) {
1044
+ return false;
1045
+ }
1046
+ const snapshot = this.workspace?.snapshot();
1047
+ if (!snapshot || snapshot.currentPathType !== 'markdown') {
1048
+ return false;
1049
+ }
1050
+ if (this.layout === 'preview') {
1051
+ const nextLayout = this.controlPolicy.layout.split
1052
+ ? 'split'
1053
+ : this.controlPolicy.layout.source ? 'source' : null;
1054
+ if (!nextLayout) {
1055
+ return false;
1056
+ }
1057
+ await this.setLayout(nextLayout);
1058
+ }
1059
+ const editor = await this.ensureCmEditor(true);
1060
+ if (!editor) {
1061
+ return false;
1062
+ }
1063
+ editor.focus();
1064
+ return openSearchPanel(editor);
1065
+ }
1066
+ closeSearch() {
1067
+ return this.cmEditor ? closeSearchPanel(this.cmEditor) : false;
1068
+ }
910
1069
  async convertToMdz() {
911
1070
  if (!this.workspace || this.workspace.mode === 'read-only') {
912
1071
  return false;
@@ -982,12 +1141,21 @@ export class MdzipWorkspaceView {
982
1141
  setControls(controls) {
983
1142
  const next = resolveMdzipControlPolicy(controls);
984
1143
  const lineNumbersChanged = next.lineNumbers !== this.controlPolicy.lineNumbers;
1144
+ const searchChanged = next.search !== this.controlPolicy.search;
985
1145
  this.controlPolicy = next;
986
1146
  if (lineNumbersChanged && this.cmEditor) {
987
1147
  this.cmEditor.dispatch({
988
1148
  effects: this.lineNumbersCompartment.reconfigure(next.lineNumbers ? lineNumbers() : [])
989
1149
  });
990
1150
  }
1151
+ if (searchChanged && this.cmEditor) {
1152
+ if (!next.search) {
1153
+ closeSearchPanel(this.cmEditor);
1154
+ }
1155
+ this.cmEditor.dispatch({
1156
+ effects: this.searchCompartment.reconfigure(next.search ? [search({ top: true }), keymap.of(searchKeymap)] : [])
1157
+ });
1158
+ }
991
1159
  const snapshot = this.workspace?.snapshot();
992
1160
  if (snapshot) {
993
1161
  this.layout = this.validLayoutForSnapshot(this.layout, snapshot);
@@ -1186,9 +1354,14 @@ export class MdzipWorkspaceView {
1186
1354
  const document = this.elPreviewContent.ownerDocument;
1187
1355
  const pending = [];
1188
1356
  for (const image of Array.from(this.elPreviewContent.querySelectorAll('img'))) {
1357
+ applyRawHtmlImageSizeAttributes(image);
1189
1358
  const source = image.getAttribute('src');
1190
1359
  // Leave external, protocol-relative, data, and fragment URLs untouched.
1191
1360
  if (!source || /^(?:[a-z][a-z\d+.-]*:|\/\/|#)/i.test(source)) {
1361
+ const alignClass = rawHtmlImageAlignClass(image);
1362
+ if (alignClass) {
1363
+ image.classList.add(alignClass);
1364
+ }
1192
1365
  continue;
1193
1366
  }
1194
1367
  // Drop the archive-relative src so the browser does not fetch the bad
@@ -1203,11 +1376,11 @@ export class MdzipWorkspaceView {
1203
1376
  slot.className = animateImageHydration
1204
1377
  ? 'mdzip-image-slot'
1205
1378
  : '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')) {
1379
+ const alignClass = rawHtmlImageAlignClass(image);
1380
+ if (alignClass === 'mdzip-image-left') {
1208
1381
  slot.classList.add('mdzip-image-align-left');
1209
1382
  }
1210
- else if (align === 'right' || image.classList.contains('mdzip-image-wrap-right')) {
1383
+ else if (alignClass === 'mdzip-image-right') {
1211
1384
  slot.classList.add('mdzip-image-align-right');
1212
1385
  }
1213
1386
  image.parentNode?.insertBefore(slot, image);
@@ -1579,6 +1752,7 @@ export class MdzipWorkspaceView {
1579
1752
  doc: initialText,
1580
1753
  extensions: [
1581
1754
  this.lineNumbersCompartment.of(this.controlPolicy.lineNumbers ? lineNumbers() : []),
1755
+ this.searchCompartment.of(this.controlPolicy.search ? [search({ top: true }), keymap.of(searchKeymap)] : []),
1582
1756
  history(),
1583
1757
  keymap.of([
1584
1758
  { key: 'Mod-b', run: () => self.runFormatShortcut('bold') },
@@ -1589,6 +1763,7 @@ export class MdzipWorkspaceView {
1589
1763
  markdown(),
1590
1764
  syntaxHighlighting(mdzipMarkdownHighlight),
1591
1765
  hardBreakMarkerHighlight,
1766
+ htmlTagMarkerHighlight,
1592
1767
  EditorView.lineWrapping,
1593
1768
  dropCursor(),
1594
1769
  mdzipEditorTheme,
@@ -1682,24 +1857,28 @@ export class MdzipWorkspaceView {
1682
1857
  const showSaveControl = this.controlPolicy.save && snapshot.mode !== 'read-only';
1683
1858
  const showZoomControl = this.controlPolicy.zoom;
1684
1859
  const showColorSchemeControl = this.controlPolicy.colorScheme;
1860
+ const showSearchControl = this.controlPolicy.search
1861
+ && canShowSource && snapshot.currentPathType === 'markdown';
1685
1862
  const showEditControls = canEdit
1686
1863
  && snapshot.currentPathType === 'markdown'
1687
1864
  && this.layout !== 'preview'
1688
1865
  && hasFormattingControls(this.controlPolicy.formatting);
1689
1866
  const showToolbar = this.controlPolicy.toolbar
1690
- && (showNavigationControl || showLayoutControls
1691
- || showSaveControl || showZoomControl || showColorSchemeControl || showEditControls);
1867
+ && (showNavigationControl || showLayoutControls || showSaveControl
1868
+ || showZoomControl || showColorSchemeControl || showSearchControl || showEditControls);
1692
1869
  this.elDocumentStrip.hidden = !showTitleControl;
1693
1870
  this.elToolbar.hidden = !showToolbar;
1694
1871
  this.elToolbarLeft.hidden = !showNavigationControl;
1695
1872
  this.elEditToolbar.hidden = !showEditControls;
1696
1873
  this.elLayoutControls.hidden = !showLayoutControls;
1697
- this.elToolbarControls.hidden = !showSaveControl && !showZoomControl && !showColorSchemeControl;
1874
+ this.elToolbarControls.hidden = !showSaveControl && !showZoomControl
1875
+ && !showColorSchemeControl && !showSearchControl;
1698
1876
  this.elNavBtn.hidden = !showNavigationControl;
1699
1877
  this.elPreviewBtn.hidden = !this.controlPolicy.layout.preview;
1700
1878
  this.elSplitBtn.hidden = !this.controlPolicy.layout.split;
1701
1879
  this.elSourceBtn.hidden = !this.controlPolicy.layout.source;
1702
1880
  this.elSaveBtn.hidden = !showSaveControl;
1881
+ this.elSearchBtn.hidden = !showSearchControl;
1703
1882
  this.elZoomBtn.hidden = !showZoomControl;
1704
1883
  this.elThemeControls.hidden = !showColorSchemeControl;
1705
1884
  this.elRoot.style.setProperty('--mdz-zoom', String(this.zoom));
@@ -1984,6 +2163,9 @@ export class MdzipWorkspaceView {
1984
2163
  void this.save();
1985
2164
  }
1986
2165
  });
2166
+ this.elSearchBtn.addEventListener('click', () => {
2167
+ void this.openSearch();
2168
+ });
1987
2169
  this.elZoomBtn.addEventListener('click', (e) => {
1988
2170
  if (!this.controlPolicy.zoom) {
1989
2171
  return;
@@ -4199,6 +4381,9 @@ const SHELL_HTML = `
4199
4381
  </div>
4200
4382
 
4201
4383
  <div class="toolbar-controls" data-ref="toolbar-controls">
4384
+ <button type="button" class="icon-toggle" data-ref="search-btn" title="Find/replace (Mod-F)" aria-label="Find/replace">
4385
+ ${SEARCH_ICON_HTML}
4386
+ </button>
4202
4387
  <button type="button" class="icon-toggle" data-ref="save-btn" title="Save" aria-label="Save">
4203
4388
  ${SAVE_ICON_HTML}
4204
4389
  </button>