collabmd 0.1.36 → 0.1.38
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 +1 -0
- package/dist/client/assets/{drawio-editor-D4Qoluwz.js → drawio-editor-BHI0pLby.js} +1 -1
- package/dist/client/assets/{drawioEditor-B65FWq-b.js → drawioEditor-DOaXwaY2.js} +2 -2
- package/dist/client/assets/{editor-session-DV80PhYL.js → editor-session-BFsA-WRc.js} +1 -1
- package/dist/client/assets/{excalidraw-editor-hiJQt1cX.js → excalidraw-editor-6pgH2kf5.js} +1 -1
- package/dist/client/assets/{excalidrawEditor-BnbblxK4.js → excalidrawEditor-Ck9hgrTa.js} +2 -2
- package/dist/client/assets/export-page-4M8Zo-AL.js +39 -0
- package/dist/client/assets/exportDocument-BAqaHlIn.css +1 -0
- package/dist/client/assets/{exportDocument-CULEGKmb.js → exportDocument-mlXOym-R.js} +2 -2
- package/dist/client/assets/{index-BjdYZiBM.js → index-CCfHrm_9.js} +2 -2
- package/dist/client/assets/{index-BryqP9QY.css → index-maPm_Kvk.css} +1 -1
- package/dist/client/assets/main-BQMPQ4CW.js +1410 -0
- package/dist/client/assets/preview-render-compiler-9SmLUP5V.js +1 -0
- package/dist/client/assets/{preview-render-compiler-Tkc6JX1O.js → preview-render-compiler-IUtgIlOM.js} +1 -1
- package/dist/client/assets/{preview-render-worker-1LBLqgKu.js → preview-render-worker-BwQ5sPy9.js} +1 -1
- package/dist/client/assets/runtime-config-BcUbZKho.js +1 -0
- package/dist/client/assets/{vault-api-client-DSx3mWkH.js → vault-api-client-BDKKvtOF.js} +1 -1
- 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 +2 -2
- package/docker-compose.yml +1 -0
- package/package.json +12 -9
- package/src/client/application/app-shell/ui-feature-shell.js +237 -0
- package/src/client/application/preview-render-compiler.js +120 -1
- package/src/client/application/preview-render-executor.js +4 -0
- package/src/client/application/preview-render-worker.js +2 -0
- package/src/client/application/preview-renderer.js +5 -2
- package/src/client/application/wiki-link-file-controller.js +7 -0
- package/src/client/application/workspace-route-controller.js +9 -0
- package/src/client/bootstrap/collabmd-app-shell.js +13 -0
- package/src/client/domain/hash-routes.js +49 -0
- package/src/client/domain/runtime-paths.js +1 -0
- package/src/client/export/export-pipeline.js +100 -26
- package/src/client/infrastructure/runtime-config.js +22 -24
- package/src/client/presentation/outline-controller.js +28 -7
- package/src/client/styles/features/preview-markdown.css +51 -0
- package/src/server/config/env.js +1 -0
- package/src/server/infrastructure/http/create-static-handler.js +2 -0
- package/dist/client/assets/export-page-BKDarp9x.js +0 -39
- package/dist/client/assets/exportDocument-BbkN85c4.css +0 -1
- package/dist/client/assets/main-QJcxyvDa.js +0 -1404
- package/dist/client/assets/preview-render-compiler-BCB8u6iM.js +0 -1
- package/dist/client/assets/runtime-config-DWTejofP.js +0 -1
|
@@ -1,7 +1,17 @@
|
|
|
1
1
|
import { supportsBacklinksForFilePath } from '../../../domain/file-kind.js';
|
|
2
2
|
import { isPlainQuickSwitcherShortcut } from '../../domain/keyboard-shortcuts.js';
|
|
3
|
+
import { createFileRouteHash, isCollabMdHashRoute } from '../../domain/hash-routes.js';
|
|
3
4
|
|
|
4
5
|
const VERSION_RELOAD_TOAST_DURATION_MS = 0;
|
|
6
|
+
const PREVIEW_ROUTE_ANCHOR_STABILIZE_MS = 2000;
|
|
7
|
+
const PREVIEW_ROUTE_ANCHOR_MAX_APPLICATIONS = 8;
|
|
8
|
+
const PREVIEW_HEADING_LINK_ICON = `
|
|
9
|
+
<svg viewBox="0 0 24 24" width="15" height="15" aria-hidden="true" focusable="false">
|
|
10
|
+
<path d="M10 13.5 14 9.5" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
|
|
11
|
+
<path d="M9.35 7.55 10.6 6.3a4.2 4.2 0 0 1 5.95 5.95l-1.25 1.25" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
|
|
12
|
+
<path d="m14.65 16.45-1.25 1.25a4.2 4.2 0 0 1-5.95-5.95L8.7 10.5" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
|
|
13
|
+
</svg>
|
|
14
|
+
`;
|
|
5
15
|
|
|
6
16
|
/**
|
|
7
17
|
* @typedef {object} UiShellContext
|
|
@@ -396,12 +406,207 @@ function isInteractiveTaskListDescendant(target) {
|
|
|
396
406
|
return Boolean(target.closest('a, button, input, select, textarea, summary, [contenteditable="true"], [role="button"], [role="link"]'));
|
|
397
407
|
}
|
|
398
408
|
|
|
409
|
+
function normalizeFragmentTargetId(rawTarget = '') {
|
|
410
|
+
const normalizedTarget = String(rawTarget ?? '').trim();
|
|
411
|
+
if (!normalizedTarget) {
|
|
412
|
+
return '';
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
try {
|
|
416
|
+
return decodeURIComponent(normalizedTarget);
|
|
417
|
+
} catch {
|
|
418
|
+
return normalizedTarget;
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
function scrollPreviewToTarget(previewContainer, target, { behavior = 'smooth' } = {}) {
|
|
423
|
+
if (!previewContainer || !target) {
|
|
424
|
+
return;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
const previewRect = previewContainer.getBoundingClientRect();
|
|
428
|
+
const targetRect = target.getBoundingClientRect();
|
|
429
|
+
const nextScrollTop = previewContainer.scrollTop + (targetRect.top - previewRect.top);
|
|
430
|
+
previewContainer.scrollTo({
|
|
431
|
+
behavior,
|
|
432
|
+
top: Math.max(nextScrollTop, 0),
|
|
433
|
+
});
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
async function copyTextToClipboard(text) {
|
|
437
|
+
await navigator.clipboard.writeText(text);
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
function getHeadingLinkLabel(heading) {
|
|
441
|
+
if (!(heading instanceof HTMLElement)) {
|
|
442
|
+
return '';
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
const headingClone = heading.cloneNode(true);
|
|
446
|
+
if (!(headingClone instanceof HTMLElement)) {
|
|
447
|
+
return heading.textContent.trim();
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
headingClone.querySelectorAll('.preview-heading-link-button').forEach((button) => button.remove());
|
|
451
|
+
return headingClone.textContent.replace(/\s+/g, ' ').trim();
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
function nowMs() {
|
|
455
|
+
return globalThis.performance?.now?.() ?? Date.now();
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
/** @this {UiShellContext} */
|
|
459
|
+
function navigatePreviewHeading(target, headingId, { behavior = 'auto' } = {}) {
|
|
460
|
+
if (!(target instanceof HTMLElement) || !headingId) {
|
|
461
|
+
return false;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
const handledByOutlineNavigation = this.outlineController?.navigateToHeading?.(target, headingId, { behavior });
|
|
465
|
+
if (handledByOutlineNavigation) {
|
|
466
|
+
return true;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
const previewContainer = this.elements.previewContainer ?? document.getElementById('previewContainer');
|
|
470
|
+
if (!previewContainer) {
|
|
471
|
+
return false;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
this.scrollSyncController?.suspendSync?.(250);
|
|
475
|
+
scrollPreviewToTarget(previewContainer, target, { behavior });
|
|
476
|
+
|
|
477
|
+
const sourceLine = Number.parseInt(target.getAttribute('data-source-line') || '', 10);
|
|
478
|
+
if (Number.isFinite(sourceLine)) {
|
|
479
|
+
this.session?.scrollToLine?.(sourceLine, 0);
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
return true;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
/** @this {UiShellContext} */
|
|
486
|
+
function createPreviewHeadingLinkUrl(anchorId) {
|
|
487
|
+
if (!this.currentFilePath || !anchorId) {
|
|
488
|
+
return '';
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
const url = new URL(window.location.href);
|
|
492
|
+
url.hash = createFileRouteHash(this.currentFilePath, {
|
|
493
|
+
anchor: anchorId,
|
|
494
|
+
drawioMode: this.currentDrawioMode ?? null,
|
|
495
|
+
});
|
|
496
|
+
return url.toString();
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
/** @this {UiShellContext} */
|
|
500
|
+
async function copyPreviewHeadingLink(anchorId) {
|
|
501
|
+
const url = this.createPreviewHeadingLinkUrl(anchorId);
|
|
502
|
+
if (!url) {
|
|
503
|
+
this.toastController.show('Failed to copy section link');
|
|
504
|
+
return;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
try {
|
|
508
|
+
await copyTextToClipboard(url);
|
|
509
|
+
this.toastController.show('Section link copied');
|
|
510
|
+
} catch {
|
|
511
|
+
this.toastController.show('Failed to copy section link');
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
/** @this {UiShellContext} */
|
|
516
|
+
function syncPreviewHeadingLinkButtons() {
|
|
517
|
+
const headings = this.elements.previewContent?.querySelectorAll?.('h1[id], h2[id], h3[id], h4[id], h5[id], h6[id]') ?? [];
|
|
518
|
+
|
|
519
|
+
Array.from(headings).forEach((heading) => {
|
|
520
|
+
if (!(heading instanceof HTMLElement) || !heading.id) {
|
|
521
|
+
return;
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
const headingLabel = getHeadingLinkLabel(heading);
|
|
525
|
+
let button = heading.querySelector(':scope > .preview-heading-link-button');
|
|
526
|
+
if (!(button instanceof HTMLButtonElement)) {
|
|
527
|
+
button = document.createElement('button');
|
|
528
|
+
button.type = 'button';
|
|
529
|
+
button.className = 'ui-icon-button preview-heading-link-button';
|
|
530
|
+
button.innerHTML = PREVIEW_HEADING_LINK_ICON;
|
|
531
|
+
heading.appendChild(button);
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
button.dataset.previewHeadingAnchor = heading.id;
|
|
535
|
+
button.setAttribute('aria-label', `Copy link to ${headingLabel}`);
|
|
536
|
+
button.setAttribute('title', 'Copy link to this section');
|
|
537
|
+
});
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
/** @this {UiShellContext} */
|
|
541
|
+
function requestPreviewRouteAnchor(anchorId, filePath = this.currentFilePath) {
|
|
542
|
+
const normalizedAnchor = String(anchorId ?? '').trim();
|
|
543
|
+
this._pendingPreviewRouteAnchor = normalizedAnchor
|
|
544
|
+
? {
|
|
545
|
+
applied: false,
|
|
546
|
+
appliedCount: 0,
|
|
547
|
+
anchorId: normalizedAnchor,
|
|
548
|
+
filePath: filePath ?? this.currentFilePath ?? null,
|
|
549
|
+
stabilizeUntil: nowMs() + PREVIEW_ROUTE_ANCHOR_STABILIZE_MS,
|
|
550
|
+
}
|
|
551
|
+
: null;
|
|
552
|
+
|
|
553
|
+
if (!this._pendingPreviewRouteAnchor) {
|
|
554
|
+
return false;
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
return this.applyPendingPreviewRouteAnchor({ behavior: 'auto', clearMissing: false });
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
/** @this {UiShellContext} */
|
|
561
|
+
function applyPendingPreviewRouteAnchor({ allowExpired = false, behavior = 'auto', clearMissing = false } = {}) {
|
|
562
|
+
const pendingAnchor = this._pendingPreviewRouteAnchor;
|
|
563
|
+
if (!pendingAnchor) {
|
|
564
|
+
return false;
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
if (!allowExpired && pendingAnchor.applied && pendingAnchor.stabilizeUntil <= nowMs()) {
|
|
568
|
+
this._pendingPreviewRouteAnchor = null;
|
|
569
|
+
return false;
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
if (pendingAnchor.filePath && this.currentFilePath && pendingAnchor.filePath !== this.currentFilePath) {
|
|
573
|
+
return false;
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
const target = document.getElementById(pendingAnchor.anchorId);
|
|
577
|
+
if (!(target instanceof HTMLElement) || !this.elements.previewContent?.contains(target)) {
|
|
578
|
+
if (!pendingAnchor.applied && clearMissing && pendingAnchor.filePath && pendingAnchor.filePath === this.currentFilePath) {
|
|
579
|
+
this._pendingPreviewRouteAnchor = null;
|
|
580
|
+
}
|
|
581
|
+
return false;
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
if (!this.navigatePreviewHeading(target, pendingAnchor.anchorId, { behavior })) {
|
|
585
|
+
return false;
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
pendingAnchor.applied = true;
|
|
589
|
+
pendingAnchor.appliedCount = (pendingAnchor.appliedCount ?? 0) + 1;
|
|
590
|
+
pendingAnchor.stabilizeUntil = nowMs() + PREVIEW_ROUTE_ANCHOR_STABILIZE_MS;
|
|
591
|
+
if (pendingAnchor.appliedCount >= PREVIEW_ROUTE_ANCHOR_MAX_APPLICATIONS) {
|
|
592
|
+
this._pendingPreviewRouteAnchor = null;
|
|
593
|
+
}
|
|
594
|
+
return true;
|
|
595
|
+
}
|
|
596
|
+
|
|
399
597
|
/** @this {UiShellContext} */
|
|
400
598
|
function handlePreviewContentClick(event) {
|
|
401
599
|
if (!(event.target instanceof Element)) {
|
|
402
600
|
return;
|
|
403
601
|
}
|
|
404
602
|
|
|
603
|
+
const headingLinkButton = event.target.closest('button.preview-heading-link-button[data-preview-heading-anchor]');
|
|
604
|
+
if (headingLinkButton instanceof HTMLButtonElement) {
|
|
605
|
+
event.preventDefault();
|
|
606
|
+
void this.copyPreviewHeadingLink(headingLinkButton.dataset.previewHeadingAnchor || '');
|
|
607
|
+
return;
|
|
608
|
+
}
|
|
609
|
+
|
|
405
610
|
const wikiLink = event.target.closest('a.wiki-link[data-wiki-target]');
|
|
406
611
|
if (wikiLink) {
|
|
407
612
|
event.preventDefault();
|
|
@@ -409,6 +614,32 @@ function handlePreviewContentClick(event) {
|
|
|
409
614
|
return;
|
|
410
615
|
}
|
|
411
616
|
|
|
617
|
+
const anchorLink = event.target.closest('a[href]');
|
|
618
|
+
const href = anchorLink?.getAttribute('href')?.trim() || '';
|
|
619
|
+
if (anchorLink && href.startsWith('#') && !isCollabMdHashRoute(href)) {
|
|
620
|
+
event.preventDefault();
|
|
621
|
+
|
|
622
|
+
if (href === '#' || href.toLowerCase() === '#top') {
|
|
623
|
+
this.scrollSyncController?.suspendSync?.(250);
|
|
624
|
+
const previewContainer = this.elements.previewContainer ?? document.getElementById('previewContainer');
|
|
625
|
+
previewContainer?.scrollTo({ top: 0, behavior: 'smooth' });
|
|
626
|
+
return;
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
const targetId = normalizeFragmentTargetId(href.slice(1));
|
|
630
|
+
if (!targetId) {
|
|
631
|
+
return;
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
const target = document.getElementById(targetId);
|
|
635
|
+
if (!target || !this.elements.previewContent?.contains(target)) {
|
|
636
|
+
return;
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
this.navigatePreviewHeading(target, targetId, { behavior: 'smooth' });
|
|
640
|
+
return;
|
|
641
|
+
}
|
|
642
|
+
|
|
412
643
|
const taskItem = event.target.closest('.task-list-item[data-source-line]');
|
|
413
644
|
if (!taskItem || isNestedTaskListClickTarget(event.target, taskItem) || isInteractiveTaskListDescendant(event.target)) {
|
|
414
645
|
return;
|
|
@@ -560,9 +791,12 @@ function clearInitialFileBootstrap() {
|
|
|
560
791
|
}
|
|
561
792
|
|
|
562
793
|
export const uiFeatureShellMethods = {
|
|
794
|
+
applyPendingPreviewRouteAnchor,
|
|
563
795
|
bindEvents,
|
|
564
796
|
clearInitialFileBootstrap,
|
|
565
797
|
closeToolbarOverflowMenu,
|
|
798
|
+
copyPreviewHeadingLink,
|
|
799
|
+
createPreviewHeadingLinkUrl,
|
|
566
800
|
getStoredLineWrapping,
|
|
567
801
|
handleConnectionChange,
|
|
568
802
|
handleDocumentKeydown,
|
|
@@ -574,11 +808,14 @@ export const uiFeatureShellMethods = {
|
|
|
574
808
|
initializeVisualViewportBinding,
|
|
575
809
|
initializeVersionMonitoring,
|
|
576
810
|
isPlainQuickSwitcherShortcut,
|
|
811
|
+
navigatePreviewHeading,
|
|
577
812
|
promptForVersionReload,
|
|
813
|
+
requestPreviewRouteAnchor,
|
|
578
814
|
scheduleBacklinkRefresh,
|
|
579
815
|
setToolbarOverflowOpen,
|
|
580
816
|
showEditorLoadError,
|
|
581
817
|
showEditorLoading,
|
|
818
|
+
syncPreviewHeadingLinkButtons,
|
|
582
819
|
syncVisualViewportBounds,
|
|
583
820
|
syncToolbarOverflowVisibility,
|
|
584
821
|
syncWrapToggle,
|
|
@@ -49,6 +49,96 @@ function escapePreviewText(content = '') {
|
|
|
49
49
|
return escapeHtml(normalizePreviewTypography(content));
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
+
function createHeadingSlug(content = '') {
|
|
53
|
+
return String(content ?? '')
|
|
54
|
+
.trim()
|
|
55
|
+
.toLowerCase()
|
|
56
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
57
|
+
.replace(/^-|-$/g, '')
|
|
58
|
+
|| 'section';
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function createHeadingId(baseId = '', headingIdCounts = new Map()) {
|
|
62
|
+
const normalizedBaseId = String(baseId ?? '').trim() || 'section';
|
|
63
|
+
const occurrenceIndex = headingIdCounts.get(normalizedBaseId) ?? 0;
|
|
64
|
+
headingIdCounts.set(normalizedBaseId, occurrenceIndex + 1);
|
|
65
|
+
return occurrenceIndex === 0 ? normalizedBaseId : `${normalizedBaseId}-${occurrenceIndex}`;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function createContextualHeadingBaseId(headingInfo, groupedHeadingInfos = []) {
|
|
69
|
+
if (!headingInfo?.slug) {
|
|
70
|
+
return 'section';
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (groupedHeadingInfos.length <= 1) {
|
|
74
|
+
return headingInfo.slug;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
for (let depth = 1; depth <= headingInfo.parentSlugs.length; depth += 1) {
|
|
78
|
+
const candidate = [
|
|
79
|
+
...headingInfo.parentSlugs.slice(-depth),
|
|
80
|
+
headingInfo.slug,
|
|
81
|
+
].join('-');
|
|
82
|
+
|
|
83
|
+
const isUniqueWithinGroup = groupedHeadingInfos.every((otherInfo) => {
|
|
84
|
+
if (otherInfo === headingInfo) {
|
|
85
|
+
return true;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const otherCandidate = [
|
|
89
|
+
...otherInfo.parentSlugs.slice(-depth),
|
|
90
|
+
otherInfo.slug,
|
|
91
|
+
].join('-');
|
|
92
|
+
return otherCandidate !== candidate;
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
if (isUniqueWithinGroup) {
|
|
96
|
+
return candidate;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return headingInfo.slug;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function assignHeadingIds(state) {
|
|
104
|
+
const headingInfos = [];
|
|
105
|
+
const parentSlugs = [];
|
|
106
|
+
|
|
107
|
+
state.tokens.forEach((token, index) => {
|
|
108
|
+
if (token.type !== 'heading_open' || token.attrGet('id')) {
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const level = Number.parseInt(token.tag.slice(1), 10);
|
|
113
|
+
if (!Number.isFinite(level) || level < 1) {
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
parentSlugs.length = Math.max(level - 1, 0);
|
|
118
|
+
const inlineToken = state.tokens[index + 1];
|
|
119
|
+
const slug = createHeadingSlug(inlineToken?.type === 'inline' ? inlineToken.content : '');
|
|
120
|
+
headingInfos.push({
|
|
121
|
+
parentSlugs: parentSlugs.filter(Boolean),
|
|
122
|
+
slug,
|
|
123
|
+
token,
|
|
124
|
+
});
|
|
125
|
+
parentSlugs[level - 1] = slug;
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
const headingsBySlug = new Map();
|
|
129
|
+
headingInfos.forEach((headingInfo) => {
|
|
130
|
+
const group = headingsBySlug.get(headingInfo.slug) ?? [];
|
|
131
|
+
group.push(headingInfo);
|
|
132
|
+
headingsBySlug.set(headingInfo.slug, group);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
const headingIdCounts = new Map();
|
|
136
|
+
headingInfos.forEach((headingInfo) => {
|
|
137
|
+
const baseId = createContextualHeadingBaseId(headingInfo, headingsBySlug.get(headingInfo.slug) ?? []);
|
|
138
|
+
headingInfo.token.attrSet('id', createHeadingId(baseId, headingIdCounts));
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
|
|
52
142
|
function renderSafeInlineBreaks(content = '') {
|
|
53
143
|
const parts = String(content).split(/<br\s*\/?>/i);
|
|
54
144
|
if (parts.length === 1) {
|
|
@@ -239,6 +329,13 @@ function resolveLocalAttachmentUrl(source = '', {
|
|
|
239
329
|
return `${attachmentApiPath}?path=${encodeURIComponent(resolvedPath)}`;
|
|
240
330
|
}
|
|
241
331
|
|
|
332
|
+
function removeTokenAttr(token, name) {
|
|
333
|
+
const attributeIndex = token.attrIndex(name);
|
|
334
|
+
if (attributeIndex >= 0) {
|
|
335
|
+
token.attrs.splice(attributeIndex, 1);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
242
339
|
function renderInlineWikiText(content, {
|
|
243
340
|
baseEmbedCounts,
|
|
244
341
|
drawioEmbedCounts,
|
|
@@ -247,6 +344,7 @@ function renderInlineWikiText(content, {
|
|
|
247
344
|
mermaidEmbedCounts,
|
|
248
345
|
plantUmlEmbedCounts,
|
|
249
346
|
sourceFilePath = '',
|
|
347
|
+
wikiLinkAutoCreate = true,
|
|
250
348
|
}) {
|
|
251
349
|
const regex = /!\[\[([^\]|#]+\.(?:base|excalidraw|drawio|mmd|mermaid|puml|plantuml))(?:#([^\]|]+))?(?:\|([^\]]+))?\]\]|\[\[([^\]|]+)(?:\|([^\]]+))?\]\]/gi;
|
|
252
350
|
let lastIndex = 0;
|
|
@@ -311,7 +409,11 @@ function renderInlineWikiText(content, {
|
|
|
311
409
|
const display = (match[5] || match[4]).trim();
|
|
312
410
|
const resolved = resolveWikiTarget(target, fileList);
|
|
313
411
|
const classes = resolved ? 'wiki-link' : 'wiki-link wiki-link-new';
|
|
314
|
-
const title = resolved
|
|
412
|
+
const title = resolved
|
|
413
|
+
? normalizePreviewTypography(display)
|
|
414
|
+
: wikiLinkAutoCreate
|
|
415
|
+
? `Create "${target}"`
|
|
416
|
+
: `Missing "${target}"`;
|
|
315
417
|
html += `<a class="${classes}" href="#" data-wiki-target="${escapeHtml(target)}" title="${escapeHtml(title)}">${escapePreviewText(display)}</a>`;
|
|
316
418
|
}
|
|
317
419
|
|
|
@@ -328,6 +430,7 @@ function renderInlineWikiText(content, {
|
|
|
328
430
|
function createMarkdownRenderer(fileList = [], {
|
|
329
431
|
attachmentApiPath = '/api/attachment',
|
|
330
432
|
sourceFilePath = '',
|
|
433
|
+
wikiLinkAutoCreate = true,
|
|
331
434
|
} = {}) {
|
|
332
435
|
const markdown = markdownIt({
|
|
333
436
|
highlight(source, language) {
|
|
@@ -367,6 +470,10 @@ function createMarkdownRenderer(fileList = [], {
|
|
|
367
470
|
});
|
|
368
471
|
});
|
|
369
472
|
|
|
473
|
+
markdown.core.ruler.push('collabmd-heading-anchors', (state) => {
|
|
474
|
+
assignHeadingIds(state);
|
|
475
|
+
});
|
|
476
|
+
|
|
370
477
|
const drawioEmbedCounts = new Map();
|
|
371
478
|
const excalidrawEmbedCounts = new Map();
|
|
372
479
|
const baseCounts = new Map();
|
|
@@ -461,6 +568,7 @@ function createMarkdownRenderer(fileList = [], {
|
|
|
461
568
|
mermaidEmbedCounts,
|
|
462
569
|
plantUmlEmbedCounts,
|
|
463
570
|
sourceFilePath,
|
|
571
|
+
wikiLinkAutoCreate,
|
|
464
572
|
})}`;
|
|
465
573
|
}
|
|
466
574
|
|
|
@@ -473,6 +581,7 @@ function createMarkdownRenderer(fileList = [], {
|
|
|
473
581
|
mermaidEmbedCounts,
|
|
474
582
|
plantUmlEmbedCounts,
|
|
475
583
|
sourceFilePath,
|
|
584
|
+
wikiLinkAutoCreate,
|
|
476
585
|
})}`;
|
|
477
586
|
}
|
|
478
587
|
|
|
@@ -484,10 +593,18 @@ function createMarkdownRenderer(fileList = [], {
|
|
|
484
593
|
mermaidEmbedCounts,
|
|
485
594
|
plantUmlEmbedCounts,
|
|
486
595
|
sourceFilePath,
|
|
596
|
+
wikiLinkAutoCreate,
|
|
487
597
|
});
|
|
488
598
|
};
|
|
489
599
|
|
|
490
600
|
markdown.renderer.rules.link_open = (tokens, index, options, env, self) => {
|
|
601
|
+
const href = tokens[index].attrGet('href') || '';
|
|
602
|
+
if (href.startsWith('#')) {
|
|
603
|
+
removeTokenAttr(tokens[index], 'target');
|
|
604
|
+
removeTokenAttr(tokens[index], 'rel');
|
|
605
|
+
return renderToken(fallbackLinkOpen, tokens, index, options, env, self);
|
|
606
|
+
}
|
|
607
|
+
|
|
491
608
|
tokens[index].attrSet('target', '_blank');
|
|
492
609
|
tokens[index].attrSet('rel', 'noopener noreferrer');
|
|
493
610
|
return renderToken(fallbackLinkOpen, tokens, index, options, env, self);
|
|
@@ -529,12 +646,14 @@ export function compilePreviewDocument({
|
|
|
529
646
|
frontmatterInteractive = false,
|
|
530
647
|
markdownText = '',
|
|
531
648
|
sourceFilePath = '',
|
|
649
|
+
wikiLinkAutoCreate = true,
|
|
532
650
|
} = {}) {
|
|
533
651
|
const normalizedMarkdown = String(markdownText);
|
|
534
652
|
const frontmatter = extractYamlFrontmatter(normalizedMarkdown);
|
|
535
653
|
const renderer = createMarkdownRenderer(fileList, {
|
|
536
654
|
attachmentApiPath,
|
|
537
655
|
sourceFilePath,
|
|
656
|
+
wikiLinkAutoCreate,
|
|
538
657
|
});
|
|
539
658
|
const renderedMarkdown = frontmatter ? frontmatter.bodyMarkdown : normalizedMarkdown;
|
|
540
659
|
const frontmatterHtml = renderFrontmatterBlock(frontmatter, {
|
|
@@ -16,6 +16,7 @@ export class PreviewRenderExecutor {
|
|
|
16
16
|
createWorkerFn = createPreviewWorker,
|
|
17
17
|
getFileList,
|
|
18
18
|
getSourceFilePath = null,
|
|
19
|
+
getWikiLinkAutoCreate = null,
|
|
19
20
|
idleTimeoutMs = IDLE_RENDER_TIMEOUT_MS,
|
|
20
21
|
requestIdleRenderFn = requestIdleRender,
|
|
21
22
|
} = {}) {
|
|
@@ -25,6 +26,7 @@ export class PreviewRenderExecutor {
|
|
|
25
26
|
this.createWorkerFn = createWorkerFn;
|
|
26
27
|
this.getFileList = getFileList;
|
|
27
28
|
this.getSourceFilePath = getSourceFilePath;
|
|
29
|
+
this.getWikiLinkAutoCreate = getWikiLinkAutoCreate;
|
|
28
30
|
this.idleTimeoutMs = idleTimeoutMs;
|
|
29
31
|
this.requestIdleRenderFn = requestIdleRenderFn;
|
|
30
32
|
this.worker = null;
|
|
@@ -119,6 +121,7 @@ export class PreviewRenderExecutor {
|
|
|
119
121
|
markdownText,
|
|
120
122
|
renderVersion,
|
|
121
123
|
sourceFilePath: this.getSourceFilePath?.() ?? '',
|
|
124
|
+
wikiLinkAutoCreate: this.getWikiLinkAutoCreate?.() ?? true,
|
|
122
125
|
});
|
|
123
126
|
});
|
|
124
127
|
}
|
|
@@ -131,6 +134,7 @@ export class PreviewRenderExecutor {
|
|
|
131
134
|
frontmatterInteractive,
|
|
132
135
|
markdownText,
|
|
133
136
|
sourceFilePath: this.getSourceFilePath?.() ?? '',
|
|
137
|
+
wikiLinkAutoCreate: this.getWikiLinkAutoCreate?.() ?? true,
|
|
134
138
|
});
|
|
135
139
|
}
|
|
136
140
|
|
|
@@ -9,6 +9,7 @@ self.onmessage = (event) => {
|
|
|
9
9
|
markdownText,
|
|
10
10
|
renderVersion,
|
|
11
11
|
sourceFilePath,
|
|
12
|
+
wikiLinkAutoCreate,
|
|
12
13
|
} = event.data ?? {};
|
|
13
14
|
|
|
14
15
|
try {
|
|
@@ -19,6 +20,7 @@ self.onmessage = (event) => {
|
|
|
19
20
|
frontmatterInteractive,
|
|
20
21
|
markdownText,
|
|
21
22
|
sourceFilePath,
|
|
23
|
+
wikiLinkAutoCreate,
|
|
22
24
|
});
|
|
23
25
|
self.postMessage({
|
|
24
26
|
html: result.html,
|
|
@@ -10,8 +10,9 @@ export class PreviewRenderer {
|
|
|
10
10
|
constructor({
|
|
11
11
|
getContent,
|
|
12
12
|
getFileList,
|
|
13
|
-
loadFileSource = null,
|
|
14
13
|
getSourceFilePath,
|
|
14
|
+
getWikiLinkAutoCreate = null,
|
|
15
|
+
loadFileSource = null,
|
|
15
16
|
onAfterRenderCommit,
|
|
16
17
|
onBeforeRenderCommit,
|
|
17
18
|
onPreviewLayoutChange,
|
|
@@ -24,8 +25,9 @@ export class PreviewRenderer {
|
|
|
24
25
|
}) {
|
|
25
26
|
this.getContent = getContent;
|
|
26
27
|
this.getFileList = getFileList;
|
|
27
|
-
this.loadFileSource = loadFileSource;
|
|
28
28
|
this.getSourceFilePath = getSourceFilePath;
|
|
29
|
+
this.getWikiLinkAutoCreate = getWikiLinkAutoCreate;
|
|
30
|
+
this.loadFileSource = loadFileSource;
|
|
29
31
|
this.onAfterRenderCommit = onAfterRenderCommit;
|
|
30
32
|
this.onBeforeRenderCommit = onBeforeRenderCommit;
|
|
31
33
|
this.onPreviewLayoutChange = onPreviewLayoutChange;
|
|
@@ -96,6 +98,7 @@ export class PreviewRenderer {
|
|
|
96
98
|
attachmentApiPath: resolveApiUrl('/attachment'),
|
|
97
99
|
getFileList: () => this.getFileList?.() ?? [],
|
|
98
100
|
getSourceFilePath: () => this.getSourceFilePath?.() ?? '',
|
|
101
|
+
getWikiLinkAutoCreate: () => this.getWikiLinkAutoCreate?.() ?? true,
|
|
99
102
|
idleTimeoutMs: IDLE_RENDER_TIMEOUT_MS,
|
|
100
103
|
});
|
|
101
104
|
|
|
@@ -8,12 +8,14 @@ export class WikiLinkFileController {
|
|
|
8
8
|
refreshExplorer,
|
|
9
9
|
toastController,
|
|
10
10
|
vaultApiClient,
|
|
11
|
+
wikiLinkAutoCreate = true,
|
|
11
12
|
}) {
|
|
12
13
|
this.getFileList = getFileList;
|
|
13
14
|
this.navigation = navigation;
|
|
14
15
|
this.refreshExplorer = refreshExplorer;
|
|
15
16
|
this.toastController = toastController;
|
|
16
17
|
this.vaultApiClient = vaultApiClient;
|
|
18
|
+
this.wikiLinkAutoCreate = wikiLinkAutoCreate;
|
|
17
19
|
}
|
|
18
20
|
|
|
19
21
|
handleWikiLinkClick(target) {
|
|
@@ -24,6 +26,11 @@ export class WikiLinkFileController {
|
|
|
24
26
|
return;
|
|
25
27
|
}
|
|
26
28
|
|
|
29
|
+
if (!this.wikiLinkAutoCreate) {
|
|
30
|
+
this.toastController.show('Wiki-link target does not exist');
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
|
|
27
34
|
const normalizedPath = this.normalizeNewWikiFilePath(target);
|
|
28
35
|
if (!normalizedPath) {
|
|
29
36
|
this.toastController.show('Cannot create an empty wiki-link target');
|
|
@@ -19,6 +19,7 @@ export class WorkspaceRouteController {
|
|
|
19
19
|
previewRenderer,
|
|
20
20
|
renderAvatars,
|
|
21
21
|
renderPresence,
|
|
22
|
+
requestPreviewRouteAnchor = null,
|
|
22
23
|
resetPreviewMode,
|
|
23
24
|
scrollSyncController,
|
|
24
25
|
setSession,
|
|
@@ -55,6 +56,7 @@ export class WorkspaceRouteController {
|
|
|
55
56
|
this.previewRenderer = previewRenderer;
|
|
56
57
|
this.renderAvatars = renderAvatars;
|
|
57
58
|
this.renderPresence = renderPresence;
|
|
59
|
+
this.requestPreviewRouteAnchor = requestPreviewRouteAnchor;
|
|
58
60
|
this.resetPreviewMode = resetPreviewMode;
|
|
59
61
|
this.scrollSyncController = scrollSyncController;
|
|
60
62
|
this.setSession = setSession;
|
|
@@ -81,6 +83,7 @@ export class WorkspaceRouteController {
|
|
|
81
83
|
|
|
82
84
|
const route = this.navigation.getHashRoute();
|
|
83
85
|
if (route.type === 'empty') {
|
|
86
|
+
this.requestPreviewRouteAnchor?.(null);
|
|
84
87
|
this.gitPanel.setSelection();
|
|
85
88
|
this.showEmptyState();
|
|
86
89
|
this.syncMainChrome({ mode: 'empty', title: 'CollabMD' });
|
|
@@ -88,30 +91,35 @@ export class WorkspaceRouteController {
|
|
|
88
91
|
}
|
|
89
92
|
|
|
90
93
|
if (route.type === 'git-diff') {
|
|
94
|
+
this.requestPreviewRouteAnchor?.(null);
|
|
91
95
|
this.setSidebarTab('git');
|
|
92
96
|
await this.showGitDiff(route);
|
|
93
97
|
return;
|
|
94
98
|
}
|
|
95
99
|
|
|
96
100
|
if (route.type === 'git-file-history') {
|
|
101
|
+
this.requestPreviewRouteAnchor?.(null);
|
|
97
102
|
this.setSidebarTab('files');
|
|
98
103
|
await this.showGitFileHistory(route);
|
|
99
104
|
return;
|
|
100
105
|
}
|
|
101
106
|
|
|
102
107
|
if (route.type === 'git-file-preview') {
|
|
108
|
+
this.requestPreviewRouteAnchor?.(null);
|
|
103
109
|
this.setSidebarTab('files');
|
|
104
110
|
await this.showGitFilePreview(route);
|
|
105
111
|
return;
|
|
106
112
|
}
|
|
107
113
|
|
|
108
114
|
if (route.type === 'git-history') {
|
|
115
|
+
this.requestPreviewRouteAnchor?.(null);
|
|
109
116
|
this.setSidebarTab('git');
|
|
110
117
|
await this.showGitHistory();
|
|
111
118
|
return;
|
|
112
119
|
}
|
|
113
120
|
|
|
114
121
|
if (route.type === 'git-commit') {
|
|
122
|
+
this.requestPreviewRouteAnchor?.(null);
|
|
115
123
|
this.setSidebarTab('git');
|
|
116
124
|
await this.showGitCommit(route);
|
|
117
125
|
return;
|
|
@@ -119,6 +127,7 @@ export class WorkspaceRouteController {
|
|
|
119
127
|
|
|
120
128
|
this.setSidebarTab('files');
|
|
121
129
|
await this.openFile(route.filePath, { drawioMode: route.drawioMode || null });
|
|
130
|
+
this.requestPreviewRouteAnchor?.(route.anchor ?? null, route.filePath);
|
|
122
131
|
}
|
|
123
132
|
|
|
124
133
|
showEmptyState() {
|