@mdzip/editor 1.5.0 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +4 -0
- package/README.md +73 -9
- package/dist/archive-utils.d.ts.map +1 -1
- package/dist/archive-utils.js +3 -3
- package/dist/archive-utils.js.map +1 -1
- package/dist/asset-cache.d.ts +1 -1
- package/dist/asset-cache.d.ts.map +1 -1
- package/dist/asset-cache.js +17 -18
- package/dist/asset-cache.js.map +1 -1
- package/dist/diff.d.ts.map +1 -1
- package/dist/diff.js +2 -3
- package/dist/diff.js.map +1 -1
- package/dist/front-matter-extension.d.ts.map +1 -1
- package/dist/front-matter-extension.js +3 -1
- package/dist/front-matter-extension.js.map +1 -1
- package/dist/icons/page-width.d.ts +6 -0
- package/dist/icons/page-width.d.ts.map +1 -0
- package/dist/icons/page-width.js +15 -0
- package/dist/icons/page-width.js.map +1 -0
- package/dist/library-info.d.ts +8 -3
- package/dist/library-info.d.ts.map +1 -1
- package/dist/library-info.js +9 -3
- package/dist/library-info.js.map +1 -1
- package/dist/mdz-archive.worker.js +738 -305
- package/dist/mermaid.d.ts +32 -0
- package/dist/mermaid.d.ts.map +1 -1
- package/dist/mermaid.js +127 -27
- package/dist/mermaid.js.map +1 -1
- package/dist/rendering.d.ts +12 -0
- package/dist/rendering.d.ts.map +1 -1
- package/dist/rendering.js +83 -4
- package/dist/rendering.js.map +1 -1
- package/dist/sha256.d.ts +10 -0
- package/dist/sha256.d.ts.map +1 -0
- package/dist/sha256.js +83 -0
- package/dist/sha256.js.map +1 -0
- package/dist/view-css.d.ts.map +1 -1
- package/dist/view-css.js +2216 -2114
- package/dist/view-css.js.map +1 -1
- package/dist/view.d.ts +184 -21
- package/dist/view.d.ts.map +1 -1
- package/dist/view.js +1153 -426
- package/dist/view.js.map +1 -1
- package/dist/workspace-view.d.ts.map +1 -1
- package/dist/workspace-view.js +6 -5
- package/dist/workspace-view.js.map +1 -1
- package/dist/workspace.d.ts +12 -0
- package/dist/workspace.d.ts.map +1 -1
- package/dist/workspace.js +62 -39
- package/dist/workspace.js.map +1 -1
- package/package.json +83 -82
package/dist/view.js
CHANGED
|
@@ -5,11 +5,13 @@ import { closeSearchPanel, openSearchPanel, search, searchKeymap } from '@codemi
|
|
|
5
5
|
import { Compartment, EditorState, RangeSetBuilder, StateEffect, StateField } from '@codemirror/state';
|
|
6
6
|
import { Decoration, EditorView, MatchDecorator, ViewPlugin, WidgetType, dropCursor, keymap, lineNumbers } from '@codemirror/view';
|
|
7
7
|
import { tags } from '@lezer/highlight';
|
|
8
|
-
import { Bold, Check, ChevronDown, ChevronRight, ClipboardPaste, ClipboardType, Code, Columns2, Copy, Eraser, Eye, File, FileBraces, FileImage, Folder, FolderOpen, Hash, Heading1, Heading2, Heading3, Heading4, Heading5, Heading6, Highlighter, ImagePlus, Info, Italic, Link2Off, Link, List, ListOrdered, CornerDownLeft, Moon, PackagePlus, PanelLeft, Pilcrow, Quote, Save, Scissors, Search, SpellCheck, SquareCode, SquarePen, Strikethrough, Sun, TextSelect, ZoomIn } from 'lucide';
|
|
8
|
+
import { Bold, Check, ChevronDown, ChevronRight, ClipboardPaste, ClipboardType, Code, Columns2, Copy, Eraser, Eye, File, FileBraces, FileImage, Folder, FolderOpen, Hash, Heading1, Heading2, Heading3, Heading4, Heading5, Heading6, Highlighter, ImagePlus, Info, Italic, Link2Off, Link, List, ListOrdered, CornerDownLeft, Monitor, Moon, PackagePlus, PanelLeft, Pilcrow, Quote, Save, Scissors, Search, SpellCheck, SquareCode, SquarePen, Strikethrough, Sun, TextSelect, ZoomIn } from 'lucide';
|
|
9
|
+
import { samePath } from '@mdzip/core-js';
|
|
9
10
|
import { browserClipboardHasImage, readBrowserClipboardImage } from './browser.js';
|
|
10
11
|
import { buildPackedArchiveBytes, isMarkdownArchivePath, resolveMarkdownEntryPoint } from './archive-utils.js';
|
|
11
12
|
import { MdzipAssetSession, mdzipArchiveSourceId, sniffImageSize } from './asset-cache.js';
|
|
12
13
|
import { MD_MARKDOWN_ICON } from './icons/md-markdown.js';
|
|
14
|
+
import { PAGE_WIDTH_DEFAULT_ICON, PAGE_WIDTH_FULL_ICON, PAGE_WIDTH_NARROW_ICON, PAGE_WIDTH_WIDE_ICON } from './icons/page-width.js';
|
|
13
15
|
import { MDZIP_RUNTIME_LIBRARIES } from './library-info.js';
|
|
14
16
|
import { MdzipWorkspaceService, extensionForMime, normalizeArchivePath, relativeArchivePath } from './workspace.js';
|
|
15
17
|
import { buildMdzipNavTree, canEditMdzipPath, escapeHtml, isOrphanedMdzipAsset, mdzipEntryIconKind, isMdzipManifestPath, isMdzipDefaultPolicyExternalLink, isMdzipExternalLink, isMdzipWorkspaceRelativeLink, parseMdzipLink, renderMdzipPreviewHtml } from './workspace-view.js';
|
|
@@ -52,6 +54,11 @@ const SEARCH_ICON_HTML = lucideIcon(Search, TOOLBAR_ICON_CLASS);
|
|
|
52
54
|
const ZOOM_ICON_HTML = lucideIcon(ZoomIn, TOOLBAR_ICON_CLASS);
|
|
53
55
|
const DARK_THEME_ICON_HTML = lucideIcon(Moon, TOOLBAR_ICON_CLASS);
|
|
54
56
|
const LIGHT_THEME_ICON_HTML = lucideIcon(Sun, TOOLBAR_ICON_CLASS);
|
|
57
|
+
const SYSTEM_THEME_ICON_HTML = lucideIcon(Monitor, TOOLBAR_ICON_CLASS);
|
|
58
|
+
const PAGE_WIDTH_NARROW_ICON_HTML = lucideIcon(PAGE_WIDTH_NARROW_ICON, TOOLBAR_ICON_CLASS);
|
|
59
|
+
const PAGE_WIDTH_DEFAULT_ICON_HTML = lucideIcon(PAGE_WIDTH_DEFAULT_ICON, TOOLBAR_ICON_CLASS);
|
|
60
|
+
const PAGE_WIDTH_WIDE_ICON_HTML = lucideIcon(PAGE_WIDTH_WIDE_ICON, TOOLBAR_ICON_CLASS);
|
|
61
|
+
const PAGE_WIDTH_FULL_ICON_HTML = lucideIcon(PAGE_WIDTH_FULL_ICON, TOOLBAR_ICON_CLASS);
|
|
55
62
|
const FORMAT_ICON_CLASS = 'format-icon';
|
|
56
63
|
const BOLD_ICON_HTML = lucideIcon(Bold, FORMAT_ICON_CLASS);
|
|
57
64
|
const ITALIC_ICON_HTML = lucideIcon(Italic, FORMAT_ICON_CLASS);
|
|
@@ -151,7 +158,8 @@ function renderContextMenuItem(item) {
|
|
|
151
158
|
? `<span class="nav-menu-shortcut">${escapeHtml(item.shortcut)}</span>`
|
|
152
159
|
: '';
|
|
153
160
|
const disabledAttrs = item.disabled ? ' disabled aria-disabled="true"' : '';
|
|
154
|
-
|
|
161
|
+
const tooltipAttr = item.tooltip ? ` data-tooltip="${escapeHtml(item.tooltip)}"` : '';
|
|
162
|
+
return `<button type="button" role="menuitem" data-menu-action="${escapeHtml(item.action)}"${disabledAttrs}${tooltipAttr}>${icon}${label}${shortcut}</button>`;
|
|
155
163
|
}
|
|
156
164
|
function renderContextMenuItems(items) {
|
|
157
165
|
return items
|
|
@@ -163,11 +171,28 @@ const PREVIEW_MAX_WIDTH_ALIASES = {
|
|
|
163
171
|
default: 900,
|
|
164
172
|
wide: 1200,
|
|
165
173
|
};
|
|
166
|
-
|
|
167
|
-
|
|
174
|
+
/** The CSS value for --mdzip-preview-content-max-width, or undefined for the built-in default. */
|
|
175
|
+
function previewMaxWidthCss(value) {
|
|
176
|
+
if (value === undefined || value === 'default')
|
|
168
177
|
return undefined;
|
|
169
|
-
|
|
178
|
+
if (typeof value === 'number')
|
|
179
|
+
return `${value}px`;
|
|
180
|
+
if (value === 'full')
|
|
181
|
+
return 'none';
|
|
182
|
+
return `calc(${PREVIEW_MAX_WIDTH_ALIASES[value]}px * var(--mdz-zoom, 1))`;
|
|
170
183
|
}
|
|
184
|
+
const PREVIEW_WIDTH_LABELS = {
|
|
185
|
+
narrow: 'Narrow',
|
|
186
|
+
default: 'Default',
|
|
187
|
+
wide: 'Wide',
|
|
188
|
+
full: 'Full Width'
|
|
189
|
+
};
|
|
190
|
+
const PREVIEW_WIDTH_ICONS_HTML = {
|
|
191
|
+
narrow: PAGE_WIDTH_NARROW_ICON_HTML,
|
|
192
|
+
default: PAGE_WIDTH_DEFAULT_ICON_HTML,
|
|
193
|
+
wide: PAGE_WIDTH_WIDE_ICON_HTML,
|
|
194
|
+
full: PAGE_WIDTH_FULL_ICON_HTML
|
|
195
|
+
};
|
|
171
196
|
const ALL_HEADINGS = [1, 2, 3, 4, 5, 6];
|
|
172
197
|
const ALL_LAYOUT_CONTROLS = {
|
|
173
198
|
source: true,
|
|
@@ -218,6 +243,7 @@ const CONTROL_PRESETS = {
|
|
|
218
243
|
lineNumbers: false,
|
|
219
244
|
save: false,
|
|
220
245
|
zoom: false,
|
|
246
|
+
previewWidth: false,
|
|
221
247
|
colorScheme: false,
|
|
222
248
|
orphanActions: false,
|
|
223
249
|
fileActions: false,
|
|
@@ -235,6 +261,7 @@ const CONTROL_PRESETS = {
|
|
|
235
261
|
lineNumbers: true,
|
|
236
262
|
save: false,
|
|
237
263
|
zoom: true,
|
|
264
|
+
previewWidth: true,
|
|
238
265
|
colorScheme: true,
|
|
239
266
|
orphanActions: false,
|
|
240
267
|
fileActions: false,
|
|
@@ -252,6 +279,7 @@ const CONTROL_PRESETS = {
|
|
|
252
279
|
lineNumbers: true,
|
|
253
280
|
save: true,
|
|
254
281
|
zoom: true,
|
|
282
|
+
previewWidth: true,
|
|
255
283
|
colorScheme: true,
|
|
256
284
|
orphanActions: true,
|
|
257
285
|
fileActions: true,
|
|
@@ -269,6 +297,7 @@ const CONTROL_PRESETS = {
|
|
|
269
297
|
lineNumbers: true,
|
|
270
298
|
save: false,
|
|
271
299
|
zoom: true,
|
|
300
|
+
previewWidth: true,
|
|
272
301
|
colorScheme: true,
|
|
273
302
|
orphanActions: true,
|
|
274
303
|
fileActions: true,
|
|
@@ -631,6 +660,44 @@ const noSpellcheckHighlight = ViewPlugin.fromClass(class {
|
|
|
631
660
|
// createCmEditor. Fully inert unless a host imageEditHandler is set — see
|
|
632
661
|
// that click handler's own gate.
|
|
633
662
|
const IMAGE_EDIT_AFFORDANCE_CLICK_EVENT = 'mdzip-image-edit-affordance-click';
|
|
663
|
+
/** Holds an archive image's original `src` in the preview, whose `src` becomes a blob: URL. */
|
|
664
|
+
const PREVIEW_IMAGE_SOURCE_ATTRIBUTE = 'data-mdzip-source';
|
|
665
|
+
// Preview chrome the editor adds around rendered content: removed, or
|
|
666
|
+
// unwrapped (children kept), when the selection is copied as HTML.
|
|
667
|
+
const PREVIEW_CHROME_REMOVE_SELECTOR = [
|
|
668
|
+
'.mdzip-code-block-header',
|
|
669
|
+
'.mdzip-chunk-sentinel',
|
|
670
|
+
'[data-mdzip-image-edit-affordance]'
|
|
671
|
+
].join(',');
|
|
672
|
+
// Subtrees left out of a plain-text copy: the chrome above, plus markup whose
|
|
673
|
+
// text isn't document text (a drawn diagram's SVG carries its stylesheet).
|
|
674
|
+
const PLAIN_TEXT_SKIP_SELECTOR = `${PREVIEW_CHROME_REMOVE_SELECTOR},style,script,template,svg,button`;
|
|
675
|
+
/**
|
|
676
|
+
* The text of rendered preview markup for a plain-text copy: the document's
|
|
677
|
+
* own text, without style or script contents, drawn diagrams or the editor's
|
|
678
|
+
* chrome. Walks the DOM (no clone), so it's cheap on a large live preview.
|
|
679
|
+
*/
|
|
680
|
+
function previewPlainText(root) {
|
|
681
|
+
const parts = [];
|
|
682
|
+
// NodeFilter: SHOW_ELEMENT | SHOW_TEXT; FILTER_ACCEPT 1, REJECT 2 (skips the subtree), SKIP 3.
|
|
683
|
+
const walker = root.ownerDocument.createTreeWalker(root, 0x1 | 0x4, {
|
|
684
|
+
acceptNode: (node) => node.nodeType === 1
|
|
685
|
+
? (node.matches(PLAIN_TEXT_SKIP_SELECTOR) ? 2 : 3)
|
|
686
|
+
: 1
|
|
687
|
+
});
|
|
688
|
+
for (let node = walker.nextNode(); node; node = walker.nextNode()) {
|
|
689
|
+
parts.push(node.data);
|
|
690
|
+
}
|
|
691
|
+
return parts.join('');
|
|
692
|
+
}
|
|
693
|
+
const PLAIN_TEXT_TOOLTIP = 'Text only, for Notepad or a code editor';
|
|
694
|
+
const FORMATTED_TEXT_TOOLTIP = 'Headings, styles and images, for Word, Google Docs or email';
|
|
695
|
+
const PREVIEW_CHROME_UNWRAP_SELECTOR = [
|
|
696
|
+
'.mdzip-chunk',
|
|
697
|
+
'.mdzip-code-block',
|
|
698
|
+
'.mdzip-code-block-body',
|
|
699
|
+
'.mdzip-image-slot'
|
|
700
|
+
].join(',');
|
|
634
701
|
const imageEditAffordanceEffect = StateEffect.define();
|
|
635
702
|
class ImageEditAffordanceWidget extends WidgetType {
|
|
636
703
|
constructor(from, to) {
|
|
@@ -694,6 +761,14 @@ function injectStyles(doc) {
|
|
|
694
761
|
style.textContent = WORKSPACE_CSS;
|
|
695
762
|
(doc.head ?? doc.documentElement).appendChild(style);
|
|
696
763
|
}
|
|
764
|
+
/**
|
|
765
|
+
* Sets a control's tooltip after startup. prepareTooltips() turns `title` into
|
|
766
|
+
* `data-tooltip` only once, so a later `title` would show a stale tooltip.
|
|
767
|
+
*/
|
|
768
|
+
function setTooltip(element, text) {
|
|
769
|
+
element.dataset['tooltip'] = text;
|
|
770
|
+
element.removeAttribute('title');
|
|
771
|
+
}
|
|
697
772
|
function lucideIcon(icon, className) {
|
|
698
773
|
const classAttr = className ? ` class="${escapeHtml(className)}"` : '';
|
|
699
774
|
const children = icon
|
|
@@ -754,34 +829,34 @@ depth = 0) {
|
|
|
754
829
|
: isImageFile(node.entry.path)
|
|
755
830
|
? IMAGE_ICON_HTML
|
|
756
831
|
: FILE_ICON_HTML;
|
|
757
|
-
const orphanBtnHtml = isOrphaned && options.allowOrphanActions ? `
|
|
758
|
-
<span class="nav-orphan-button" role="button" tabindex="0"
|
|
759
|
-
title="Orphaned asset" aria-label="Orphaned asset actions"
|
|
760
|
-
data-orphan-path="${safePath}">
|
|
761
|
-
${ORPHAN_ICON_HTML}
|
|
832
|
+
const orphanBtnHtml = isOrphaned && options.allowOrphanActions ? `
|
|
833
|
+
<span class="nav-orphan-button" role="button" tabindex="0"
|
|
834
|
+
title="Orphaned asset" aria-label="Orphaned asset actions"
|
|
835
|
+
data-orphan-path="${safePath}">
|
|
836
|
+
${ORPHAN_ICON_HTML}
|
|
762
837
|
</span>` : '';
|
|
763
838
|
const draggable = options.allowDrag && !isManifest;
|
|
764
|
-
return `<button type="button" class="${classes}" title="${title}"
|
|
765
|
-
data-nav-path="${safePath}" data-orphan="${isOrphaned ? 'true' : ''}"${draggable ? ' draggable="true"' : ''}>
|
|
766
|
-
${guides}<span class="nav-caret"></span>
|
|
767
|
-
<span class="nav-file-icon ${iconKind}">${iconHtml}</span>
|
|
768
|
-
${orphanBtnHtml}
|
|
769
|
-
<span class="nav-label">${safeName}</span>
|
|
839
|
+
return `<button type="button" class="${classes}" title="${title}"
|
|
840
|
+
data-nav-path="${safePath}" data-orphan="${isOrphaned ? 'true' : ''}"${draggable ? ' draggable="true"' : ''}>
|
|
841
|
+
${guides}<span class="nav-caret"></span>
|
|
842
|
+
<span class="nav-file-icon ${iconKind}">${iconHtml}</span>
|
|
843
|
+
${orphanBtnHtml}
|
|
844
|
+
<span class="nav-label">${safeName}</span>
|
|
770
845
|
</button>`;
|
|
771
846
|
}
|
|
772
847
|
const childTrail = depth === 0 ? [] : [...trail, !isLast];
|
|
773
848
|
const children = node.children
|
|
774
849
|
.map((child, index) => renderNavNode(child, state, options, childTrail, index === node.children.length - 1, depth + 1))
|
|
775
850
|
.join('');
|
|
776
|
-
const pending = options.pendingFolders.has(node.path
|
|
777
|
-
return `<details class="nav-directory${pending ? ' pending-folder' : ''}" open data-nav-dir="${escapeHtml(node.path)}">
|
|
778
|
-
<summary${pending ? ` title="${escapeHtml(node.path)} — not saved until it contains a file"` : ''}>
|
|
779
|
-
${guides}<span class="nav-caret" aria-hidden="true"></span>
|
|
780
|
-
<span class="nav-folder-icon closed">${FOLDER_CLOSED_ICON_HTML}</span>
|
|
781
|
-
<span class="nav-folder-icon open">${FOLDER_OPEN_ICON_HTML}</span>
|
|
782
|
-
<span class="nav-label">${escapeHtml(node.name)}</span>
|
|
783
|
-
</summary>
|
|
784
|
-
<div class="nav-directory-children">${children}</div>
|
|
851
|
+
const pending = options.pendingFolders.has(node.path);
|
|
852
|
+
return `<details class="nav-directory${pending ? ' pending-folder' : ''}" open data-nav-dir="${escapeHtml(node.path)}">
|
|
853
|
+
<summary${pending ? ` title="${escapeHtml(node.path)} — not saved until it contains a file"` : ''}>
|
|
854
|
+
${guides}<span class="nav-caret" aria-hidden="true"></span>
|
|
855
|
+
<span class="nav-folder-icon closed">${FOLDER_CLOSED_ICON_HTML}</span>
|
|
856
|
+
<span class="nav-folder-icon open">${FOLDER_OPEN_ICON_HTML}</span>
|
|
857
|
+
<span class="nav-label">${escapeHtml(node.name)}</span>
|
|
858
|
+
</summary>
|
|
859
|
+
<div class="nav-directory-children">${children}</div>
|
|
785
860
|
</details>`;
|
|
786
861
|
}
|
|
787
862
|
// Inserts view-local pending (not yet saved) folders into the nav tree so they
|
|
@@ -796,7 +871,8 @@ function mergePendingFolders(nodes, pendingPaths) {
|
|
|
796
871
|
let prefix = '';
|
|
797
872
|
for (const segment of path.split('/').filter(Boolean)) {
|
|
798
873
|
prefix = prefix ? `${prefix}/${segment}` : segment;
|
|
799
|
-
|
|
874
|
+
// Folders that differ only in case are different folders (spec 1.2.0 §5.4.1).
|
|
875
|
+
let dir = children.find((n) => !n.entry && n.path === prefix);
|
|
800
876
|
if (!dir) {
|
|
801
877
|
dir = { name: segment, path: prefix, children: [] };
|
|
802
878
|
children.push(dir);
|
|
@@ -857,6 +933,24 @@ export class MdzipWorkspaceView {
|
|
|
857
933
|
this.navVisible = true;
|
|
858
934
|
this.zoom = 1;
|
|
859
935
|
this.zoomOpen = false;
|
|
936
|
+
this.themeMenuOpen = false;
|
|
937
|
+
this.widthMenuOpen = false;
|
|
938
|
+
/** Choice whose icon the page-width button shows, so render() only swaps it on change. */
|
|
939
|
+
this.widthMenuIconChoice = null;
|
|
940
|
+
/** Choice whose icon the theme menu button shows, so render() only swaps it on change. */
|
|
941
|
+
this.themeMenuIconPreference = null;
|
|
942
|
+
this.colorSchemePreference = 'system';
|
|
943
|
+
/** Re-resolves the scheme when the OS setting changes while 'system' is chosen. */
|
|
944
|
+
this.onSystemColorSchemeChange = () => {
|
|
945
|
+
if (this.colorSchemePreference !== 'system' || this.destroyed) {
|
|
946
|
+
return;
|
|
947
|
+
}
|
|
948
|
+
const next = this.resolveColorScheme('system');
|
|
949
|
+
if (next !== this.colorScheme) {
|
|
950
|
+
this.colorScheme = next;
|
|
951
|
+
this.render();
|
|
952
|
+
}
|
|
953
|
+
};
|
|
860
954
|
this.titleDialogOpen = false;
|
|
861
955
|
this.metadataDialogOpen = false;
|
|
862
956
|
this.titleDraft = '';
|
|
@@ -1021,6 +1115,10 @@ export class MdzipWorkspaceView {
|
|
|
1021
1115
|
// True while onPreviewRendered runs — see scrollToAnchor().
|
|
1022
1116
|
this.firingPreviewRendered = false;
|
|
1023
1117
|
this.destroyed = false;
|
|
1118
|
+
/** Counts preview Ctrl+C copies, so a late image upgrade never overwrites a newer copy. */
|
|
1119
|
+
this.previewCopyId = 0;
|
|
1120
|
+
/** The preview Ctrl+A selection's boundaries; Ctrl+C copies the whole document while it's unchanged. */
|
|
1121
|
+
this.previewSelectAll = null;
|
|
1024
1122
|
// After an anchor scroll, content above the target can still change height
|
|
1025
1123
|
// (images loading or easing open, a diagram rendering) and push it out of
|
|
1026
1124
|
// view. This holds the target at the pane's top edge until the user
|
|
@@ -1049,6 +1147,8 @@ export class MdzipWorkspaceView {
|
|
|
1049
1147
|
this.previewMemo = null;
|
|
1050
1148
|
this.previewGeneration = 0;
|
|
1051
1149
|
this.previewAbort = null;
|
|
1150
|
+
/** Aborts an in-flight in-place re-theme (see rethemePreview). */
|
|
1151
|
+
this.rethemeAbort = null;
|
|
1052
1152
|
this.previewHandles = [];
|
|
1053
1153
|
// Whether the latest preview generation has finished mounting and hydrating
|
|
1054
1154
|
// its images; drives `whenRendered()` and gates the hydration callback.
|
|
@@ -1068,10 +1168,10 @@ export class MdzipWorkspaceView {
|
|
|
1068
1168
|
this.contentDensity = options.contentDensity ?? 'comfortable';
|
|
1069
1169
|
this.previewMaxWidth = options.previewMaxWidth;
|
|
1070
1170
|
this.layout = options.initialLayout ?? defaultLayoutForPolicy(this.controlPolicy);
|
|
1071
|
-
this.
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1171
|
+
this.systemColorSchemeQuery = container.ownerDocument.defaultView?.matchMedia?.('(prefers-color-scheme: dark)') ?? null;
|
|
1172
|
+
this.systemColorSchemeQuery?.addEventListener?.('change', this.onSystemColorSchemeChange);
|
|
1173
|
+
this.colorSchemePreference = options.initialColorScheme ?? 'system';
|
|
1174
|
+
this.colorScheme = this.resolveColorScheme(this.colorSchemePreference);
|
|
1075
1175
|
this.navVisible = options.navigationButtonActive ?? this.navVisible;
|
|
1076
1176
|
this.markdownRenderer = options.markdownRenderer;
|
|
1077
1177
|
this.markdownExtensions = options.markdownExtensions ?? [];
|
|
@@ -1099,8 +1199,16 @@ export class MdzipWorkspaceView {
|
|
|
1099
1199
|
this.elSaveBtn = q('[data-ref="save-btn"]');
|
|
1100
1200
|
this.elSearchBtn = q('[data-ref="search-btn"]');
|
|
1101
1201
|
this.elZoomBtn = q('[data-ref="zoom-btn"]');
|
|
1202
|
+
this.elWidthControls = q('[data-ref="width-controls"]');
|
|
1203
|
+
this.elWidthMenuBtn = q('[data-ref="width-menu-btn"]');
|
|
1204
|
+
this.elWidthMenuIcon = q('[data-ref="width-menu-icon"]');
|
|
1205
|
+
this.elWidthPopover = q('[data-ref="width-popover"]');
|
|
1102
1206
|
this.elThemeControls = q('[data-ref="theme-controls"]');
|
|
1103
1207
|
this.elDarkThemeBtn = q('[data-ref="dark-theme-btn"]');
|
|
1208
|
+
this.elThemeMenuBtn = q('[data-ref="theme-menu-btn"]');
|
|
1209
|
+
this.elThemeMenuIcon = q('[data-ref="theme-menu-icon"]');
|
|
1210
|
+
this.elThemePopover = q('[data-ref="theme-popover"]');
|
|
1211
|
+
this.elSystemThemeBtn = q('[data-ref="system-theme-btn"]');
|
|
1104
1212
|
this.elLightThemeBtn = q('[data-ref="light-theme-btn"]');
|
|
1105
1213
|
this.elZoomPopover = q('[data-ref="zoom-popover"]');
|
|
1106
1214
|
this.elZoomLevel = q('[data-ref="zoom-level"]');
|
|
@@ -1451,6 +1559,7 @@ export class MdzipWorkspaceView {
|
|
|
1451
1559
|
this.cmEditor?.focus();
|
|
1452
1560
|
}
|
|
1453
1561
|
destroy() {
|
|
1562
|
+
this.systemColorSchemeQuery?.removeEventListener?.('change', this.onSystemColorSchemeChange);
|
|
1454
1563
|
this.conversionDocumentGeneration += 1;
|
|
1455
1564
|
this.resolveImageInsertDialog(null);
|
|
1456
1565
|
this.resolvePackFilesDialog(null);
|
|
@@ -1601,10 +1710,11 @@ export class MdzipWorkspaceView {
|
|
|
1601
1710
|
this.applyDensityClasses();
|
|
1602
1711
|
}
|
|
1603
1712
|
/**
|
|
1604
|
-
* Sets the preview reading-column width
|
|
1605
|
-
*
|
|
1606
|
-
*
|
|
1607
|
-
*
|
|
1713
|
+
* Sets the preview reading-column width, as the toolbar's page-width menu
|
|
1714
|
+
* does: `'narrow'`, `'default'`, `'wide'` (all scale with zoom) or `'full'`.
|
|
1715
|
+
* A number is an exact pixel width that doesn't scale with zoom, and leaves
|
|
1716
|
+
* no menu item checked. `undefined` is `'default'` (see
|
|
1717
|
+
* {@link MdzipPreviewMaxWidth}).
|
|
1608
1718
|
*/
|
|
1609
1719
|
setPreviewMaxWidth(value) {
|
|
1610
1720
|
if (value === this.previewMaxWidth) {
|
|
@@ -1691,6 +1801,8 @@ export class MdzipWorkspaceView {
|
|
|
1691
1801
|
resetPreviewState() {
|
|
1692
1802
|
this.previewAbort?.abort();
|
|
1693
1803
|
this.previewAbort = null;
|
|
1804
|
+
this.rethemeAbort?.abort();
|
|
1805
|
+
this.rethemeAbort = null;
|
|
1694
1806
|
this.previewGeneration += 1;
|
|
1695
1807
|
this.destroyPreviewHandles();
|
|
1696
1808
|
this.previewMemo = null;
|
|
@@ -1740,6 +1852,64 @@ export class MdzipWorkspaceView {
|
|
|
1740
1852
|
&& memo.text === snapshot.currentText
|
|
1741
1853
|
&& memo.colorScheme === this.colorScheme;
|
|
1742
1854
|
}
|
|
1855
|
+
/**
|
|
1856
|
+
* True when only the color scheme changed since `memo`, and the preview can
|
|
1857
|
+
* be kept: the default renderer (whose HTML doesn't depend on the scheme),
|
|
1858
|
+
* every extension that transforms or mounts content can re-theme in place,
|
|
1859
|
+
* and the current render has finished mounting (otherwise its chunks were
|
|
1860
|
+
* rendered, or are still rendering, with the old scheme).
|
|
1861
|
+
*/
|
|
1862
|
+
canRethemePreviewInPlace(memo, snapshot) {
|
|
1863
|
+
return memo.colorScheme !== this.colorScheme
|
|
1864
|
+
&& memo.path === snapshot.currentPath
|
|
1865
|
+
&& memo.pathType === snapshot.currentPathType
|
|
1866
|
+
&& memo.text === snapshot.currentText
|
|
1867
|
+
&& this.renderingService.supportsChunking
|
|
1868
|
+
&& this.previewMountedGeneration === this.previewGeneration
|
|
1869
|
+
&& !this.chunkedRenderState?.mounting
|
|
1870
|
+
&& this.markdownExtensions.every((extension) => !!extension.updateColorScheme
|
|
1871
|
+
|| !(extension.transformMarkdown || extension.transformHtml || extension.mount));
|
|
1872
|
+
}
|
|
1873
|
+
/**
|
|
1874
|
+
* Applies a scheme-only change without re-rendering: the root's theme class
|
|
1875
|
+
* (set in render()) re-colors the DOM; chunks not mounted yet will render
|
|
1876
|
+
* with the new scheme; cached chunk HTML (which may hold old-scheme diagram
|
|
1877
|
+
* SVG) is dropped; and each extension re-themes its mounted output.
|
|
1878
|
+
*/
|
|
1879
|
+
rethemePreview(snapshot, memo) {
|
|
1880
|
+
memo.colorScheme = this.colorScheme;
|
|
1881
|
+
if (this.chunkedRenderState) {
|
|
1882
|
+
this.chunkedRenderState.context = { ...this.chunkedRenderState.context, colorScheme: this.colorScheme };
|
|
1883
|
+
}
|
|
1884
|
+
this.chunkHtmlCache = null;
|
|
1885
|
+
if (snapshot.currentPathType !== 'markdown') {
|
|
1886
|
+
return;
|
|
1887
|
+
}
|
|
1888
|
+
this.rethemeAbort?.abort();
|
|
1889
|
+
const abort = new AbortController();
|
|
1890
|
+
this.rethemeAbort = abort;
|
|
1891
|
+
const context = this.createMarkdownContext(snapshot, abort.signal);
|
|
1892
|
+
const generation = this.previewGeneration;
|
|
1893
|
+
const report = (error) => {
|
|
1894
|
+
if (generation === this.previewGeneration && !abort.signal.aborted) {
|
|
1895
|
+
this.options.onFailed?.(error);
|
|
1896
|
+
}
|
|
1897
|
+
};
|
|
1898
|
+
for (const extension of this.markdownExtensions) {
|
|
1899
|
+
if (!extension.updateColorScheme) {
|
|
1900
|
+
continue;
|
|
1901
|
+
}
|
|
1902
|
+
try {
|
|
1903
|
+
const result = extension.updateColorScheme(this.elPreviewContent, context);
|
|
1904
|
+
if (isThenable(result)) {
|
|
1905
|
+
void Promise.resolve(result).catch(report);
|
|
1906
|
+
}
|
|
1907
|
+
}
|
|
1908
|
+
catch (error) {
|
|
1909
|
+
report(error);
|
|
1910
|
+
}
|
|
1911
|
+
}
|
|
1912
|
+
}
|
|
1743
1913
|
updatePreview(snapshot, entryClaimed) {
|
|
1744
1914
|
this.lastContentChangeTime = performance.now();
|
|
1745
1915
|
// Captured before anything below can move it — see
|
|
@@ -1764,6 +1934,12 @@ export class MdzipWorkspaceView {
|
|
|
1764
1934
|
// mounted extension handles.
|
|
1765
1935
|
return;
|
|
1766
1936
|
}
|
|
1937
|
+
if (memo && this.canRethemePreviewInPlace(memo, snapshot)) {
|
|
1938
|
+
// Only the color scheme changed: CSS re-colors the existing DOM, and
|
|
1939
|
+
// extensions re-theme their own output, so nothing moves.
|
|
1940
|
+
this.rethemePreview(snapshot, memo);
|
|
1941
|
+
return;
|
|
1942
|
+
}
|
|
1767
1943
|
const animateImageHydration = this.shouldAnimateImageHydration(memo, snapshot);
|
|
1768
1944
|
// A same-document, text-only edit with an existing chunked structure can
|
|
1769
1945
|
// be reconciled against (only the chunks whose source actually changed
|
|
@@ -1787,6 +1963,8 @@ export class MdzipWorkspaceView {
|
|
|
1787
1963
|
const priorChunkedState = reconcileEligible ? this.chunkedRenderState : null;
|
|
1788
1964
|
this.previewAbort?.abort();
|
|
1789
1965
|
this.previewAbort = null;
|
|
1966
|
+
this.rethemeAbort?.abort();
|
|
1967
|
+
this.rethemeAbort = null;
|
|
1790
1968
|
if (!priorChunkedState) {
|
|
1791
1969
|
this.destroyPreviewHandles();
|
|
1792
1970
|
}
|
|
@@ -1958,6 +2136,8 @@ export class MdzipWorkspaceView {
|
|
|
1958
2136
|
}
|
|
1959
2137
|
continue;
|
|
1960
2138
|
}
|
|
2139
|
+
// The src is replaced by a blob: URL; Copy as HTML restores it from here.
|
|
2140
|
+
image.setAttribute(PREVIEW_IMAGE_SOURCE_ATTRIBUTE, source);
|
|
1961
2141
|
const known = this.assetSession.resolveKnownImage(source, context.currentPath);
|
|
1962
2142
|
if (known) {
|
|
1963
2143
|
if (known.width && known.height && !image.hasAttribute('width') && !image.hasAttribute('height')) {
|
|
@@ -2598,10 +2778,15 @@ export class MdzipWorkspaceView {
|
|
|
2598
2778
|
// Same race this field guards against everywhere else: an edit landing
|
|
2599
2779
|
// while this scroll-triggered continuation is mid-flight must not
|
|
2600
2780
|
// reconcile against a cursor that doesn't yet reflect it.
|
|
2781
|
+
// The live context, not the one captured when this sentinel was armed:
|
|
2782
|
+
// a color scheme change since then (rethemePreview) replaces it, and
|
|
2783
|
+
// chunks mounted from here on must render with the new scheme.
|
|
2784
|
+
let liveContext = context;
|
|
2601
2785
|
if (this.chunkedRenderState?.generation === generation) {
|
|
2602
2786
|
this.chunkedRenderState.mounting = true;
|
|
2787
|
+
liveContext = this.chunkedRenderState.context;
|
|
2603
2788
|
}
|
|
2604
|
-
void this.mountChunkBatch(records, nextCursor,
|
|
2789
|
+
void this.mountChunkBatch(records, nextCursor, liveContext, generation, animateImageHydration, () => { })
|
|
2605
2790
|
.then((newCursor) => {
|
|
2606
2791
|
// Record whatever this batch actually mounted regardless of
|
|
2607
2792
|
// whether a newer edit has since landed — recordChunkProgress's
|
|
@@ -2620,7 +2805,7 @@ export class MdzipWorkspaceView {
|
|
|
2620
2805
|
this.chunkedRenderState.mounting = false;
|
|
2621
2806
|
}
|
|
2622
2807
|
if (newCursor < records.length) {
|
|
2623
|
-
this.armChunkSentinel(records, newCursor,
|
|
2808
|
+
this.armChunkSentinel(records, newCursor, liveContext, generation, animateImageHydration);
|
|
2624
2809
|
}
|
|
2625
2810
|
});
|
|
2626
2811
|
};
|
|
@@ -3300,6 +3485,13 @@ export class MdzipWorkspaceView {
|
|
|
3300
3485
|
const showSaveControl = this.controlPolicy.save && snapshot.mode !== 'read-only';
|
|
3301
3486
|
const showZoomControl = this.controlPolicy.zoom;
|
|
3302
3487
|
const showColorSchemeControl = this.controlPolicy.colorScheme;
|
|
3488
|
+
// Only the rendered Markdown preview has a reading column to size, so the
|
|
3489
|
+
// menu is hidden in edit (source) layout and for images and other files.
|
|
3490
|
+
const showWidthControl = this.controlPolicy.previewWidth && !entryClaimed
|
|
3491
|
+
&& snapshot.currentPathType === 'markdown' && this.layout !== 'source';
|
|
3492
|
+
if (!showWidthControl) {
|
|
3493
|
+
this.widthMenuOpen = false;
|
|
3494
|
+
}
|
|
3303
3495
|
const showSearchControl = this.controlPolicy.search
|
|
3304
3496
|
&& canShowSource && snapshot.currentPathType === 'markdown';
|
|
3305
3497
|
const showEditControls = canEdit
|
|
@@ -3308,14 +3500,15 @@ export class MdzipWorkspaceView {
|
|
|
3308
3500
|
&& hasFormattingControls(this.controlPolicy.formatting);
|
|
3309
3501
|
const showToolbar = this.controlPolicy.toolbar
|
|
3310
3502
|
&& (showNavigationControl || showLayoutControls || showSaveControl
|
|
3311
|
-
|| showZoomControl ||
|
|
3503
|
+
|| showZoomControl || showWidthControl || showColorSchemeControl || showSearchControl
|
|
3504
|
+
|| showEditControls);
|
|
3312
3505
|
this.elDocumentStrip.hidden = !showTitleControl;
|
|
3313
3506
|
this.elToolbar.hidden = !showToolbar;
|
|
3314
3507
|
this.elToolbarLeft.hidden = !showNavigationControl;
|
|
3315
3508
|
this.elEditToolbar.hidden = !showEditControls;
|
|
3316
3509
|
this.elLayoutControls.hidden = !showLayoutControls;
|
|
3317
3510
|
this.elToolbarControls.hidden = !showSaveControl && !showZoomControl
|
|
3318
|
-
&& !showColorSchemeControl && !showSearchControl;
|
|
3511
|
+
&& !showWidthControl && !showColorSchemeControl && !showSearchControl;
|
|
3319
3512
|
this.elNavBtn.hidden = !showNavigationControl;
|
|
3320
3513
|
this.elPreviewBtn.hidden = !this.controlPolicy.layout.preview;
|
|
3321
3514
|
this.elSplitBtn.hidden = !this.controlPolicy.layout.split;
|
|
@@ -3323,16 +3516,17 @@ export class MdzipWorkspaceView {
|
|
|
3323
3516
|
this.elSaveBtn.hidden = !showSaveControl;
|
|
3324
3517
|
this.elSearchBtn.hidden = !showSearchControl;
|
|
3325
3518
|
this.elZoomBtn.hidden = !showZoomControl;
|
|
3519
|
+
this.elWidthControls.hidden = !showWidthControl;
|
|
3326
3520
|
this.elThemeControls.hidden = !showColorSchemeControl;
|
|
3327
3521
|
this.elRoot.style.setProperty('--mdz-zoom', String(this.zoom));
|
|
3328
3522
|
this.elRoot.style.setProperty('--nav-pane-width', `${this.navPaneWidth}px`);
|
|
3329
3523
|
this.elRoot.style.setProperty('--split-edit-ratio', String(this.splitRatio));
|
|
3330
|
-
const
|
|
3331
|
-
if (
|
|
3524
|
+
const previewMaxWidth = previewMaxWidthCss(this.previewMaxWidth);
|
|
3525
|
+
if (previewMaxWidth === undefined) {
|
|
3332
3526
|
this.elRoot.style.removeProperty('--mdzip-preview-content-max-width');
|
|
3333
3527
|
}
|
|
3334
3528
|
else {
|
|
3335
|
-
this.elRoot.style.setProperty('--mdzip-preview-content-max-width',
|
|
3529
|
+
this.elRoot.style.setProperty('--mdzip-preview-content-max-width', previewMaxWidth);
|
|
3336
3530
|
}
|
|
3337
3531
|
this.elRoot.classList.toggle('resizing', this.resizing);
|
|
3338
3532
|
this.elRoot.classList.toggle('mdzip-theme-dark', this.colorScheme === 'dark');
|
|
@@ -3392,10 +3586,41 @@ export class MdzipWorkspaceView {
|
|
|
3392
3586
|
this.elNavBtn.setAttribute('aria-pressed', String(this.navVisible));
|
|
3393
3587
|
}
|
|
3394
3588
|
this.elZoomBtn.classList.toggle('active', this.zoomOpen);
|
|
3395
|
-
|
|
3396
|
-
|
|
3397
|
-
|
|
3398
|
-
|
|
3589
|
+
for (const [button, preference] of this.themeMenuItems()) {
|
|
3590
|
+
button.classList.toggle('active', this.colorSchemePreference === preference);
|
|
3591
|
+
button.setAttribute('aria-checked', String(this.colorSchemePreference === preference));
|
|
3592
|
+
}
|
|
3593
|
+
if (this.themeMenuIconPreference !== this.colorSchemePreference) {
|
|
3594
|
+
this.themeMenuIconPreference = this.colorSchemePreference;
|
|
3595
|
+
this.elThemeMenuIcon.innerHTML = this.colorSchemePreference === 'system'
|
|
3596
|
+
? SYSTEM_THEME_ICON_HTML
|
|
3597
|
+
: this.colorSchemePreference === 'dark' ? DARK_THEME_ICON_HTML : LIGHT_THEME_ICON_HTML;
|
|
3598
|
+
}
|
|
3599
|
+
const schemeLabel = this.colorSchemePreference === 'system'
|
|
3600
|
+
? `System (${this.colorScheme})`
|
|
3601
|
+
: this.colorSchemePreference === 'dark' ? 'Dark' : 'Light';
|
|
3602
|
+
setTooltip(this.elThemeMenuBtn, `Color scheme: ${schemeLabel}`);
|
|
3603
|
+
this.elThemeMenuBtn.setAttribute('aria-label', `Color scheme: ${schemeLabel}`);
|
|
3604
|
+
this.elThemeMenuBtn.classList.toggle('active', this.themeMenuOpen);
|
|
3605
|
+
this.elThemeMenuBtn.setAttribute('aria-expanded', String(this.themeMenuOpen));
|
|
3606
|
+
this.elThemePopover.hidden = !this.themeMenuOpen;
|
|
3607
|
+
const widthChoice = this.previewWidthChoice();
|
|
3608
|
+
for (const [button, choice] of this.widthMenuItems()) {
|
|
3609
|
+
button.classList.toggle('active', widthChoice === choice);
|
|
3610
|
+
button.setAttribute('aria-checked', String(widthChoice === choice));
|
|
3611
|
+
}
|
|
3612
|
+
// A custom pixel width (set by the host) shows the default icon, with no item checked.
|
|
3613
|
+
const widthIconChoice = widthChoice ?? 'default';
|
|
3614
|
+
if (this.widthMenuIconChoice !== widthIconChoice) {
|
|
3615
|
+
this.widthMenuIconChoice = widthIconChoice;
|
|
3616
|
+
this.elWidthMenuIcon.innerHTML = PREVIEW_WIDTH_ICONS_HTML[widthIconChoice];
|
|
3617
|
+
}
|
|
3618
|
+
// Measuring the pane forces layout, so only while the menu is open; hover
|
|
3619
|
+
// and focus on the button refresh the labels too.
|
|
3620
|
+
this.updateWidthLabels(this.widthMenuOpen);
|
|
3621
|
+
this.elWidthMenuBtn.classList.toggle('active', this.widthMenuOpen);
|
|
3622
|
+
this.elWidthMenuBtn.setAttribute('aria-expanded', String(this.widthMenuOpen));
|
|
3623
|
+
this.elWidthPopover.hidden = !this.widthMenuOpen;
|
|
3399
3624
|
this.elZoomPopover.hidden = !this.zoomOpen;
|
|
3400
3625
|
this.elZoomLevel.textContent = `${Math.round(this.zoom * 100)}%`;
|
|
3401
3626
|
const showNavigationPane = this.controlPolicy.navigation
|
|
@@ -3415,7 +3640,7 @@ export class MdzipWorkspaceView {
|
|
|
3415
3640
|
allowOrphanActions,
|
|
3416
3641
|
allowFileActions,
|
|
3417
3642
|
allowDrag: snapshot.mode !== 'read-only' && snapshot.sourceFormat === 'mdz',
|
|
3418
|
-
pendingFolders: new Set(
|
|
3643
|
+
pendingFolders: new Set(this.pendingNewFolders)
|
|
3419
3644
|
};
|
|
3420
3645
|
this.elNavTree.innerHTML = navTree
|
|
3421
3646
|
.map((n, i) => renderNavNode(n, snapshot, navRenderOptions, [], i === navTree.length - 1, 0))
|
|
@@ -3552,9 +3777,9 @@ export class MdzipWorkspaceView {
|
|
|
3552
3777
|
this.pendingNewFolders.clear();
|
|
3553
3778
|
return;
|
|
3554
3779
|
}
|
|
3555
|
-
const
|
|
3780
|
+
const paths = snapshot.content.paths.map((entry) => entry.path);
|
|
3556
3781
|
for (const folder of [...this.pendingNewFolders]) {
|
|
3557
|
-
if (
|
|
3782
|
+
if (paths.some((path) => path.startsWith(`${folder}/`))) {
|
|
3558
3783
|
this.pendingNewFolders.delete(folder);
|
|
3559
3784
|
}
|
|
3560
3785
|
}
|
|
@@ -3563,8 +3788,10 @@ export class MdzipWorkspaceView {
|
|
|
3563
3788
|
const doc = this.elRoot.ownerDocument;
|
|
3564
3789
|
doc.addEventListener('click', () => {
|
|
3565
3790
|
this.closeFormatMenus();
|
|
3566
|
-
if (this.zoomOpen || this.contextMenuState) {
|
|
3791
|
+
if (this.zoomOpen || this.themeMenuOpen || this.widthMenuOpen || this.contextMenuState) {
|
|
3567
3792
|
this.zoomOpen = false;
|
|
3793
|
+
this.themeMenuOpen = false;
|
|
3794
|
+
this.widthMenuOpen = false;
|
|
3568
3795
|
this.contextMenuState = null;
|
|
3569
3796
|
this.render();
|
|
3570
3797
|
}
|
|
@@ -3573,6 +3800,12 @@ export class MdzipWorkspaceView {
|
|
|
3573
3800
|
if (e.key !== 'Escape') {
|
|
3574
3801
|
return;
|
|
3575
3802
|
}
|
|
3803
|
+
if (this.themeMenuOpen) {
|
|
3804
|
+
this.closeThemeMenu(true);
|
|
3805
|
+
}
|
|
3806
|
+
if (this.widthMenuOpen) {
|
|
3807
|
+
this.closeWidthMenu(true);
|
|
3808
|
+
}
|
|
3576
3809
|
if (this.contextMenuState || this.deleteDialogState || this.nameDialogState) {
|
|
3577
3810
|
this.contextMenuState = null;
|
|
3578
3811
|
this.deleteDialogState = null;
|
|
@@ -3581,11 +3814,12 @@ export class MdzipWorkspaceView {
|
|
|
3581
3814
|
}
|
|
3582
3815
|
});
|
|
3583
3816
|
// Ctrl/Cmd+A normally selects the whole page, which in split layout grabs
|
|
3584
|
-
// both panes' text at once.
|
|
3585
|
-
//
|
|
3586
|
-
//
|
|
3587
|
-
//
|
|
3588
|
-
//
|
|
3817
|
+
// both panes' text at once. With focus inside the preview pane (see the
|
|
3818
|
+
// mousedown handler below for how it gets there) it selects just the
|
|
3819
|
+
// rendered content; the source editor keeps its own defaultKeymap binding
|
|
3820
|
+
// since CodeMirror's content root sits outside this element. The
|
|
3821
|
+
// selection is remembered so the copy handler can copy the whole
|
|
3822
|
+
// document, including chunks not mounted yet, while it's unchanged.
|
|
3589
3823
|
doc.addEventListener('keydown', (e) => {
|
|
3590
3824
|
if (e.key.toLowerCase() !== 'a' || !(e.ctrlKey || e.metaKey) || e.shiftKey || e.altKey) {
|
|
3591
3825
|
return;
|
|
@@ -3594,7 +3828,7 @@ export class MdzipWorkspaceView {
|
|
|
3594
3828
|
return;
|
|
3595
3829
|
}
|
|
3596
3830
|
e.preventDefault();
|
|
3597
|
-
|
|
3831
|
+
this.selectAllPreview();
|
|
3598
3832
|
});
|
|
3599
3833
|
this.elNavBtn.addEventListener('click', () => {
|
|
3600
3834
|
if (!this.controlPolicy.navigation) {
|
|
@@ -3661,6 +3895,8 @@ export class MdzipWorkspaceView {
|
|
|
3661
3895
|
}
|
|
3662
3896
|
e.stopPropagation();
|
|
3663
3897
|
this.zoomOpen = !this.zoomOpen;
|
|
3898
|
+
this.themeMenuOpen = false;
|
|
3899
|
+
this.widthMenuOpen = false;
|
|
3664
3900
|
this.render();
|
|
3665
3901
|
});
|
|
3666
3902
|
this.elZoomPopover.addEventListener('click', (e) => e.stopPropagation());
|
|
@@ -3670,8 +3906,47 @@ export class MdzipWorkspaceView {
|
|
|
3670
3906
|
.addEventListener('click', () => this.setZoom(this.zoom + 0.1));
|
|
3671
3907
|
this.elZoomPopover.querySelector('[data-action="zoom-reset"]')
|
|
3672
3908
|
.addEventListener('click', () => this.setZoom(1));
|
|
3673
|
-
this.
|
|
3674
|
-
|
|
3909
|
+
this.elThemeMenuBtn.addEventListener('click', (e) => {
|
|
3910
|
+
e.stopPropagation();
|
|
3911
|
+
this.themeMenuOpen = !this.themeMenuOpen;
|
|
3912
|
+
this.zoomOpen = false;
|
|
3913
|
+
this.widthMenuOpen = false;
|
|
3914
|
+
this.render();
|
|
3915
|
+
// Keyboard activation (Enter/Space reports detail 0) moves focus into the
|
|
3916
|
+
// menu; a mouse click doesn't, so no focus tooltip covers the icons.
|
|
3917
|
+
if (this.themeMenuOpen && e.detail === 0) {
|
|
3918
|
+
this.themeMenuItems().find(([, preference]) => preference === this.colorSchemePreference)?.[0].focus();
|
|
3919
|
+
}
|
|
3920
|
+
});
|
|
3921
|
+
this.elThemePopover.addEventListener('click', (e) => e.stopPropagation());
|
|
3922
|
+
this.elThemePopover.addEventListener('keydown', (e) => {
|
|
3923
|
+
this.moveMenuFocus(e, this.themeMenuItems().map(([button]) => button));
|
|
3924
|
+
});
|
|
3925
|
+
for (const [button, preference] of this.themeMenuItems()) {
|
|
3926
|
+
button.addEventListener('click', () => this.chooseColorSchemeFromMenu(preference));
|
|
3927
|
+
}
|
|
3928
|
+
this.elWidthMenuBtn.addEventListener('click', (e) => {
|
|
3929
|
+
e.stopPropagation();
|
|
3930
|
+
this.widthMenuOpen = !this.widthMenuOpen;
|
|
3931
|
+
this.zoomOpen = false;
|
|
3932
|
+
this.themeMenuOpen = false;
|
|
3933
|
+
this.render();
|
|
3934
|
+
// As with the theme menu, only keyboard activation moves focus inside.
|
|
3935
|
+
if (this.widthMenuOpen && e.detail === 0) {
|
|
3936
|
+
const items = this.widthMenuItems();
|
|
3937
|
+
const choice = this.previewWidthChoice() ?? 'default';
|
|
3938
|
+
(items.find(([, item]) => item === choice) ?? items[0])[0].focus();
|
|
3939
|
+
}
|
|
3940
|
+
});
|
|
3941
|
+
this.elWidthMenuBtn.addEventListener('pointerenter', () => this.updateWidthLabels(true));
|
|
3942
|
+
this.elWidthMenuBtn.addEventListener('focus', () => this.updateWidthLabels(true));
|
|
3943
|
+
this.elWidthPopover.addEventListener('click', (e) => e.stopPropagation());
|
|
3944
|
+
this.elWidthPopover.addEventListener('keydown', (e) => {
|
|
3945
|
+
this.moveMenuFocus(e, this.widthMenuItems().map(([button]) => button));
|
|
3946
|
+
});
|
|
3947
|
+
for (const [button, choice] of this.widthMenuItems()) {
|
|
3948
|
+
button.addEventListener('click', () => this.choosePreviewWidthFromMenu(choice));
|
|
3949
|
+
}
|
|
3675
3950
|
this.elEditToolbar.addEventListener('click', (event) => {
|
|
3676
3951
|
const menuToggle = event.target
|
|
3677
3952
|
.closest('[data-format-menu-toggle]');
|
|
@@ -3973,6 +4248,41 @@ export class MdzipWorkspaceView {
|
|
|
3973
4248
|
e.stopPropagation();
|
|
3974
4249
|
this.render();
|
|
3975
4250
|
});
|
|
4251
|
+
// Ctrl+C on a preview selection copies Formatted Text, like the menu item:
|
|
4252
|
+
// clean markup and plain text now (the copy event is synchronous), then,
|
|
4253
|
+
// when the selection has images, the same copy again with them embedded,
|
|
4254
|
+
// since the preview's own blob: URLs only resolve inside this view.
|
|
4255
|
+
doc.addEventListener('copy', (e) => {
|
|
4256
|
+
const selection = doc.defaultView?.getSelection();
|
|
4257
|
+
if (this.destroyed || !e.clipboardData || !selection || selection.isCollapsed || selection.rangeCount === 0
|
|
4258
|
+
|| !this.elPreviewContent.contains(selection.anchorNode)
|
|
4259
|
+
|| this.workspace?.snapshot().currentPathType !== 'markdown') {
|
|
4260
|
+
return;
|
|
4261
|
+
}
|
|
4262
|
+
const range = selection.getRangeAt(0);
|
|
4263
|
+
const all = this.previewSelectAll;
|
|
4264
|
+
if (all && range.startContainer === all.start && range.startOffset === all.startOffset
|
|
4265
|
+
&& range.endContainer === all.end && range.endOffset === all.endOffset) {
|
|
4266
|
+
// Ctrl+A, Ctrl+C: the whole document as Formatted Text, like Copy All >
|
|
4267
|
+
// Formatted Text, so chunks not mounted yet are copied too. Nothing
|
|
4268
|
+
// goes on the clipboard synchronously: the mounted part alone would
|
|
4269
|
+
// be a silently partial copy.
|
|
4270
|
+
e.preventDefault();
|
|
4271
|
+
void this.copyAllWithImagesPreviewContent();
|
|
4272
|
+
return;
|
|
4273
|
+
}
|
|
4274
|
+
const { text, html } = this.previewSelectionContent(range);
|
|
4275
|
+
if (!html) {
|
|
4276
|
+
return;
|
|
4277
|
+
}
|
|
4278
|
+
e.preventDefault();
|
|
4279
|
+
e.clipboardData.setData('text/plain', text);
|
|
4280
|
+
e.clipboardData.setData('text/html', html);
|
|
4281
|
+
const copyId = (this.previewCopyId += 1);
|
|
4282
|
+
if (this.assetSession && /<img\b/i.test(html)) {
|
|
4283
|
+
void this.upgradePreviewCopyImages(copyId, html, text);
|
|
4284
|
+
}
|
|
4285
|
+
});
|
|
3976
4286
|
this.elPreviewPane.addEventListener('contextmenu', (e) => {
|
|
3977
4287
|
if (!this.controlPolicy.contextMenu.preview) {
|
|
3978
4288
|
return;
|
|
@@ -3981,11 +4291,12 @@ export class MdzipWorkspaceView {
|
|
|
3981
4291
|
// button collapses the browser selection before the action runs (same
|
|
3982
4292
|
// reason the editor menu captures its CodeMirror range up front).
|
|
3983
4293
|
const domSelection = this.elPreviewPane.ownerDocument.defaultView?.getSelection();
|
|
3984
|
-
const
|
|
3985
|
-
&& this.elPreviewContent.contains(domSelection.anchorNode)
|
|
3986
|
-
|
|
3987
|
-
|
|
3988
|
-
|
|
4294
|
+
const selected = domSelection && !domSelection.isCollapsed
|
|
4295
|
+
&& this.elPreviewContent.contains(domSelection.anchorNode);
|
|
4296
|
+
const { text, html } = selected && domSelection.rangeCount > 0
|
|
4297
|
+
? this.previewSelectionContent(domSelection.getRangeAt(0))
|
|
4298
|
+
: { text: '', html: '' };
|
|
4299
|
+
this.contextMenuState = { kind: 'preview', text, html, x: e.clientX, y: e.clientY };
|
|
3989
4300
|
if (this.previewMenuItems().length === 0) {
|
|
3990
4301
|
this.contextMenuState = null;
|
|
3991
4302
|
return;
|
|
@@ -4117,7 +4428,7 @@ export class MdzipWorkspaceView {
|
|
|
4117
4428
|
async openPathAndScrollToAnchor(path, fragment) {
|
|
4118
4429
|
const generationBefore = this.previewGeneration;
|
|
4119
4430
|
this.pendingAnchorFragment = fragment || null;
|
|
4120
|
-
await this.
|
|
4431
|
+
await this.openPathOnly(path);
|
|
4121
4432
|
// No new render started (already on that document, or the open failed):
|
|
4122
4433
|
// nothing will call firePreviewRendered, so apply the anchor now.
|
|
4123
4434
|
if (this.pendingAnchorFragment !== null && this.previewGeneration === generationBefore) {
|
|
@@ -4290,6 +4601,10 @@ export class MdzipWorkspaceView {
|
|
|
4290
4601
|
if (collectMdzipHeadingIds(record.tokens).some((id) => id.toLowerCase() === lower)) {
|
|
4291
4602
|
return true;
|
|
4292
4603
|
}
|
|
4604
|
+
// The footnotes section (#58) renders footnote-<label> targets.
|
|
4605
|
+
if (lower.startsWith('footnote-') && record.tokens.some((token) => token.type === 'footnotes')) {
|
|
4606
|
+
return true;
|
|
4607
|
+
}
|
|
4293
4608
|
// An explicit <a id="x"> / <a name="x"> anchor written in the source.
|
|
4294
4609
|
const escaped = fragment.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
4295
4610
|
return new RegExp(`(?:id|name)\\s*=\\s*["']${escaped}["']`, 'i').test(record.key);
|
|
@@ -4359,9 +4674,53 @@ export class MdzipWorkspaceView {
|
|
|
4359
4674
|
this.anchorPin?.disconnect();
|
|
4360
4675
|
this.anchorPin = null;
|
|
4361
4676
|
}
|
|
4362
|
-
|
|
4677
|
+
/**
|
|
4678
|
+
* Opens another file of the current workspace, as clicking it in the file
|
|
4679
|
+
* tree does, optionally landing on a heading or anchor in it (#53). Paths
|
|
4680
|
+
* match the archive exactly (case-sensitive after NFC normalization).
|
|
4681
|
+
*
|
|
4682
|
+
* Resolves `true` once the file is shown and, when `anchor` is given, the
|
|
4683
|
+
* preview has scrolled to it — with the same pinning as {@link scrollToAnchor}
|
|
4684
|
+
* while images above it load. Resolves `false` for a path that isn't in the
|
|
4685
|
+
* workspace (the current file stays open) or an anchor that isn't found
|
|
4686
|
+
* (the file stays open at its top).
|
|
4687
|
+
*/
|
|
4688
|
+
async openPath(path, options = {}) {
|
|
4689
|
+
if (!(await this.openPathOnly(path))) {
|
|
4690
|
+
return false;
|
|
4691
|
+
}
|
|
4692
|
+
return options.anchor === undefined ? true : this.scrollToAnchor(options.anchor);
|
|
4693
|
+
}
|
|
4694
|
+
/**
|
|
4695
|
+
* Changes the file name shown in the title bar and used to save, without
|
|
4696
|
+
* reopening the workspace or changing the selected file, e.g. after Save As
|
|
4697
|
+
* (#51). Returns false, changing nothing, when no workspace is open or the
|
|
4698
|
+
* new name implies a different format (`.md` vs `.mdz`): that needs a reopen.
|
|
4699
|
+
*/
|
|
4700
|
+
setFileName(fileName) {
|
|
4701
|
+
const snapshot = this.workspace?.snapshot();
|
|
4702
|
+
if (!this.workspace || !snapshot) {
|
|
4703
|
+
return false;
|
|
4704
|
+
}
|
|
4705
|
+
const lower = fileName.toLowerCase();
|
|
4706
|
+
const impliedFormat = /\.(?:md|markdown|mdown|mkd)$/.test(lower)
|
|
4707
|
+
? 'markdown'
|
|
4708
|
+
: lower.endsWith('.mdz') ? 'mdz' : snapshot.sourceFormat;
|
|
4709
|
+
if (impliedFormat !== snapshot.sourceFormat) {
|
|
4710
|
+
return false;
|
|
4711
|
+
}
|
|
4712
|
+
if (this.workspace.setFileName(fileName)) {
|
|
4713
|
+
this.render();
|
|
4714
|
+
}
|
|
4715
|
+
return true;
|
|
4716
|
+
}
|
|
4717
|
+
/** The archive path of the file currently shown, or null before a workspace opens. */
|
|
4718
|
+
getCurrentPath() {
|
|
4719
|
+
return this.workspace?.snapshot().currentPath ?? null;
|
|
4720
|
+
}
|
|
4721
|
+
async openPathOnly(path) {
|
|
4363
4722
|
if (!this.workspace) {
|
|
4364
|
-
return;
|
|
4723
|
+
return false;
|
|
4365
4724
|
}
|
|
4366
4725
|
try {
|
|
4367
4726
|
const opened = await this.workspace.openPath(path);
|
|
@@ -4369,9 +4728,11 @@ export class MdzipWorkspaceView {
|
|
|
4369
4728
|
this.layout = this.validLayoutForSnapshot(this.layout, this.workspace.snapshot());
|
|
4370
4729
|
this.render();
|
|
4371
4730
|
}
|
|
4731
|
+
return opened;
|
|
4372
4732
|
}
|
|
4373
4733
|
catch (error) {
|
|
4374
4734
|
this.options.onFailed?.(error);
|
|
4735
|
+
return false;
|
|
4375
4736
|
}
|
|
4376
4737
|
}
|
|
4377
4738
|
handleTooltipPointer(event) {
|
|
@@ -4403,9 +4764,16 @@ export class MdzipWorkspaceView {
|
|
|
4403
4764
|
}
|
|
4404
4765
|
showTooltipForElementNow(element) {
|
|
4405
4766
|
const rect = element.getBoundingClientRect();
|
|
4406
|
-
|
|
4767
|
+
// A menu item's tip goes beside it, so it doesn't cover the items above.
|
|
4768
|
+
const placement = element.closest('.nav-context-menu') ? 'side' : 'above';
|
|
4769
|
+
this.showTooltip(element.dataset['tooltip'] ?? '', rect.left + rect.width / 2, rect.top, placement, rect);
|
|
4407
4770
|
}
|
|
4408
|
-
|
|
4771
|
+
/**
|
|
4772
|
+
* Shows the tooltip centered above (anchorX, anchorY), or, with `side`
|
|
4773
|
+
* placement, beside `anchorRect` (to its right, or its left when there's no
|
|
4774
|
+
* room), vertically centered on it.
|
|
4775
|
+
*/
|
|
4776
|
+
showTooltip(text, anchorX, anchorY, placement = 'above', anchorRect) {
|
|
4409
4777
|
if (!text) {
|
|
4410
4778
|
this.hideTooltip();
|
|
4411
4779
|
return;
|
|
@@ -4418,9 +4786,23 @@ export class MdzipWorkspaceView {
|
|
|
4418
4786
|
const offset = 8;
|
|
4419
4787
|
this.elTooltip.textContent = text;
|
|
4420
4788
|
this.elTooltip.hidden = false;
|
|
4789
|
+
// Measured at the origin, so its previous position doesn't make it wrap early.
|
|
4790
|
+
this.elTooltip.style.left = '0px';
|
|
4791
|
+
this.elTooltip.style.top = '0px';
|
|
4421
4792
|
const rect = this.elTooltip.getBoundingClientRect();
|
|
4422
|
-
|
|
4423
|
-
|
|
4793
|
+
let x;
|
|
4794
|
+
let y;
|
|
4795
|
+
if (placement === 'side' && anchorRect) {
|
|
4796
|
+
const right = anchorRect.right + offset;
|
|
4797
|
+
x = right + rect.width <= window.innerWidth - viewportPadding
|
|
4798
|
+
? right
|
|
4799
|
+
: Math.max(viewportPadding, anchorRect.left - offset - rect.width);
|
|
4800
|
+
y = Math.max(viewportPadding, Math.min(anchorRect.top + (anchorRect.height - rect.height) / 2, window.innerHeight - rect.height - viewportPadding));
|
|
4801
|
+
}
|
|
4802
|
+
else {
|
|
4803
|
+
x = Math.max(viewportPadding, Math.min(anchorX - rect.width / 2, window.innerWidth - rect.width - viewportPadding));
|
|
4804
|
+
y = Math.max(viewportPadding, anchorY - rect.height - offset);
|
|
4805
|
+
}
|
|
4424
4806
|
this.tooltipState = { text, x, y };
|
|
4425
4807
|
this.elTooltip.style.left = `${Math.round(x)}px`;
|
|
4426
4808
|
this.elTooltip.style.top = `${Math.round(y)}px`;
|
|
@@ -5222,21 +5604,43 @@ export class MdzipWorkspaceView {
|
|
|
5222
5604
|
return [];
|
|
5223
5605
|
}
|
|
5224
5606
|
const items = [];
|
|
5225
|
-
|
|
5226
|
-
|
|
5227
|
-
|
|
5228
|
-
|
|
5229
|
-
|
|
5230
|
-
|
|
5231
|
-
label: '
|
|
5232
|
-
|
|
5233
|
-
|
|
5234
|
-
|
|
5235
|
-
|
|
5236
|
-
|
|
5237
|
-
|
|
5238
|
-
|
|
5239
|
-
|
|
5607
|
+
const markdown = this.workspace?.snapshot().currentPathType === 'markdown';
|
|
5608
|
+
// Copy: the selection, in the same formats as Copy All below (no
|
|
5609
|
+
// Markdown: see there). A selection of only an image has no text, but
|
|
5610
|
+
// still has markup.
|
|
5611
|
+
const copy = [];
|
|
5612
|
+
if (state.text.trim()) {
|
|
5613
|
+
copy.push({ action: 'preview-copy', label: 'Plain Text', tooltip: PLAIN_TEXT_TOOLTIP });
|
|
5614
|
+
}
|
|
5615
|
+
if (markdown && state.html) {
|
|
5616
|
+
copy.push({
|
|
5617
|
+
action: 'preview-copy-selection-formatted',
|
|
5618
|
+
label: 'Formatted Text',
|
|
5619
|
+
shortcut: this.editorShortcut('C'),
|
|
5620
|
+
tooltip: FORMATTED_TEXT_TOOLTIP
|
|
5621
|
+
}, { action: 'preview-copy-selection-html', label: 'HTML', tooltip: "The selection's HTML markup, as text" });
|
|
5622
|
+
}
|
|
5623
|
+
if (copy.length > 0) {
|
|
5624
|
+
items.push({ action: 'preview-copy-menu', label: 'Copy', icon: MENU_COPY_ICON_HTML, submenu: copy }, null);
|
|
5625
|
+
}
|
|
5626
|
+
// Copy All: the whole document, in the format each item names. Markdown
|
|
5627
|
+
// and HTML are source copies (#56); the preview can't map a selection
|
|
5628
|
+
// back to exact Markdown (the source pane's own Copy does that).
|
|
5629
|
+
const copyAll = [
|
|
5630
|
+
{ action: 'preview-copy-all', label: 'Plain Text', tooltip: PLAIN_TEXT_TOOLTIP },
|
|
5631
|
+
{
|
|
5632
|
+
action: 'preview-copy-all-images',
|
|
5633
|
+
label: 'Formatted Text',
|
|
5634
|
+
shortcut: `${this.editorShortcut('A')}, ${this.editorShortcut('C')}`,
|
|
5635
|
+
tooltip: FORMATTED_TEXT_TOOLTIP
|
|
5636
|
+
}
|
|
5637
|
+
];
|
|
5638
|
+
if (markdown) {
|
|
5639
|
+
copyAll.push({ action: 'preview-copy-markdown', label: 'Markdown', tooltip: "The document's Markdown source" }, { action: 'preview-copy-html', label: 'HTML', tooltip: "The document's HTML markup, as text" });
|
|
5640
|
+
}
|
|
5641
|
+
items.push({ action: 'preview-copy-all-menu', label: 'Copy All', icon: MENU_COPY_ICON_HTML, submenu: copyAll });
|
|
5642
|
+
// Same item and group as the source editor's menu.
|
|
5643
|
+
items.push(null, { action: 'preview-select-all', label: 'Select All', icon: MENU_SELECT_ALL_ICON_HTML, shortcut: this.editorShortcut('A') });
|
|
5240
5644
|
return items;
|
|
5241
5645
|
}
|
|
5242
5646
|
handlePreviewMenuAction(action) {
|
|
@@ -5253,6 +5657,27 @@ export class MdzipWorkspaceView {
|
|
|
5253
5657
|
case 'preview-copy-all-images':
|
|
5254
5658
|
void this.copyAllWithImagesPreviewContent();
|
|
5255
5659
|
break;
|
|
5660
|
+
case 'preview-copy-html':
|
|
5661
|
+
void this.copyHtmlPreviewContent();
|
|
5662
|
+
break;
|
|
5663
|
+
case 'preview-copy-markdown': {
|
|
5664
|
+
const markdown = this.workspace?.snapshot().currentText ?? '';
|
|
5665
|
+
void this.writeClipboardText(markdown, 'Markdown copied to clipboard')
|
|
5666
|
+
.then((outcome) => this.finishCopyNotification(false, outcome));
|
|
5667
|
+
break;
|
|
5668
|
+
}
|
|
5669
|
+
case 'preview-select-all':
|
|
5670
|
+
this.elPreviewPane.focus({ preventScroll: true });
|
|
5671
|
+
this.selectAllPreview();
|
|
5672
|
+
break;
|
|
5673
|
+
case 'preview-copy-selection-formatted':
|
|
5674
|
+
void this.copyPreviewSelectionFormatted(state.html, state.text)
|
|
5675
|
+
.then((outcome) => this.finishCopyNotification(false, outcome));
|
|
5676
|
+
break;
|
|
5677
|
+
case 'preview-copy-selection-html':
|
|
5678
|
+
void this.writeClipboardText(state.html, 'HTML copied to clipboard as text')
|
|
5679
|
+
.then((outcome) => this.finishCopyNotification(false, outcome));
|
|
5680
|
+
break;
|
|
5256
5681
|
case 'preview-copy':
|
|
5257
5682
|
// Partial Copy never shows the render dialog (there's nothing to
|
|
5258
5683
|
// drain), so its confirmation is always the brief toast.
|
|
@@ -5331,19 +5756,198 @@ export class MdzipWorkspaceView {
|
|
|
5331
5756
|
}
|
|
5332
5757
|
}
|
|
5333
5758
|
/** Writes `text` to the clipboard. Returns null if there was nothing to copy (a true no-op, not a failure); otherwise a success message or the failure text (also reported via `onFailed`, but that's a host callback with no guaranteed visible surface — the caller needs the text itself to show the user). */
|
|
5334
|
-
|
|
5759
|
+
copyPreviewSelection(text) {
|
|
5760
|
+
return this.writeClipboardText(text, 'Plain text copied to clipboard');
|
|
5761
|
+
}
|
|
5762
|
+
/**
|
|
5763
|
+
* A preview selection as the Copy items copy it. `html`: the selected
|
|
5764
|
+
* markup inside its enclosing elements (so part of a list item is still
|
|
5765
|
+
* `<ul><li>…`), without the editor's own chrome (code-block headers, image
|
|
5766
|
+
* slots and edit buttons, chunk wrappers) and with archive images' `src`
|
|
5767
|
+
* back to their archive paths instead of blob: URLs. `text`: its plain
|
|
5768
|
+
* text, cleaned the same way as Copy All's (see previewPlainText).
|
|
5769
|
+
*/
|
|
5770
|
+
previewSelectionContent(range) {
|
|
5771
|
+
const doc = this.elPreviewContent.ownerDocument;
|
|
5772
|
+
let content = range.cloneContents();
|
|
5773
|
+
let ancestor = range.commonAncestorContainer;
|
|
5774
|
+
if (ancestor.nodeType !== 1) {
|
|
5775
|
+
ancestor = ancestor.parentNode;
|
|
5776
|
+
}
|
|
5777
|
+
while (ancestor && ancestor.nodeType === 1 && ancestor !== this.elPreviewContent
|
|
5778
|
+
&& this.elPreviewContent.contains(ancestor)) {
|
|
5779
|
+
const wrapper = ancestor.cloneNode(false);
|
|
5780
|
+
wrapper.appendChild(content);
|
|
5781
|
+
content = wrapper;
|
|
5782
|
+
ancestor = ancestor.parentNode;
|
|
5783
|
+
}
|
|
5784
|
+
const container = doc.createElement('div');
|
|
5785
|
+
container.appendChild(content);
|
|
5786
|
+
const text = previewPlainText(container);
|
|
5787
|
+
for (const element of Array.from(container.querySelectorAll(PREVIEW_CHROME_REMOVE_SELECTOR))) {
|
|
5788
|
+
element.remove();
|
|
5789
|
+
}
|
|
5790
|
+
for (const element of Array.from(container.querySelectorAll(PREVIEW_CHROME_UNWRAP_SELECTOR))) {
|
|
5791
|
+
element.replaceWith(...Array.from(element.childNodes));
|
|
5792
|
+
}
|
|
5793
|
+
for (const image of Array.from(container.querySelectorAll('img'))) {
|
|
5794
|
+
const source = image.getAttribute(PREVIEW_IMAGE_SOURCE_ATTRIBUTE);
|
|
5795
|
+
if (source !== null) {
|
|
5796
|
+
image.setAttribute('src', source);
|
|
5797
|
+
image.removeAttribute(PREVIEW_IMAGE_SOURCE_ATTRIBUTE);
|
|
5798
|
+
}
|
|
5799
|
+
for (const name of Array.from(image.classList)) {
|
|
5800
|
+
if (name === 'mdzip-image-loading') {
|
|
5801
|
+
image.classList.remove(name);
|
|
5802
|
+
}
|
|
5803
|
+
}
|
|
5804
|
+
if (!image.classList.length) {
|
|
5805
|
+
image.removeAttribute('class');
|
|
5806
|
+
}
|
|
5807
|
+
}
|
|
5808
|
+
return { text, html: container.innerHTML.trim() };
|
|
5809
|
+
}
|
|
5810
|
+
/**
|
|
5811
|
+
* Selects all of the rendered document (Ctrl+A and the menu's Select All),
|
|
5812
|
+
* remembering the selection so Ctrl+C copies the whole document while it's
|
|
5813
|
+
* unchanged (see the copy handler).
|
|
5814
|
+
*/
|
|
5815
|
+
selectAllPreview() {
|
|
5816
|
+
const doc = this.elPreviewContent.ownerDocument;
|
|
5817
|
+
const selection = doc.defaultView?.getSelection();
|
|
5818
|
+
if (!selection) {
|
|
5819
|
+
return;
|
|
5820
|
+
}
|
|
5821
|
+
const range = doc.createRange();
|
|
5822
|
+
range.selectNodeContents(this.elPreviewContent);
|
|
5823
|
+
selection.removeAllRanges();
|
|
5824
|
+
selection.addRange(range);
|
|
5825
|
+
this.previewSelectAll = {
|
|
5826
|
+
start: range.startContainer, startOffset: range.startOffset,
|
|
5827
|
+
end: range.endContainer, endOffset: range.endOffset
|
|
5828
|
+
};
|
|
5829
|
+
}
|
|
5830
|
+
/**
|
|
5831
|
+
* Re-copies a Ctrl+C selection with its images embedded. Best effort: the
|
|
5832
|
+
* synchronous copy already on the clipboard stays if this fails, and it's
|
|
5833
|
+
* skipped if another copy happened meanwhile.
|
|
5834
|
+
*/
|
|
5835
|
+
async upgradePreviewCopyImages(copyId, html, text) {
|
|
5836
|
+
const snapshot = this.workspace?.snapshot();
|
|
5837
|
+
const clipboard = this.editorClipboard();
|
|
5838
|
+
const ClipboardItemCtor = this.elPreviewContent.ownerDocument.defaultView?.ClipboardItem;
|
|
5839
|
+
if (!this.assetSession || !snapshot || !clipboard || !ClipboardItemCtor || !('write' in clipboard)) {
|
|
5840
|
+
return;
|
|
5841
|
+
}
|
|
5842
|
+
try {
|
|
5843
|
+
const embedded = await this.assetSession.rewriteHtmlEmbeddingImages(html, snapshot.currentPath, new AbortController().signal);
|
|
5844
|
+
if (copyId !== this.previewCopyId || this.destroyed) {
|
|
5845
|
+
return;
|
|
5846
|
+
}
|
|
5847
|
+
await clipboard.write([new ClipboardItemCtor({
|
|
5848
|
+
'text/html': new Blob([embedded], { type: 'text/html' }),
|
|
5849
|
+
'text/plain': new Blob([text], { type: 'text/plain' })
|
|
5850
|
+
})]);
|
|
5851
|
+
}
|
|
5852
|
+
catch {
|
|
5853
|
+
// The plain copy is already on the clipboard.
|
|
5854
|
+
}
|
|
5855
|
+
}
|
|
5856
|
+
/**
|
|
5857
|
+
* Copies a preview selection as formatted text (Copy > Formatted Text, and
|
|
5858
|
+
* Ctrl+C's image upgrade): its HTML with archive images embedded as data:
|
|
5859
|
+
* URLs, so they survive pasting into Word or email, plus its plain text.
|
|
5860
|
+
*/
|
|
5861
|
+
async copyPreviewSelectionFormatted(html, text) {
|
|
5862
|
+
const snapshot = this.workspace?.snapshot();
|
|
5863
|
+
let imageCount = 0;
|
|
5864
|
+
let embedded = html;
|
|
5865
|
+
if (this.assetSession && snapshot && /<img\b/i.test(html)) {
|
|
5866
|
+
try {
|
|
5867
|
+
embedded = await this.assetSession.rewriteHtmlEmbeddingImages(html, snapshot.currentPath, new AbortController().signal, (_done, total) => { imageCount = total; });
|
|
5868
|
+
}
|
|
5869
|
+
catch (error) {
|
|
5870
|
+
this.options.onFailed?.(error);
|
|
5871
|
+
}
|
|
5872
|
+
}
|
|
5873
|
+
return this.writeRichClipboard(embedded, text, imageCount);
|
|
5874
|
+
}
|
|
5875
|
+
/** `writeText` with the same timeout and outcome reporting as {@link copyPreviewSelection}; `message` is the success confirmation. */
|
|
5876
|
+
async writeClipboardText(text, message) {
|
|
5335
5877
|
if (!text) {
|
|
5336
5878
|
return null;
|
|
5337
5879
|
}
|
|
5338
5880
|
try {
|
|
5339
5881
|
await this.withTimeout(Promise.resolve(this.editorClipboard()?.writeText(text)), this.clipboardFallbackWriteTimeoutMs, 'Clipboard write timed out.');
|
|
5340
|
-
return { message
|
|
5882
|
+
return { message };
|
|
5341
5883
|
}
|
|
5342
5884
|
catch (error) {
|
|
5343
5885
|
this.options.onFailed?.(error);
|
|
5344
5886
|
return { error: error instanceof Error ? error.message : String(error) };
|
|
5345
5887
|
}
|
|
5346
5888
|
}
|
|
5889
|
+
/**
|
|
5890
|
+
* Copies the whole document's rendered HTML markup as plain text (#56), so
|
|
5891
|
+
* any paste target, Notepad included, gets the raw `<h1>…</h1><p>…</p>`.
|
|
5892
|
+
* Rendered fresh like Copy All with Images, not read from the live preview
|
|
5893
|
+
* (which holds hydrated `blob:` image URLs, lazy-mount sentinels and
|
|
5894
|
+
* code-block chrome): images keep their archive-relative `src`, and
|
|
5895
|
+
* extension `mount()` output (e.g. mermaid diagrams) copies as its
|
|
5896
|
+
* pre-render markup. Large documents get the same cancelable progress
|
|
5897
|
+
* dialog and "ready to copy" step. A host's custom (non-chunking) renderer
|
|
5898
|
+
* renders the document in one pass instead.
|
|
5899
|
+
*/
|
|
5900
|
+
async copyHtmlPreviewContent() {
|
|
5901
|
+
const snapshot = this.workspace?.snapshot();
|
|
5902
|
+
if (!snapshot || snapshot.currentPathType !== 'markdown') {
|
|
5903
|
+
return;
|
|
5904
|
+
}
|
|
5905
|
+
const message = 'HTML copied to clipboard as text';
|
|
5906
|
+
const abort = new AbortController();
|
|
5907
|
+
let dialogShown = false;
|
|
5908
|
+
const showDialogTimer = setTimeout(() => {
|
|
5909
|
+
dialogShown = true;
|
|
5910
|
+
this.copyRenderDialogState = { done: 0, total: 0, label: 'Rendering the document', abort };
|
|
5911
|
+
this.render();
|
|
5912
|
+
}, 200);
|
|
5913
|
+
let html;
|
|
5914
|
+
try {
|
|
5915
|
+
const context = this.createMarkdownContext(snapshot, abort.signal);
|
|
5916
|
+
if (this.renderingService.supportsChunking) {
|
|
5917
|
+
const cacheGeneration = this.previewMemoMatchesSnapshot(snapshot) ? this.previewGeneration : null;
|
|
5918
|
+
html = await this.renderFullDocumentHtml(snapshot, context, abort.signal, cacheGeneration, (done, total) => {
|
|
5919
|
+
if (!dialogShown)
|
|
5920
|
+
return;
|
|
5921
|
+
this.copyRenderDialogState = { done, total, label: 'Rendering the document', abort };
|
|
5922
|
+
this.updateCopyRenderDialogProgress();
|
|
5923
|
+
});
|
|
5924
|
+
}
|
|
5925
|
+
else {
|
|
5926
|
+
html = await this.renderingService.renderMarkdown(snapshot.currentText, context);
|
|
5927
|
+
}
|
|
5928
|
+
}
|
|
5929
|
+
catch (error) {
|
|
5930
|
+
clearTimeout(showDialogTimer);
|
|
5931
|
+
if (error?.name === 'AbortError') {
|
|
5932
|
+
if (dialogShown) {
|
|
5933
|
+
this.copyRenderDialogState = null;
|
|
5934
|
+
this.render();
|
|
5935
|
+
}
|
|
5936
|
+
}
|
|
5937
|
+
else {
|
|
5938
|
+
this.finishCopyNotification(dialogShown, { error: error instanceof Error ? error.message : String(error) });
|
|
5939
|
+
this.options.onFailed?.(error);
|
|
5940
|
+
}
|
|
5941
|
+
return;
|
|
5942
|
+
}
|
|
5943
|
+
clearTimeout(showDialogTimer);
|
|
5944
|
+
if (dialogShown) {
|
|
5945
|
+
this.armCopyReady(() => this.writeClipboardText(html, message));
|
|
5946
|
+
return;
|
|
5947
|
+
}
|
|
5948
|
+
const outcome = await this.writeClipboardText(html, message);
|
|
5949
|
+
this.finishCopyNotification(false, outcome);
|
|
5950
|
+
}
|
|
5347
5951
|
/** Syncs just the copy-render dialog's progress bar/text from `copyRenderDialogState` — see `copyAllPreviewContent` for why this bypasses `render()`. No-op while the dialog isn't showing. */
|
|
5348
5952
|
updateCopyRenderDialogProgress() {
|
|
5349
5953
|
if (!this.copyRenderDialogState) {
|
|
@@ -5369,7 +5973,7 @@ export class MdzipWorkspaceView {
|
|
|
5369
5973
|
async copyAllPreviewContent() {
|
|
5370
5974
|
const pending = this.chunkedRenderState;
|
|
5371
5975
|
if (!pending || pending.cursor >= pending.records.length) {
|
|
5372
|
-
const outcome = await this.copyPreviewSelection(this.elPreviewContent
|
|
5976
|
+
const outcome = await this.copyPreviewSelection(previewPlainText(this.elPreviewContent));
|
|
5373
5977
|
this.finishCopyNotification(false, outcome);
|
|
5374
5978
|
return;
|
|
5375
5979
|
}
|
|
@@ -5415,10 +6019,10 @@ export class MdzipWorkspaceView {
|
|
|
5415
6019
|
// activation by now, so the actual write waits for a fresh one
|
|
5416
6020
|
// (the dialog's own Copy button) instead of firing automatically.
|
|
5417
6021
|
// See copyReadyState's doc comment.
|
|
5418
|
-
this.armCopyReady(() => this.copyPreviewSelection(this.elPreviewContent
|
|
6022
|
+
this.armCopyReady(() => this.copyPreviewSelection(previewPlainText(this.elPreviewContent)));
|
|
5419
6023
|
return;
|
|
5420
6024
|
}
|
|
5421
|
-
const outcome = await this.copyPreviewSelection(this.elPreviewContent
|
|
6025
|
+
const outcome = await this.copyPreviewSelection(previewPlainText(this.elPreviewContent));
|
|
5422
6026
|
this.finishCopyNotification(false, outcome);
|
|
5423
6027
|
}
|
|
5424
6028
|
/**
|
|
@@ -5577,7 +6181,7 @@ export class MdzipWorkspaceView {
|
|
|
5577
6181
|
clearTimeout(showDialogTimer);
|
|
5578
6182
|
const scratch = this.elPreviewContent.ownerDocument.createElement('div');
|
|
5579
6183
|
scratch.innerHTML = finalHtml;
|
|
5580
|
-
const plainText = scratch
|
|
6184
|
+
const plainText = previewPlainText(scratch);
|
|
5581
6185
|
if (dialogShown) {
|
|
5582
6186
|
// Same reasoning as copyAllPreviewContent: the prepare phase (often
|
|
5583
6187
|
// the slower of the two here, once image embedding is involved) has
|
|
@@ -5621,8 +6225,8 @@ export class MdzipWorkspaceView {
|
|
|
5621
6225
|
await this.withTimeout(clipboard.write([item]), this.clipboardWriteTimeoutMs, 'Clipboard write timed out.');
|
|
5622
6226
|
return {
|
|
5623
6227
|
message: imageCount > 0
|
|
5624
|
-
? `
|
|
5625
|
-
: '
|
|
6228
|
+
? `Formatted text with ${imageCount} image${imageCount === 1 ? '' : 's'} copied to clipboard`
|
|
6229
|
+
: 'Formatted text copied to clipboard'
|
|
5626
6230
|
};
|
|
5627
6231
|
}
|
|
5628
6232
|
catch (error) {
|
|
@@ -6121,22 +6725,122 @@ export class MdzipWorkspaceView {
|
|
|
6121
6725
|
* reacting to `data-vscode-theme-kind` changes via MutationObserver — so the
|
|
6122
6726
|
* editor follows the host theme without being recreated (recreation would
|
|
6123
6727
|
* destroy the CodeMirror instance and lose unsaved edits). No-op when the
|
|
6124
|
-
* scheme is unchanged.
|
|
6125
|
-
*
|
|
6728
|
+
* scheme is unchanged. `'system'` follows the operating system's setting
|
|
6729
|
+
* from then on. Does not fire `onColorSchemeChanged` or
|
|
6730
|
+
* `onColorSchemePreferenceChanged`, which only report choices made in the
|
|
6731
|
+
* built-in toolbar menu.
|
|
6126
6732
|
*/
|
|
6127
6733
|
setColorScheme(colorScheme) {
|
|
6128
|
-
|
|
6734
|
+
const next = this.resolveColorScheme(colorScheme);
|
|
6735
|
+
if (this.colorSchemePreference === colorScheme && this.colorScheme === next) {
|
|
6736
|
+
return;
|
|
6737
|
+
}
|
|
6738
|
+
this.colorSchemePreference = colorScheme;
|
|
6739
|
+
this.colorScheme = next;
|
|
6740
|
+
this.render();
|
|
6741
|
+
}
|
|
6742
|
+
/** The light/dark scheme a choice means right now; `'system'` reads the OS setting. */
|
|
6743
|
+
resolveColorScheme(preference) {
|
|
6744
|
+
if (preference !== 'system') {
|
|
6745
|
+
return preference;
|
|
6746
|
+
}
|
|
6747
|
+
return this.systemColorSchemeQuery?.matches ? 'dark' : 'light';
|
|
6748
|
+
}
|
|
6749
|
+
themeMenuItems() {
|
|
6750
|
+
return [
|
|
6751
|
+
[this.elLightThemeBtn, 'light'],
|
|
6752
|
+
[this.elDarkThemeBtn, 'dark'],
|
|
6753
|
+
[this.elSystemThemeBtn, 'system']
|
|
6754
|
+
];
|
|
6755
|
+
}
|
|
6756
|
+
/** Arrow keys cycle focus through a toolbar menu's items. */
|
|
6757
|
+
moveMenuFocus(e, buttons) {
|
|
6758
|
+
if (e.key !== 'ArrowDown' && e.key !== 'ArrowUp') {
|
|
6759
|
+
return;
|
|
6760
|
+
}
|
|
6761
|
+
e.preventDefault();
|
|
6762
|
+
const current = buttons.indexOf(this.elRoot.ownerDocument.activeElement);
|
|
6763
|
+
const step = e.key === 'ArrowDown' ? 1 : -1;
|
|
6764
|
+
buttons[(current + step + buttons.length) % buttons.length].focus();
|
|
6765
|
+
}
|
|
6766
|
+
widthMenuItems() {
|
|
6767
|
+
return Array.from(this.elWidthPopover.querySelectorAll('[data-width]'))
|
|
6768
|
+
.map((button) => [button, button.dataset['width']]);
|
|
6769
|
+
}
|
|
6770
|
+
/** The menu choice matching the current width, or null for a custom pixel width. */
|
|
6771
|
+
previewWidthChoice() {
|
|
6772
|
+
if (this.previewMaxWidth === undefined) {
|
|
6773
|
+
return 'default';
|
|
6774
|
+
}
|
|
6775
|
+
return typeof this.previewMaxWidth === 'number' ? null : this.previewMaxWidth;
|
|
6776
|
+
}
|
|
6777
|
+
/**
|
|
6778
|
+
* A choice's label: its own width (base width times zoom) and, when `measure`
|
|
6779
|
+
* is set and the preview pane is narrower than that, the width it fits to.
|
|
6780
|
+
* Full Width has no number: it is always the pane's width.
|
|
6781
|
+
*/
|
|
6782
|
+
previewWidthLabel(choice, measure) {
|
|
6783
|
+
if (choice === 'full') {
|
|
6784
|
+
return PREVIEW_WIDTH_LABELS[choice];
|
|
6785
|
+
}
|
|
6786
|
+
const width = Math.round(PREVIEW_MAX_WIDTH_ALIASES[choice] * this.zoom);
|
|
6787
|
+
const pane = measure ? this.elPreviewPane.clientWidth : 0;
|
|
6788
|
+
const fit = pane > 0 && pane < width ? `, fits to ${pane} px` : '';
|
|
6789
|
+
return `${PREVIEW_WIDTH_LABELS[choice]} (${width} px${fit})`;
|
|
6790
|
+
}
|
|
6791
|
+
/** Labels each width menu item, and the button, with the pixel width it gives. */
|
|
6792
|
+
updateWidthLabels(measure) {
|
|
6793
|
+
for (const [button, choice] of this.widthMenuItems()) {
|
|
6794
|
+
const label = this.previewWidthLabel(choice, measure);
|
|
6795
|
+
setTooltip(button, label);
|
|
6796
|
+
button.setAttribute('aria-label', label);
|
|
6797
|
+
}
|
|
6798
|
+
const choice = this.previewWidthChoice();
|
|
6799
|
+
const label = `Page width: ${choice ? this.previewWidthLabel(choice, measure) : `${this.previewMaxWidth} px`}`;
|
|
6800
|
+
setTooltip(this.elWidthMenuBtn, label);
|
|
6801
|
+
this.elWidthMenuBtn.setAttribute('aria-label', label);
|
|
6802
|
+
}
|
|
6803
|
+
choosePreviewWidthFromMenu(choice) {
|
|
6804
|
+
const changed = this.previewWidthChoice() !== choice;
|
|
6805
|
+
this.previewMaxWidth = choice;
|
|
6806
|
+
this.closeWidthMenu(true);
|
|
6807
|
+
if (changed) {
|
|
6808
|
+
this.options.onPreviewWidthChanged?.(choice);
|
|
6809
|
+
}
|
|
6810
|
+
}
|
|
6811
|
+
/** Closes the page-width menu; `refocus` returns focus to its button (keyboard use). */
|
|
6812
|
+
closeWidthMenu(refocus) {
|
|
6813
|
+
this.widthMenuOpen = false;
|
|
6814
|
+
this.render();
|
|
6815
|
+
if (refocus) {
|
|
6816
|
+
this.elWidthMenuBtn.focus();
|
|
6817
|
+
}
|
|
6818
|
+
}
|
|
6819
|
+
chooseColorSchemeFromMenu(preference) {
|
|
6820
|
+
this.setColorSchemeFromToolbar(preference);
|
|
6821
|
+
this.closeThemeMenu(true);
|
|
6822
|
+
}
|
|
6823
|
+
/** Closes the color scheme menu; `refocus` returns focus to its button (keyboard use). */
|
|
6824
|
+
closeThemeMenu(refocus) {
|
|
6825
|
+
if (!this.themeMenuOpen) {
|
|
6129
6826
|
return;
|
|
6130
6827
|
}
|
|
6131
|
-
this.
|
|
6828
|
+
this.themeMenuOpen = false;
|
|
6132
6829
|
this.render();
|
|
6830
|
+
if (refocus) {
|
|
6831
|
+
this.elThemeMenuBtn.focus();
|
|
6832
|
+
}
|
|
6133
6833
|
}
|
|
6134
|
-
setColorSchemeFromToolbar(
|
|
6135
|
-
if (this.
|
|
6834
|
+
setColorSchemeFromToolbar(preference) {
|
|
6835
|
+
if (this.colorSchemePreference === preference) {
|
|
6136
6836
|
return;
|
|
6137
6837
|
}
|
|
6138
|
-
this.
|
|
6139
|
-
this.
|
|
6838
|
+
const before = this.colorScheme;
|
|
6839
|
+
this.setColorScheme(preference);
|
|
6840
|
+
this.options.onColorSchemePreferenceChanged?.(preference);
|
|
6841
|
+
if (this.colorScheme !== before) {
|
|
6842
|
+
this.options.onColorSchemeChanged?.(this.colorScheme);
|
|
6843
|
+
}
|
|
6140
6844
|
}
|
|
6141
6845
|
async setLayout(requested) {
|
|
6142
6846
|
const snapshot = this.workspace?.snapshot();
|
|
@@ -6160,7 +6864,7 @@ export class MdzipWorkspaceView {
|
|
|
6160
6864
|
return { kind: 'directory', path: '' };
|
|
6161
6865
|
}
|
|
6162
6866
|
fileMenuTarget(path, snapshot) {
|
|
6163
|
-
const entry = snapshot.content.paths.find((item) => item.path
|
|
6867
|
+
const entry = snapshot.content.paths.find((item) => samePath(item.path, path));
|
|
6164
6868
|
if (!entry) {
|
|
6165
6869
|
return null;
|
|
6166
6870
|
}
|
|
@@ -6168,9 +6872,9 @@ export class MdzipWorkspaceView {
|
|
|
6168
6872
|
kind: 'file',
|
|
6169
6873
|
path: entry.path,
|
|
6170
6874
|
orphaned: snapshot.content.orphanedAssetPaths
|
|
6171
|
-
.some((orphan) => orphan
|
|
6875
|
+
.some((orphan) => samePath(orphan, entry.path)),
|
|
6172
6876
|
isMarkdown: entry.isMarkdown,
|
|
6173
|
-
isEntryPoint: entry.path
|
|
6877
|
+
isEntryPoint: samePath(entry.path, snapshot.content.entryPoint),
|
|
6174
6878
|
isImage: entry.isImage,
|
|
6175
6879
|
isManifest: isMdzipManifestPath(entry.path)
|
|
6176
6880
|
};
|
|
@@ -6216,7 +6920,7 @@ export class MdzipWorkspaceView {
|
|
|
6216
6920
|
}
|
|
6217
6921
|
if (target.isImage) {
|
|
6218
6922
|
const cover = snapshot.content.manifest?.cover;
|
|
6219
|
-
stateGroup.push(cover && cover.
|
|
6923
|
+
stateGroup.push(cover && samePath(cover.replace(/^\.\//, ''), target.path)
|
|
6220
6924
|
? { action: 'remove-cover', label: 'Remove Cover Image' }
|
|
6221
6925
|
: { action: 'set-cover', label: 'Set as Cover Image' });
|
|
6222
6926
|
}
|
|
@@ -6469,7 +7173,7 @@ export class MdzipWorkspaceView {
|
|
|
6469
7173
|
if (!editor || !snapshot || snapshot.sourceFormat !== 'mdz') {
|
|
6470
7174
|
return;
|
|
6471
7175
|
}
|
|
6472
|
-
const entry = snapshot.content.paths.find((item) => item.path
|
|
7176
|
+
const entry = snapshot.content.paths.find((item) => samePath(item.path, archivePath));
|
|
6473
7177
|
if (!entry || isMdzipManifestPath(entry.path)) {
|
|
6474
7178
|
return;
|
|
6475
7179
|
}
|
|
@@ -6622,7 +7326,7 @@ export class MdzipWorkspaceView {
|
|
|
6622
7326
|
async moveFileToDirectory(path, targetDir) {
|
|
6623
7327
|
const baseName = path.slice(path.lastIndexOf('/') + 1);
|
|
6624
7328
|
const newPath = targetDir ? `${targetDir}/${baseName}` : baseName;
|
|
6625
|
-
if (newPath
|
|
7329
|
+
if (newPath === path) {
|
|
6626
7330
|
return;
|
|
6627
7331
|
}
|
|
6628
7332
|
try {
|
|
@@ -7000,325 +7704,348 @@ function formatMetadataValue(value) {
|
|
|
7000
7704
|
}
|
|
7001
7705
|
return 'Not available';
|
|
7002
7706
|
}
|
|
7003
|
-
const SHELL_HTML = `
|
|
7004
|
-
<section class="mdzip-root">
|
|
7005
|
-
<div class="document-strip" data-ref="document-strip" hidden>
|
|
7006
|
-
<button type="button" class="title-button" data-ref="title-btn"></button>
|
|
7007
|
-
<button type="button" class="document-info-button" data-ref="document-info-btn"
|
|
7008
|
-
title="Document information" aria-label="Document information">
|
|
7009
|
-
${INFO_ICON_HTML}
|
|
7010
|
-
</button>
|
|
7011
|
-
</div>
|
|
7012
|
-
|
|
7013
|
-
<header class="toolbar" data-ref="toolbar" hidden>
|
|
7014
|
-
<div class="toolbar-start">
|
|
7015
|
-
<div class="toolbar-left">
|
|
7016
|
-
<button type="button" class="icon-toggle nav-toggle" data-ref="nav-btn" title="Toggle contents" aria-label="Toggle contents">
|
|
7017
|
-
${NAV_TOGGLE_ICON_HTML}
|
|
7018
|
-
</button>
|
|
7019
|
-
</div>
|
|
7020
|
-
|
|
7021
|
-
<div class="edit-toolbar" data-ref="edit-toolbar" role="toolbar" aria-label="Markdown formatting">
|
|
7022
|
-
<div class="edit-toolbar-group">
|
|
7023
|
-
<button type="button" data-format="bold" data-format-control="bold" title="Bold" aria-label="Bold">${BOLD_ICON_HTML}</button>
|
|
7024
|
-
<button type="button" data-format="italic" data-format-control="italic" title="Italic" aria-label="Italic">${ITALIC_ICON_HTML}</button>
|
|
7025
|
-
<button type="button" data-format="strike" data-format-control="strikethrough" title="Strikethrough" aria-label="Strikethrough">${STRIKE_ICON_HTML}</button>
|
|
7026
|
-
<div class="format-menu" data-format-control="headings">
|
|
7027
|
-
<button type="button" class="format-menu-toggle" data-format-menu-toggle
|
|
7028
|
-
aria-label="Heading" aria-haspopup="menu" aria-expanded="false">
|
|
7029
|
-
${HEADING_ICON_HTML}${CHEVRON_ICON_HTML}
|
|
7030
|
-
</button>
|
|
7031
|
-
<div class="format-menu-popover" data-format-menu role="menu" aria-label="Heading level" hidden>
|
|
7032
|
-
<button type="button" role="menuitem" data-format="paragraph">Paragraph</button>
|
|
7033
|
-
<button type="button" role="menuitem" data-format="heading-1" data-heading-level="1"><strong>H1</strong> Heading 1</button>
|
|
7034
|
-
<button type="button" role="menuitem" data-format="heading-2" data-heading-level="2"><strong>H2</strong> Heading 2</button>
|
|
7035
|
-
<button type="button" role="menuitem" data-format="heading-3" data-heading-level="3"><strong>H3</strong> Heading 3</button>
|
|
7036
|
-
<button type="button" role="menuitem" data-format="heading-4" data-heading-level="4"><strong>H4</strong> Heading 4</button>
|
|
7037
|
-
<button type="button" role="menuitem" data-format="heading-5" data-heading-level="5"><strong>H5</strong> Heading 5</button>
|
|
7038
|
-
<button type="button" role="menuitem" data-format="heading-6" data-heading-level="6"><strong>H6</strong> Heading 6</button>
|
|
7039
|
-
</div>
|
|
7040
|
-
</div>
|
|
7041
|
-
</div>
|
|
7042
|
-
<span class="edit-toolbar-divider" aria-hidden="true"></span>
|
|
7043
|
-
<div class="edit-toolbar-group">
|
|
7044
|
-
<button type="button" data-format="bullet-list" data-format-control="bulletList" title="Bulleted list" aria-label="Bulleted list">${BULLET_LIST_ICON_HTML}</button>
|
|
7045
|
-
<button type="button" data-format="ordered-list" data-format-control="orderedList" title="Numbered list" aria-label="Numbered list">${ORDERED_LIST_ICON_HTML}</button>
|
|
7046
|
-
<div class="format-menu" data-format-control="code">
|
|
7047
|
-
<button type="button" class="format-menu-toggle" data-format-menu-toggle
|
|
7048
|
-
aria-label="Code" aria-haspopup="menu" aria-expanded="false">
|
|
7049
|
-
${CODE_ICON_HTML}${CHEVRON_ICON_HTML}
|
|
7050
|
-
</button>
|
|
7051
|
-
<div class="format-menu-popover" data-format-menu role="menu" aria-label="Code format" hidden>
|
|
7052
|
-
<button type="button" role="menuitem" data-format="code" data-code-kind="inline">Inline code</button>
|
|
7053
|
-
<button type="button" role="menuitem" data-format="code-block" data-code-kind="block">Code block</button>
|
|
7054
|
-
</div>
|
|
7055
|
-
</div>
|
|
7056
|
-
<button type="button" data-format="quote" data-format-control="blockquote" title="Blockquote" aria-label="Blockquote">${QUOTE_ICON_HTML}</button>
|
|
7057
|
-
<button type="button" data-format="line-break" data-format-control="lineBreak" title="Line break" aria-label="Line break">${LINE_BREAK_ICON_HTML}</button>
|
|
7058
|
-
</div>
|
|
7059
|
-
<span class="edit-toolbar-divider" aria-hidden="true"></span>
|
|
7060
|
-
<div class="edit-toolbar-group">
|
|
7061
|
-
<button type="button" data-format="link" data-format-control="link" title="Link" aria-label="Link">${LINK_ICON_HTML}</button>
|
|
7062
|
-
<button type="button" data-format="image" data-format-control="image" title="Image" aria-label="Image">${IMAGE_FORMAT_ICON_HTML}</button>
|
|
7063
|
-
<input type="file" data-ref="image-input" accept="image/*" hidden />
|
|
7064
|
-
</div>
|
|
7065
|
-
</div>
|
|
7066
|
-
</div>
|
|
7067
|
-
|
|
7068
|
-
<div class="toolbar-buttons view-mode-toggle-group" data-ref="layout-controls" role="group" aria-label="Editor layout">
|
|
7069
|
-
<button type="button" class="icon-toggle view-mode-toggle" data-ref="source-btn" title="Edit" aria-label="Edit" aria-pressed="false">
|
|
7070
|
-
<span class="commandbar-flex-container" data-ref="source-icon">${SOURCE_EDIT_ICON_HTML}</span>
|
|
7071
|
-
</button>
|
|
7072
|
-
<button type="button" class="icon-toggle view-mode-toggle" data-ref="split-btn" title="Split" aria-label="Split" aria-pressed="false">
|
|
7073
|
-
<span class="commandbar-flex-container">
|
|
7074
|
-
${SPLIT_ICON_HTML}
|
|
7075
|
-
</span>
|
|
7076
|
-
</button>
|
|
7077
|
-
<button type="button" class="icon-toggle view-mode-toggle" data-ref="preview-btn" title="View" aria-label="View" aria-pressed="false">
|
|
7078
|
-
<span class="commandbar-flex-container">
|
|
7079
|
-
${PREVIEW_ICON_HTML}
|
|
7080
|
-
</span>
|
|
7081
|
-
</button>
|
|
7082
|
-
</div>
|
|
7083
|
-
|
|
7084
|
-
<div class="toolbar-controls" data-ref="toolbar-controls">
|
|
7085
|
-
<button type="button" class="icon-toggle" data-ref="search-btn" title="Find/replace (Mod-F)" aria-label="Find/replace">
|
|
7086
|
-
${SEARCH_ICON_HTML}
|
|
7087
|
-
</button>
|
|
7088
|
-
<button type="button" class="icon-toggle" data-ref="save-btn" title="Save" aria-label="Save">
|
|
7089
|
-
${SAVE_ICON_HTML}
|
|
7090
|
-
</button>
|
|
7091
|
-
<button type="button" class="icon-toggle zoom-toggle" data-ref="zoom-btn" title="Zoom controls" aria-label="Zoom controls">
|
|
7092
|
-
${ZOOM_ICON_HTML}
|
|
7093
|
-
</button>
|
|
7094
|
-
<div class="
|
|
7095
|
-
<button type="button" class="icon-toggle
|
|
7096
|
-
title="
|
|
7097
|
-
|
|
7098
|
-
</button>
|
|
7099
|
-
<
|
|
7100
|
-
|
|
7101
|
-
${
|
|
7102
|
-
|
|
7103
|
-
|
|
7104
|
-
|
|
7105
|
-
|
|
7106
|
-
|
|
7107
|
-
|
|
7108
|
-
|
|
7109
|
-
|
|
7110
|
-
|
|
7111
|
-
|
|
7112
|
-
|
|
7113
|
-
|
|
7114
|
-
|
|
7115
|
-
|
|
7116
|
-
|
|
7117
|
-
|
|
7118
|
-
|
|
7119
|
-
|
|
7120
|
-
|
|
7121
|
-
|
|
7122
|
-
|
|
7123
|
-
|
|
7124
|
-
|
|
7125
|
-
|
|
7126
|
-
|
|
7127
|
-
|
|
7128
|
-
|
|
7129
|
-
<
|
|
7130
|
-
|
|
7131
|
-
|
|
7132
|
-
|
|
7133
|
-
|
|
7134
|
-
|
|
7135
|
-
|
|
7136
|
-
|
|
7137
|
-
|
|
7138
|
-
|
|
7139
|
-
|
|
7140
|
-
<
|
|
7141
|
-
|
|
7142
|
-
|
|
7143
|
-
|
|
7144
|
-
|
|
7145
|
-
|
|
7146
|
-
|
|
7147
|
-
|
|
7148
|
-
|
|
7149
|
-
|
|
7150
|
-
|
|
7151
|
-
|
|
7152
|
-
|
|
7153
|
-
|
|
7154
|
-
<
|
|
7155
|
-
|
|
7156
|
-
|
|
7157
|
-
|
|
7158
|
-
|
|
7159
|
-
|
|
7160
|
-
|
|
7161
|
-
|
|
7162
|
-
|
|
7163
|
-
|
|
7164
|
-
|
|
7165
|
-
|
|
7166
|
-
|
|
7167
|
-
|
|
7168
|
-
|
|
7169
|
-
|
|
7170
|
-
|
|
7171
|
-
|
|
7172
|
-
|
|
7173
|
-
|
|
7174
|
-
|
|
7175
|
-
|
|
7176
|
-
|
|
7177
|
-
|
|
7178
|
-
|
|
7179
|
-
|
|
7180
|
-
|
|
7181
|
-
|
|
7182
|
-
|
|
7183
|
-
|
|
7184
|
-
|
|
7185
|
-
|
|
7186
|
-
|
|
7187
|
-
|
|
7188
|
-
|
|
7189
|
-
|
|
7190
|
-
|
|
7191
|
-
|
|
7192
|
-
|
|
7193
|
-
|
|
7194
|
-
|
|
7195
|
-
|
|
7196
|
-
|
|
7197
|
-
|
|
7198
|
-
|
|
7199
|
-
|
|
7200
|
-
|
|
7201
|
-
|
|
7202
|
-
|
|
7203
|
-
|
|
7204
|
-
|
|
7205
|
-
|
|
7206
|
-
|
|
7207
|
-
|
|
7208
|
-
|
|
7209
|
-
|
|
7210
|
-
|
|
7211
|
-
|
|
7212
|
-
|
|
7213
|
-
|
|
7214
|
-
|
|
7215
|
-
|
|
7216
|
-
|
|
7217
|
-
|
|
7218
|
-
|
|
7219
|
-
|
|
7220
|
-
|
|
7221
|
-
|
|
7222
|
-
|
|
7223
|
-
|
|
7224
|
-
|
|
7225
|
-
|
|
7226
|
-
|
|
7227
|
-
<
|
|
7228
|
-
<
|
|
7229
|
-
<
|
|
7230
|
-
<
|
|
7231
|
-
|
|
7232
|
-
|
|
7233
|
-
|
|
7234
|
-
|
|
7235
|
-
<
|
|
7236
|
-
|
|
7237
|
-
|
|
7238
|
-
|
|
7239
|
-
|
|
7240
|
-
|
|
7241
|
-
|
|
7242
|
-
|
|
7243
|
-
|
|
7244
|
-
|
|
7245
|
-
|
|
7246
|
-
|
|
7247
|
-
|
|
7248
|
-
|
|
7249
|
-
|
|
7250
|
-
|
|
7251
|
-
|
|
7252
|
-
|
|
7253
|
-
|
|
7254
|
-
|
|
7255
|
-
|
|
7256
|
-
|
|
7257
|
-
<
|
|
7258
|
-
<
|
|
7259
|
-
|
|
7260
|
-
|
|
7261
|
-
|
|
7262
|
-
|
|
7263
|
-
|
|
7264
|
-
|
|
7265
|
-
|
|
7266
|
-
|
|
7267
|
-
|
|
7268
|
-
<button type="button" data-action="cancel-
|
|
7269
|
-
<button type="button" class="save-title" data-
|
|
7270
|
-
|
|
7271
|
-
|
|
7272
|
-
|
|
7273
|
-
|
|
7274
|
-
|
|
7275
|
-
|
|
7276
|
-
|
|
7277
|
-
|
|
7278
|
-
<
|
|
7279
|
-
|
|
7280
|
-
|
|
7281
|
-
|
|
7282
|
-
|
|
7283
|
-
|
|
7284
|
-
|
|
7285
|
-
|
|
7286
|
-
|
|
7287
|
-
|
|
7288
|
-
|
|
7289
|
-
|
|
7290
|
-
|
|
7291
|
-
|
|
7292
|
-
|
|
7293
|
-
|
|
7294
|
-
|
|
7295
|
-
|
|
7296
|
-
|
|
7297
|
-
|
|
7298
|
-
|
|
7299
|
-
|
|
7300
|
-
|
|
7301
|
-
|
|
7302
|
-
|
|
7303
|
-
|
|
7304
|
-
<
|
|
7305
|
-
<
|
|
7306
|
-
|
|
7307
|
-
|
|
7308
|
-
|
|
7309
|
-
|
|
7310
|
-
|
|
7311
|
-
|
|
7312
|
-
|
|
7313
|
-
|
|
7314
|
-
|
|
7315
|
-
|
|
7316
|
-
|
|
7317
|
-
|
|
7318
|
-
|
|
7319
|
-
|
|
7320
|
-
|
|
7321
|
-
|
|
7322
|
-
</
|
|
7707
|
+
const SHELL_HTML = `
|
|
7708
|
+
<section class="mdzip-root">
|
|
7709
|
+
<div class="document-strip" data-ref="document-strip" hidden>
|
|
7710
|
+
<button type="button" class="title-button" data-ref="title-btn"></button>
|
|
7711
|
+
<button type="button" class="document-info-button" data-ref="document-info-btn"
|
|
7712
|
+
title="Document information" aria-label="Document information">
|
|
7713
|
+
${INFO_ICON_HTML}
|
|
7714
|
+
</button>
|
|
7715
|
+
</div>
|
|
7716
|
+
|
|
7717
|
+
<header class="toolbar" data-ref="toolbar" hidden>
|
|
7718
|
+
<div class="toolbar-start">
|
|
7719
|
+
<div class="toolbar-left">
|
|
7720
|
+
<button type="button" class="icon-toggle nav-toggle" data-ref="nav-btn" title="Toggle contents" aria-label="Toggle contents">
|
|
7721
|
+
${NAV_TOGGLE_ICON_HTML}
|
|
7722
|
+
</button>
|
|
7723
|
+
</div>
|
|
7724
|
+
|
|
7725
|
+
<div class="edit-toolbar" data-ref="edit-toolbar" role="toolbar" aria-label="Markdown formatting">
|
|
7726
|
+
<div class="edit-toolbar-group">
|
|
7727
|
+
<button type="button" data-format="bold" data-format-control="bold" title="Bold" aria-label="Bold">${BOLD_ICON_HTML}</button>
|
|
7728
|
+
<button type="button" data-format="italic" data-format-control="italic" title="Italic" aria-label="Italic">${ITALIC_ICON_HTML}</button>
|
|
7729
|
+
<button type="button" data-format="strike" data-format-control="strikethrough" title="Strikethrough" aria-label="Strikethrough">${STRIKE_ICON_HTML}</button>
|
|
7730
|
+
<div class="format-menu" data-format-control="headings">
|
|
7731
|
+
<button type="button" class="format-menu-toggle" data-format-menu-toggle
|
|
7732
|
+
aria-label="Heading" aria-haspopup="menu" aria-expanded="false">
|
|
7733
|
+
${HEADING_ICON_HTML}${CHEVRON_ICON_HTML}
|
|
7734
|
+
</button>
|
|
7735
|
+
<div class="format-menu-popover" data-format-menu role="menu" aria-label="Heading level" hidden>
|
|
7736
|
+
<button type="button" role="menuitem" data-format="paragraph">Paragraph</button>
|
|
7737
|
+
<button type="button" role="menuitem" data-format="heading-1" data-heading-level="1"><strong>H1</strong> Heading 1</button>
|
|
7738
|
+
<button type="button" role="menuitem" data-format="heading-2" data-heading-level="2"><strong>H2</strong> Heading 2</button>
|
|
7739
|
+
<button type="button" role="menuitem" data-format="heading-3" data-heading-level="3"><strong>H3</strong> Heading 3</button>
|
|
7740
|
+
<button type="button" role="menuitem" data-format="heading-4" data-heading-level="4"><strong>H4</strong> Heading 4</button>
|
|
7741
|
+
<button type="button" role="menuitem" data-format="heading-5" data-heading-level="5"><strong>H5</strong> Heading 5</button>
|
|
7742
|
+
<button type="button" role="menuitem" data-format="heading-6" data-heading-level="6"><strong>H6</strong> Heading 6</button>
|
|
7743
|
+
</div>
|
|
7744
|
+
</div>
|
|
7745
|
+
</div>
|
|
7746
|
+
<span class="edit-toolbar-divider" aria-hidden="true"></span>
|
|
7747
|
+
<div class="edit-toolbar-group">
|
|
7748
|
+
<button type="button" data-format="bullet-list" data-format-control="bulletList" title="Bulleted list" aria-label="Bulleted list">${BULLET_LIST_ICON_HTML}</button>
|
|
7749
|
+
<button type="button" data-format="ordered-list" data-format-control="orderedList" title="Numbered list" aria-label="Numbered list">${ORDERED_LIST_ICON_HTML}</button>
|
|
7750
|
+
<div class="format-menu" data-format-control="code">
|
|
7751
|
+
<button type="button" class="format-menu-toggle" data-format-menu-toggle
|
|
7752
|
+
aria-label="Code" aria-haspopup="menu" aria-expanded="false">
|
|
7753
|
+
${CODE_ICON_HTML}${CHEVRON_ICON_HTML}
|
|
7754
|
+
</button>
|
|
7755
|
+
<div class="format-menu-popover" data-format-menu role="menu" aria-label="Code format" hidden>
|
|
7756
|
+
<button type="button" role="menuitem" data-format="code" data-code-kind="inline">Inline code</button>
|
|
7757
|
+
<button type="button" role="menuitem" data-format="code-block" data-code-kind="block">Code block</button>
|
|
7758
|
+
</div>
|
|
7759
|
+
</div>
|
|
7760
|
+
<button type="button" data-format="quote" data-format-control="blockquote" title="Blockquote" aria-label="Blockquote">${QUOTE_ICON_HTML}</button>
|
|
7761
|
+
<button type="button" data-format="line-break" data-format-control="lineBreak" title="Line break" aria-label="Line break">${LINE_BREAK_ICON_HTML}</button>
|
|
7762
|
+
</div>
|
|
7763
|
+
<span class="edit-toolbar-divider" aria-hidden="true"></span>
|
|
7764
|
+
<div class="edit-toolbar-group">
|
|
7765
|
+
<button type="button" data-format="link" data-format-control="link" title="Link" aria-label="Link">${LINK_ICON_HTML}</button>
|
|
7766
|
+
<button type="button" data-format="image" data-format-control="image" title="Image" aria-label="Image">${IMAGE_FORMAT_ICON_HTML}</button>
|
|
7767
|
+
<input type="file" data-ref="image-input" accept="image/*" hidden />
|
|
7768
|
+
</div>
|
|
7769
|
+
</div>
|
|
7770
|
+
</div>
|
|
7771
|
+
|
|
7772
|
+
<div class="toolbar-buttons view-mode-toggle-group" data-ref="layout-controls" role="group" aria-label="Editor layout">
|
|
7773
|
+
<button type="button" class="icon-toggle view-mode-toggle" data-ref="source-btn" title="Edit" aria-label="Edit" aria-pressed="false">
|
|
7774
|
+
<span class="commandbar-flex-container" data-ref="source-icon">${SOURCE_EDIT_ICON_HTML}</span>
|
|
7775
|
+
</button>
|
|
7776
|
+
<button type="button" class="icon-toggle view-mode-toggle" data-ref="split-btn" title="Split" aria-label="Split" aria-pressed="false">
|
|
7777
|
+
<span class="commandbar-flex-container">
|
|
7778
|
+
${SPLIT_ICON_HTML}
|
|
7779
|
+
</span>
|
|
7780
|
+
</button>
|
|
7781
|
+
<button type="button" class="icon-toggle view-mode-toggle" data-ref="preview-btn" title="View" aria-label="View" aria-pressed="false">
|
|
7782
|
+
<span class="commandbar-flex-container">
|
|
7783
|
+
${PREVIEW_ICON_HTML}
|
|
7784
|
+
</span>
|
|
7785
|
+
</button>
|
|
7786
|
+
</div>
|
|
7787
|
+
|
|
7788
|
+
<div class="toolbar-controls" data-ref="toolbar-controls">
|
|
7789
|
+
<button type="button" class="icon-toggle" data-ref="search-btn" title="Find/replace (Mod-F)" aria-label="Find/replace">
|
|
7790
|
+
${SEARCH_ICON_HTML}
|
|
7791
|
+
</button>
|
|
7792
|
+
<button type="button" class="icon-toggle" data-ref="save-btn" title="Save" aria-label="Save">
|
|
7793
|
+
${SAVE_ICON_HTML}
|
|
7794
|
+
</button>
|
|
7795
|
+
<button type="button" class="icon-toggle zoom-toggle" data-ref="zoom-btn" title="Zoom controls" aria-label="Zoom controls">
|
|
7796
|
+
${ZOOM_ICON_HTML}
|
|
7797
|
+
</button>
|
|
7798
|
+
<div class="toolbar-menu width-menu" data-ref="width-controls">
|
|
7799
|
+
<button type="button" class="icon-toggle toolbar-menu-toggle" data-ref="width-menu-btn"
|
|
7800
|
+
title="Page width" aria-label="Page width" aria-haspopup="menu" aria-expanded="false">
|
|
7801
|
+
<span class="toolbar-menu-icon" data-ref="width-menu-icon">${PAGE_WIDTH_DEFAULT_ICON_HTML}</span>
|
|
7802
|
+
</button>
|
|
7803
|
+
<div class="toolbar-popover" data-ref="width-popover" role="menu" aria-label="Page width" hidden>
|
|
7804
|
+
${['narrow', 'default', 'wide', 'full'].map((choice) => `
|
|
7805
|
+
<button type="button" role="menuitemradio" class="icon-toggle toolbar-menu-item" data-width="${choice}"
|
|
7806
|
+
title="${PREVIEW_WIDTH_LABELS[choice]}" aria-label="${PREVIEW_WIDTH_LABELS[choice]}" aria-checked="false">
|
|
7807
|
+
${PREVIEW_WIDTH_ICONS_HTML[choice]}
|
|
7808
|
+
</button>`).join('')}
|
|
7809
|
+
</div>
|
|
7810
|
+
</div>
|
|
7811
|
+
<div class="theme-menu" data-ref="theme-controls">
|
|
7812
|
+
<button type="button" class="icon-toggle theme-menu-toggle" data-ref="theme-menu-btn"
|
|
7813
|
+
title="Color scheme" aria-label="Color scheme" aria-haspopup="menu" aria-expanded="false">
|
|
7814
|
+
<span class="theme-menu-icon" data-ref="theme-menu-icon">${SYSTEM_THEME_ICON_HTML}</span>
|
|
7815
|
+
</button>
|
|
7816
|
+
<div class="theme-popover" data-ref="theme-popover" role="menu" aria-label="Color scheme" hidden>
|
|
7817
|
+
<button type="button" role="menuitemradio" class="icon-toggle theme-toggle" data-ref="light-theme-btn"
|
|
7818
|
+
title="Light mode" aria-label="Light mode" aria-checked="false">
|
|
7819
|
+
${LIGHT_THEME_ICON_HTML}
|
|
7820
|
+
</button>
|
|
7821
|
+
<button type="button" role="menuitemradio" class="icon-toggle theme-toggle" data-ref="dark-theme-btn"
|
|
7822
|
+
title="Dark mode" aria-label="Dark mode" aria-checked="false">
|
|
7823
|
+
${DARK_THEME_ICON_HTML}
|
|
7824
|
+
</button>
|
|
7825
|
+
<button type="button" role="menuitemradio" class="icon-toggle theme-toggle" data-ref="system-theme-btn"
|
|
7826
|
+
title="System setting" aria-label="System setting" aria-checked="false">
|
|
7827
|
+
${SYSTEM_THEME_ICON_HTML}
|
|
7828
|
+
</button>
|
|
7829
|
+
</div>
|
|
7830
|
+
</div>
|
|
7831
|
+
<div class="zoom-popover" data-ref="zoom-popover" hidden role="group" aria-label="Zoom">
|
|
7832
|
+
<span class="zoom-level" data-ref="zoom-level">100%</span>
|
|
7833
|
+
<span class="zoom-stepper">
|
|
7834
|
+
<button type="button" data-action="zoom-out" title="Zoom out" aria-label="Zoom out">-</button>
|
|
7835
|
+
<button type="button" data-action="zoom-in" title="Zoom in" aria-label="Zoom in">+</button>
|
|
7836
|
+
</span>
|
|
7837
|
+
<button type="button" class="zoom-reset" data-action="zoom-reset" title="Reset zoom">Reset</button>
|
|
7838
|
+
</div>
|
|
7839
|
+
</div>
|
|
7840
|
+
</header>
|
|
7841
|
+
|
|
7842
|
+
<div class="workspace-shell" data-ref="workspace-shell" hidden>
|
|
7843
|
+
<aside class="nav-pane" data-ref="nav-pane" aria-label="Package contents">
|
|
7844
|
+
<div class="nav-tree" data-ref="nav-tree"></div>
|
|
7845
|
+
</aside>
|
|
7846
|
+
<div class="nav-resizer" data-ref="nav-resizer"
|
|
7847
|
+
role="separator" aria-orientation="vertical" aria-label="Resize contents pane"></div>
|
|
7848
|
+
|
|
7849
|
+
<div class="pane-stack" data-ref="pane-stack">
|
|
7850
|
+
<section class="pane edit-pane" data-ref="edit-pane">
|
|
7851
|
+
<div class="editor-host" data-ref="editor-host"></div>
|
|
7852
|
+
</section>
|
|
7853
|
+
<div class="split-resizer" data-ref="split-resizer"
|
|
7854
|
+
role="separator" aria-orientation="vertical" aria-label="Resize split panes"></div>
|
|
7855
|
+
<section class="pane preview-pane" data-ref="preview-pane" tabindex="-1">
|
|
7856
|
+
<article class="preview-content" data-ref="preview-content"></article>
|
|
7857
|
+
</section>
|
|
7858
|
+
<section class="pane entry-pane" data-ref="entry-pane"></section>
|
|
7859
|
+
</div>
|
|
7860
|
+
</div>
|
|
7861
|
+
|
|
7862
|
+
<div class="title-dialog-backdrop" data-ref="title-dialog" hidden
|
|
7863
|
+
role="dialog" aria-modal="true" aria-labelledby="mdzip-title-dialog-heading">
|
|
7864
|
+
<div class="title-dialog">
|
|
7865
|
+
<h3 id="mdzip-title-dialog-heading">Set Document Title</h3>
|
|
7866
|
+
<p>This is the package-level title stored in manifest.json.</p>
|
|
7867
|
+
<input type="text" maxlength="120" data-ref="title-input" aria-label="Document title" />
|
|
7868
|
+
<p class="title-dialog-validation" data-ref="title-validation" hidden>Title cannot be empty.</p>
|
|
7869
|
+
<p>If unset, consumers may fall back to entry point, filename, or first heading.</p>
|
|
7870
|
+
<div class="title-dialog-actions">
|
|
7871
|
+
<button type="button" class="reset-title" data-ref="title-reset-btn">Reset</button>
|
|
7872
|
+
<button type="button" data-action="cancel-title">Cancel</button>
|
|
7873
|
+
<button type="button" class="save-title" data-ref="title-save-btn">Save</button>
|
|
7874
|
+
</div>
|
|
7875
|
+
</div>
|
|
7876
|
+
</div>
|
|
7877
|
+
|
|
7878
|
+
<div class="title-dialog-backdrop" data-ref="conversion-dialog" hidden
|
|
7879
|
+
role="dialog" aria-modal="true" aria-labelledby="mdzip-conversion-dialog-heading">
|
|
7880
|
+
<div class="title-dialog">
|
|
7881
|
+
<h3 id="mdzip-conversion-dialog-heading">Convert to MDZ?</h3>
|
|
7882
|
+
<p>
|
|
7883
|
+
Regular Markdown files contain only text. Convert this document to an
|
|
7884
|
+
MDZ package to add images, package navigation, and document metadata.
|
|
7885
|
+
</p>
|
|
7886
|
+
<p>The next save will produce an .mdz file.</p>
|
|
7887
|
+
<div class="title-dialog-actions">
|
|
7888
|
+
<button type="button" data-action="cancel-conversion">Cancel</button>
|
|
7889
|
+
<button type="button" class="save-title" data-ref="conversion-confirm-btn">Convert</button>
|
|
7890
|
+
</div>
|
|
7891
|
+
</div>
|
|
7892
|
+
</div>
|
|
7893
|
+
|
|
7894
|
+
<div class="title-dialog-backdrop" data-ref="image-insert-dialog" hidden
|
|
7895
|
+
role="dialog" aria-modal="true" aria-labelledby="mdzip-image-insert-dialog-heading">
|
|
7896
|
+
<div class="title-dialog image-insert-dialog">
|
|
7897
|
+
<h3 id="mdzip-image-insert-dialog-heading">Insert Image</h3>
|
|
7898
|
+
<p>Choose the Markdown inserted for this image.</p>
|
|
7899
|
+
<fieldset class="image-insert-options">
|
|
7900
|
+
<legend>Output</legend>
|
|
7901
|
+
<label>
|
|
7902
|
+
<input type="radio" name="mdzip-image-insert-mode" value="markdown"
|
|
7903
|
+
data-ref="image-insert-mode-markdown" checked />
|
|
7904
|
+
Markdown image
|
|
7905
|
+
</label>
|
|
7906
|
+
<label>
|
|
7907
|
+
<input type="radio" name="mdzip-image-insert-mode" value="html"
|
|
7908
|
+
data-ref="image-insert-mode-html" />
|
|
7909
|
+
HTML <img> with sizing
|
|
7910
|
+
</label>
|
|
7911
|
+
</fieldset>
|
|
7912
|
+
<label class="image-insert-field">
|
|
7913
|
+
<span>Alt text</span>
|
|
7914
|
+
<input type="text" data-ref="image-insert-alt" />
|
|
7915
|
+
</label>
|
|
7916
|
+
<div class="image-insert-grid">
|
|
7917
|
+
<label class="image-insert-field">
|
|
7918
|
+
<span>Size by</span>
|
|
7919
|
+
<select data-ref="image-insert-size-mode">
|
|
7920
|
+
<option value="original">Original size</option>
|
|
7921
|
+
<option value="width" selected>Width</option>
|
|
7922
|
+
<option value="height">Height</option>
|
|
7923
|
+
<option value="percent">Percent</option>
|
|
7924
|
+
</select>
|
|
7925
|
+
</label>
|
|
7926
|
+
<label class="image-insert-field">
|
|
7927
|
+
<span>Value</span>
|
|
7928
|
+
<input type="number" min="1" step="1" data-ref="image-insert-size-value" />
|
|
7929
|
+
</label>
|
|
7930
|
+
</div>
|
|
7931
|
+
<label class="image-insert-field">
|
|
7932
|
+
<span>Position</span>
|
|
7933
|
+
<select data-ref="image-insert-position">
|
|
7934
|
+
<option value="inline">Inline/default</option>
|
|
7935
|
+
<option value="left">Left</option>
|
|
7936
|
+
<option value="center">Center</option>
|
|
7937
|
+
<option value="right">Right</option>
|
|
7938
|
+
<option value="wrap-left">Wrap left</option>
|
|
7939
|
+
<option value="wrap-right">Wrap right</option>
|
|
7940
|
+
</select>
|
|
7941
|
+
</label>
|
|
7942
|
+
<div class="title-dialog-actions">
|
|
7943
|
+
<button type="button" data-action="cancel-image-insert">Cancel</button>
|
|
7944
|
+
<button type="button" class="save-title" data-ref="image-insert-confirm-btn">Insert</button>
|
|
7945
|
+
</div>
|
|
7946
|
+
</div>
|
|
7947
|
+
</div>
|
|
7948
|
+
|
|
7949
|
+
<div class="title-dialog-backdrop" data-ref="pack-files-dialog" hidden
|
|
7950
|
+
role="dialog" aria-modal="true" aria-labelledby="mdzip-pack-files-dialog-heading">
|
|
7951
|
+
<div class="title-dialog pack-files-dialog">
|
|
7952
|
+
<h3 id="mdzip-pack-files-dialog-heading">Package Markdown Files</h3>
|
|
7953
|
+
<p>These files contain more than one Markdown document. Choose how to package them.</p>
|
|
7954
|
+
<fieldset class="pack-files-options">
|
|
7955
|
+
<legend>Mode</legend>
|
|
7956
|
+
<label>
|
|
7957
|
+
<input type="radio" name="mdzip-pack-files-mode" value="document"
|
|
7958
|
+
data-ref="pack-files-mode-document" checked />
|
|
7959
|
+
Document — one primary page; the rest are attached
|
|
7960
|
+
</label>
|
|
7961
|
+
<label>
|
|
7962
|
+
<input type="radio" name="mdzip-pack-files-mode" value="project"
|
|
7963
|
+
data-ref="pack-files-mode-project" />
|
|
7964
|
+
Project — a set of related documents
|
|
7965
|
+
</label>
|
|
7966
|
+
</fieldset>
|
|
7967
|
+
<label class="pack-files-field">
|
|
7968
|
+
<span>Entry point</span>
|
|
7969
|
+
<select data-ref="pack-files-entry"></select>
|
|
7970
|
+
</label>
|
|
7971
|
+
<div class="title-dialog-actions">
|
|
7972
|
+
<button type="button" data-action="cancel-pack-files">Cancel</button>
|
|
7973
|
+
<button type="button" class="save-title" data-ref="pack-files-confirm-btn">Package</button>
|
|
7974
|
+
</div>
|
|
7975
|
+
</div>
|
|
7976
|
+
</div>
|
|
7977
|
+
|
|
7978
|
+
<div class="title-dialog-backdrop" data-ref="copy-render-dialog" hidden
|
|
7979
|
+
role="dialog" aria-modal="true" aria-labelledby="mdzip-copy-render-dialog-heading">
|
|
7980
|
+
<div class="title-dialog copy-render-dialog">
|
|
7981
|
+
<h3 id="mdzip-copy-render-dialog-heading" data-ref="copy-render-heading">Copying document...</h3>
|
|
7982
|
+
<div data-ref="copy-render-progress-section">
|
|
7983
|
+
<p data-ref="copy-render-progress-text">Rendering the full document...</p>
|
|
7984
|
+
<div class="copy-render-progress-track">
|
|
7985
|
+
<div class="copy-render-progress-bar" data-ref="copy-render-progress-bar"></div>
|
|
7986
|
+
</div>
|
|
7987
|
+
</div>
|
|
7988
|
+
<p data-ref="copy-render-ready-text" hidden>
|
|
7989
|
+
The document is ready. Browsers only allow a clipboard write right after a click, so this needs one more —
|
|
7990
|
+
click Copy to finish.
|
|
7991
|
+
</p>
|
|
7992
|
+
<p data-ref="copy-render-done-text" hidden></p>
|
|
7993
|
+
<div class="title-dialog-actions">
|
|
7994
|
+
<button type="button" data-action="cancel-copy-render" data-ref="copy-render-cancel-btn">Cancel</button>
|
|
7995
|
+
<button type="button" data-action="cancel-copy-ready" data-ref="copy-render-ready-cancel-btn" hidden>Cancel</button>
|
|
7996
|
+
<button type="button" class="save-title" data-action="confirm-copy-ready" data-ref="copy-render-confirm-btn" hidden>Copy</button>
|
|
7997
|
+
<button type="button" class="save-title" data-action="dismiss-copy-render" data-ref="copy-render-dismiss-btn" hidden>Dismiss</button>
|
|
7998
|
+
</div>
|
|
7999
|
+
</div>
|
|
8000
|
+
</div>
|
|
8001
|
+
|
|
8002
|
+
<div class="title-dialog-backdrop" data-ref="metadata-dialog" hidden
|
|
8003
|
+
role="dialog" aria-modal="true" aria-labelledby="mdzip-metadata-dialog-heading">
|
|
8004
|
+
<div class="title-dialog metadata-dialog">
|
|
8005
|
+
<h3 id="mdzip-metadata-dialog-heading">Document Information</h3>
|
|
8006
|
+
<dl data-ref="metadata-list"></dl>
|
|
8007
|
+
<h4>Libraries</h4>
|
|
8008
|
+
<dl data-ref="library-list"></dl>
|
|
8009
|
+
<div class="title-dialog-actions">
|
|
8010
|
+
<button type="button" class="save-title" data-action="close-metadata">Close</button>
|
|
8011
|
+
</div>
|
|
8012
|
+
</div>
|
|
8013
|
+
</div>
|
|
8014
|
+
|
|
8015
|
+
<div class="title-dialog-backdrop" data-ref="name-dialog" hidden
|
|
8016
|
+
role="dialog" aria-modal="true" aria-labelledby="mdzip-name-dialog-heading">
|
|
8017
|
+
<div class="title-dialog">
|
|
8018
|
+
<h3 id="mdzip-name-dialog-heading" data-ref="name-dialog-heading">New Markdown File</h3>
|
|
8019
|
+
<input type="text" maxlength="260" data-ref="name-input" aria-label="File name" />
|
|
8020
|
+
<p class="title-dialog-validation" data-ref="name-validation" hidden></p>
|
|
8021
|
+
<div class="title-dialog-actions">
|
|
8022
|
+
<button type="button" data-action="cancel-name">Cancel</button>
|
|
8023
|
+
<button type="button" class="save-title" data-ref="name-confirm-btn">Create</button>
|
|
8024
|
+
</div>
|
|
8025
|
+
</div>
|
|
8026
|
+
</div>
|
|
8027
|
+
|
|
8028
|
+
<div class="title-dialog-backdrop" data-ref="delete-dialog" hidden
|
|
8029
|
+
role="dialog" aria-modal="true" aria-labelledby="mdzip-delete-dialog-heading">
|
|
8030
|
+
<div class="title-dialog">
|
|
8031
|
+
<h3 id="mdzip-delete-dialog-heading">Delete File?</h3>
|
|
8032
|
+
<p data-ref="delete-dialog-text"></p>
|
|
8033
|
+
<div class="title-dialog-actions">
|
|
8034
|
+
<button type="button" data-action="cancel-delete">Cancel</button>
|
|
8035
|
+
<button type="button" class="danger-action" data-ref="delete-confirm-btn">Delete</button>
|
|
8036
|
+
</div>
|
|
8037
|
+
</div>
|
|
8038
|
+
</div>
|
|
8039
|
+
|
|
8040
|
+
<input type="file" data-ref="replace-input" hidden />
|
|
8041
|
+
|
|
8042
|
+
<div class="nav-context-menu" data-ref="nav-menu" hidden role="menu"></div>
|
|
8043
|
+
|
|
8044
|
+
<div class="mdzip-tooltip" data-ref="tooltip" role="tooltip" hidden></div>
|
|
8045
|
+
|
|
8046
|
+
<div class="mdzip-copy-toast" data-ref="copy-toast" role="status" aria-live="polite" hidden></div>
|
|
8047
|
+
|
|
8048
|
+
<p class="mdzip-empty" data-ref="empty-state">No MDZip workspace loaded.</p>
|
|
8049
|
+
</section>
|
|
7323
8050
|
`;
|
|
7324
8051
|
//# sourceMappingURL=view.js.map
|