Compare commits

...

16 Commits

Author SHA1 Message Date
Philipinho 0a447e91bb fix markdown import 2024-07-22 18:39:44 +01:00
Philipinho 48e76aa9f4 v0.2.8 2024-07-22 16:36:06 +01:00
Philipinho 2bd6422a35 Show new workspace role on change 2024-07-22 16:35:00 +01:00
olivierIllogika 407a1aff3b only owner can assign owner role (#108)
* backend fix: https://github.com/docmost/docmost/commit/b4bc184cb3749a3faa5a00d5a1240faacd4b1035
2024-07-22 16:18:09 +01:00
Philipinho b4bc184cb3 prevent admin role from managing owner role (backend) 2024-07-22 16:16:33 +01:00
Philipinho 109dbdbe02 cleanup log 2024-07-22 15:59:43 +01:00
Philipinho 2df7de5828 fix table commands type error 2024-07-22 15:43:43 +01:00
Philipinho 373fc86e47 preserve details tag in markdown export 2024-07-22 14:09:52 +01:00
Philipinho 5052a9ea40 Support math export in Markdown 2024-07-22 13:20:00 +01:00
Philipinho cd47c79d86 Make math node handling better 2024-07-22 13:05:07 +01:00
Philipinho 78746938b7 fix export format state 2024-07-22 13:02:13 +01:00
Philipinho 4d2936627c fix: generate ydoc state during page import to prevent duplicate nodes on the editor 2024-07-22 11:02:43 +01:00
Philipinho d2ecd28047 fix: localize attachment type
* fixes #86
2024-07-22 10:58:32 +01:00
Philipinho bb92ca75e9 use logger 2024-07-21 21:57:31 +01:00
Philipinho 8f3e2ff663 fix editor placeholder bug 2024-07-21 20:50:08 +01:00
Philipinho 89f6311e46 * Make page import handling better 2024-07-21 20:48:33 +01:00
27 changed files with 262 additions and 170 deletions
+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"name": "client", "name": "client",
"private": true, "private": true,
"version": "0.2.7", "version": "0.2.8",
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",
"build": "tsc && vite build", "build": "tsc && vite build",
@@ -23,7 +23,7 @@ const RoleButton = forwardRef<HTMLButtonElement, RoleButtonProps>(
), ),
); );
interface SpaceRoleMenuProps { interface RoleMenuProps {
roles: IRoleData[]; roles: IRoleData[];
roleName: string; roleName: string;
onChange?: (value: string) => void; onChange?: (value: string) => void;
@@ -35,7 +35,7 @@ export default function RoleSelectMenu({
roleName, roleName,
onChange, onChange,
disabled, disabled,
}: SpaceRoleMenuProps) { }: RoleMenuProps) {
return ( return (
<Menu withArrow> <Menu withArrow>
<Menu.Target> <Menu.Target>
@@ -4,13 +4,14 @@ import { TextAlign } from "@tiptap/extension-text-align";
import { TaskList } from "@tiptap/extension-task-list"; import { TaskList } from "@tiptap/extension-task-list";
import { TaskItem } from "@tiptap/extension-task-item"; import { TaskItem } from "@tiptap/extension-task-item";
import { Underline } from "@tiptap/extension-underline"; import { Underline } from "@tiptap/extension-underline";
import { Link } from "@tiptap/extension-link";
import { Superscript } from "@tiptap/extension-superscript"; import { Superscript } from "@tiptap/extension-superscript";
import SubScript from "@tiptap/extension-subscript"; import SubScript from "@tiptap/extension-subscript";
import { Highlight } from "@tiptap/extension-highlight"; import { Highlight } from "@tiptap/extension-highlight";
import { Typography } from "@tiptap/extension-typography"; import { Typography } from "@tiptap/extension-typography";
import { TextStyle } from "@tiptap/extension-text-style"; import { TextStyle } from "@tiptap/extension-text-style";
import { Color } from "@tiptap/extension-color"; import { Color } from "@tiptap/extension-color";
import Table from "@tiptap/extension-table";
import TableHeader from "@tiptap/extension-table-header";
import CodeBlockLowlight from "@tiptap/extension-code-block-lowlight"; import CodeBlockLowlight from "@tiptap/extension-code-block-lowlight";
import SlashCommand from "@/features/editor/extensions/slash-command"; import SlashCommand from "@/features/editor/extensions/slash-command";
import { Collaboration } from "@tiptap/extension-collaboration"; import { Collaboration } from "@tiptap/extension-collaboration";
@@ -23,8 +24,6 @@ import {
DetailsSummary, DetailsSummary,
MathBlock, MathBlock,
MathInline, MathInline,
Table,
TableHeader,
TableCell, TableCell,
TableRow, TableRow,
TrailingNode, TrailingNode,
@@ -66,7 +65,9 @@ export const mainExtensions = [
if (node.type.name === "detailsSummary") { if (node.type.name === "detailsSummary") {
return "Toggle title"; return "Toggle title";
} }
return 'Write anything. Enter "/" for commands'; if (node.type.name === "paragraph") {
return 'Write anything. Enter "/" for commands';
}
}, },
includeChildren: true, includeChildren: true,
}), }),
@@ -95,10 +96,16 @@ export const mainExtensions = [
class: "comment-mark", class: "comment-mark",
}, },
}), }),
Table,
Table.configure({
resizable: true,
lastColumnResizable: false,
allowTableNodeSelection: true,
}),
TableRow, TableRow,
TableCell, TableCell,
TableHeader, TableHeader,
MathInline.configure({ MathInline.configure({
view: MathInlineView, view: MathInlineView,
}), }),
@@ -56,7 +56,7 @@ export default function PageExportModal({
<div> <div>
<Text size="md">Format</Text> <Text size="md">Format</Text>
</div> </div>
<ExportFormatSelection onChange={handleChange} /> <ExportFormatSelection format={format} onChange={handleChange} />
</Group> </Group>
<Group justify="center" mt="md"> <Group justify="center" mt="md">
@@ -72,16 +72,17 @@ export default function PageExportModal({
} }
interface ExportFormatSelection { interface ExportFormatSelection {
format: ExportFormat;
onChange: (value: string) => void; onChange: (value: string) => void;
} }
function ExportFormatSelection({ onChange }: ExportFormatSelection) { function ExportFormatSelection({ format, onChange }: ExportFormatSelection) {
return ( return (
<Select <Select
data={[ data={[
{ value: "markdown", label: "Markdown" }, { value: "markdown", label: "Markdown" },
{ value: "html", label: "HTML" }, { value: "html", label: "HTML" },
]} ]}
defaultValue={ExportFormat.Markdown} defaultValue={format}
onChange={onChange} onChange={onChange}
styles={{ wrapper: { maxWidth: 120 } }} styles={{ wrapper: { maxWidth: 120 } }}
comboboxProps={{ width: "120" }} comboboxProps={{ width: "120" }}
@@ -84,14 +84,14 @@ function ImportFormatSelection({ spaceId, onClose }: ImportFormatSelection) {
} }
} }
const newTreeNodes = buildTree(pages); if (pages?.length > 0 && pageCount > 0) {
const fullTree = treeData.concat(newTreeNodes); const newTreeNodes = buildTree(pages);
const fullTree = treeData.concat(newTreeNodes);
if (newTreeNodes?.length && fullTree?.length > 0) { if (newTreeNodes?.length && fullTree?.length > 0) {
setTreeData(fullTree); setTreeData(fullTree);
} }
if (pageCount > 0) {
const pageCountText = pageCount === 1 ? "1 page" : `${pageCount} pages`; const pageCountText = pageCount === 1 ? "1 page" : `${pageCount} pages`;
notifications.update({ notifications.update({
@@ -11,11 +11,14 @@ import {
userRoleData, userRoleData,
} from "@/features/workspace/types/user-role-data.ts"; } from "@/features/workspace/types/user-role-data.ts";
import useUserRole from "@/hooks/use-user-role.tsx"; import useUserRole from "@/hooks/use-user-role.tsx";
import { UserRole } from "@/lib/types.ts";
export default function WorkspaceMembersTable() { export default function WorkspaceMembersTable() {
const { data, isLoading } = useWorkspaceMembersQuery({ limit: 100 }); const { data, isLoading } = useWorkspaceMembersQuery({ limit: 100 });
const changeMemberRoleMutation = useChangeMemberRoleMutation(); const changeMemberRoleMutation = useChangeMemberRoleMutation();
const { isAdmin } = useUserRole(); const { isAdmin, isOwner } = useUserRole();
const assignableUserRoles = isOwner ? userRoleData : userRoleData.filter((role) => role.value !== UserRole.OWNER);
const handleRoleChange = async ( const handleRoleChange = async (
userId: string, userId: string,
@@ -69,7 +72,7 @@ export default function WorkspaceMembersTable() {
<Table.Td> <Table.Td>
<RoleSelectMenu <RoleSelectMenu
roles={userRoleData} roles={assignableUserRoles}
roleName={getUserRoleLabel(user.role)} roleName={getUserRoleLabel(user.role)}
onChange={(newRole) => onChange={(newRole) =>
handleRoleChange(user.id, user.role, newRole) handleRoleChange(user.id, user.role, newRole)
@@ -53,9 +53,10 @@ export function useChangeMemberRoleMutation() {
return useMutation<any, Error, any>({ return useMutation<any, Error, any>({
mutationFn: (data) => changeMemberRole(data), mutationFn: (data) => changeMemberRole(data),
onSuccess: (data, variables) => { onSuccess: (data, variables) => {
// TODO: change in cache instead
notifications.show({ message: "Member role updated successfully" }); notifications.show({ message: "Member role updated successfully" });
queryClient.refetchQueries({ queryClient.refetchQueries({
queryKey: ["workspaceMembers", variables.spaceId], queryKey: ["workspaceMembers"],
}); });
}, },
onError: (error) => { onError: (error) => {
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "server", "name": "server",
"version": "0.2.7", "version": "0.2.8",
"description": "", "description": "",
"author": "", "author": "",
"private": true, "private": true,
@@ -10,6 +10,8 @@ import { Typography } from '@tiptap/extension-typography';
import { TextStyle } from '@tiptap/extension-text-style'; import { TextStyle } from '@tiptap/extension-text-style';
import { Color } from '@tiptap/extension-color'; import { Color } from '@tiptap/extension-color';
import { Youtube } from '@tiptap/extension-youtube'; import { Youtube } from '@tiptap/extension-youtube';
import Table from '@tiptap/extension-table';
import TableHeader from '@tiptap/extension-table-header';
import { import {
Callout, Callout,
Comment, Comment,
@@ -19,16 +21,18 @@ import {
LinkExtension, LinkExtension,
MathBlock, MathBlock,
MathInline, MathInline,
Table,
TableCell, TableCell,
TableHeader,
TableRow, TableRow,
TiptapImage, TiptapImage,
TiptapVideo, TiptapVideo,
TrailingNode, TrailingNode,
} from '@docmost/editor-ext'; } from '@docmost/editor-ext';
import { generateText, JSONContent } from '@tiptap/core'; import { generateText, JSONContent } from '@tiptap/core';
import { generateHTML, generateJSON } from '../common/helpers/prosemirror/html'; import { generateHTML } 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';
export const tiptapExtensions = [ export const tiptapExtensions = [
StarterKit, StarterKit,
@@ -59,7 +59,7 @@ export class AttachmentService {
}); });
} catch (err) { } catch (err) {
// delete uploaded file on error // delete uploaded file on error
console.error(err); this.logger.error(err);
} }
return attachment; return attachment;
@@ -1,5 +1,6 @@
import { import {
BadRequestException, BadRequestException,
ForbiddenException,
Injectable, Injectable,
NotFoundException, NotFoundException,
} from '@nestjs/common'; } from '@nestjs/common';
@@ -217,11 +218,21 @@ export class WorkspaceService {
) { ) {
const user = await this.userRepo.findById(userRoleDto.userId, workspaceId); const user = await this.userRepo.findById(userRoleDto.userId, workspaceId);
const newRole = userRoleDto.role.toLowerCase();
if (!user) { if (!user) {
throw new BadRequestException('Workspace member not found'); throw new BadRequestException('Workspace member not found');
} }
if (user.role === userRoleDto.role) { // prevent ADMIN from managing OWNER role
if (
(authUser.role === UserRole.ADMIN && newRole === UserRole.OWNER) ||
(authUser.role === UserRole.ADMIN && user.role === UserRole.OWNER)
) {
throw new ForbiddenException();
}
if (user.role === newRole) {
return user; return user;
} }
@@ -238,7 +249,7 @@ export class WorkspaceService {
await this.userRepo.updateUser( await this.userRepo.updateUser(
{ {
role: userRoleDto.role, role: newRole,
}, },
user.id, user.id,
workspaceId, workspaceId,
@@ -18,11 +18,11 @@ export function turndown(html: string): string {
highlightedCodeBlock, highlightedCodeBlock,
taskList, taskList,
callout, callout,
toggleListTitle, preserveDetail,
toggleListBody,
listParagraph, listParagraph,
mathInline,
mathBlock,
]); ]);
return turndownService.turndown(html).replaceAll('<br>', ' '); return turndownService.turndown(html).replaceAll('<br>', ' ');
} }
@@ -72,29 +72,51 @@ function taskList(turndownService: TurndownService) {
}); });
} }
function toggleListTitle(turndownService: TurndownService) { function preserveDetail(turndownService: TurndownService) {
turndownService.addRule('toggleListTitle', { turndownService.addRule('preserveDetail', {
filter: function (node: HTMLInputElement) { filter: function (node: HTMLInputElement) {
return ( return node.nodeName === 'DETAILS';
node.nodeName === 'SUMMARY' && node.parentNode.nodeName === 'DETAILS'
);
}, },
replacement: function (content: any, node: HTMLInputElement) { replacement: function (content: any, node: HTMLInputElement) {
return '- ' + content; // TODO: preserve summary of nested details
const summary = node.querySelector(':scope > summary');
let detailSummary = '';
if (summary) {
detailSummary = `<summary>${turndownService.turndown(summary.innerHTML)}</summary>`;
summary.remove();
}
const detailsContent = turndownService.turndown(node.innerHTML);
return `\n<details>\n${detailSummary}\n\n${detailsContent}\n\n</details>\n`;
}, },
}); });
} }
function toggleListBody(turndownService: TurndownService) { function mathInline(turndownService: TurndownService) {
turndownService.addRule('toggleListContent', { turndownService.addRule('mathInline', {
filter: function (node: HTMLInputElement) { filter: function (node: HTMLInputElement) {
return ( return (
node.getAttribute('data-type') === 'detailsContent' && node.nodeName === 'SPAN' &&
node.parentNode.nodeName === 'DETAILS' node.getAttribute('data-type') === 'mathInline'
); );
}, },
replacement: function (content: any, node: HTMLInputElement) { replacement: function (content: any, node: HTMLInputElement) {
return ` ${content.replace(/\n/g, '\n ')} `; 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$$${content}$$\n`;
}, },
}); });
} }
@@ -20,6 +20,7 @@ import {
} from '../../core/casl/interfaces/space-ability.type'; } from '../../core/casl/interfaces/space-ability.type';
import { FileInterceptor } from '../../common/interceptors/file.interceptor'; import { FileInterceptor } from '../../common/interceptors/file.interceptor';
import * as bytes from 'bytes'; import * as bytes from 'bytes';
import * as path from 'path';
import { MAX_FILE_SIZE } from '../../core/attachment/attachment.constants'; import { MAX_FILE_SIZE } from '../../core/attachment/attachment.constants';
import { ImportService } from './import.service'; import { ImportService } from './import.service';
import { AuthWorkspace } from '../../common/decorators/auth-workspace.decorator'; import { AuthWorkspace } from '../../common/decorators/auth-workspace.decorator';
@@ -42,6 +43,8 @@ export class ImportController {
@AuthUser() user: User, @AuthUser() user: User,
@AuthWorkspace() workspace: Workspace, @AuthWorkspace() workspace: Workspace,
) { ) {
const validFileExtensions = ['.md', '.html'];
const maxFileSize = bytes(MAX_FILE_SIZE); const maxFileSize = bytes(MAX_FILE_SIZE);
let file = null; let file = null;
@@ -62,6 +65,12 @@ export class ImportController {
throw new BadRequestException('Failed to upload file'); throw new BadRequestException('Failed to upload file');
} }
if (
!validFileExtensions.includes(path.extname(file.filename).toLowerCase())
) {
throw new BadRequestException('Invalid import file type.');
}
const spaceId = file.fields?.spaceId?.value; const spaceId = file.fields?.spaceId?.value;
if (!spaceId) { if (!spaceId) {
@@ -3,13 +3,17 @@ import { PageRepo } from '@docmost/db/repos/page/page.repo';
import { MultipartFile } from '@fastify/multipart'; import { MultipartFile } from '@fastify/multipart';
import { sanitize } from 'sanitize-filename-ts'; import { sanitize } from 'sanitize-filename-ts';
import * as path from 'path'; import * as path from 'path';
import { htmlToJson } from '../../collaboration/collaboration.util'; import {
import { marked } from 'marked'; htmlToJson,
tiptapExtensions,
} from '../../collaboration/collaboration.util';
import { InjectKysely } from 'nestjs-kysely'; import { InjectKysely } from 'nestjs-kysely';
import { KyselyDB } from '@docmost/db/types/kysely.types'; import { KyselyDB } from '@docmost/db/types/kysely.types';
import { generateSlugId } from '../../common/helpers'; import { generateSlugId } from '../../common/helpers';
import { generateJitteredKeyBetween } from 'fractional-indexing-jittered'; import { generateJitteredKeyBetween } from 'fractional-indexing-jittered';
import { transformHTML } from './utils/html.utils'; import { markdownToHtml } from './utils/marked.utils';
import { TiptapTransformer } from '@hocuspocus/transformer';
import * as Y from 'yjs';
@Injectable() @Injectable()
export class ImportService { export class ImportService {
@@ -30,22 +34,25 @@ export class ImportService {
const fileBuffer = await file.toBuffer(); const fileBuffer = await file.toBuffer();
const fileName = sanitize(file.filename).slice(0, 255).split('.')[0]; const fileName = sanitize(file.filename).slice(0, 255).split('.')[0];
const fileExtension = path.extname(file.filename).toLowerCase(); const fileExtension = path.extname(file.filename).toLowerCase();
const fileMimeType = file.mimetype;
const fileContent = fileBuffer.toString(); const fileContent = fileBuffer.toString();
let prosemirrorState = null; let prosemirrorState = null;
let createdPage = null; let createdPage = null;
if (fileExtension.endsWith('.md') && fileMimeType === 'text/markdown') { try {
prosemirrorState = await this.processMarkdown(fileContent); if (fileExtension.endsWith('.md')) {
} prosemirrorState = await this.processMarkdown(fileContent);
} else if (fileExtension.endsWith('.html')) {
if (fileExtension.endsWith('.html') && fileMimeType === 'text/html') { prosemirrorState = await this.processHTML(fileContent);
prosemirrorState = await this.processHTML(fileContent); }
} catch (err) {
const message = 'Error processing file content';
this.logger.error(message, err);
throw new BadRequestException(message);
} }
if (!prosemirrorState) { if (!prosemirrorState) {
const message = 'Unsupported file format or mime type'; const message = 'Failed to create ProseMirror state';
this.logger.error(message); this.logger.error(message);
throw new BadRequestException(message); throw new BadRequestException(message);
} }
@@ -63,14 +70,19 @@ export class ImportService {
slugId: generateSlugId(), slugId: generateSlugId(),
title: pageTitle, title: pageTitle,
content: prosemirrorJson, content: prosemirrorJson,
ydoc: await this.createYdoc(prosemirrorJson),
position: pagePosition, position: pagePosition,
spaceId: spaceId, spaceId: spaceId,
creatorId: userId, creatorId: userId,
workspaceId: workspaceId, workspaceId: workspaceId,
lastUpdatedById: userId, lastUpdatedById: userId,
}); });
this.logger.debug(
`Successfully imported "${title}${fileExtension}. ID: ${createdPage.id} - SlugId: ${createdPage.slugId}"`,
);
} catch (err) { } catch (err) {
const message = 'Failed to create page'; const message = 'Failed to create imported page';
this.logger.error(message, err); this.logger.error(message, err);
throw new BadRequestException(message); throw new BadRequestException(message);
} }
@@ -80,14 +92,37 @@ export class ImportService {
} }
async processMarkdown(markdownInput: string): Promise<any> { async processMarkdown(markdownInput: string): Promise<any> {
// turn markdown to html try {
const html = await marked.parse(markdownInput); const html = await markdownToHtml(markdownInput);
return await this.processHTML(html); return this.processHTML(html);
} catch (err) {
throw err;
}
} }
async processHTML(htmlInput: string): Promise<any> { async processHTML(htmlInput: string): Promise<any> {
// turn html to prosemirror state try {
return htmlToJson(transformHTML(htmlInput)); return htmlToJson(htmlInput);
} catch (err) {
throw err;
}
}
async createYdoc(prosemirrorJson: any): Promise<Buffer | null> {
if (prosemirrorJson) {
this.logger.debug(`Converting prosemirror json state to ydoc`);
const ydoc = TiptapTransformer.toYdoc(
prosemirrorJson,
'default',
tiptapExtensions,
);
Y.encodeStateAsUpdate(ydoc);
return Buffer.from(Y.encodeStateAsUpdate(ydoc));
}
return null;
} }
extractTitleAndRemoveHeading(prosemirrorState: any) { extractTitleAndRemoveHeading(prosemirrorState: any) {
@@ -1,80 +0,0 @@
import { Window, DOMParser } from 'happy-dom';
function transformTaskList(html: string): string {
const window = new Window();
const doc = new DOMParser(window).parseFromString(html, 'text/html');
const ulElements = doc.querySelectorAll('ul');
ulElements.forEach((ul) => {
let isTaskList = false;
const liElements = ul.querySelectorAll('li');
liElements.forEach((li) => {
const checkbox = li.querySelector('input[type="checkbox"]');
if (checkbox) {
isTaskList = true;
// Add taskItem data type
li.setAttribute('data-type', 'taskItem');
// Set data-checked attribute based on the checkbox state
// @ts-ignore
li.setAttribute('data-checked', checkbox.checked ? 'true' : 'false');
// Remove the checkbox from the li
checkbox.remove();
// Move the content of <p> out of the <p> and remove <p>
const pElements = li.querySelectorAll('p');
pElements.forEach((p) => {
// Append the content of the <p> element to its parent (the <li> element)
while (p.firstChild) {
li.appendChild(p.firstChild);
}
// Remove the now empty <p> element
p.remove();
});
}
});
// If any <li> contains a checkbox, mark the <ul> as a task list
if (isTaskList) {
ul.setAttribute('data-type', 'taskList');
}
});
return doc.body.innerHTML;
}
function transformCallouts(html: string): string {
const window = new Window();
const doc = new DOMParser(window).parseFromString(html, 'text/html');
const calloutRegex = /:::(\w+)\s*([\s\S]*?)\s*:::/g;
const createCalloutDiv = (type: string, content: string): HTMLElement => {
const div = doc.createElement('div');
div.setAttribute('data-type', 'callout');
div.setAttribute('data-callout-type', type);
const p = doc.createElement('p');
p.textContent = content.trim();
div.appendChild(p);
return div as unknown as HTMLElement;
};
const pElements = doc.querySelectorAll('p');
pElements.forEach((p) => {
if (calloutRegex.test(p.innerHTML) && !p.closest('ul, ol')) {
calloutRegex.lastIndex = 0;
const [, type, content] = calloutRegex.exec(p.innerHTML) || [];
const calloutDiv = createCalloutDiv(type, content);
// @ts-ignore
p.replaceWith(calloutDiv);
}
});
return doc.body.innerHTML;
}
export function transformHTML(html: string): string {
return transformTaskList(transformCallouts(html));
}
@@ -0,0 +1,36 @@
import { marked } from 'marked';
marked.use({
renderer: {
// @ts-ignore
list(body: string, isOrdered: boolean, start: number) {
if (isOrdered) {
const startAttr = start !== 1 ? ` start="${start}"` : '';
return `<ol ${startAttr}>\n${body}</ol>\n`;
}
const dataType = body.includes(`<input`) ? ' data-type="taskList"' : '';
return `<ul${dataType}>\n${body}</ul>\n`;
},
// @ts-ignore
listitem({ text, raw, task: isTask, checked: isChecked }): string {
if (!isTask) {
return `<li>${text}</li>\n`;
}
const checkedAttr = isChecked
? 'data-checked="true"'
: 'data-checked="false"';
return `<li data-type="taskItem" ${checkedAttr}>${text}</li>\n`;
},
},
});
export async function markdownToHtml(markdownInput: string): Promise<string> {
const YAML_FONT_MATTER_REGEX = /^\s*---[\s\S]*?---\s*/;
const markdown = markdownInput
.replace(YAML_FONT_MATTER_REGEX, '')
.trimStart();
return marked.parse(markdown);
}
+2 -1
View File
@@ -1,7 +1,7 @@
{ {
"name": "docmost", "name": "docmost",
"homepage": "https://docmost.com", "homepage": "https://docmost.com",
"version": "0.2.7", "version": "0.2.8",
"private": true, "private": true,
"scripts": { "scripts": {
"build": "nx run-many -t build", "build": "nx run-many -t build",
@@ -53,6 +53,7 @@
"@tiptap/extension-typography": "^2.5.4", "@tiptap/extension-typography": "^2.5.4",
"@tiptap/extension-underline": "^2.5.4", "@tiptap/extension-underline": "^2.5.4",
"@tiptap/extension-youtube": "^2.5.4", "@tiptap/extension-youtube": "^2.5.4",
"@tiptap/html": "^2.5.4",
"@tiptap/pm": "^2.5.4", "@tiptap/pm": "^2.5.4",
"@tiptap/react": "^2.5.4", "@tiptap/react": "^2.5.4",
"@tiptap/starter-kit": "^2.5.4", "@tiptap/starter-kit": "^2.5.4",
@@ -1,7 +1,7 @@
import { type EditorState, Plugin, PluginKey } from "@tiptap/pm/state"; import { type EditorState, Plugin, PluginKey } from "@tiptap/pm/state";
import { Decoration, DecorationSet } from "@tiptap/pm/view"; import { Decoration, DecorationSet } from "@tiptap/pm/view";
import { IAttachment } from "client/src/lib/types";
import { MediaUploadOptions, UploadFn } from "../media-utils"; import { MediaUploadOptions, UploadFn } from "../media-utils";
import { IAttachment } from "../types";
const uploadKey = new PluginKey("image-upload"); const uploadKey = new PluginKey("image-upload");
@@ -36,7 +36,9 @@ export const MathBlock = Node.create({
return { return {
text: { text: {
default: "", default: "",
parseHTML: (element) => element.innerHTML.split("$")[1], parseHTML: (element) => {
return element.innerHTML;
},
}, },
}; };
}, },
@@ -44,7 +46,7 @@ export const MathBlock = Node.create({
parseHTML() { parseHTML() {
return [ return [
{ {
tag: "div", tag: `div[data-type="${this.name}"]`,
getAttrs: (node: HTMLElement) => { getAttrs: (node: HTMLElement) => {
return node.hasAttribute("data-katex") ? {} : false; return node.hasAttribute("data-katex") ? {} : false;
}, },
@@ -55,8 +57,8 @@ export const MathBlock = Node.create({
renderHTML({ HTMLAttributes }) { renderHTML({ HTMLAttributes }) {
return [ return [
"div", "div",
{}, { "data-type": this.name, "data-katex": true },
["div", { "data-katex": true }, `$$${HTMLAttributes.text}$$`], `${HTMLAttributes.text}`,
]; ];
}, },
@@ -37,7 +37,9 @@ export const MathInline = Node.create<MathInlineOption>({
return { return {
text: { text: {
default: "", default: "",
parseHTML: (element) => element.innerHTML.split("$")[1], parseHTML: (element) => {
return element.innerHTML;
},
}, },
}; };
}, },
@@ -45,7 +47,7 @@ export const MathInline = Node.create<MathInlineOption>({
parseHTML() { parseHTML() {
return [ return [
{ {
tag: "span", tag: `span[data-type="${this.name}"]`,
getAttrs: (node: HTMLElement) => { getAttrs: (node: HTMLElement) => {
return node.hasAttribute("data-katex") ? {} : false; return node.hasAttribute("data-katex") ? {} : false;
}, },
@@ -54,7 +56,11 @@ export const MathInline = Node.create<MathInlineOption>({
}, },
renderHTML({ HTMLAttributes }) { renderHTML({ HTMLAttributes }) {
return ["span", { "data-katex": true }, `$${HTMLAttributes.text}$` || {}]; return [
"span",
{ "data-type": this.name, "data-katex": true },
`${HTMLAttributes.text}`,
];
}, },
addNodeView() { addNodeView() {
@@ -1,3 +0,0 @@
import TiptapTableHeader from "@tiptap/extension-table-header";
export const TableHeader = TiptapTableHeader.configure();
@@ -1,4 +1,2 @@
export * from "./table-extension";
export * from "./header";
export * from "./row"; export * from "./row";
export * from "./cell"; export * from "./cell";
@@ -1,7 +0,0 @@
import TiptapTable from "@tiptap/extension-table";
export const Table = TiptapTable.configure({
resizable: true,
lastColumnResizable: false,
allowTableNodeSelection: true,
});
+17
View File
@@ -0,0 +1,17 @@
// repetition for now
export interface IAttachment {
id: string;
fileName: string;
filePath: string;
fileSize: number;
fileExt: string;
mimeType: string;
type: string;
creatorId: string;
pageId: string | null;
spaceId: string | null;
workspaceId: string;
createdAt: string;
updatedAt: string;
deletedAt: string | null;
}
+1 -1
View File
@@ -3,7 +3,7 @@ import { Editor, findParentNode, isTextSelection } from "@tiptap/core";
import { Selection, Transaction } from "@tiptap/pm/state"; import { Selection, Transaction } from "@tiptap/pm/state";
import { CellSelection, TableMap } from "@tiptap/pm/tables"; import { CellSelection, TableMap } from "@tiptap/pm/tables";
import { Node, ResolvedPos } from "@tiptap/pm/model"; import { Node, ResolvedPos } from "@tiptap/pm/model";
import { Table } from "./table/table-extension"; import Table from "@tiptap/extension-table";
export const isRectSelected = (rect: any) => (selection: CellSelection) => { export const isRectSelected = (rect: any) => (selection: CellSelection) => {
const map = TableMap.get(selection.$anchorCell.node(-1)); const map = TableMap.get(selection.$anchorCell.node(-1));
@@ -1,7 +1,7 @@
import { type EditorState, Plugin, PluginKey } from "@tiptap/pm/state"; import { type EditorState, Plugin, PluginKey } from "@tiptap/pm/state";
import { Decoration, DecorationSet } from "@tiptap/pm/view"; import { Decoration, DecorationSet } from "@tiptap/pm/view";
import { IAttachment } from "client/src/lib/types";
import { MediaUploadOptions, UploadFn } from "../media-utils"; import { MediaUploadOptions, UploadFn } from "../media-utils";
import { IAttachment } from "../types";
const uploadKey = new PluginKey("video-upload"); const uploadKey = new PluginKey("video-upload");
+38 -9
View File
@@ -119,6 +119,9 @@ importers:
'@tiptap/extension-youtube': '@tiptap/extension-youtube':
specifier: ^2.5.4 specifier: ^2.5.4
version: 2.5.4(@tiptap/core@2.5.4(@tiptap/pm@2.5.4)) version: 2.5.4(@tiptap/core@2.5.4(@tiptap/pm@2.5.4))
'@tiptap/html':
specifier: ^2.5.4
version: 2.5.4(@tiptap/core@2.5.4(@tiptap/pm@2.5.4))(@tiptap/pm@2.5.4)
'@tiptap/pm': '@tiptap/pm':
specifier: ^2.5.4 specifier: ^2.5.4
version: 2.5.4 version: 2.5.4
@@ -3780,6 +3783,12 @@ packages:
peerDependencies: peerDependencies:
'@tiptap/core': ^2.5.4 '@tiptap/core': ^2.5.4
'@tiptap/html@2.5.4':
resolution: {integrity: sha512-Fcvsa7kkO+Id7WBFimDN5zdHksVGVnyHnffaN/PaAgbKmzP53BC38Pd0XuHS+KL6btqQIFE2GlqNYnyIos7i+g==}
peerDependencies:
'@tiptap/core': ^2.5.4
'@tiptap/pm': ^2.5.4
'@tiptap/pm@2.5.4': '@tiptap/pm@2.5.4':
resolution: {integrity: sha512-oFIsuniptdUXn93x4aM2sVN3hYKo9Fj55zAkYrWhwxFYUYcPxd5ibra2we+wRK5TaiPu098wpC+yMSTZ/KKMpA==} resolution: {integrity: sha512-oFIsuniptdUXn93x4aM2sVN3hYKo9Fj55zAkYrWhwxFYUYcPxd5ibra2we+wRK5TaiPu098wpC+yMSTZ/KKMpA==}
@@ -4757,6 +4766,10 @@ packages:
css-to-mat@1.1.1: css-to-mat@1.1.1:
resolution: {integrity: sha512-kvpxFYZb27jRd2vium35G7q5XZ2WJ9rWjDUMNT36M3Hc41qCrLXFM5iEKMGXcrPsKfXEN+8l/riB4QzwwwiEyQ==} resolution: {integrity: sha512-kvpxFYZb27jRd2vium35G7q5XZ2WJ9rWjDUMNT36M3Hc41qCrLXFM5iEKMGXcrPsKfXEN+8l/riB4QzwwwiEyQ==}
css-what@6.1.0:
resolution: {integrity: sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==}
engines: {node: '>= 6'}
cssesc@3.0.0: cssesc@3.0.0:
resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==}
engines: {node: '>=4'} engines: {node: '>=4'}
@@ -6178,8 +6191,8 @@ packages:
makeerror@1.0.12: makeerror@1.0.12:
resolution: {integrity: sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==} resolution: {integrity: sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==}
markdown-it@14.0.0: markdown-it@14.1.0:
resolution: {integrity: sha512-seFjF0FIcPt4P9U39Bq1JYblX0KZCjDLFFQPHpL5AzHpqPEKtosxmdq/LTVZnjfH7tjt9BxStm+wXcDBNuYmzw==} resolution: {integrity: sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==}
hasBin: true hasBin: true
marked@13.0.2: marked@13.0.2:
@@ -7790,8 +7803,8 @@ packages:
engines: {node: '>=14.17'} engines: {node: '>=14.17'}
hasBin: true hasBin: true
uc.micro@2.0.0: uc.micro@2.1.0:
resolution: {integrity: sha512-DffL94LsNOccVn4hyfRe5rdKa273swqeA5DJpMOeFmEn1wCDc7nAbbB0gXlgBCL7TNzeTv6G7XVWzan7iJtfig==} resolution: {integrity: sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==}
uid2@1.0.0: uid2@1.0.0:
resolution: {integrity: sha512-+I6aJUv63YAcY9n4mQreLUt0d4lvwkkopDNmpomkAUz0fAkEMV9pRWxN0EjhW1YfRhcuyHg2v3mwddCDW1+LFQ==} resolution: {integrity: sha512-+I6aJUv63YAcY9n4mQreLUt0d4lvwkkopDNmpomkAUz0fAkEMV9pRWxN0EjhW1YfRhcuyHg2v3mwddCDW1+LFQ==}
@@ -8160,6 +8173,10 @@ packages:
resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==}
engines: {node: '>=10'} engines: {node: '>=10'}
zeed-dom@0.10.11:
resolution: {integrity: sha512-7ukbu6aQKx34OQ7PfUIxOuAhk2MvyZY/t4/IJsVzy76zuMzfhE74+Dbyp8SHiUJPTPkF0FflP1KVrGJ7gk9IHw==}
engines: {node: '>=14.13.1'}
zod@3.23.8: zod@3.23.8:
resolution: {integrity: sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==} resolution: {integrity: sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==}
@@ -12059,6 +12076,12 @@ snapshots:
dependencies: dependencies:
'@tiptap/core': 2.5.4(@tiptap/pm@2.5.4) '@tiptap/core': 2.5.4(@tiptap/pm@2.5.4)
'@tiptap/html@2.5.4(@tiptap/core@2.5.4(@tiptap/pm@2.5.4))(@tiptap/pm@2.5.4)':
dependencies:
'@tiptap/core': 2.5.4(@tiptap/pm@2.5.4)
'@tiptap/pm': 2.5.4
zeed-dom: 0.10.11
'@tiptap/pm@2.5.4': '@tiptap/pm@2.5.4':
dependencies: dependencies:
prosemirror-changeset: 2.2.1 prosemirror-changeset: 2.2.1
@@ -13272,6 +13295,8 @@ snapshots:
'@daybrush/utils': 1.13.0 '@daybrush/utils': 1.13.0
'@scena/matrix': 1.1.1 '@scena/matrix': 1.1.1
css-what@6.1.0: {}
cssesc@3.0.0: {} cssesc@3.0.0: {}
cssstyle@3.0.0: cssstyle@3.0.0:
@@ -14899,7 +14924,7 @@ snapshots:
linkify-it@5.0.0: linkify-it@5.0.0:
dependencies: dependencies:
uc.micro: 2.0.0 uc.micro: 2.1.0
linkifyjs@4.1.3: {} linkifyjs@4.1.3: {}
@@ -14994,14 +15019,14 @@ snapshots:
dependencies: dependencies:
tmpl: 1.0.5 tmpl: 1.0.5
markdown-it@14.0.0: markdown-it@14.1.0:
dependencies: dependencies:
argparse: 2.0.1 argparse: 2.0.1
entities: 4.5.0 entities: 4.5.0
linkify-it: 5.0.0 linkify-it: 5.0.0
mdurl: 2.0.0 mdurl: 2.0.0
punycode.js: 2.3.1 punycode.js: 2.3.1
uc.micro: 2.0.0 uc.micro: 2.1.0
marked@13.0.2: {} marked@13.0.2: {}
@@ -15690,7 +15715,7 @@ snapshots:
prosemirror-markdown@1.13.0: prosemirror-markdown@1.13.0:
dependencies: dependencies:
markdown-it: 14.0.0 markdown-it: 14.1.0
prosemirror-model: 1.22.1 prosemirror-model: 1.22.1
prosemirror-menu@1.2.4: prosemirror-menu@1.2.4:
@@ -16778,7 +16803,7 @@ snapshots:
typescript@5.5.2: {} typescript@5.5.2: {}
uc.micro@2.0.0: {} uc.micro@2.1.0: {}
uid2@1.0.0: {} uid2@1.0.0: {}
@@ -17101,4 +17126,8 @@ snapshots:
yocto-queue@0.1.0: {} yocto-queue@0.1.0: {}
zeed-dom@0.10.11:
dependencies:
css-what: 6.1.0
zod@3.23.8: {} zod@3.23.8: {}