Files
docmost/apps/server/src/common/helpers/prosemirror/utils.ts
T
Philip Okugbe 895c1817ae feat: bug fixes (#2084)
* handle enter in inline code

* fix: duplicate comment cache

* track link nodes (backlinks)

* fix en-US translation

* fix internal a-links

* overrides

* 0.71.1
2026-04-05 13:45:36 +01:00

181 lines
4.4 KiB
TypeScript

import { Node } from '@tiptap/pm/model';
import {
jsonToNode,
tiptapExtensions,
} from '../../../collaboration/collaboration.util';
import { validate as isValidUUID } from 'uuid';
import { Transform } from '@tiptap/pm/transform';
import { TiptapTransformer } from '@hocuspocus/transformer';
import * as Y from 'yjs';
import {
INTERNAL_LINK_REGEX,
extractPageSlugId,
} from '../../../integrations/export/utils';
export interface MentionNode {
id: string;
label: string;
entityType: 'user' | 'page';
entityId: string;
creatorId: string;
}
export function extractMentions(prosemirrorJson: any) {
const mentionList: MentionNode[] = [];
const doc = jsonToNode(prosemirrorJson);
doc.descendants((node: Node) => {
if (node.type.name === 'mention') {
if (
node.attrs.id &&
!mentionList.some((mention) => mention.id === node.attrs.id)
) {
mentionList.push({
id: node.attrs.id,
label: node.attrs.label,
entityType: node.attrs.entityType,
entityId: node.attrs.entityId,
creatorId: node.attrs.creatorId,
});
}
}
});
return mentionList;
}
export function extractUserMentions(mentionList: MentionNode[]): MentionNode[] {
const userList = [];
for (const mention of mentionList) {
if (mention.entityType === 'user') {
userList.push(mention);
}
}
return userList as MentionNode[];
}
export function extractPageMentions(mentionList: MentionNode[]): MentionNode[] {
const pageMentionList = [];
for (const mention of mentionList) {
if (
mention.entityType === 'page' &&
!pageMentionList.some(
(pageMention) => pageMention.entityId === mention.entityId,
)
) {
pageMentionList.push(mention);
}
}
return pageMentionList as MentionNode[];
}
export function extractInternalLinkSlugIds(prosemirrorJson: any): string[] {
const slugIds: string[] = [];
const doc = jsonToNode(prosemirrorJson);
doc.descendants((node: Node) => {
for (const mark of node.marks) {
if (mark.type.name === 'link' && mark.attrs.internal && mark.attrs.href) {
const match = mark.attrs.href.match(INTERNAL_LINK_REGEX);
if (match) {
const slugId = extractPageSlugId(match[5]);
if (slugId && !slugIds.includes(slugId)) {
slugIds.push(slugId);
}
}
}
}
});
return slugIds;
}
export function extractUserMentionIdsFromJson(json: any): string[] {
const userIds: string[] = [];
function walk(node: any) {
if (!node) return;
if (
node.type === 'mention' &&
node.attrs?.entityType === 'user' &&
node.attrs?.entityId &&
!userIds.includes(node.attrs.entityId)
) {
userIds.push(node.attrs.entityId);
}
if (Array.isArray(node.content)) {
for (const child of node.content) {
walk(child);
}
}
}
walk(json);
return userIds;
}
export function getProsemirrorContent(content: any) {
return (
content ?? {
type: 'doc',
content: [{ type: 'paragraph', attrs: { textAlign: 'left' } }],
}
);
}
export function isAttachmentNode(nodeType: string) {
const attachmentNodeTypes = [
'attachment',
'image',
'video',
'audio',
'pdf',
'excalidraw',
'drawio',
];
return attachmentNodeTypes.includes(nodeType);
}
export function getAttachmentIds(prosemirrorJson: any) {
const doc = jsonToNode(prosemirrorJson);
const attachmentIds = [];
doc?.descendants((node: Node) => {
if (isAttachmentNode(node.type.name)) {
if (node.attrs.attachmentId && isValidUUID(node.attrs.attachmentId)) {
if (!attachmentIds.includes(node.attrs.attachmentId)) {
attachmentIds.push(node.attrs.attachmentId);
}
}
}
});
return attachmentIds;
}
export function removeMarkTypeFromDoc(doc: Node, markName: string): Node {
const { schema } = doc.type;
const markType = schema.marks[markName];
if (!markType) {
return doc;
}
const tr = new Transform(doc).removeMark(0, doc.content.size, markType);
return tr.doc;
}
export function createYdocFromJson(prosemirrorJson: any): Buffer | null {
if (prosemirrorJson) {
const ydoc = TiptapTransformer.toYdoc(
prosemirrorJson,
'default',
tiptapExtensions,
);
Y.encodeStateAsUpdate(ydoc);
return Buffer.from(Y.encodeStateAsUpdate(ydoc));
}
return null;
}