Merge branch 'main' into feat/cursor-pagination

This commit is contained in:
Philipinho
2026-01-25 00:18:13 +00:00
95 changed files with 2710 additions and 2413 deletions
@@ -26,7 +26,7 @@ export class CollaborationGateway {
) {
this.redisConfig = parseRedisUrl(this.environmentService.getRedisUrl());
this.hocuspocus = HocuspocusServer.configure({
this.hocuspocus = new Hocuspocus({
debounce: 10000,
maxDebounce: 45000,
unloadImmediately: false,
@@ -65,6 +65,6 @@ export class CollaborationGateway {
}
async destroy(): Promise<void> {
await this.hocuspocus.destroy();
//await this.hocuspocus.destroy();
}
}
@@ -1,14 +1,12 @@
import { StarterKit } from '@tiptap/starter-kit';
import { TextAlign } from '@tiptap/extension-text-align';
import { TaskList } from '@tiptap/extension-task-list';
import { TaskItem } from '@tiptap/extension-task-item';
import { Underline } from '@tiptap/extension-underline';
import { Superscript } from '@tiptap/extension-superscript';
import SubScript from '@tiptap/extension-subscript';
import { Typography } from '@tiptap/extension-typography';
import { TextStyle } from '@tiptap/extension-text-style';
import { Color } from '@tiptap/extension-color';
import { Youtube } from '@tiptap/extension-youtube';
import { TaskList, TaskItem } from '@tiptap/extension-list';
import {
Heading,
Callout,
@@ -42,11 +40,14 @@ import { generateHTML, generateJSON } from '../common/helpers/prosemirror/html';
// @tiptap/html library works best for generating prosemirror json state but not HTML
// see: https://github.com/ueberdosis/tiptap/issues/5352
// see:https://github.com/ueberdosis/tiptap/issues/4089
//import { generateJSON } from '@tiptap/html';
import { Node } from '@tiptap/pm/model';
export const tiptapExtensions = [
StarterKit.configure({
codeBlock: false,
link: false,
trailingNode: false,
heading: false,
}),
Heading,
@@ -59,7 +60,6 @@ export const tiptapExtensions = [
TaskItem.configure({
nested: true,
}),
Underline,
LinkExtension,
Superscript,
SubScript,
@@ -69,7 +69,7 @@ export class AuthenticationExtension implements Extension {
}
if (userSpaceRole === SpaceRole.READER) {
data.connection.readOnly = true;
data.connectionConfig.readOnly = true;
this.logger.debug(`User granted readonly access to page: ${pageId}`);
}
@@ -181,7 +181,9 @@ export class AttachmentController {
}
try {
const fileStream = await this.storageService.read(attachment.filePath);
const fileStream = await this.storageService.readStream(
attachment.filePath,
);
res.headers({
'Content-Type': attachment.mimeType,
'Cache-Control': 'private, max-age=3600',
@@ -241,7 +243,9 @@ export class AttachmentController {
}
try {
const fileStream = await this.storageService.read(attachment.filePath);
const fileStream = await this.storageService.readStream(
attachment.filePath,
);
res.headers({
'Content-Type': attachment.mimeType,
'Cache-Control': 'public, max-age=3600',
@@ -367,14 +371,14 @@ export class AttachmentController {
const filePath = `${getAttachmentFolderPath(attachmentType, workspace.id)}/${fileName}`;
try {
const fileStream = await this.storageService.read(filePath);
const fileStream = await this.storageService.readStream(filePath);
res.headers({
'Content-Type': getMimeType(filePath),
'Cache-Control': 'private, max-age=86400',
});
return res.send(fileStream);
} catch (err) {
// this.logger.error(err);
// this.logger.error(err);
throw new NotFoundException('File not found');
}
}
@@ -55,7 +55,7 @@ export class ExportController {
throw new ForbiddenException();
}
const zipFileBuffer = await this.exportService.exportPages(
const zipFileStream = await this.exportService.exportPages(
dto.pageId,
dto.format,
dto.includeAttachments,
@@ -70,7 +70,7 @@ export class ExportController {
'attachment; filename="' + encodeURIComponent(fileName) + '"',
});
res.send(zipFileBuffer);
res.send(zipFileStream);
}
@UseGuards(JwtAuthGuard)
@@ -100,6 +100,6 @@ export class ExportController {
'"',
});
res.send(exportFile.fileBuffer);
res.send(exportFile.fileStream);
}
}
@@ -5,7 +5,6 @@ import {
NotFoundException,
} from '@nestjs/common';
import { jsonToHtml, jsonToNode } from '../../collaboration/collaboration.util';
import { turndown } from './turndown-utils';
import { ExportFormat } from './dto/export-dto';
import { Page } from '@docmost/db/types/entity.types';
import { InjectKysely } from 'nestjs-kysely';
@@ -31,6 +30,7 @@ import {
getAttachmentIds,
getProsemirrorContent,
} from '../../common/helpers/prosemirror/utils';
import { htmlToMarkdown } from '@docmost/editor-ext';
@Injectable()
export class ExportService {
@@ -83,7 +83,7 @@ export class ExportService {
/<colgroup[^>]*>[\s\S]*?<\/colgroup>/gim,
'',
);
return turndown(newPageHtml);
return htmlToMarkdown(newPageHtml);
}
return;
@@ -177,7 +177,7 @@ export class ExportService {
const fileName = `${space.name}-space-export.zip`;
return {
fileBuffer: zipFile,
fileStream: zipFile,
fileName,
};
}
@@ -1,165 +0,0 @@
import * as TurndownService from '@joplin/turndown';
import * as TurndownPluginGfm from '@joplin/turndown-plugin-gfm';
import * as path from 'path';
export function turndown(html: string): string {
const turndownService = new TurndownService({
headingStyle: 'atx',
codeBlockStyle: 'fenced',
hr: '---',
bulletListMarker: '-',
});
const tables = TurndownPluginGfm.tables;
const strikethrough = TurndownPluginGfm.strikethrough;
const highlightedCodeBlock = TurndownPluginGfm.highlightedCodeBlock;
turndownService.use([
tables,
strikethrough,
highlightedCodeBlock,
taskList,
callout,
preserveDetail,
listParagraph,
mathInline,
mathBlock,
iframeEmbed,
video,
]);
return turndownService.turndown(html).replaceAll('<br>', ' ');
}
function listParagraph(turndownService: TurndownService) {
turndownService.addRule('paragraph', {
filter: ['p'],
replacement: (content: any, node: HTMLInputElement) => {
if (node.parentElement?.nodeName === 'LI') {
return content;
}
return `\n\n${content}\n\n`;
},
});
}
function callout(turndownService: TurndownService) {
turndownService.addRule('callout', {
filter: function (node: HTMLInputElement) {
return (
node.nodeName === 'DIV' && node.getAttribute('data-type') === 'callout'
);
},
replacement: function (content: any, node: HTMLInputElement) {
const calloutType = node.getAttribute('data-callout-type');
return `\n\n:::${calloutType}\n${content.trim()}\n:::\n\n`;
},
});
}
function taskList(turndownService: TurndownService) {
turndownService.addRule('taskListItem', {
filter: function (node: HTMLInputElement) {
return (
node.getAttribute('data-type') === 'taskItem' &&
node.parentNode.nodeName === 'UL'
);
},
replacement: function (content: any, node: HTMLInputElement) {
const checkbox = node.querySelector(
'input[type="checkbox"]',
) as HTMLInputElement;
const isChecked = checkbox.checked;
// Process content like regular list items
content = content
.replace(/^\n+/, '') // remove leading newlines
.replace(/\n+$/, '\n') // replace trailing newlines with just a single one
.replace(/\n/gm, '\n '); // indent nested content with 2 spaces
// Create the checkbox prefix
const prefix = `- ${isChecked ? '[x]' : '[ ]'} `;
return prefix + content + (node.nextSibling && !/\n$/.test(content) ? '\n' : '');
},
});
}
function preserveDetail(turndownService: TurndownService) {
turndownService.addRule('preserveDetail', {
filter: function (node: HTMLInputElement) {
return node.nodeName === 'DETAILS';
},
replacement: function (content: any, node: HTMLInputElement) {
const summary = node.querySelector(':scope > summary');
let detailSummary = '';
if (summary) {
detailSummary = `<summary>${turndownService.turndown(summary.innerHTML)}</summary>`;
}
const detailsContent = Array.from(node.childNodes)
.filter((child) => child.nodeName !== 'SUMMARY')
.map((child) =>
child.nodeType === 1
? turndownService.turndown((child as HTMLElement).outerHTML)
: child.textContent,
)
.join('');
return `\n<details>\n${detailSummary}\n\n${detailsContent}\n\n</details>\n`;
},
});
}
function mathInline(turndownService: TurndownService) {
turndownService.addRule('mathInline', {
filter: function (node: HTMLInputElement) {
return (
node.nodeName === 'SPAN' &&
node.getAttribute('data-type') === 'mathInline'
);
},
replacement: function (content: any, node: HTMLInputElement) {
return `$${content}$`;
},
});
}
function mathBlock(turndownService: TurndownService) {
turndownService.addRule('mathBlock', {
filter: function (node: HTMLInputElement) {
return (
node.nodeName === 'DIV' &&
node.getAttribute('data-type') === 'mathBlock'
);
},
replacement: function (content: any, node: HTMLInputElement) {
return `\n$$\n${content}\n$$\n`;
},
});
}
function iframeEmbed(turndownService: TurndownService) {
turndownService.addRule('iframeEmbed', {
filter: function (node: HTMLInputElement) {
return node.nodeName === 'IFRAME';
},
replacement: function (content: any, node: HTMLInputElement) {
const src = node.getAttribute('src');
return '[' + src + '](' + src + ')';
},
});
}
function video(turndownService: TurndownService) {
turndownService.addRule('video', {
filter: function (node: HTMLInputElement) {
return node.tagName === 'VIDEO';
},
replacement: function (content: any, node: HTMLInputElement) {
const src = node.getAttribute('src') || '';
const name = path.basename(src);
return '[' + name + '](' + src + ')';
},
});
}