collabmd 0.1.47 → 0.1.48
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -6
- package/dist/client/assets/JetBrainsMono-Regular-BQaDgvhP.woff2 +0 -0
- package/dist/client/assets/babel-CY5KOv-I.js +16 -0
- package/dist/client/assets/comment-ui-shared-DuoyznXW.js +7 -0
- package/dist/client/assets/dist-ChRoo3IB.js +9 -0
- package/dist/client/assets/document-formatter-Dxl_a8XO.js +2 -0
- package/dist/client/assets/{drawio-editor-BjrxTe3l.js → drawio-editor-BVaNNtYp.js} +2 -2
- package/dist/client/assets/{drawioEditor-DUVZjSSr.js → drawioEditor-BkIBlXx0.js} +2 -2
- package/dist/client/assets/editor-session-DMyVdHLj.js +54 -0
- package/dist/client/assets/embedded-editor-base-B3l6Xy65.css +1 -0
- package/dist/client/assets/estree-CuT_pB8K.js +44 -0
- package/dist/client/assets/{excalidraw-editor-BlcN6umJ.css → excalidraw-editor-BbSbI80a.css} +1 -1
- package/dist/client/assets/excalidraw-editor-BjYaNEMY.js +9 -0
- package/dist/client/assets/{excalidrawEditor-B0jOK3lv.js → excalidrawEditor-ChqF_d82.js} +2 -2
- package/dist/client/assets/export-page-Dyu0XCbu.js +57 -0
- package/dist/client/assets/exportDocument-CJBx4Xjx.css +1 -0
- package/dist/client/assets/{exportDocument-Dg9MjkIk.js → exportDocument-Dgvfa2Sd.js} +2 -2
- package/dist/client/assets/{file-icon-svg-CQAjlWjK.js → file-icon-svg-Y_kpF957.js} +1 -1
- package/dist/client/assets/file-kind-BwMIcZbi.js +1 -0
- package/dist/client/assets/file-search-DDLyyLDU.js +1 -0
- package/dist/client/assets/html-Cdnr2qzU.js +25 -0
- package/dist/client/assets/index-CSNIMyOP.css +1 -0
- package/dist/client/assets/index-DG3N2Ps-.js +2 -0
- package/dist/client/assets/main-D0wCxpVn.js +903 -0
- package/dist/client/assets/markdown-CByV-4CU.js +70 -0
- package/dist/client/assets/mermaid.core-BJRLUQZE.js +11 -0
- package/dist/client/assets/{preview-render-compiler-BApvlLLo.js → preview-render-compiler-CrJIooVD.js} +2 -2
- package/dist/client/assets/preview-render-compiler-O0JxBVNF.js +1 -0
- package/dist/client/assets/{preview-render-profile-CE-5rVTC.js → preview-render-profile-B4RWYWHt.js} +1 -1
- package/dist/client/assets/{preview-render-worker-BmN79bVc.js → preview-render-worker-_QTxqKvP.js} +22 -22
- package/dist/client/assets/quick-switcher-controller-CaEjEtq_.js +1 -0
- package/dist/client/assets/standalone-6z5x8CWe.js +29 -0
- package/dist/client/assets/{vault-api-client-_LpV1aIn.js → vault-api-client-BkwdFjx0.js} +2 -2
- package/dist/client/assets/{es-CAsWR6hG.js → vault-utils-DvoSftJT.js} +1 -1
- package/dist/client/assets/yaml-9ltHPDNO.js +154 -0
- package/dist/client/drawio-editor.html +1 -1
- package/dist/client/excalidraw-editor.html +1 -1
- package/dist/client/export-document.html +2 -2
- package/dist/client/index.html +41 -19
- package/package.json +4 -1
- package/src/client/app/index.html +39 -17
- package/src/client/application/app-shell/export-feature.js +17 -1
- package/src/client/application/app-shell/git-feature.js +28 -9
- package/src/client/application/app-shell/ui-feature-identity.js +13 -0
- package/src/client/application/app-shell/ui-feature-shell.js +164 -9
- package/src/client/application/app-shell-elements.js +4 -0
- package/src/client/application/workspace-coordinator.js +21 -4
- package/src/client/application/workspace-preview-controller.js +120 -2
- package/src/client/application/workspace-route-controller.js +4 -1
- package/src/client/assets/vendor/jetbrains-mono/JetBrainsMono-Regular.woff2 +0 -0
- package/src/client/assets/vendor/jetbrains-mono/OFL.txt +93 -0
- package/src/client/bootstrap/collabmd-app-shell.js +21 -5
- package/src/client/domain/document-formatter.js +193 -0
- package/src/client/domain/file-search.js +73 -0
- package/src/client/domain/git-labels.js +5 -0
- package/src/client/domain/vault-paths.js +6 -4
- package/src/client/domain/wiki-link-completions.js +20 -20
- package/src/client/drawio-editor.js +8 -3
- package/src/client/excalidraw-editor.js +152 -6
- package/src/client/export/export-host.js +56 -35
- package/src/client/export/export-page.js +14 -4
- package/src/client/export/export-pipeline.js +266 -25
- package/src/client/infrastructure/editor-session.js +4 -0
- package/src/client/infrastructure/editor-view-adapter.js +60 -14
- package/src/client/presentation/bases-preview-controller.js +35 -11
- package/src/client/presentation/file-action-controller.js +15 -2
- package/src/client/presentation/file-explorer-controller.js +3 -0
- package/src/client/presentation/file-explorer-view.js +2 -1
- package/src/client/presentation/file-history-view-controller.js +6 -1
- package/src/client/presentation/file-tree-state.js +1 -11
- package/src/client/presentation/git-diff-view-controller.js +36 -11
- package/src/client/presentation/git-panel-controller.js +10 -6
- package/src/client/presentation/quick-switcher-controller.js +54 -154
- package/src/client/styles/components/scrollbars.css +10 -0
- package/src/client/styles/export-document.css +44 -29
- package/src/client/styles/features/collaboration-presence.css +13 -1
- package/src/client/styles/features/preview-markdown.css +54 -0
- package/src/client/styles/foundation/tokens.css +8 -0
- package/src/client/styles/layout/shell.css +76 -24
- package/src/client/styles/layout/view-modes.css +2 -0
- package/src/client/styles/overrides/codemirror.css +31 -19
- package/src/client/styles/surfaces/excalidraw-editor.css +42 -0
- package/src/domain/file-kind.js +12 -1
- package/src/server/domain/ripgrep-search-service.js +110 -37
- package/src/server/infrastructure/persistence/path-utils.js +10 -4
- package/src/server/infrastructure/persistence/vault-file-store.js +2 -0
- package/dist/client/assets/comment-ui-shared-DWkk9Vh3.js +0 -7
- package/dist/client/assets/editor-session-DfZuRgB_.js +0 -54
- package/dist/client/assets/embedded-editor-base-DvMynhKu.css +0 -1
- package/dist/client/assets/excalidraw-editor-D7pnUCLH.js +0 -9
- package/dist/client/assets/export-page-Bs3xE1fg.js +0 -57
- package/dist/client/assets/exportDocument-PnYU3v1s.css +0 -1
- package/dist/client/assets/file-kind-BbqgGzpc.js +0 -1
- package/dist/client/assets/index-BIpgxyGZ.css +0 -1
- package/dist/client/assets/index-Z5sI0Bj-.js +0 -2
- package/dist/client/assets/main-Dwnj9Gaq.js +0 -903
- package/dist/client/assets/mermaid.core-CLZBlpIW.js +0 -11
- package/dist/client/assets/preview-render-compiler-BQ_3h9vD.js +0 -1
- package/dist/client/assets/quick-switcher-controller-ButWPmCI.js +0 -12
- package/dist/client/assets/vault-utils-wdQ0RNR9.js +0 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { supportsBacklinksForFilePath } from '../../../domain/file-kind.js';
|
|
1
|
+
import { getVaultFileKind, supportsBacklinksForFilePath } from '../../../domain/file-kind.js';
|
|
2
2
|
import { isPlainQuickSwitcherShortcut } from '../../domain/keyboard-shortcuts.js';
|
|
3
3
|
import { createFileRouteHash, isCollabMdHashRoute } from '../../domain/hash-routes.js';
|
|
4
4
|
|
|
@@ -189,6 +189,13 @@ function bindToolbarEvents() {
|
|
|
189
189
|
this.closeToolbarOverflowMenu?.();
|
|
190
190
|
});
|
|
191
191
|
|
|
192
|
+
this.elements.reviewFileChangesButton?.addEventListener('click', () => {
|
|
193
|
+
this.handleGitDiffSelection(this.currentFilePath, {
|
|
194
|
+
closeSidebarOnMobile: true,
|
|
195
|
+
scope: 'all',
|
|
196
|
+
});
|
|
197
|
+
});
|
|
198
|
+
|
|
192
199
|
this.elements.fileHistoryButton?.addEventListener('click', () => {
|
|
193
200
|
const route = this.navigation.getHashRoute();
|
|
194
201
|
if (route.type === 'git-file-preview') {
|
|
@@ -210,7 +217,7 @@ function bindToolbarEvents() {
|
|
|
210
217
|
/** @this {UiShellContext} */
|
|
211
218
|
function bindDialogEvents() {
|
|
212
219
|
this.elements.displayNameCancel?.addEventListener('click', () => {
|
|
213
|
-
this.
|
|
220
|
+
this.handleDisplayNameCancel();
|
|
214
221
|
});
|
|
215
222
|
|
|
216
223
|
this.elements.gitCommitCancel?.addEventListener('click', () => {
|
|
@@ -230,7 +237,7 @@ function bindDialogEvents() {
|
|
|
230
237
|
this.elements.gitCommitInput.value = '';
|
|
231
238
|
}
|
|
232
239
|
if (this.elements.gitCommitSubmit) {
|
|
233
|
-
this.elements.gitCommitSubmit.textContent = 'Commit
|
|
240
|
+
this.elements.gitCommitSubmit.textContent = 'Commit included files';
|
|
234
241
|
}
|
|
235
242
|
});
|
|
236
243
|
|
|
@@ -255,6 +262,19 @@ function bindDialogEvents() {
|
|
|
255
262
|
});
|
|
256
263
|
}
|
|
257
264
|
|
|
265
|
+
/** @this {UiShellContext} */
|
|
266
|
+
function focusEditor(event) {
|
|
267
|
+
event?.preventDefault?.();
|
|
268
|
+
const container = this.elements.editorContainer;
|
|
269
|
+
if (!container) return;
|
|
270
|
+
|
|
271
|
+
const target = container.querySelector('.cm-content, iframe, [contenteditable="true"]') ?? container;
|
|
272
|
+
if (target === container && !container.hasAttribute('tabindex')) {
|
|
273
|
+
container.setAttribute('tabindex', '-1');
|
|
274
|
+
}
|
|
275
|
+
target.focus();
|
|
276
|
+
}
|
|
277
|
+
|
|
258
278
|
/** @this {UiShellContext} */
|
|
259
279
|
function bindEvents() {
|
|
260
280
|
const stopFollowing = () => this.stopFollowingUser?.();
|
|
@@ -263,6 +283,7 @@ function bindEvents() {
|
|
|
263
283
|
this.elements.editorContainer?.addEventListener('wheel', stopFollowing, { passive: true });
|
|
264
284
|
this.elements.previewContainer?.addEventListener('pointerdown', stopFollowing);
|
|
265
285
|
this.elements.previewContainer?.addEventListener('wheel', stopFollowing, { passive: true });
|
|
286
|
+
this.elements.skipToEditor?.addEventListener('click', (event) => this.focusEditor(event));
|
|
266
287
|
|
|
267
288
|
this.elements.emptyStateNewFileBtn?.addEventListener('click', () => {
|
|
268
289
|
this.fileExplorer.actionController.openRootCreateMenu({
|
|
@@ -329,6 +350,10 @@ function bindEvents() {
|
|
|
329
350
|
this.runEditorCommand?.('openSearch');
|
|
330
351
|
});
|
|
331
352
|
|
|
353
|
+
this.elements.editorFormatButton?.addEventListener('click', () => {
|
|
354
|
+
void this.formatCurrentDocument();
|
|
355
|
+
});
|
|
356
|
+
|
|
332
357
|
this.elements.toolbarOverflowToggle?.addEventListener('click', (event) => {
|
|
333
358
|
event.preventDefault();
|
|
334
359
|
this.closePresencePanel?.();
|
|
@@ -508,7 +533,7 @@ function getHeadingLinkLabel(heading) {
|
|
|
508
533
|
return heading.textContent.trim();
|
|
509
534
|
}
|
|
510
535
|
|
|
511
|
-
headingClone.querySelectorAll('.preview-heading-link-button').forEach((button) => button.remove());
|
|
536
|
+
headingClone.querySelectorAll('.preview-heading-fold-button, .preview-heading-link-button').forEach((button) => button.remove());
|
|
512
537
|
return headingClone.textContent.replace(/\s+/g, ' ').trim();
|
|
513
538
|
}
|
|
514
539
|
|
|
@@ -549,6 +574,7 @@ function createPreviewHeadingLinkUrl(anchorId) {
|
|
|
549
574
|
return '';
|
|
550
575
|
}
|
|
551
576
|
|
|
577
|
+
// pi-lens-ignore: unchecked-throwing-call-js
|
|
552
578
|
const url = new URL(window.location.href);
|
|
553
579
|
url.hash = createFileRouteHash(this.currentFilePath, {
|
|
554
580
|
anchor: anchorId,
|
|
@@ -594,6 +620,7 @@ function syncPreviewCodeCopyButtons() {
|
|
|
594
620
|
const pre = code.parentElement;
|
|
595
621
|
const wrapper = document.createElement('div');
|
|
596
622
|
wrapper.className = 'preview-code-block';
|
|
623
|
+
// pi-lens-ignore: no-inner-html-js
|
|
597
624
|
wrapper.innerHTML = `
|
|
598
625
|
<div class="preview-code-actions">
|
|
599
626
|
<button type="button" class="ui-button ui-button--secondary ui-button--compact preview-code-copy-button" aria-label="Copy code" title="Copy code">
|
|
@@ -605,6 +632,90 @@ function syncPreviewCodeCopyButtons() {
|
|
|
605
632
|
});
|
|
606
633
|
}
|
|
607
634
|
|
|
635
|
+
function applyPreviewHeadingFolds(previewContent) {
|
|
636
|
+
const parents = new Set(Array.from(previewContent?.querySelectorAll?.('.preview-heading-fold-button') ?? [])
|
|
637
|
+
.map((button) => button.closest('h1, h2, h3, h4, h5, h6')?.parentElement)
|
|
638
|
+
.filter(Boolean));
|
|
639
|
+
|
|
640
|
+
parents.forEach((parent) => {
|
|
641
|
+
const collapsedLevels = [];
|
|
642
|
+
Array.from(parent.children).forEach((child) => {
|
|
643
|
+
const level = /^H([1-6])$/.exec(child.tagName)?.[1];
|
|
644
|
+
if (level) {
|
|
645
|
+
const headingLevel = Number(level);
|
|
646
|
+
while (collapsedLevels.at(-1) >= headingLevel) {
|
|
647
|
+
collapsedLevels.pop();
|
|
648
|
+
}
|
|
649
|
+
child.hidden = collapsedLevels.length > 0;
|
|
650
|
+
if (child.dataset.previewHeadingCollapsed === 'true') {
|
|
651
|
+
collapsedLevels.push(headingLevel);
|
|
652
|
+
}
|
|
653
|
+
} else {
|
|
654
|
+
child.hidden = collapsedLevels.length > 0;
|
|
655
|
+
}
|
|
656
|
+
});
|
|
657
|
+
});
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
/** @this {UiShellContext} */
|
|
661
|
+
function unfoldPreviewHeading(target) {
|
|
662
|
+
if (!(target instanceof HTMLElement) || !/^H[1-6]$/.test(target.tagName)) {
|
|
663
|
+
return false;
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
const collapsedHeadings = [];
|
|
667
|
+
let ancestorLevel = Number(target.tagName[1]) + 1;
|
|
668
|
+
for (let heading = target; heading; heading = heading.previousElementSibling) {
|
|
669
|
+
const level = Number(/^H([1-6])$/.exec(heading.tagName)?.[1]);
|
|
670
|
+
if (!level || level >= ancestorLevel) {
|
|
671
|
+
continue;
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
ancestorLevel = level;
|
|
675
|
+
if (heading.dataset.previewHeadingCollapsed === 'true') {
|
|
676
|
+
collapsedHeadings.push(heading);
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
if (collapsedHeadings.length === 0) {
|
|
681
|
+
return false;
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
collapsedHeadings.forEach((heading) => delete heading.dataset.previewHeadingCollapsed);
|
|
685
|
+
this.syncPreviewHeadingFoldButtons();
|
|
686
|
+
this.scrollSyncController?.invalidatePreviewBlocks?.();
|
|
687
|
+
this.schedulePreviewLayoutSync?.({ delayMs: 0 });
|
|
688
|
+
this.refreshCommentUiLayout?.();
|
|
689
|
+
return true;
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
/** @this {UiShellContext} */
|
|
693
|
+
function syncPreviewHeadingFoldButtons() {
|
|
694
|
+
const headings = this.elements.previewContent?.querySelectorAll?.('h1, h2, h3, h4, h5, h6') ?? [];
|
|
695
|
+
|
|
696
|
+
Array.from(headings).forEach((heading) => {
|
|
697
|
+
if (!(heading instanceof HTMLElement)) {
|
|
698
|
+
return;
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
const headingLabel = getHeadingLinkLabel(heading);
|
|
702
|
+
let button = heading.querySelector(':scope > .preview-heading-fold-button');
|
|
703
|
+
if (!(button instanceof HTMLButtonElement)) {
|
|
704
|
+
button = document.createElement('button');
|
|
705
|
+
button.type = 'button';
|
|
706
|
+
button.className = 'ui-icon-button preview-heading-fold-button';
|
|
707
|
+
heading.prepend(button);
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
const collapsed = heading.dataset.previewHeadingCollapsed === 'true';
|
|
711
|
+
button.setAttribute('aria-expanded', String(!collapsed));
|
|
712
|
+
button.setAttribute('aria-label', `${collapsed ? 'Expand' : 'Collapse'} ${headingLabel}`);
|
|
713
|
+
button.setAttribute('title', `${collapsed ? 'Expand' : 'Collapse'} section`);
|
|
714
|
+
});
|
|
715
|
+
|
|
716
|
+
applyPreviewHeadingFolds(this.elements.previewContent);
|
|
717
|
+
}
|
|
718
|
+
|
|
608
719
|
/** @this {UiShellContext} */
|
|
609
720
|
function syncPreviewHeadingLinkButtons() {
|
|
610
721
|
const headings = this.elements.previewContent?.querySelectorAll?.('h1[id], h2[id], h3[id], h4[id], h5[id], h6[id]') ?? [];
|
|
@@ -620,6 +731,7 @@ function syncPreviewHeadingLinkButtons() {
|
|
|
620
731
|
button = document.createElement('button');
|
|
621
732
|
button.type = 'button';
|
|
622
733
|
button.className = 'ui-icon-button preview-heading-link-button';
|
|
734
|
+
// pi-lens-ignore: no-inner-html-js
|
|
623
735
|
button.innerHTML = PREVIEW_HEADING_LINK_ICON;
|
|
624
736
|
heading.appendChild(button);
|
|
625
737
|
}
|
|
@@ -693,6 +805,18 @@ function handlePreviewContentClick(event) {
|
|
|
693
805
|
return;
|
|
694
806
|
}
|
|
695
807
|
|
|
808
|
+
const foldButton = event.target.closest('button.preview-heading-fold-button');
|
|
809
|
+
const heading = foldButton?.closest('h1, h2, h3, h4, h5, h6');
|
|
810
|
+
if (foldButton instanceof HTMLButtonElement && heading instanceof HTMLElement) {
|
|
811
|
+
event.preventDefault();
|
|
812
|
+
heading.dataset.previewHeadingCollapsed = String(heading.dataset.previewHeadingCollapsed !== 'true');
|
|
813
|
+
this.syncPreviewHeadingFoldButtons();
|
|
814
|
+
this.scrollSyncController?.invalidatePreviewBlocks?.();
|
|
815
|
+
this.schedulePreviewLayoutSync?.({ delayMs: 0 });
|
|
816
|
+
this.refreshCommentUiLayout?.();
|
|
817
|
+
return;
|
|
818
|
+
}
|
|
819
|
+
|
|
696
820
|
const codeCopyButton = event.target.closest('button.preview-code-copy-button');
|
|
697
821
|
if (codeCopyButton instanceof HTMLButtonElement) {
|
|
698
822
|
event.preventDefault();
|
|
@@ -846,6 +970,29 @@ function getStoredLineWrapping() {
|
|
|
846
970
|
return this.preferences.getLineWrappingEnabled();
|
|
847
971
|
}
|
|
848
972
|
|
|
973
|
+
/** @this {UiShellContext} */
|
|
974
|
+
async function formatCurrentDocument() {
|
|
975
|
+
const button = this.elements.editorFormatButton;
|
|
976
|
+
button?.setAttribute('disabled', '');
|
|
977
|
+
|
|
978
|
+
try {
|
|
979
|
+
const result = await this.session?.formatDocument?.(this.currentFilePath);
|
|
980
|
+
const messages = {
|
|
981
|
+
changed: 'Document changed while formatting. Try again.',
|
|
982
|
+
formatted: getVaultFileKind(this.currentFilePath) === 'plantuml'
|
|
983
|
+
? 'PlantUML indentation applied'
|
|
984
|
+
: 'Document formatted',
|
|
985
|
+
unchanged: 'Document is already formatted',
|
|
986
|
+
unsupported: 'Formatting is unavailable for this file type',
|
|
987
|
+
};
|
|
988
|
+
this.toastController.show(messages[result] ?? messages.unsupported);
|
|
989
|
+
} catch (error) {
|
|
990
|
+
this.toastController.show(`Formatting failed: ${error?.message || 'Unknown error'}`, 5000);
|
|
991
|
+
} finally {
|
|
992
|
+
button?.removeAttribute('disabled');
|
|
993
|
+
}
|
|
994
|
+
}
|
|
995
|
+
|
|
849
996
|
/** @this {UiShellContext} */
|
|
850
997
|
function toggleVimMode() {
|
|
851
998
|
const currentState = this.session?.isVimModeEnabled?.() ?? this.getStoredVimMode();
|
|
@@ -916,13 +1063,17 @@ function hideEditorLoading() {
|
|
|
916
1063
|
}
|
|
917
1064
|
|
|
918
1065
|
/** @this {UiShellContext} */
|
|
919
|
-
function showEditorLoadError() {
|
|
1066
|
+
function showEditorLoadError(message = 'Failed to load file') {
|
|
920
1067
|
if (!this.elements.editorContainer) return;
|
|
1068
|
+
const loading = document.createElement('div');
|
|
1069
|
+
const text = document.createElement('span');
|
|
1070
|
+
loading.className = 'editor-loading';
|
|
1071
|
+
loading.id = 'editorLoading';
|
|
1072
|
+
text.className = 'loading-text';
|
|
1073
|
+
text.textContent = message;
|
|
1074
|
+
loading.appendChild(text);
|
|
921
1075
|
this.elements.editorContainer.classList.remove('is-loading-editor');
|
|
922
|
-
this.elements.editorContainer.
|
|
923
|
-
<div class="editor-loading" id="editorLoading">
|
|
924
|
-
<span class="loading-text">Failed to load file</span>
|
|
925
|
-
</div>`;
|
|
1076
|
+
this.elements.editorContainer.replaceChildren(loading);
|
|
926
1077
|
}
|
|
927
1078
|
|
|
928
1079
|
/** @this {UiShellContext} */
|
|
@@ -938,6 +1089,8 @@ export const uiFeatureShellMethods = {
|
|
|
938
1089
|
copyPreviewCodeBlock,
|
|
939
1090
|
copyPreviewHeadingLink,
|
|
940
1091
|
createPreviewHeadingLinkUrl,
|
|
1092
|
+
focusEditor,
|
|
1093
|
+
formatCurrentDocument,
|
|
941
1094
|
getStoredLineWrapping,
|
|
942
1095
|
getStoredVimMode,
|
|
943
1096
|
handleConnectionChange,
|
|
@@ -958,6 +1111,7 @@ export const uiFeatureShellMethods = {
|
|
|
958
1111
|
showEditorLoadError,
|
|
959
1112
|
showEditorLoading,
|
|
960
1113
|
syncPreviewCodeCopyButtons,
|
|
1114
|
+
syncPreviewHeadingFoldButtons,
|
|
961
1115
|
syncPreviewHeadingLinkButtons,
|
|
962
1116
|
syncVisualViewportBounds,
|
|
963
1117
|
syncToolbarOverflowVisibility,
|
|
@@ -965,5 +1119,6 @@ export const uiFeatureShellMethods = {
|
|
|
965
1119
|
syncWrapToggle,
|
|
966
1120
|
toggleToolbarOverflowMenu,
|
|
967
1121
|
toggleLineWrapping,
|
|
1122
|
+
unfoldPreviewHeading,
|
|
968
1123
|
toggleVimMode,
|
|
969
1124
|
};
|
|
@@ -37,6 +37,7 @@ export function bindAppShellElements(doc = document) {
|
|
|
37
37
|
displayNameTitle: doc.getElementById('displayNameTitle'),
|
|
38
38
|
editNameButton: doc.getElementById('editNameBtn'),
|
|
39
39
|
editorFindButton: doc.getElementById('editorFindBtn'),
|
|
40
|
+
editorFormatButton: doc.getElementById('editorFormatBtn'),
|
|
40
41
|
editorContainer: doc.getElementById('editorContainer'),
|
|
41
42
|
exportDocxButton: doc.getElementById('exportDocxBtn'),
|
|
42
43
|
exportHtmlButton: doc.getElementById('exportHtmlBtn'),
|
|
@@ -55,12 +56,15 @@ export function bindAppShellElements(doc = document) {
|
|
|
55
56
|
gitOperationStatus: doc.getElementById('gitOperationStatus'),
|
|
56
57
|
lineInfo: doc.getElementById('lineInfo'),
|
|
57
58
|
mobileViewToggle: doc.getElementById('mobileViewToggle'),
|
|
59
|
+
htmlPreviewMaximizeButton: doc.getElementById('htmlPreviewMaximize'),
|
|
58
60
|
previewContent: doc.getElementById('previewContent'),
|
|
59
61
|
previewContainer: doc.getElementById('previewContainer'),
|
|
62
|
+
reviewFileChangesButton: doc.getElementById('reviewFileChangesBtn'),
|
|
60
63
|
presencePanel: doc.getElementById('presencePanel'),
|
|
61
64
|
presencePanelList: doc.getElementById('presencePanelList'),
|
|
62
65
|
presencePanelStatus: doc.getElementById('presencePanelStatus'),
|
|
63
66
|
searchFilesButton: doc.getElementById('searchFilesBtn'),
|
|
67
|
+
skipToEditor: doc.getElementById('skipToEditor'),
|
|
64
68
|
toolbarSearchButton: doc.getElementById('toolbarSearchBtn'),
|
|
65
69
|
shareButton: doc.getElementById('shareBtn'),
|
|
66
70
|
toastContainer: doc.getElementById('toastContainer'),
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
isBaseFilePath,
|
|
3
|
+
isHtmlFilePath,
|
|
3
4
|
isMarkdownFilePath,
|
|
4
5
|
supportsBacklinksForFilePath,
|
|
5
6
|
} from '../../domain/file-kind.js';
|
|
@@ -51,6 +52,7 @@ export class WorkspaceCoordinator {
|
|
|
51
52
|
onRenderBasePreview,
|
|
52
53
|
onRenderExcalidrawPreview,
|
|
53
54
|
onRenderDrawioPreview,
|
|
55
|
+
onRenderHtmlPreview,
|
|
54
56
|
onRenderImagePreview,
|
|
55
57
|
onRenderPdfPreview,
|
|
56
58
|
onRenderStructurizrPreview,
|
|
@@ -103,6 +105,7 @@ export class WorkspaceCoordinator {
|
|
|
103
105
|
this.onRenderBasePreview = onRenderBasePreview;
|
|
104
106
|
this.onRenderDrawioPreview = onRenderDrawioPreview;
|
|
105
107
|
this.onRenderExcalidrawPreview = onRenderExcalidrawPreview;
|
|
108
|
+
this.onRenderHtmlPreview = onRenderHtmlPreview;
|
|
106
109
|
this.onRenderImagePreview = onRenderImagePreview;
|
|
107
110
|
this.onRenderPdfPreview = onRenderPdfPreview;
|
|
108
111
|
this.onRenderStructurizrPreview = onRenderStructurizrPreview;
|
|
@@ -180,6 +183,7 @@ export class WorkspaceCoordinator {
|
|
|
180
183
|
isDrawio = false,
|
|
181
184
|
filePath,
|
|
182
185
|
isExcalidraw = false,
|
|
186
|
+
isHtml = false,
|
|
183
187
|
isImage = false,
|
|
184
188
|
isPdf = false,
|
|
185
189
|
supportsBacklinks,
|
|
@@ -187,6 +191,7 @@ export class WorkspaceCoordinator {
|
|
|
187
191
|
if (isExcalidraw) this.onRenderExcalidrawPreview(filePath);
|
|
188
192
|
if (isBase || isBaseFilePath(filePath)) this.onRenderBasePreview(filePath);
|
|
189
193
|
if (isDrawio) this.onRenderDrawioPreview(filePath);
|
|
194
|
+
if (isHtml) this.onRenderHtmlPreview({ content: this.session?.getText?.() ?? '' });
|
|
190
195
|
if (isImage) this.onRenderImagePreview(filePath);
|
|
191
196
|
if (isPdf) this.onRenderPdfPreview(filePath);
|
|
192
197
|
if (this.isStructurizrWorkspaceFile(filePath)) {
|
|
@@ -200,7 +205,14 @@ export class WorkspaceCoordinator {
|
|
|
200
205
|
|
|
201
206
|
async openFile(filePath, { drawioMode = null } = {}) {
|
|
202
207
|
if (!this.isTabActive()) {
|
|
203
|
-
return;
|
|
208
|
+
return false;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
if (!filePath || !this.getFileList().includes(filePath)) {
|
|
212
|
+
this.cleanupSession();
|
|
213
|
+
this.stateStore.sessionLoadToken += 1;
|
|
214
|
+
this.onFileOpenError({ code: 'not-found', filePath });
|
|
215
|
+
return false;
|
|
204
216
|
}
|
|
205
217
|
|
|
206
218
|
const normalizedDrawioMode = drawioMode ?? null;
|
|
@@ -210,6 +222,7 @@ export class WorkspaceCoordinator {
|
|
|
210
222
|
const isBase = this.isBaseFile(filePath);
|
|
211
223
|
const isImage = this.isImageFile(filePath);
|
|
212
224
|
const isPdf = this.isPdfFile(filePath);
|
|
225
|
+
const isHtml = isHtmlFilePath(filePath);
|
|
213
226
|
const isMermaid = this.isMermaidFile(filePath);
|
|
214
227
|
const isPlantUml = this.isPlantUmlFile(filePath);
|
|
215
228
|
const isStructurizrWorkspace = this.isStructurizrWorkspaceFile(filePath);
|
|
@@ -221,7 +234,7 @@ export class WorkspaceCoordinator {
|
|
|
221
234
|
) {
|
|
222
235
|
this.onUpdateActiveFile(filePath);
|
|
223
236
|
this.onUpdateLobbyCurrentFile(filePath);
|
|
224
|
-
return;
|
|
237
|
+
return true;
|
|
225
238
|
}
|
|
226
239
|
|
|
227
240
|
const loadToken = ++this.stateStore.sessionLoadToken;
|
|
@@ -252,7 +265,7 @@ export class WorkspaceCoordinator {
|
|
|
252
265
|
session: null,
|
|
253
266
|
supportsBacklinks: chromeState.supportsBacklinks,
|
|
254
267
|
});
|
|
255
|
-
return;
|
|
268
|
+
return true;
|
|
256
269
|
}
|
|
257
270
|
|
|
258
271
|
const EditorSession = await this.loadEditorSessionClass();
|
|
@@ -272,6 +285,7 @@ export class WorkspaceCoordinator {
|
|
|
272
285
|
|
|
273
286
|
this.onContentChange({
|
|
274
287
|
isBase,
|
|
288
|
+
isHtml,
|
|
275
289
|
isMermaid,
|
|
276
290
|
isPlantUml,
|
|
277
291
|
isStructurizrWorkspace,
|
|
@@ -313,6 +327,7 @@ export class WorkspaceCoordinator {
|
|
|
313
327
|
filePath,
|
|
314
328
|
isBase,
|
|
315
329
|
isExcalidraw,
|
|
330
|
+
isHtml,
|
|
316
331
|
session,
|
|
317
332
|
supportsBacklinks: chromeState.supportsBacklinks,
|
|
318
333
|
});
|
|
@@ -426,6 +441,7 @@ export class WorkspaceCoordinator {
|
|
|
426
441
|
session.ensureInitialContent?.();
|
|
427
442
|
await readySession('post-initialize');
|
|
428
443
|
}
|
|
444
|
+
return true;
|
|
429
445
|
} catch (error) {
|
|
430
446
|
console.error('[app] Failed to initialize editor:', error);
|
|
431
447
|
session.destroy();
|
|
@@ -438,7 +454,8 @@ export class WorkspaceCoordinator {
|
|
|
438
454
|
return;
|
|
439
455
|
}
|
|
440
456
|
|
|
441
|
-
this.onFileOpenError();
|
|
457
|
+
this.onFileOpenError({ code: 'load-failed', filePath });
|
|
458
|
+
return false;
|
|
442
459
|
}
|
|
443
460
|
}
|
|
444
461
|
}
|
|
@@ -1,11 +1,40 @@
|
|
|
1
1
|
import {
|
|
2
2
|
isBaseFilePath,
|
|
3
3
|
isDiagramFilePath,
|
|
4
|
+
isHtmlFilePath,
|
|
4
5
|
isMarkdownFilePath,
|
|
5
6
|
isStructurizrFilePath,
|
|
6
7
|
} from '../../domain/file-kind.js';
|
|
8
|
+
import { canFormatDocument } from '../domain/document-formatter.js';
|
|
9
|
+
import { setDiagramActionButtonIcon } from '../domain/diagram-action-icons.js';
|
|
7
10
|
import { resolveApiUrl } from '../domain/runtime-paths.js';
|
|
8
11
|
|
|
12
|
+
const HTML_PREVIEW_CSP = "default-src 'none'; base-uri about:; form-action 'none'; object-src 'none'; frame-src 'none'; worker-src 'none'; connect-src 'none'; style-src 'unsafe-inline'; img-src data: blob:; font-src data:; media-src data: blob:";
|
|
13
|
+
|
|
14
|
+
function createHtmlPreviewDocument(content, allowScripts = false) {
|
|
15
|
+
const scriptSource = allowScripts ? "'unsafe-inline'" : "'none'";
|
|
16
|
+
return `<meta http-equiv="Content-Security-Policy" content="${HTML_PREVIEW_CSP}; script-src ${scriptSource};"><base href="about:srcdoc">${String(content ?? '')}`;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function hasHtmlScripts(content) {
|
|
20
|
+
const preview = new DOMParser().parseFromString(content, 'text/html');
|
|
21
|
+
if (preview.scripts.length > 0) return true;
|
|
22
|
+
return [...preview.querySelectorAll('*')].some((element) => [...element.attributes].some(({ name, value }) => (
|
|
23
|
+
(name.startsWith('on') && name in element)
|
|
24
|
+
|| ((name === 'href' || name === 'xlink:href') && /^\s*javascript:/iu.test(value))
|
|
25
|
+
)));
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function syncFormatDocumentButton(button, filePath, isPlantUml) {
|
|
29
|
+
if (!button) return;
|
|
30
|
+
const actionLabel = isPlantUml ? 'Indent PlantUML document' : 'Format document';
|
|
31
|
+
button.classList.toggle('hidden', !canFormatDocument(filePath));
|
|
32
|
+
button.setAttribute('aria-label', actionLabel);
|
|
33
|
+
button.setAttribute('title', actionLabel);
|
|
34
|
+
const label = button.querySelector('.ui-action-label');
|
|
35
|
+
if (label) label.textContent = isPlantUml ? 'Indent' : 'Format';
|
|
36
|
+
}
|
|
37
|
+
|
|
9
38
|
export class WorkspacePreviewController {
|
|
10
39
|
constructor({
|
|
11
40
|
backlinksPanel,
|
|
@@ -43,6 +72,14 @@ export class WorkspacePreviewController {
|
|
|
43
72
|
updateTheme() {},
|
|
44
73
|
};
|
|
45
74
|
this.elements = elements;
|
|
75
|
+
this.htmlPreviewShell = null;
|
|
76
|
+
if (this.elements.htmlPreviewMaximizeButton) {
|
|
77
|
+
this.elements.htmlPreviewMaximizeButton.addEventListener('click', () => {
|
|
78
|
+
const isMaximized = this.htmlPreviewShell?.classList.contains('is-maximized');
|
|
79
|
+
this.setHtmlPreviewMaximized(!isMaximized);
|
|
80
|
+
});
|
|
81
|
+
setDiagramActionButtonIcon(this.elements.htmlPreviewMaximizeButton, 'maximize');
|
|
82
|
+
}
|
|
46
83
|
this.excalidrawEmbed = excalidrawEmbed;
|
|
47
84
|
this.getDisplayName = getDisplayName;
|
|
48
85
|
this.getSession = getSession;
|
|
@@ -91,13 +128,29 @@ export class WorkspacePreviewController {
|
|
|
91
128
|
return source;
|
|
92
129
|
}
|
|
93
130
|
|
|
131
|
+
setHtmlPreviewMaximized(maximized) {
|
|
132
|
+
const isMaximized = Boolean(maximized && this.htmlPreviewShell);
|
|
133
|
+
this.htmlPreviewShell?.classList.toggle('is-maximized', isMaximized);
|
|
134
|
+
globalThis.document?.body?.classList.toggle('html-preview-maximized-open', isMaximized);
|
|
135
|
+
|
|
136
|
+
const button = this.elements.htmlPreviewMaximizeButton;
|
|
137
|
+
if (button) {
|
|
138
|
+
setDiagramActionButtonIcon(button, isMaximized ? 'restore' : 'maximize');
|
|
139
|
+
button.title = isMaximized ? 'Restore HTML preview' : 'Maximize HTML preview';
|
|
140
|
+
button.setAttribute('aria-label', button.title);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
94
144
|
resetPreviewMode() {
|
|
145
|
+
this.setHtmlPreviewMaximized(false);
|
|
146
|
+
this.htmlPreviewShell = null;
|
|
95
147
|
this.pdfPreview.cancel();
|
|
96
148
|
this.elements.previewContent?.classList.remove('is-drawio-file-preview');
|
|
97
149
|
this.elements.previewContent?.classList.remove('is-excalidraw-file-preview');
|
|
98
150
|
this.elements.previewContent?.classList.remove('is-base-file-preview');
|
|
99
151
|
this.elements.previewContent?.classList.remove('is-image-file-preview');
|
|
100
152
|
this.elements.previewContent?.classList.remove('is-pdf-file-preview');
|
|
153
|
+
this.elements.previewContent?.classList.remove('is-html-file-preview');
|
|
101
154
|
this.elements.previewContent?.classList.remove('is-mermaid-file-preview');
|
|
102
155
|
this.elements.previewContent?.classList.remove('is-plantuml-file-preview');
|
|
103
156
|
this.elements.previewContent?.classList.remove('is-structurizr-file-preview');
|
|
@@ -110,6 +163,7 @@ export class WorkspacePreviewController {
|
|
|
110
163
|
const isBase = this.isBaseFile(filePath);
|
|
111
164
|
const isImage = this.isImageFile(filePath);
|
|
112
165
|
const isPdf = this.isPdfFile(filePath);
|
|
166
|
+
const isHtml = isHtmlFilePath(filePath);
|
|
113
167
|
const isMarkdown = isMarkdownFilePath(filePath);
|
|
114
168
|
const isMermaid = this.isMermaidFile(filePath);
|
|
115
169
|
const isPlantUml = this.isPlantUmlFile(filePath);
|
|
@@ -120,9 +174,11 @@ export class WorkspacePreviewController {
|
|
|
120
174
|
this.backlinksPanel.setDisplayMode?.(usesHeaderBacklinks ? 'header' : 'dock');
|
|
121
175
|
|
|
122
176
|
this.elements.editorFindButton?.classList.toggle('hidden', !isMarkdown);
|
|
177
|
+
syncFormatDocumentButton(this.elements.editorFormatButton, filePath, isPlantUml);
|
|
123
178
|
this.elements.markdownToolbar?.classList.toggle('hidden', !isMarkdown);
|
|
124
179
|
this.elements.exportMenuGroup?.classList.toggle('hidden', !isMarkdown);
|
|
125
|
-
this.elements.
|
|
180
|
+
this.elements.htmlPreviewMaximizeButton?.classList.toggle('hidden', !isHtml);
|
|
181
|
+
this.elements.outlineToggle?.classList.toggle('hidden', isDiagramFile || isImage || isPdf || isHtml || isBase);
|
|
126
182
|
this.elements.previewContent?.classList.toggle('is-mermaid-file-preview', isMermaid);
|
|
127
183
|
this.elements.previewContent?.classList.toggle('is-plantuml-file-preview', isPlantUml);
|
|
128
184
|
this.elements.previewContent?.classList.toggle('is-structurizr-file-preview', isStructurizr);
|
|
@@ -132,7 +188,7 @@ export class WorkspacePreviewController {
|
|
|
132
188
|
this.backlinksPanel.clear();
|
|
133
189
|
}
|
|
134
190
|
|
|
135
|
-
if ((isDrawio && drawioMode !== 'text') || isExcalidraw || isImage || isPdf || (isBase && preferPreviewForBase)) {
|
|
191
|
+
if ((isDrawio && drawioMode !== 'text') || isExcalidraw || isHtml || isImage || isPdf || (isBase && preferPreviewForBase)) {
|
|
136
192
|
this.layoutController.setView('preview', { persist: false });
|
|
137
193
|
this.outlineController.close();
|
|
138
194
|
this.backlinksPanel.clear();
|
|
@@ -292,6 +348,68 @@ export class WorkspacePreviewController {
|
|
|
292
348
|
this.schedulePreviewLayoutSyncCallback({ delayMs: 0 });
|
|
293
349
|
}
|
|
294
350
|
|
|
351
|
+
renderHtmlFilePreview({ content = '' } = {}) {
|
|
352
|
+
const previewElement = this.elements.previewContent;
|
|
353
|
+
if (!previewElement) {
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
const wasMaximized = this.htmlPreviewShell?.classList.contains('is-maximized') ?? false;
|
|
358
|
+
this.videoEmbed?.detachForCommit();
|
|
359
|
+
this.drawioEmbed.detachForCommit();
|
|
360
|
+
this.excalidrawEmbed.detachForCommit();
|
|
361
|
+
this.resetPreviewMode();
|
|
362
|
+
previewElement.classList.add('is-html-file-preview');
|
|
363
|
+
const renderHost = this.previewRenderer.ensureRenderHost();
|
|
364
|
+
this.previewRenderer.normalizePreviewChildren(renderHost);
|
|
365
|
+
|
|
366
|
+
const source = String(content ?? '');
|
|
367
|
+
const iframe = document.createElement('iframe');
|
|
368
|
+
iframe.className = 'html-file-preview-frame';
|
|
369
|
+
iframe.title = 'HTML preview';
|
|
370
|
+
iframe.referrerPolicy = 'no-referrer';
|
|
371
|
+
iframe.setAttribute('allow', "camera 'none'; microphone 'none'; geolocation 'none'; clipboard-read 'none'; clipboard-write 'none'");
|
|
372
|
+
iframe.setAttribute('sandbox', '');
|
|
373
|
+
// ponytail: full iframe replacement resets script consent whenever the content changes.
|
|
374
|
+
iframe.srcdoc = createHtmlPreviewDocument(source);
|
|
375
|
+
|
|
376
|
+
const shell = document.createElement('div');
|
|
377
|
+
shell.className = 'html-file-preview-shell';
|
|
378
|
+
|
|
379
|
+
if (hasHtmlScripts(source)) {
|
|
380
|
+
const scriptGate = document.createElement('div');
|
|
381
|
+
scriptGate.className = 'html-file-preview-script-gate';
|
|
382
|
+
scriptGate.textContent = 'Scripts are disabled by default. Run them only if you trust this HTML file.';
|
|
383
|
+
|
|
384
|
+
const runScriptsButton = document.createElement('button');
|
|
385
|
+
runScriptsButton.type = 'button';
|
|
386
|
+
runScriptsButton.className = 'ui-button ui-button--secondary ui-button--compact';
|
|
387
|
+
runScriptsButton.textContent = 'Run scripts';
|
|
388
|
+
runScriptsButton.addEventListener('click', () => {
|
|
389
|
+
iframe.setAttribute('sandbox', 'allow-scripts');
|
|
390
|
+
iframe.srcdoc = createHtmlPreviewDocument(source, true);
|
|
391
|
+
scriptGate.remove();
|
|
392
|
+
});
|
|
393
|
+
scriptGate.append(runScriptsButton);
|
|
394
|
+
shell.append(scriptGate);
|
|
395
|
+
}
|
|
396
|
+
shell.append(iframe);
|
|
397
|
+
this.htmlPreviewShell = shell;
|
|
398
|
+
this.setHtmlPreviewMaximized(wasMaximized);
|
|
399
|
+
|
|
400
|
+
if (renderHost) {
|
|
401
|
+
renderHost.replaceChildren(shell);
|
|
402
|
+
renderHost.style.minHeight = '';
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
previewElement.dataset.renderPhase = 'ready';
|
|
406
|
+
this.outlineController.close();
|
|
407
|
+
this.backlinksPanel.clear();
|
|
408
|
+
this.scrollSyncController.setLargeDocumentMode(false);
|
|
409
|
+
this.scrollSyncController.invalidatePreviewBlocks();
|
|
410
|
+
this.schedulePreviewLayoutSyncCallback({ delayMs: 0 });
|
|
411
|
+
}
|
|
412
|
+
|
|
295
413
|
async renderBaseFilePreview(filePath, { source = null } = {}) {
|
|
296
414
|
const previewElement = this.elements.previewContent;
|
|
297
415
|
if (!previewElement) {
|
|
@@ -285,8 +285,11 @@ export class WorkspaceRouteController {
|
|
|
285
285
|
this.fileHistoryView?.hide?.();
|
|
286
286
|
this.clearStaticPreviewDocument?.();
|
|
287
287
|
this.syncMainChrome({ mode: 'editor' });
|
|
288
|
-
await this.workspaceCoordinator.openFile(filePath, options);
|
|
288
|
+
const didOpen = await this.workspaceCoordinator.openFile(filePath, options);
|
|
289
289
|
this.setSession(this.workspaceCoordinator.getSession());
|
|
290
|
+
if (didOpen === false) {
|
|
291
|
+
return false;
|
|
292
|
+
}
|
|
290
293
|
|
|
291
294
|
if (shouldRevealInTree) {
|
|
292
295
|
this.revealFileInTree(filePath, { clearSearch: true });
|