mirror of
https://github.com/docmost/docmost.git
synced 2025-11-13 04:22:37 +10:00
Compare commits
15 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 48e76aa9f4 | |||
| 2bd6422a35 | |||
| 407a1aff3b | |||
| b4bc184cb3 | |||
| 109dbdbe02 | |||
| 2df7de5828 | |||
| 373fc86e47 | |||
| 5052a9ea40 | |||
| cd47c79d86 | |||
| 78746938b7 | |||
| 4d2936627c | |||
| d2ecd28047 | |||
| bb92ca75e9 | |||
| 8f3e2ff663 | |||
| 89f6311e46 |
@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "client",
|
||||
"private": true,
|
||||
"version": "0.2.7",
|
||||
"version": "0.2.8",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc && vite build",
|
||||
|
||||
@ -23,7 +23,7 @@ const RoleButton = forwardRef<HTMLButtonElement, RoleButtonProps>(
|
||||
),
|
||||
);
|
||||
|
||||
interface SpaceRoleMenuProps {
|
||||
interface RoleMenuProps {
|
||||
roles: IRoleData[];
|
||||
roleName: string;
|
||||
onChange?: (value: string) => void;
|
||||
@ -35,7 +35,7 @@ export default function RoleSelectMenu({
|
||||
roleName,
|
||||
onChange,
|
||||
disabled,
|
||||
}: SpaceRoleMenuProps) {
|
||||
}: RoleMenuProps) {
|
||||
return (
|
||||
<Menu withArrow>
|
||||
<Menu.Target>
|
||||
|
||||
@ -4,13 +4,14 @@ 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 { Link } from "@tiptap/extension-link";
|
||||
import { Superscript } from "@tiptap/extension-superscript";
|
||||
import SubScript from "@tiptap/extension-subscript";
|
||||
import { Highlight } from "@tiptap/extension-highlight";
|
||||
import { Typography } from "@tiptap/extension-typography";
|
||||
import { TextStyle } from "@tiptap/extension-text-style";
|
||||
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 SlashCommand from "@/features/editor/extensions/slash-command";
|
||||
import { Collaboration } from "@tiptap/extension-collaboration";
|
||||
@ -23,8 +24,6 @@ import {
|
||||
DetailsSummary,
|
||||
MathBlock,
|
||||
MathInline,
|
||||
Table,
|
||||
TableHeader,
|
||||
TableCell,
|
||||
TableRow,
|
||||
TrailingNode,
|
||||
@ -66,7 +65,9 @@ export const mainExtensions = [
|
||||
if (node.type.name === "detailsSummary") {
|
||||
return "Toggle title";
|
||||
}
|
||||
return 'Write anything. Enter "/" for commands';
|
||||
if (node.type.name === "paragraph") {
|
||||
return 'Write anything. Enter "/" for commands';
|
||||
}
|
||||
},
|
||||
includeChildren: true,
|
||||
}),
|
||||
@ -95,10 +96,16 @@ export const mainExtensions = [
|
||||
class: "comment-mark",
|
||||
},
|
||||
}),
|
||||
Table,
|
||||
|
||||
Table.configure({
|
||||
resizable: true,
|
||||
lastColumnResizable: false,
|
||||
allowTableNodeSelection: true,
|
||||
}),
|
||||
TableRow,
|
||||
TableCell,
|
||||
TableHeader,
|
||||
|
||||
MathInline.configure({
|
||||
view: MathInlineView,
|
||||
}),
|
||||
|
||||
@ -56,7 +56,7 @@ export default function PageExportModal({
|
||||
<div>
|
||||
<Text size="md">Format</Text>
|
||||
</div>
|
||||
<ExportFormatSelection onChange={handleChange} />
|
||||
<ExportFormatSelection format={format} onChange={handleChange} />
|
||||
</Group>
|
||||
|
||||
<Group justify="center" mt="md">
|
||||
@ -72,16 +72,17 @@ export default function PageExportModal({
|
||||
}
|
||||
|
||||
interface ExportFormatSelection {
|
||||
format: ExportFormat;
|
||||
onChange: (value: string) => void;
|
||||
}
|
||||
function ExportFormatSelection({ onChange }: ExportFormatSelection) {
|
||||
function ExportFormatSelection({ format, onChange }: ExportFormatSelection) {
|
||||
return (
|
||||
<Select
|
||||
data={[
|
||||
{ value: "markdown", label: "Markdown" },
|
||||
{ value: "html", label: "HTML" },
|
||||
]}
|
||||
defaultValue={ExportFormat.Markdown}
|
||||
defaultValue={format}
|
||||
onChange={onChange}
|
||||
styles={{ wrapper: { maxWidth: 120 } }}
|
||||
comboboxProps={{ width: "120" }}
|
||||
|
||||
@ -84,14 +84,14 @@ function ImportFormatSelection({ spaceId, onClose }: ImportFormatSelection) {
|
||||
}
|
||||
}
|
||||
|
||||
const newTreeNodes = buildTree(pages);
|
||||
const fullTree = treeData.concat(newTreeNodes);
|
||||
if (pages?.length > 0 && pageCount > 0) {
|
||||
const newTreeNodes = buildTree(pages);
|
||||
const fullTree = treeData.concat(newTreeNodes);
|
||||
|
||||
if (newTreeNodes?.length && fullTree?.length > 0) {
|
||||
setTreeData(fullTree);
|
||||
}
|
||||
if (newTreeNodes?.length && fullTree?.length > 0) {
|
||||
setTreeData(fullTree);
|
||||
}
|
||||
|
||||
if (pageCount > 0) {
|
||||
const pageCountText = pageCount === 1 ? "1 page" : `${pageCount} pages`;
|
||||
|
||||
notifications.update({
|
||||
|
||||
@ -11,11 +11,14 @@ import {
|
||||
userRoleData,
|
||||
} from "@/features/workspace/types/user-role-data.ts";
|
||||
import useUserRole from "@/hooks/use-user-role.tsx";
|
||||
import { UserRole } from "@/lib/types.ts";
|
||||
|
||||
export default function WorkspaceMembersTable() {
|
||||
const { data, isLoading } = useWorkspaceMembersQuery({ limit: 100 });
|
||||
const changeMemberRoleMutation = useChangeMemberRoleMutation();
|
||||
const { isAdmin } = useUserRole();
|
||||
const { isAdmin, isOwner } = useUserRole();
|
||||
|
||||
const assignableUserRoles = isOwner ? userRoleData : userRoleData.filter((role) => role.value !== UserRole.OWNER);
|
||||
|
||||
const handleRoleChange = async (
|
||||
userId: string,
|
||||
@ -69,7 +72,7 @@ export default function WorkspaceMembersTable() {
|
||||
|
||||
<Table.Td>
|
||||
<RoleSelectMenu
|
||||
roles={userRoleData}
|
||||
roles={assignableUserRoles}
|
||||
roleName={getUserRoleLabel(user.role)}
|
||||
onChange={(newRole) =>
|
||||
handleRoleChange(user.id, user.role, newRole)
|
||||
|
||||
@ -53,9 +53,10 @@ export function useChangeMemberRoleMutation() {
|
||||
return useMutation<any, Error, any>({
|
||||
mutationFn: (data) => changeMemberRole(data),
|
||||
onSuccess: (data, variables) => {
|
||||
// TODO: change in cache instead
|
||||
notifications.show({ message: "Member role updated successfully" });
|
||||
queryClient.refetchQueries({
|
||||
queryKey: ["workspaceMembers", variables.spaceId],
|
||||
queryKey: ["workspaceMembers"],
|
||||
});
|
||||
},
|
||||
onError: (error) => {
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "server",
|
||||
"version": "0.2.7",
|
||||
"version": "0.2.8",
|
||||
"description": "",
|
||||
"author": "",
|
||||
"private": true,
|
||||
|
||||
@ -10,6 +10,8 @@ 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 Table from '@tiptap/extension-table';
|
||||
import TableHeader from '@tiptap/extension-table-header';
|
||||
import {
|
||||
Callout,
|
||||
Comment,
|
||||
@ -19,16 +21,18 @@ import {
|
||||
LinkExtension,
|
||||
MathBlock,
|
||||
MathInline,
|
||||
Table,
|
||||
TableCell,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
TiptapImage,
|
||||
TiptapVideo,
|
||||
TrailingNode,
|
||||
} from '@docmost/editor-ext';
|
||||
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 = [
|
||||
StarterKit,
|
||||
|
||||
@ -59,7 +59,7 @@ export class AttachmentService {
|
||||
});
|
||||
} catch (err) {
|
||||
// delete uploaded file on error
|
||||
console.error(err);
|
||||
this.logger.error(err);
|
||||
}
|
||||
|
||||
return attachment;
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import {
|
||||
BadRequestException,
|
||||
ForbiddenException,
|
||||
Injectable,
|
||||
NotFoundException,
|
||||
} from '@nestjs/common';
|
||||
@ -217,11 +218,21 @@ export class WorkspaceService {
|
||||
) {
|
||||
const user = await this.userRepo.findById(userRoleDto.userId, workspaceId);
|
||||
|
||||
const newRole = userRoleDto.role.toLowerCase();
|
||||
|
||||
if (!user) {
|
||||
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;
|
||||
}
|
||||
|
||||
@ -238,7 +249,7 @@ export class WorkspaceService {
|
||||
|
||||
await this.userRepo.updateUser(
|
||||
{
|
||||
role: userRoleDto.role,
|
||||
role: newRole,
|
||||
},
|
||||
user.id,
|
||||
workspaceId,
|
||||
|
||||
@ -18,11 +18,11 @@ export function turndown(html: string): string {
|
||||
highlightedCodeBlock,
|
||||
taskList,
|
||||
callout,
|
||||
toggleListTitle,
|
||||
toggleListBody,
|
||||
preserveDetail,
|
||||
listParagraph,
|
||||
mathInline,
|
||||
mathBlock,
|
||||
]);
|
||||
|
||||
return turndownService.turndown(html).replaceAll('<br>', ' ');
|
||||
}
|
||||
|
||||
@ -72,29 +72,51 @@ function taskList(turndownService: TurndownService) {
|
||||
});
|
||||
}
|
||||
|
||||
function toggleListTitle(turndownService: TurndownService) {
|
||||
turndownService.addRule('toggleListTitle', {
|
||||
function preserveDetail(turndownService: TurndownService) {
|
||||
turndownService.addRule('preserveDetail', {
|
||||
filter: function (node: HTMLInputElement) {
|
||||
return (
|
||||
node.nodeName === 'SUMMARY' && node.parentNode.nodeName === 'DETAILS'
|
||||
);
|
||||
return node.nodeName === 'DETAILS';
|
||||
},
|
||||
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) {
|
||||
turndownService.addRule('toggleListContent', {
|
||||
function mathInline(turndownService: TurndownService) {
|
||||
turndownService.addRule('mathInline', {
|
||||
filter: function (node: HTMLInputElement) {
|
||||
return (
|
||||
node.getAttribute('data-type') === 'detailsContent' &&
|
||||
node.parentNode.nodeName === 'DETAILS'
|
||||
node.nodeName === 'SPAN' &&
|
||||
node.getAttribute('data-type') === 'mathInline'
|
||||
);
|
||||
},
|
||||
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';
|
||||
import { FileInterceptor } from '../../common/interceptors/file.interceptor';
|
||||
import * as bytes from 'bytes';
|
||||
import * as path from 'path';
|
||||
import { MAX_FILE_SIZE } from '../../core/attachment/attachment.constants';
|
||||
import { ImportService } from './import.service';
|
||||
import { AuthWorkspace } from '../../common/decorators/auth-workspace.decorator';
|
||||
@ -42,6 +43,8 @@ export class ImportController {
|
||||
@AuthUser() user: User,
|
||||
@AuthWorkspace() workspace: Workspace,
|
||||
) {
|
||||
const validFileExtensions = ['.md', '.html'];
|
||||
|
||||
const maxFileSize = bytes(MAX_FILE_SIZE);
|
||||
|
||||
let file = null;
|
||||
@ -62,6 +65,12 @@ export class ImportController {
|
||||
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;
|
||||
|
||||
if (!spaceId) {
|
||||
|
||||
@ -3,13 +3,17 @@ import { PageRepo } from '@docmost/db/repos/page/page.repo';
|
||||
import { MultipartFile } from '@fastify/multipart';
|
||||
import { sanitize } from 'sanitize-filename-ts';
|
||||
import * as path from 'path';
|
||||
import { htmlToJson } from '../../collaboration/collaboration.util';
|
||||
import { marked } from 'marked';
|
||||
import {
|
||||
htmlToJson,
|
||||
tiptapExtensions,
|
||||
} from '../../collaboration/collaboration.util';
|
||||
import { InjectKysely } from 'nestjs-kysely';
|
||||
import { KyselyDB } from '@docmost/db/types/kysely.types';
|
||||
import { generateSlugId } from '../../common/helpers';
|
||||
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()
|
||||
export class ImportService {
|
||||
@ -36,16 +40,23 @@ export class ImportService {
|
||||
let prosemirrorState = null;
|
||||
let createdPage = null;
|
||||
|
||||
if (fileExtension.endsWith('.md') && fileMimeType === 'text/markdown') {
|
||||
prosemirrorState = await this.processMarkdown(fileContent);
|
||||
}
|
||||
|
||||
if (fileExtension.endsWith('.html') && fileMimeType === 'text/html') {
|
||||
prosemirrorState = await this.processHTML(fileContent);
|
||||
try {
|
||||
if (fileExtension.endsWith('.md') && fileMimeType === 'text/markdown') {
|
||||
prosemirrorState = await this.processMarkdown(fileContent);
|
||||
} else if (
|
||||
fileExtension.endsWith('.html') &&
|
||||
fileMimeType === 'text/html'
|
||||
) {
|
||||
prosemirrorState = await this.processHTML(fileContent);
|
||||
}
|
||||
} catch (err) {
|
||||
const message = 'Error processing file content';
|
||||
this.logger.error(message, err);
|
||||
throw new BadRequestException(message);
|
||||
}
|
||||
|
||||
if (!prosemirrorState) {
|
||||
const message = 'Unsupported file format or mime type';
|
||||
const message = 'Failed to create ProseMirror state';
|
||||
this.logger.error(message);
|
||||
throw new BadRequestException(message);
|
||||
}
|
||||
@ -63,14 +74,19 @@ export class ImportService {
|
||||
slugId: generateSlugId(),
|
||||
title: pageTitle,
|
||||
content: prosemirrorJson,
|
||||
ydoc: await this.createYdoc(prosemirrorJson),
|
||||
position: pagePosition,
|
||||
spaceId: spaceId,
|
||||
creatorId: userId,
|
||||
workspaceId: workspaceId,
|
||||
lastUpdatedById: userId,
|
||||
});
|
||||
|
||||
this.logger.debug(
|
||||
`Successfully imported "${title}${fileExtension}. ID: ${createdPage.id} - SlugId: ${createdPage.slugId}"`,
|
||||
);
|
||||
} catch (err) {
|
||||
const message = 'Failed to create page';
|
||||
const message = 'Failed to create imported page';
|
||||
this.logger.error(message, err);
|
||||
throw new BadRequestException(message);
|
||||
}
|
||||
@ -80,14 +96,37 @@ export class ImportService {
|
||||
}
|
||||
|
||||
async processMarkdown(markdownInput: string): Promise<any> {
|
||||
// turn markdown to html
|
||||
const html = await marked.parse(markdownInput);
|
||||
return await this.processHTML(html);
|
||||
try {
|
||||
const html = await markdownToHtml(markdownInput);
|
||||
return this.processHTML(html);
|
||||
} catch (err) {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
async processHTML(htmlInput: string): Promise<any> {
|
||||
// turn html to prosemirror state
|
||||
return htmlToJson(transformHTML(htmlInput));
|
||||
try {
|
||||
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) {
|
||||
|
||||
@ -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));
|
||||
}
|
||||
36
apps/server/src/integrations/import/utils/marked.utils.ts
Normal file
36
apps/server/src/integrations/import/utils/marked.utils.ts
Normal file
@ -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);
|
||||
}
|
||||
@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "docmost",
|
||||
"homepage": "https://docmost.com",
|
||||
"version": "0.2.7",
|
||||
"version": "0.2.8",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"build": "nx run-many -t build",
|
||||
@ -53,6 +53,7 @@
|
||||
"@tiptap/extension-typography": "^2.5.4",
|
||||
"@tiptap/extension-underline": "^2.5.4",
|
||||
"@tiptap/extension-youtube": "^2.5.4",
|
||||
"@tiptap/html": "^2.5.4",
|
||||
"@tiptap/pm": "^2.5.4",
|
||||
"@tiptap/react": "^2.5.4",
|
||||
"@tiptap/starter-kit": "^2.5.4",
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { type EditorState, Plugin, PluginKey } from "@tiptap/pm/state";
|
||||
import { Decoration, DecorationSet } from "@tiptap/pm/view";
|
||||
import { IAttachment } from "client/src/lib/types";
|
||||
import { MediaUploadOptions, UploadFn } from "../media-utils";
|
||||
import { IAttachment } from "../types";
|
||||
|
||||
const uploadKey = new PluginKey("image-upload");
|
||||
|
||||
|
||||
@ -36,7 +36,9 @@ export const MathBlock = Node.create({
|
||||
return {
|
||||
text: {
|
||||
default: "",
|
||||
parseHTML: (element) => element.innerHTML.split("$")[1],
|
||||
parseHTML: (element) => {
|
||||
return element.innerHTML;
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
@ -44,7 +46,7 @@ export const MathBlock = Node.create({
|
||||
parseHTML() {
|
||||
return [
|
||||
{
|
||||
tag: "div",
|
||||
tag: `div[data-type="${this.name}"]`,
|
||||
getAttrs: (node: HTMLElement) => {
|
||||
return node.hasAttribute("data-katex") ? {} : false;
|
||||
},
|
||||
@ -55,8 +57,8 @@ export const MathBlock = Node.create({
|
||||
renderHTML({ HTMLAttributes }) {
|
||||
return [
|
||||
"div",
|
||||
{},
|
||||
["div", { "data-katex": true }, `$$${HTMLAttributes.text}$$`],
|
||||
{ "data-type": this.name, "data-katex": true },
|
||||
`${HTMLAttributes.text}`,
|
||||
];
|
||||
},
|
||||
|
||||
|
||||
@ -37,7 +37,9 @@ export const MathInline = Node.create<MathInlineOption>({
|
||||
return {
|
||||
text: {
|
||||
default: "",
|
||||
parseHTML: (element) => element.innerHTML.split("$")[1],
|
||||
parseHTML: (element) => {
|
||||
return element.innerHTML;
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
@ -45,7 +47,7 @@ export const MathInline = Node.create<MathInlineOption>({
|
||||
parseHTML() {
|
||||
return [
|
||||
{
|
||||
tag: "span",
|
||||
tag: `span[data-type="${this.name}"]`,
|
||||
getAttrs: (node: HTMLElement) => {
|
||||
return node.hasAttribute("data-katex") ? {} : false;
|
||||
},
|
||||
@ -54,7 +56,11 @@ export const MathInline = Node.create<MathInlineOption>({
|
||||
},
|
||||
|
||||
renderHTML({ HTMLAttributes }) {
|
||||
return ["span", { "data-katex": true }, `$${HTMLAttributes.text}$` || {}];
|
||||
return [
|
||||
"span",
|
||||
{ "data-type": this.name, "data-katex": true },
|
||||
`${HTMLAttributes.text}`,
|
||||
];
|
||||
},
|
||||
|
||||
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 "./cell";
|
||||
|
||||
@ -1,7 +0,0 @@
|
||||
import TiptapTable from "@tiptap/extension-table";
|
||||
|
||||
export const Table = TiptapTable.configure({
|
||||
resizable: true,
|
||||
lastColumnResizable: false,
|
||||
allowTableNodeSelection: true,
|
||||
});
|
||||
17
packages/editor-ext/src/lib/types.ts
Normal file
17
packages/editor-ext/src/lib/types.ts
Normal 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;
|
||||
}
|
||||
@ -3,7 +3,7 @@ import { Editor, findParentNode, isTextSelection } from "@tiptap/core";
|
||||
import { Selection, Transaction } from "@tiptap/pm/state";
|
||||
import { CellSelection, TableMap } from "@tiptap/pm/tables";
|
||||
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) => {
|
||||
const map = TableMap.get(selection.$anchorCell.node(-1));
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { type EditorState, Plugin, PluginKey } from "@tiptap/pm/state";
|
||||
import { Decoration, DecorationSet } from "@tiptap/pm/view";
|
||||
import { IAttachment } from "client/src/lib/types";
|
||||
import { MediaUploadOptions, UploadFn } from "../media-utils";
|
||||
import { IAttachment } from "../types";
|
||||
|
||||
const uploadKey = new PluginKey("video-upload");
|
||||
|
||||
|
||||
47
pnpm-lock.yaml
generated
47
pnpm-lock.yaml
generated
@ -119,6 +119,9 @@ importers:
|
||||
'@tiptap/extension-youtube':
|
||||
specifier: ^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':
|
||||
specifier: ^2.5.4
|
||||
version: 2.5.4
|
||||
@ -3780,6 +3783,12 @@ packages:
|
||||
peerDependencies:
|
||||
'@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':
|
||||
resolution: {integrity: sha512-oFIsuniptdUXn93x4aM2sVN3hYKo9Fj55zAkYrWhwxFYUYcPxd5ibra2we+wRK5TaiPu098wpC+yMSTZ/KKMpA==}
|
||||
|
||||
@ -4757,6 +4766,10 @@ packages:
|
||||
css-to-mat@1.1.1:
|
||||
resolution: {integrity: sha512-kvpxFYZb27jRd2vium35G7q5XZ2WJ9rWjDUMNT36M3Hc41qCrLXFM5iEKMGXcrPsKfXEN+8l/riB4QzwwwiEyQ==}
|
||||
|
||||
css-what@6.1.0:
|
||||
resolution: {integrity: sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==}
|
||||
engines: {node: '>= 6'}
|
||||
|
||||
cssesc@3.0.0:
|
||||
resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==}
|
||||
engines: {node: '>=4'}
|
||||
@ -6178,8 +6191,8 @@ packages:
|
||||
makeerror@1.0.12:
|
||||
resolution: {integrity: sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==}
|
||||
|
||||
markdown-it@14.0.0:
|
||||
resolution: {integrity: sha512-seFjF0FIcPt4P9U39Bq1JYblX0KZCjDLFFQPHpL5AzHpqPEKtosxmdq/LTVZnjfH7tjt9BxStm+wXcDBNuYmzw==}
|
||||
markdown-it@14.1.0:
|
||||
resolution: {integrity: sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==}
|
||||
hasBin: true
|
||||
|
||||
marked@13.0.2:
|
||||
@ -7790,8 +7803,8 @@ packages:
|
||||
engines: {node: '>=14.17'}
|
||||
hasBin: true
|
||||
|
||||
uc.micro@2.0.0:
|
||||
resolution: {integrity: sha512-DffL94LsNOccVn4hyfRe5rdKa273swqeA5DJpMOeFmEn1wCDc7nAbbB0gXlgBCL7TNzeTv6G7XVWzan7iJtfig==}
|
||||
uc.micro@2.1.0:
|
||||
resolution: {integrity: sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==}
|
||||
|
||||
uid2@1.0.0:
|
||||
resolution: {integrity: sha512-+I6aJUv63YAcY9n4mQreLUt0d4lvwkkopDNmpomkAUz0fAkEMV9pRWxN0EjhW1YfRhcuyHg2v3mwddCDW1+LFQ==}
|
||||
@ -8160,6 +8173,10 @@ packages:
|
||||
resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
zeed-dom@0.10.11:
|
||||
resolution: {integrity: sha512-7ukbu6aQKx34OQ7PfUIxOuAhk2MvyZY/t4/IJsVzy76zuMzfhE74+Dbyp8SHiUJPTPkF0FflP1KVrGJ7gk9IHw==}
|
||||
engines: {node: '>=14.13.1'}
|
||||
|
||||
zod@3.23.8:
|
||||
resolution: {integrity: sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==}
|
||||
|
||||
@ -12059,6 +12076,12 @@ snapshots:
|
||||
dependencies:
|
||||
'@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':
|
||||
dependencies:
|
||||
prosemirror-changeset: 2.2.1
|
||||
@ -13272,6 +13295,8 @@ snapshots:
|
||||
'@daybrush/utils': 1.13.0
|
||||
'@scena/matrix': 1.1.1
|
||||
|
||||
css-what@6.1.0: {}
|
||||
|
||||
cssesc@3.0.0: {}
|
||||
|
||||
cssstyle@3.0.0:
|
||||
@ -14899,7 +14924,7 @@ snapshots:
|
||||
|
||||
linkify-it@5.0.0:
|
||||
dependencies:
|
||||
uc.micro: 2.0.0
|
||||
uc.micro: 2.1.0
|
||||
|
||||
linkifyjs@4.1.3: {}
|
||||
|
||||
@ -14994,14 +15019,14 @@ snapshots:
|
||||
dependencies:
|
||||
tmpl: 1.0.5
|
||||
|
||||
markdown-it@14.0.0:
|
||||
markdown-it@14.1.0:
|
||||
dependencies:
|
||||
argparse: 2.0.1
|
||||
entities: 4.5.0
|
||||
linkify-it: 5.0.0
|
||||
mdurl: 2.0.0
|
||||
punycode.js: 2.3.1
|
||||
uc.micro: 2.0.0
|
||||
uc.micro: 2.1.0
|
||||
|
||||
marked@13.0.2: {}
|
||||
|
||||
@ -15690,7 +15715,7 @@ snapshots:
|
||||
|
||||
prosemirror-markdown@1.13.0:
|
||||
dependencies:
|
||||
markdown-it: 14.0.0
|
||||
markdown-it: 14.1.0
|
||||
prosemirror-model: 1.22.1
|
||||
|
||||
prosemirror-menu@1.2.4:
|
||||
@ -16778,7 +16803,7 @@ snapshots:
|
||||
|
||||
typescript@5.5.2: {}
|
||||
|
||||
uc.micro@2.0.0: {}
|
||||
uc.micro@2.1.0: {}
|
||||
|
||||
uid2@1.0.0: {}
|
||||
|
||||
@ -17101,4 +17126,8 @@ snapshots:
|
||||
|
||||
yocto-queue@0.1.0: {}
|
||||
|
||||
zeed-dom@0.10.11:
|
||||
dependencies:
|
||||
css-what: 6.1.0
|
||||
|
||||
zod@3.23.8: {}
|
||||
|
||||
Reference in New Issue
Block a user