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.
Files changed (44) hide show
  1. package/README.md +1 -0
  2. package/dist/client/assets/{drawio-editor-D4Qoluwz.js → drawio-editor-BHI0pLby.js} +1 -1
  3. package/dist/client/assets/{drawioEditor-B65FWq-b.js → drawioEditor-DOaXwaY2.js} +2 -2
  4. package/dist/client/assets/{editor-session-DV80PhYL.js → editor-session-BFsA-WRc.js} +1 -1
  5. package/dist/client/assets/{excalidraw-editor-hiJQt1cX.js → excalidraw-editor-6pgH2kf5.js} +1 -1
  6. package/dist/client/assets/{excalidrawEditor-BnbblxK4.js → excalidrawEditor-Ck9hgrTa.js} +2 -2
  7. package/dist/client/assets/export-page-4M8Zo-AL.js +39 -0
  8. package/dist/client/assets/exportDocument-BAqaHlIn.css +1 -0
  9. package/dist/client/assets/{exportDocument-CULEGKmb.js → exportDocument-mlXOym-R.js} +2 -2
  10. package/dist/client/assets/{index-BjdYZiBM.js → index-CCfHrm_9.js} +2 -2
  11. package/dist/client/assets/{index-BryqP9QY.css → index-maPm_Kvk.css} +1 -1
  12. package/dist/client/assets/main-BQMPQ4CW.js +1410 -0
  13. package/dist/client/assets/preview-render-compiler-9SmLUP5V.js +1 -0
  14. package/dist/client/assets/{preview-render-compiler-Tkc6JX1O.js → preview-render-compiler-IUtgIlOM.js} +1 -1
  15. package/dist/client/assets/{preview-render-worker-1LBLqgKu.js → preview-render-worker-BwQ5sPy9.js} +1 -1
  16. package/dist/client/assets/runtime-config-BcUbZKho.js +1 -0
  17. package/dist/client/assets/{vault-api-client-DSx3mWkH.js → vault-api-client-BDKKvtOF.js} +1 -1
  18. package/dist/client/drawio-editor.html +1 -1
  19. package/dist/client/excalidraw-editor.html +1 -1
  20. package/dist/client/export-document.html +2 -2
  21. package/dist/client/index.html +2 -2
  22. package/docker-compose.yml +1 -0
  23. package/package.json +12 -9
  24. package/src/client/application/app-shell/ui-feature-shell.js +237 -0
  25. package/src/client/application/preview-render-compiler.js +120 -1
  26. package/src/client/application/preview-render-executor.js +4 -0
  27. package/src/client/application/preview-render-worker.js +2 -0
  28. package/src/client/application/preview-renderer.js +5 -2
  29. package/src/client/application/wiki-link-file-controller.js +7 -0
  30. package/src/client/application/workspace-route-controller.js +9 -0
  31. package/src/client/bootstrap/collabmd-app-shell.js +13 -0
  32. package/src/client/domain/hash-routes.js +49 -0
  33. package/src/client/domain/runtime-paths.js +1 -0
  34. package/src/client/export/export-pipeline.js +100 -26
  35. package/src/client/infrastructure/runtime-config.js +22 -24
  36. package/src/client/presentation/outline-controller.js +28 -7
  37. package/src/client/styles/features/preview-markdown.css +51 -0
  38. package/src/server/config/env.js +1 -0
  39. package/src/server/infrastructure/http/create-static-handler.js +2 -0
  40. package/dist/client/assets/export-page-BKDarp9x.js +0 -39
  41. package/dist/client/assets/exportDocument-BbkN85c4.css +0 -1
  42. package/dist/client/assets/main-QJcxyvDa.js +0 -1404
  43. package/dist/client/assets/preview-render-compiler-BCB8u6iM.js +0 -1
  44. 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 ? normalizePreviewTypography(display) : `Create "${target}"`;
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() {