@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/README.md +63 -0
- package/dist/library-info.d.ts +2 -2
- package/dist/library-info.js +2 -2
- package/dist/view-css.d.ts.map +1 -1
- package/dist/view-css.js +257 -53
- package/dist/view-css.js.map +1 -1
- package/dist/view.d.ts +91 -2
- package/dist/view.d.ts.map +1 -1
- package/dist/view.js +500 -35
- package/dist/view.js.map +1 -1
- package/dist/workspace-view.d.ts +1 -1
- package/dist/workspace-view.d.ts.map +1 -1
- package/dist/workspace-view.js +38 -3
- package/dist/workspace-view.js.map +1 -1
- package/dist/workspace.d.ts +1 -0
- package/dist/workspace.d.ts.map +1 -1
- package/dist/workspace.js +3 -1
- package/dist/workspace.js.map +1 -1
- package/package.json +2 -2
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
|
-
|
|
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
|
|
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
|
-
|
|
1105
|
+
if (animateImageHydration) {
|
|
1106
|
+
image.classList.add('mdzip-image-loading');
|
|
1107
|
+
}
|
|
977
1108
|
const slot = document.createElement('span');
|
|
978
|
-
slot.className =
|
|
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
|
-
|
|
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
|
-
|
|
1503
|
-
|
|
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
|
|
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.
|
|
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 ``;
|
|
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 <img> 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">
|