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.
Files changed (100) hide show
  1. package/README.md +9 -6
  2. package/dist/client/assets/JetBrainsMono-Regular-BQaDgvhP.woff2 +0 -0
  3. package/dist/client/assets/babel-CY5KOv-I.js +16 -0
  4. package/dist/client/assets/comment-ui-shared-DuoyznXW.js +7 -0
  5. package/dist/client/assets/dist-ChRoo3IB.js +9 -0
  6. package/dist/client/assets/document-formatter-Dxl_a8XO.js +2 -0
  7. package/dist/client/assets/{drawio-editor-BjrxTe3l.js → drawio-editor-BVaNNtYp.js} +2 -2
  8. package/dist/client/assets/{drawioEditor-DUVZjSSr.js → drawioEditor-BkIBlXx0.js} +2 -2
  9. package/dist/client/assets/editor-session-DMyVdHLj.js +54 -0
  10. package/dist/client/assets/embedded-editor-base-B3l6Xy65.css +1 -0
  11. package/dist/client/assets/estree-CuT_pB8K.js +44 -0
  12. package/dist/client/assets/{excalidraw-editor-BlcN6umJ.css → excalidraw-editor-BbSbI80a.css} +1 -1
  13. package/dist/client/assets/excalidraw-editor-BjYaNEMY.js +9 -0
  14. package/dist/client/assets/{excalidrawEditor-B0jOK3lv.js → excalidrawEditor-ChqF_d82.js} +2 -2
  15. package/dist/client/assets/export-page-Dyu0XCbu.js +57 -0
  16. package/dist/client/assets/exportDocument-CJBx4Xjx.css +1 -0
  17. package/dist/client/assets/{exportDocument-Dg9MjkIk.js → exportDocument-Dgvfa2Sd.js} +2 -2
  18. package/dist/client/assets/{file-icon-svg-CQAjlWjK.js → file-icon-svg-Y_kpF957.js} +1 -1
  19. package/dist/client/assets/file-kind-BwMIcZbi.js +1 -0
  20. package/dist/client/assets/file-search-DDLyyLDU.js +1 -0
  21. package/dist/client/assets/html-Cdnr2qzU.js +25 -0
  22. package/dist/client/assets/index-CSNIMyOP.css +1 -0
  23. package/dist/client/assets/index-DG3N2Ps-.js +2 -0
  24. package/dist/client/assets/main-D0wCxpVn.js +903 -0
  25. package/dist/client/assets/markdown-CByV-4CU.js +70 -0
  26. package/dist/client/assets/mermaid.core-BJRLUQZE.js +11 -0
  27. package/dist/client/assets/{preview-render-compiler-BApvlLLo.js → preview-render-compiler-CrJIooVD.js} +2 -2
  28. package/dist/client/assets/preview-render-compiler-O0JxBVNF.js +1 -0
  29. package/dist/client/assets/{preview-render-profile-CE-5rVTC.js → preview-render-profile-B4RWYWHt.js} +1 -1
  30. package/dist/client/assets/{preview-render-worker-BmN79bVc.js → preview-render-worker-_QTxqKvP.js} +22 -22
  31. package/dist/client/assets/quick-switcher-controller-CaEjEtq_.js +1 -0
  32. package/dist/client/assets/standalone-6z5x8CWe.js +29 -0
  33. package/dist/client/assets/{vault-api-client-_LpV1aIn.js → vault-api-client-BkwdFjx0.js} +2 -2
  34. package/dist/client/assets/{es-CAsWR6hG.js → vault-utils-DvoSftJT.js} +1 -1
  35. package/dist/client/assets/yaml-9ltHPDNO.js +154 -0
  36. package/dist/client/drawio-editor.html +1 -1
  37. package/dist/client/excalidraw-editor.html +1 -1
  38. package/dist/client/export-document.html +2 -2
  39. package/dist/client/index.html +41 -19
  40. package/package.json +4 -1
  41. package/src/client/app/index.html +39 -17
  42. package/src/client/application/app-shell/export-feature.js +17 -1
  43. package/src/client/application/app-shell/git-feature.js +28 -9
  44. package/src/client/application/app-shell/ui-feature-identity.js +13 -0
  45. package/src/client/application/app-shell/ui-feature-shell.js +164 -9
  46. package/src/client/application/app-shell-elements.js +4 -0
  47. package/src/client/application/workspace-coordinator.js +21 -4
  48. package/src/client/application/workspace-preview-controller.js +120 -2
  49. package/src/client/application/workspace-route-controller.js +4 -1
  50. package/src/client/assets/vendor/jetbrains-mono/JetBrainsMono-Regular.woff2 +0 -0
  51. package/src/client/assets/vendor/jetbrains-mono/OFL.txt +93 -0
  52. package/src/client/bootstrap/collabmd-app-shell.js +21 -5
  53. package/src/client/domain/document-formatter.js +193 -0
  54. package/src/client/domain/file-search.js +73 -0
  55. package/src/client/domain/git-labels.js +5 -0
  56. package/src/client/domain/vault-paths.js +6 -4
  57. package/src/client/domain/wiki-link-completions.js +20 -20
  58. package/src/client/drawio-editor.js +8 -3
  59. package/src/client/excalidraw-editor.js +152 -6
  60. package/src/client/export/export-host.js +56 -35
  61. package/src/client/export/export-page.js +14 -4
  62. package/src/client/export/export-pipeline.js +266 -25
  63. package/src/client/infrastructure/editor-session.js +4 -0
  64. package/src/client/infrastructure/editor-view-adapter.js +60 -14
  65. package/src/client/presentation/bases-preview-controller.js +35 -11
  66. package/src/client/presentation/file-action-controller.js +15 -2
  67. package/src/client/presentation/file-explorer-controller.js +3 -0
  68. package/src/client/presentation/file-explorer-view.js +2 -1
  69. package/src/client/presentation/file-history-view-controller.js +6 -1
  70. package/src/client/presentation/file-tree-state.js +1 -11
  71. package/src/client/presentation/git-diff-view-controller.js +36 -11
  72. package/src/client/presentation/git-panel-controller.js +10 -6
  73. package/src/client/presentation/quick-switcher-controller.js +54 -154
  74. package/src/client/styles/components/scrollbars.css +10 -0
  75. package/src/client/styles/export-document.css +44 -29
  76. package/src/client/styles/features/collaboration-presence.css +13 -1
  77. package/src/client/styles/features/preview-markdown.css +54 -0
  78. package/src/client/styles/foundation/tokens.css +8 -0
  79. package/src/client/styles/layout/shell.css +76 -24
  80. package/src/client/styles/layout/view-modes.css +2 -0
  81. package/src/client/styles/overrides/codemirror.css +31 -19
  82. package/src/client/styles/surfaces/excalidraw-editor.css +42 -0
  83. package/src/domain/file-kind.js +12 -1
  84. package/src/server/domain/ripgrep-search-service.js +110 -37
  85. package/src/server/infrastructure/persistence/path-utils.js +10 -4
  86. package/src/server/infrastructure/persistence/vault-file-store.js +2 -0
  87. package/dist/client/assets/comment-ui-shared-DWkk9Vh3.js +0 -7
  88. package/dist/client/assets/editor-session-DfZuRgB_.js +0 -54
  89. package/dist/client/assets/embedded-editor-base-DvMynhKu.css +0 -1
  90. package/dist/client/assets/excalidraw-editor-D7pnUCLH.js +0 -9
  91. package/dist/client/assets/export-page-Bs3xE1fg.js +0 -57
  92. package/dist/client/assets/exportDocument-PnYU3v1s.css +0 -1
  93. package/dist/client/assets/file-kind-BbqgGzpc.js +0 -1
  94. package/dist/client/assets/index-BIpgxyGZ.css +0 -1
  95. package/dist/client/assets/index-Z5sI0Bj-.js +0 -2
  96. package/dist/client/assets/main-Dwnj9Gaq.js +0 -903
  97. package/dist/client/assets/mermaid.core-CLZBlpIW.js +0 -11
  98. package/dist/client/assets/preview-render-compiler-BQ_3h9vD.js +0 -1
  99. package/dist/client/assets/quick-switcher-controller-ButWPmCI.js +0 -12
  100. package/dist/client/assets/vault-utils-wdQ0RNR9.js +0 -1
@@ -14,7 +14,9 @@ import {
14
14
  sanitizeSvgMarkup,
15
15
  } from '../application/preview-diagram-utils.js';
16
16
  import { compilePreviewDocument } from '../application/preview-render-compiler.js';
17
- import { stripVaultFileExtension } from '../../domain/file-kind.js';
17
+ import { isMarkdownFilePath, stripVaultFileExtension } from '../../domain/file-kind.js';
18
+ import { resolveVaultRelativePath } from '../../domain/vault-paths.js';
19
+ import { resolveWikiTargetPath } from '../../domain/wiki-link-resolver.js';
18
20
  import { parseSceneJson, sceneToInitialData } from '../domain/excalidraw-scene.js';
19
21
  import { escapeHtml } from '../domain/vault-utils.js';
20
22
  import { downloadBlob } from '../browser-utils.js';
@@ -22,7 +24,7 @@ import { resolveApiUrl, resolveAppUrl } from '../infrastructure/runtime-config.j
22
24
 
23
25
  const EXPORT_PAGE_SOURCE = 'collabmd-export-page';
24
26
  const EXPORT_HOST_SOURCE = 'collabmd-export-host';
25
- const LIGHT_EXPORT_MERMAID_THEME = Object.freeze({
27
+ const EXPORT_MERMAID_CONFIG = Object.freeze({
26
28
  htmlLabels: false,
27
29
  flowchart: {
28
30
  defaultRenderer: 'dagre-wrapper',
@@ -33,7 +35,6 @@ const LIGHT_EXPORT_MERMAID_THEME = Object.freeze({
33
35
  useMaxWidth: true,
34
36
  },
35
37
  startOnLoad: false,
36
- theme: 'default',
37
38
  });
38
39
  const EXPORT_ASSET_FETCH_TIMEOUT_MS = 10_000;
39
40
  const EXPORT_ASSET_MAX_BYTES = 10 * 1024 * 1024;
@@ -77,6 +78,10 @@ function createAssetId(prefix = 'asset') {
77
78
  return `${prefix}-${assetCounter.toString(36)}`;
78
79
  }
79
80
 
81
+ function normalizeExportTheme(theme) {
82
+ return theme === 'dark' ? 'dark' : 'light';
83
+ }
84
+
80
85
  function createDocumentTitle(filePath, title = '') {
81
86
  const normalizedTitle = String(title ?? '').trim();
82
87
  if (normalizedTitle) {
@@ -87,6 +92,65 @@ function createDocumentTitle(filePath, title = '') {
87
92
  return stripVaultFileExtension(leaf) || 'document';
88
93
  }
89
94
 
95
+ function createDocumentAnchorId(filePath) {
96
+ return `export-doc-${encodeURIComponent(String(filePath ?? '').toLowerCase()).replace(/%/gu, '-').replace(/\./gu, '-')}`;
97
+ }
98
+
99
+ function createExportHeadingSlug(value) {
100
+ return String(value ?? '').trim().toLowerCase().replace(/[^a-z0-9]+/gu, '-').replace(/^-|-$/gu, '');
101
+ }
102
+
103
+ function rewriteDirectoryDocumentLinks(container, { fileList, sourceFilePath }) {
104
+ const exportedPaths = new Set(fileList);
105
+ const sourceAnchorId = createDocumentAnchorId(sourceFilePath);
106
+
107
+ Array.from(container.querySelectorAll('[id]')).forEach((element) => {
108
+ element.id = `${sourceAnchorId}-${element.id}`;
109
+ });
110
+
111
+ Array.from(container.querySelectorAll('a[href]')).forEach((link) => {
112
+ const rawHref = String(link.getAttribute('href') || '').trim();
113
+ const wikiTarget = String(link.getAttribute('data-wiki-target') || '').trim();
114
+ if (wikiTarget) {
115
+ const [targetPath, targetHeading = ''] = wikiTarget.split('#', 2);
116
+ const resolved = targetPath ? resolveWikiTargetPath(targetPath, fileList) : sourceFilePath;
117
+ if (resolved && exportedPaths.has(resolved)) {
118
+ const headingSlug = createExportHeadingSlug(targetHeading);
119
+ const targetAnchorId = createDocumentAnchorId(resolved);
120
+ link.setAttribute('href', headingSlug ? `#${targetAnchorId}-${headingSlug}` : `#${targetAnchorId}`);
121
+ link.removeAttribute('target');
122
+ link.removeAttribute('rel');
123
+ }
124
+ return;
125
+ }
126
+
127
+ if (!rawHref || /^(?:[a-z][a-z\d+.-]*:|\/\/)/iu.test(rawHref)) {
128
+ return;
129
+ }
130
+
131
+ if (rawHref.startsWith('#')) {
132
+ link.setAttribute('href', `#${sourceAnchorId}-${rawHref.slice(1)}`);
133
+ return;
134
+ }
135
+
136
+ const [pathPart, fragment = ''] = rawHref.split('#', 2);
137
+ const resolved = resolveVaultRelativePath(sourceFilePath, pathPart);
138
+ if (!exportedPaths.has(resolved)) {
139
+ return;
140
+ }
141
+
142
+ const targetAnchorId = createDocumentAnchorId(resolved);
143
+ link.setAttribute('href', fragment ? `#${targetAnchorId}-${fragment}` : `#${targetAnchorId}`);
144
+ link.removeAttribute('target');
145
+ link.removeAttribute('rel');
146
+ });
147
+ }
148
+
149
+ function replaceChildrenFromHtml(target, html) {
150
+ const parsed = new DOMParser().parseFromString(`<body>${String(html ?? '')}</body>`, 'text/html');
151
+ target.replaceChildren(...parsed.body.childNodes);
152
+ }
153
+
90
154
  function encodeSvgBase64DataUrl(svgMarkup) {
91
155
  const bytes = new TextEncoder().encode(String(svgMarkup ?? ''));
92
156
  const chunkSize = 0x8000;
@@ -117,7 +181,7 @@ function trimSvgCanvas(svgMarkup, { padding = 16 } = {}) {
117
181
  probe.style.top = '0';
118
182
  probe.style.visibility = 'hidden';
119
183
  probe.style.pointerEvents = 'none';
120
- probe.innerHTML = svgMarkup;
184
+ replaceChildrenFromHtml(probe, svgMarkup);
121
185
  document.body.appendChild(probe);
122
186
 
123
187
  try {
@@ -167,7 +231,7 @@ async function svgMarkupToPngDataUrl(svgMarkup) {
167
231
  type: 'image/svg+xml;charset=utf-8',
168
232
  }));
169
233
  const svgProbe = document.createElement('div');
170
- svgProbe.innerHTML = svgMarkup;
234
+ replaceChildrenFromHtml(svgProbe, svgMarkup);
171
235
  const svgElement = svgProbe.querySelector('svg');
172
236
 
173
237
  try {
@@ -638,7 +702,7 @@ function createDiagramFigureElement({
638
702
  if (svgMarkup) {
639
703
  const surface = document.createElement('div');
640
704
  surface.className = 'export-diagram-surface';
641
- surface.innerHTML = svgMarkup;
705
+ replaceChildrenFromHtml(surface, svgMarkup);
642
706
 
643
707
  const svgElement = surface.querySelector('svg');
644
708
  if (svgElement) {
@@ -678,7 +742,7 @@ async function ensureMermaidRuntime() {
678
742
  throw new Error('Mermaid runtime failed to initialize');
679
743
  }
680
744
 
681
- runtime.initialize(LIGHT_EXPORT_MERMAID_THEME);
745
+ runtime.initialize({ ...EXPORT_MERMAID_CONFIG, theme: 'default' });
682
746
  return runtime;
683
747
  });
684
748
 
@@ -708,7 +772,10 @@ function prepareMermaidSource(source) {
708
772
 
709
773
  async function renderMermaidToSvgMarkup(source) {
710
774
  const mermaid = await ensureMermaidRuntime();
711
- mermaid.initialize(LIGHT_EXPORT_MERMAID_THEME);
775
+ mermaid.initialize({
776
+ ...EXPORT_MERMAID_CONFIG,
777
+ theme: document.documentElement.dataset.theme === 'dark' ? 'dark' : 'default',
778
+ });
712
779
  const renderHost = document.createElement('div');
713
780
  renderHost.style.position = 'fixed';
714
781
  renderHost.style.left = '-10000px';
@@ -861,6 +928,97 @@ async function resolveDiagramShell(snapshot, shell, { label, prefix, render }) {
861
928
  }));
862
929
  }
863
930
 
931
+ function getBaseCellText(cell) {
932
+ if (cell?.text != null) {
933
+ return String(cell.text);
934
+ }
935
+ if (Array.isArray(cell?.value)) {
936
+ return cell.value.join(', ');
937
+ }
938
+ return cell?.value == null ? '' : String(cell.value);
939
+ }
940
+
941
+ async function resolveBaseEmbed(snapshot, placeholder) {
942
+ const response = await fetchJson(resolveApiUrl('/base/query'), {
943
+ body: JSON.stringify({
944
+ activeFilePath: snapshot.filePath,
945
+ path: placeholder.dataset.basePath || '',
946
+ search: '',
947
+ source: placeholder.dataset.baseSource || null,
948
+ sourcePath: placeholder.dataset.baseSourcePath || snapshot.filePath,
949
+ view: placeholder.dataset.baseView || '',
950
+ }),
951
+ headers: { 'Content-Type': 'application/json' },
952
+ method: 'POST',
953
+ });
954
+ const result = response.result;
955
+ if (!result || !Array.isArray(result.columns) || !Array.isArray(result.groups)) {
956
+ throw new Error('Base query returned no rendered view');
957
+ }
958
+
959
+ // ponytail: static tables preserve base data; share card/list renderers if those export layouts become necessary.
960
+ if (result.view?.type && result.view.type !== 'table') {
961
+ snapshot.warnings.push(`Base ${result.view.type} view exported as a static table`);
962
+ }
963
+
964
+ const shell = document.createElement('section');
965
+ shell.className = 'bases-shell bases-shell-static';
966
+ const toolbar = document.createElement('header');
967
+ toolbar.className = 'bases-toolbar';
968
+ const toolbarMain = document.createElement('div');
969
+ toolbarMain.className = 'bases-toolbar-main';
970
+ const title = document.createElement('strong');
971
+ title.textContent = result.view?.name || 'Base view';
972
+ const meta = document.createElement('div');
973
+ meta.className = 'bases-meta';
974
+ meta.textContent = `${result.totalRows ?? 0} results`;
975
+ toolbarMain.append(title, meta);
976
+ toolbar.appendChild(toolbarMain);
977
+ shell.appendChild(toolbar);
978
+
979
+ const content = document.createElement('div');
980
+ content.className = 'bases-content';
981
+ result.groups.forEach((group) => {
982
+ if (result.groups.length > 1) {
983
+ const heading = document.createElement('h3');
984
+ heading.textContent = group.label || group.key || 'Group';
985
+ content.appendChild(heading);
986
+ }
987
+ const tableShell = document.createElement('div');
988
+ tableShell.className = 'bases-table-shell';
989
+ const table = document.createElement('table');
990
+ table.className = 'bases-table';
991
+ const headRow = table.createTHead().insertRow();
992
+ result.columns.forEach((column) => {
993
+ const heading = document.createElement('th');
994
+ heading.scope = 'col';
995
+ heading.textContent = column.label || column.id || '';
996
+ headRow.appendChild(heading);
997
+ });
998
+ const body = table.createTBody();
999
+ (group.rows || []).forEach((row) => {
1000
+ const tableRow = body.insertRow();
1001
+ result.columns.forEach((column, columnIndex) => {
1002
+ const cell = tableRow.insertCell();
1003
+ const text = getBaseCellText(row.cells?.[column.id]);
1004
+ if (columnIndex === 0 && row.path) {
1005
+ const link = document.createElement('a');
1006
+ link.href = buildWikiLinkHref(row.path);
1007
+ link.dataset.wikiTarget = row.path;
1008
+ link.textContent = text;
1009
+ cell.appendChild(link);
1010
+ } else {
1011
+ cell.textContent = text;
1012
+ }
1013
+ });
1014
+ });
1015
+ tableShell.appendChild(table);
1016
+ content.appendChild(tableShell);
1017
+ });
1018
+ shell.appendChild(content);
1019
+ placeholder.replaceWith(shell);
1020
+ }
1021
+
864
1022
  async function resolveExcalidrawEmbed(snapshot, placeholder) {
865
1023
  const target = String(placeholder.dataset.embedTarget ?? '').trim();
866
1024
  if (!target) {
@@ -1026,6 +1184,20 @@ export async function resolveExportAssets(snapshot, {
1026
1184
  }
1027
1185
  }
1028
1186
 
1187
+ const baseEmbeds = Array.from(container.querySelectorAll('.bases-embed-placeholder[data-base-key]'));
1188
+ for (const placeholder of baseEmbeds) {
1189
+ try {
1190
+ await resolveBaseEmbed(snapshot, placeholder);
1191
+ } catch (error) {
1192
+ const message = error instanceof Error ? error.message : 'Unknown error';
1193
+ snapshot.warnings.push(`Base export failed: ${message}`);
1194
+ placeholder.replaceWith(Object.assign(document.createElement('p'), {
1195
+ className: 'export-warning',
1196
+ textContent: `Base export failed: ${message}`,
1197
+ }));
1198
+ }
1199
+ }
1200
+
1029
1201
  const excalidrawEmbeds = Array.from(container.querySelectorAll('.excalidraw-embed-placeholder[data-embed-key]'));
1030
1202
  for (const placeholder of excalidrawEmbeds) {
1031
1203
  try {
@@ -1057,9 +1229,11 @@ export async function resolveExportAssets(snapshot, {
1057
1229
  }
1058
1230
 
1059
1231
  export async function prepareExportSnapshot({
1232
+ directoryFileList = [],
1060
1233
  fileList = [],
1061
1234
  filePath,
1062
1235
  markdownText = '',
1236
+ theme = 'light',
1063
1237
  title = '',
1064
1238
  } = {}) {
1065
1239
  const normalizedFilePath = String(filePath ?? '').trim();
@@ -1070,6 +1244,7 @@ export async function prepareExportSnapshot({
1070
1244
  generatedAt: new Date().toISOString(),
1071
1245
  html: '',
1072
1246
  sourceMarkdown: normalizedMarkdown,
1247
+ theme: normalizeExportTheme(theme),
1073
1248
  title: createDocumentTitle(normalizedFilePath, title),
1074
1249
  warnings: [],
1075
1250
  };
@@ -1082,13 +1257,78 @@ export async function prepareExportSnapshot({
1082
1257
  });
1083
1258
  const container = document.createElement('div');
1084
1259
  container.className = 'preview-content export-content';
1085
- container.innerHTML = compiled.html;
1260
+ replaceChildrenFromHtml(container, compiled.html);
1086
1261
 
1087
1262
  await resolveExportAssets(snapshot, { container });
1263
+ if (directoryFileList.length > 0) {
1264
+ rewriteDirectoryDocumentLinks(container, {
1265
+ fileList: directoryFileList,
1266
+ sourceFilePath: normalizedFilePath,
1267
+ });
1268
+ }
1088
1269
  snapshot.html = container.innerHTML;
1089
1270
  return snapshot;
1090
1271
  }
1091
1272
 
1273
+ export async function prepareDirectoryExportSnapshot({
1274
+ currentFilePath = '',
1275
+ currentMarkdownText = '',
1276
+ directoryPath,
1277
+ fileList = [],
1278
+ theme = 'light',
1279
+ } = {}) {
1280
+ const normalizedDirectoryPath = String(directoryPath ?? '').replace(/\\/gu, '/').replace(/^\/+|\/+$/gu, '').trim();
1281
+ const directoryPrefix = `${normalizedDirectoryPath}/`;
1282
+ const markdownPaths = fileList
1283
+ .filter((filePath) => isMarkdownFilePath(filePath) && String(filePath).startsWith(directoryPrefix))
1284
+ .sort((left, right) => left.localeCompare(right, undefined, { sensitivity: 'base' }));
1285
+ if (markdownPaths.length === 0) {
1286
+ throw new Error('The selected folder contains no markdown notes');
1287
+ }
1288
+
1289
+ const documents = [];
1290
+ const warnings = [];
1291
+ for (const filePath of markdownPaths) {
1292
+ const markdownText = filePath === currentFilePath
1293
+ ? String(currentMarkdownText ?? '')
1294
+ : await fetchTextFile(filePath);
1295
+ const documentSnapshot = await prepareExportSnapshot({
1296
+ directoryFileList: markdownPaths,
1297
+ fileList,
1298
+ filePath,
1299
+ markdownText,
1300
+ theme,
1301
+ });
1302
+ warnings.push(...documentSnapshot.warnings.map((warning) => `${filePath}: ${warning}`));
1303
+ documents.push(documentSnapshot);
1304
+ }
1305
+
1306
+ const title = createDocumentTitle(normalizedDirectoryPath);
1307
+ const tableOfContents = documents.map((documentSnapshot) => (
1308
+ `<li><a href="#${createDocumentAnchorId(documentSnapshot.filePath)}">${escapeHtml(documentSnapshot.filePath)}</a></li>`
1309
+ )).join('');
1310
+ const html = [
1311
+ `<nav class="export-directory-toc" aria-label="Exported notes"><h1>${escapeHtml(title)}</h1><ol>${tableOfContents}</ol></nav>`,
1312
+ ...documents.map((documentSnapshot) => [
1313
+ `<section class="export-document-section" id="${createDocumentAnchorId(documentSnapshot.filePath)}">`,
1314
+ `<header class="export-document-heading"><h1>${escapeHtml(documentSnapshot.title)}</h1><p>${escapeHtml(documentSnapshot.filePath)}</p></header>`,
1315
+ documentSnapshot.html,
1316
+ '</section>',
1317
+ ].join('')),
1318
+ ].join('');
1319
+
1320
+ return {
1321
+ assets: {},
1322
+ filePath: normalizedDirectoryPath,
1323
+ generatedAt: new Date().toISOString(),
1324
+ html,
1325
+ sourceMarkdown: '',
1326
+ theme: normalizeExportTheme(theme),
1327
+ title,
1328
+ warnings,
1329
+ };
1330
+ }
1331
+
1092
1332
  function applyDocxTableStyles(table, {
1093
1333
  borderColor = '#d1d5db',
1094
1334
  cellPadding = '8px 10px',
@@ -1224,12 +1464,7 @@ function createDocxCodeBlockTable(pre) {
1224
1464
  paragraph.style.color = '#111827';
1225
1465
 
1226
1466
  const text = (line || '').replace(/\t/g, ' ');
1227
- if (!text) {
1228
- paragraph.innerHTML = '&nbsp;';
1229
- } else {
1230
- paragraph.textContent = text;
1231
- paragraph.innerHTML = paragraph.innerHTML.replace(/ /g, '&nbsp;');
1232
- }
1467
+ paragraph.textContent = text ? text.replace(/ /gu, '\u00a0') : '\u00a0';
1233
1468
 
1234
1469
  contentNodes.push(paragraph);
1235
1470
  });
@@ -1244,7 +1479,7 @@ function createDocxCodeBlockTable(pre) {
1244
1479
 
1245
1480
  export function buildDocxHtmlDocument(snapshot) {
1246
1481
  const template = document.createElement('template');
1247
- template.innerHTML = snapshot.html;
1482
+ replaceChildrenFromHtml(template.content, snapshot.html);
1248
1483
 
1249
1484
  Array.from(template.content.querySelectorAll('.table-wrapper')).forEach((wrapper) => {
1250
1485
  const table = wrapper.querySelector(':scope > table');
@@ -1411,20 +1646,21 @@ export function buildDocxHtmlDocument(snapshot) {
1411
1646
 
1412
1647
  export function buildHtmlDocument(snapshot) {
1413
1648
  const template = document.createElement('template');
1414
- template.innerHTML = snapshot.html;
1649
+ replaceChildrenFromHtml(template.content, snapshot.html);
1415
1650
  Array.from(template.content.querySelectorAll('[data-export-docx-src]')).forEach((element) => {
1416
1651
  element.removeAttribute('data-export-docx-src');
1417
1652
  });
1418
1653
 
1654
+ const theme = normalizeExportTheme(snapshot.theme);
1419
1655
  return `<!DOCTYPE html>
1420
- <html lang="en" data-theme="light">
1656
+ <html lang="en" data-theme="${theme}">
1421
1657
  <head>
1422
1658
  <meta charset="UTF-8">
1423
1659
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
1424
1660
  <title>${escapeHtml(snapshot.title)}</title>
1425
1661
  <style>${HTML_EXPORT_STYLES.replace(/<\/style/giu, '<\\/style')}</style>
1426
1662
  </head>
1427
- <body data-theme="light">
1663
+ <body data-theme="${theme}">
1428
1664
  <main class="export-page-shell">
1429
1665
  <article class="preview-content export-content">${template.innerHTML}</article>
1430
1666
  </main>
@@ -1535,11 +1771,11 @@ export function postExportPageMessage(type, payload = {}) {
1535
1771
  }
1536
1772
 
1537
1773
  export async function waitForBootstrapPayload() {
1538
- const requestUrl = new URL(window.location.href);
1539
- const requestedAction = requestUrl.searchParams.get('action');
1774
+ const searchParams = new URLSearchParams(window.location.search);
1775
+ const requestedAction = searchParams.get('action');
1540
1776
  const action = ['html', 'pdf'].includes(requestedAction) ? requestedAction : 'docx';
1541
- const filePath = requestUrl.searchParams.get('file') || '';
1542
- const jobId = requestUrl.searchParams.get('job') || '';
1777
+ const filePath = searchParams.get('file') || '';
1778
+ const jobId = searchParams.get('job') || window.name || '';
1543
1779
 
1544
1780
  if (jobId && window.opener && !window.opener.closed) {
1545
1781
  postExportPageMessage('ready', { jobId });
@@ -1567,11 +1803,15 @@ export async function waitForBootstrapPayload() {
1567
1803
 
1568
1804
  cleanup();
1569
1805
  resolve({
1570
- action,
1806
+ action: ['html', 'pdf'].includes(payload.action) ? payload.action : action,
1807
+ currentFilePath: payload.currentFilePath || '',
1808
+ currentMarkdownText: payload.currentMarkdownText || '',
1809
+ directoryPath: payload.directoryPath || '',
1571
1810
  fileList: Array.isArray(payload.fileList) ? payload.fileList : [],
1572
1811
  filePath: payload.filePath || filePath,
1573
- title: payload.title || '',
1574
1812
  markdownText: payload.markdownText || '',
1813
+ theme: normalizeExportTheme(payload.theme),
1814
+ title: payload.title || '',
1575
1815
  });
1576
1816
  };
1577
1817
 
@@ -1584,6 +1824,7 @@ export async function waitForBootstrapPayload() {
1584
1824
  fileList: [],
1585
1825
  filePath,
1586
1826
  markdownText: filePath ? await fetchTextFile(filePath) : '',
1827
+ theme: 'light',
1587
1828
  title: '',
1588
1829
  };
1589
1830
  }
@@ -274,6 +274,10 @@ export class EditorSession {
274
274
  this.viewAdapter.requestMeasure();
275
275
  }
276
276
 
277
+ formatDocument(filePath) {
278
+ return this.viewAdapter.formatDocument(filePath);
279
+ }
280
+
277
281
  runEditorCommand(commandId) {
278
282
  return this.viewAdapter.runEditorCommand(commandId);
279
283
  }
@@ -2,14 +2,11 @@ import { vim } from '@replit/codemirror-vim';
2
2
  import { autocompletion, closeBrackets, closeBracketsKeymap } from '@codemirror/autocomplete';
3
3
  import {
4
4
  defaultKeymap,
5
- history,
6
- historyKeymap,
7
5
  indentLess,
8
6
  indentMore,
9
7
  indentWithTab,
10
- redo,
11
- undo,
12
8
  } from '@codemirror/commands';
9
+ import { html } from '@codemirror/lang-html';
13
10
  import { markdown, markdownLanguage } from '@codemirror/lang-markdown';
14
11
  import { yaml } from '@codemirror/lang-yaml';
15
12
  import {
@@ -44,15 +41,17 @@ import {
44
41
  lineNumbers,
45
42
  rectangularSelection,
46
43
  } from '@codemirror/view';
47
- import { yCollab } from 'y-codemirror.next';
44
+ import { yCollab, yUndoManagerKeymap } from 'y-codemirror.next';
48
45
 
49
46
  import {
50
47
  isBaseFilePath,
48
+ isHtmlFilePath,
51
49
  isMermaidFilePath,
52
50
  isPlantUmlFilePath,
53
51
  isStructurizrFilePath,
54
52
  } from '../../domain/file-kind.js';
55
53
  import { normalizeCommentQuote } from '../../domain/comment-threads.js';
54
+ import { formatDocumentText } from '../domain/document-formatter.js';
56
55
  import { createMarkdownToolbarEdit } from '../domain/markdown-formatting.js';
57
56
  import { mermaidLanguage, mermaidLanguageDescription } from '../domain/mermaid-language.js';
58
57
  import { wikiLinkCompletions } from '../domain/wiki-link-completions.js';
@@ -149,8 +148,6 @@ const EDITOR_COMMANDS = Object.freeze({
149
148
  indentLess,
150
149
  indentMore,
151
150
  openSearch: openSearchPanelPreservingViewport,
152
- redo,
153
- undo,
154
151
  });
155
152
 
156
153
  class RemoteUpdateCaretWidget extends WidgetType {
@@ -391,6 +388,10 @@ function createEditorTheme(theme) {
391
388
  }
392
389
 
393
390
  export function createLanguageExtension(filePath) {
391
+ if (isHtmlFilePath(filePath)) {
392
+ return html();
393
+ }
394
+
394
395
  if (isMermaidFilePath(filePath)) {
395
396
  return mermaidLanguage;
396
397
  }
@@ -441,6 +442,7 @@ export class EditorViewAdapter {
441
442
  this.onSelectionChanged = onSelectionChanged;
442
443
  this.onViewportChanged = onViewportChanged;
443
444
  this.editorView = null;
445
+ this.undoManager = null;
444
446
  this.themeCompartment = new Compartment();
445
447
  this.syntaxThemeCompartment = new Compartment();
446
448
  this.lineWrappingCompartment = new Compartment();
@@ -477,11 +479,11 @@ export class EditorViewAdapter {
477
479
  }
478
480
 
479
481
  getBaseExtensions(filePath, { readOnly = false } = {}) {
482
+ const wikiLinkCompletionSource = wikiLinkCompletions(this.getFileList);
480
483
  const extensions = [
481
484
  lineNumbers(),
482
485
  highlightActiveLineGutter(),
483
486
  highlightSpecialChars(),
484
- history(),
485
487
  foldGutter(),
486
488
  drawSelection(),
487
489
  EditorState.allowMultipleSelections.of(true),
@@ -489,9 +491,10 @@ export class EditorViewAdapter {
489
491
  syntaxHighlighting(defaultHighlightStyle, { fallback: true }),
490
492
  bracketMatching({ renderMatch: renderBracketMatch }),
491
493
  closeBrackets(),
492
- autocompletion({
493
- override: [wikiLinkCompletions(this.getFileList)],
494
- }),
494
+ autocompletion(),
495
+ EditorState.languageData.of(() => [{
496
+ autocomplete: wikiLinkCompletionSource,
497
+ }]),
495
498
  rectangularSelection(),
496
499
  crosshairCursor(),
497
500
  highlightActiveLine(),
@@ -501,7 +504,6 @@ export class EditorViewAdapter {
501
504
  keymap.of([
502
505
  ...closeBracketsKeymap,
503
506
  ...defaultKeymap,
504
- ...historyKeymap,
505
507
  ...searchKeymap.map((binding) => (
506
508
  binding.run === openSearchPanel
507
509
  ? { ...binding, run: openSearchPanelPreservingViewport }
@@ -593,10 +595,12 @@ export class EditorViewAdapter {
593
595
 
594
596
  initialize({ awareness, filePath, undoManager, ytext }) {
595
597
  const preserveScrollTop = this.editorView?.scrollDOM?.scrollTop ?? 0;
598
+ this.undoManager = undoManager;
596
599
  const state = EditorState.create({
597
600
  doc: ytext.toString(),
598
601
  extensions: [
599
602
  ...this.getBaseExtensions(filePath),
603
+ keymap.of(yUndoManagerKeymap),
600
604
  yCollab(ytext, awareness, { undoManager }),
601
605
  ],
602
606
  });
@@ -609,6 +613,7 @@ export class EditorViewAdapter {
609
613
 
610
614
  initializeProvisional({ content = '', filePath }) {
611
615
  const preserveScrollTop = this.editorView?.scrollDOM?.scrollTop ?? 0;
616
+ this.undoManager = null;
612
617
  const state = EditorState.create({
613
618
  doc: String(content ?? ''),
614
619
  extensions: this.getBaseExtensions(filePath, { readOnly: true }),
@@ -629,6 +634,7 @@ export class EditorViewAdapter {
629
634
  clearTimeout(this.remoteUpdateFlashTimer);
630
635
  this.remoteUpdateFlashTimer = 0;
631
636
  }
637
+ this.undoManager = null;
632
638
  this.teardownEditorView({ clearContainer: true });
633
639
  }
634
640
 
@@ -709,18 +715,58 @@ export class EditorViewAdapter {
709
715
  this.editorView?.requestMeasure();
710
716
  }
711
717
 
718
+ async formatDocument(filePath) {
719
+ const view = this.editorView;
720
+ if (!view || view.state.readOnly) {
721
+ return 'unsupported';
722
+ }
723
+
724
+ const source = view.state.doc.toString();
725
+ const formatted = await formatDocumentText(filePath, source);
726
+ if (formatted == null) {
727
+ return 'unsupported';
728
+ }
729
+ if (this.editorView !== view || view.state.doc.toString() !== source) {
730
+ return 'changed';
731
+ }
732
+ if (formatted === source) {
733
+ return 'unchanged';
734
+ }
735
+
736
+ const selection = EditorSelection.create(view.state.selection.ranges.map(({ anchor, head }) => (
737
+ EditorSelection.range(Math.min(anchor, formatted.length), Math.min(head, formatted.length))
738
+ )));
739
+ view.dispatch({
740
+ changes: { from: 0, to: source.length, insert: formatted },
741
+ selection,
742
+ scrollIntoView: true,
743
+ userEvent: 'input.format',
744
+ });
745
+ view.focus();
746
+ return 'formatted';
747
+ }
748
+
712
749
  runEditorCommand(commandId) {
713
750
  if (!this.editorView) {
714
751
  return false;
715
752
  }
716
753
 
717
- const command = EDITOR_COMMANDS[String(commandId ?? '')];
754
+ const normalizedCommandId = String(commandId ?? '');
755
+ if ((normalizedCommandId === 'undo' || normalizedCommandId === 'redo') && this.undoManager) {
756
+ const applied = this.undoManager[normalizedCommandId]() != null;
757
+ if (applied) {
758
+ this.editorView.focus();
759
+ }
760
+ return applied;
761
+ }
762
+
763
+ const command = EDITOR_COMMANDS[normalizedCommandId];
718
764
  if (typeof command !== 'function') {
719
765
  return false;
720
766
  }
721
767
 
722
768
  const applied = command(this.editorView);
723
- if (applied && commandId !== 'openSearch') {
769
+ if (applied && normalizedCommandId !== 'openSearch') {
724
770
  this.editorView.focus();
725
771
  }
726
772