mirror of
https://github.com/docmost/docmost.git
synced 2026-05-08 23:33:09 +08:00
202 lines
5.1 KiB
TypeScript
202 lines
5.1 KiB
TypeScript
import { jsonToNode } from 'src/collaboration/collaboration.util';
|
|
import { ExportFormat } from './dto/export-dto';
|
|
import { Node } from '@tiptap/pm/model';
|
|
import { validate as isValidUUID } from 'uuid';
|
|
import * as path from 'path';
|
|
import { Page } from '@docmost/db/types/entity.types';
|
|
|
|
export type PageExportTree = Record<string, Page[]>;
|
|
|
|
export const INTERNAL_LINK_REGEX =
|
|
/^(https?:\/\/)?([^\/]+)?(\/s\/([^\/]+)\/)?p\/([a-zA-Z0-9-]+)\/?$/;
|
|
|
|
export function getExportExtension(format: string) {
|
|
if (format === ExportFormat.HTML) {
|
|
return '.html';
|
|
}
|
|
|
|
if (format === ExportFormat.Markdown) {
|
|
return '.md';
|
|
}
|
|
return;
|
|
}
|
|
|
|
export function getPageTitle(title: string) {
|
|
return title ? title : 'untitled';
|
|
}
|
|
|
|
export function getProsemirrorContent(content: any) {
|
|
return (
|
|
content ?? {
|
|
type: 'doc',
|
|
content: [{ type: 'paragraph', attrs: { textAlign: 'left' } }],
|
|
}
|
|
);
|
|
}
|
|
|
|
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 isAttachmentNode(nodeType: string) {
|
|
const attachmentNodeTypes = [
|
|
'attachment',
|
|
'image',
|
|
'video',
|
|
'excalidraw',
|
|
'drawio',
|
|
];
|
|
return attachmentNodeTypes.includes(nodeType);
|
|
}
|
|
|
|
export function updateAttachmentUrlsToLocalPaths(prosemirrorJson: any) {
|
|
const doc = jsonToNode(prosemirrorJson);
|
|
if (!doc) return null;
|
|
|
|
// Helper function to replace specific URL prefixes
|
|
const replacePrefix = (url: string): string => {
|
|
const prefixes = ['/files', '/api/files'];
|
|
for (const prefix of prefixes) {
|
|
if (url.startsWith(prefix)) {
|
|
return url.replace(prefix, 'files');
|
|
}
|
|
}
|
|
return url;
|
|
};
|
|
|
|
doc?.descendants((node: Node) => {
|
|
if (isAttachmentNode(node.type.name)) {
|
|
if (node.attrs.src) {
|
|
// @ts-ignore
|
|
node.attrs.src = replacePrefix(node.attrs.src);
|
|
}
|
|
if (node.attrs.url) {
|
|
// @ts-ignore
|
|
node.attrs.url = replacePrefix(node.attrs.url);
|
|
}
|
|
}
|
|
});
|
|
|
|
return doc.toJSON();
|
|
}
|
|
|
|
export function replaceInternalLinks(
|
|
prosemirrorJson: any,
|
|
slugIdToPath: Record<string, string>,
|
|
currentPagePath: string,
|
|
) {
|
|
const doc = jsonToNode(prosemirrorJson);
|
|
|
|
doc.descendants((node: Node) => {
|
|
for (const mark of node.marks) {
|
|
if (mark.type.name === 'link' && mark.attrs.href) {
|
|
const match = mark.attrs.href.match(INTERNAL_LINK_REGEX);
|
|
if (match) {
|
|
const markLink = mark.attrs.href;
|
|
|
|
const slugId = extractPageSlugId(match[5]);
|
|
const localPath = slugIdToPath[slugId];
|
|
|
|
if (!localPath) {
|
|
continue;
|
|
}
|
|
|
|
const relativePath = computeRelativePath(currentPagePath, localPath);
|
|
|
|
//@ts-expect-error
|
|
mark.attrs.href = relativePath;
|
|
//@ts-expect-error
|
|
mark.attrs.target = '_self';
|
|
if (node.isText) {
|
|
// if link and text are same, use page title
|
|
if (markLink === node.text) {
|
|
//@ts-expect-error
|
|
node.text = getInternalLinkPageName(relativePath);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
return doc.toJSON();
|
|
}
|
|
|
|
export function getInternalLinkPageName(path: string): string {
|
|
return decodeURIComponent(
|
|
path?.split('/').pop().split('.').slice(0, -1).join('.'),
|
|
);
|
|
}
|
|
|
|
export function extractPageSlugId(input: string): string {
|
|
if (!input) {
|
|
return undefined;
|
|
}
|
|
const parts = input.split('-');
|
|
return parts.length > 1 ? parts[parts.length - 1] : input;
|
|
}
|
|
|
|
export function buildTree(pages: Page[]): PageExportTree {
|
|
const tree: PageExportTree = {};
|
|
const titleCount: Record<string, Record<string, number>> = {};
|
|
|
|
for (const page of pages) {
|
|
const parentPageId = page.parentPageId;
|
|
|
|
if (!titleCount[parentPageId]) {
|
|
titleCount[parentPageId] = {};
|
|
}
|
|
|
|
let title = getPageTitle(page.title);
|
|
|
|
if (titleCount[parentPageId][title]) {
|
|
title = `${title} (${titleCount[parentPageId][title]})`;
|
|
titleCount[parentPageId][getPageTitle(page.title)] += 1;
|
|
} else {
|
|
titleCount[parentPageId][title] = 1;
|
|
}
|
|
|
|
page.title = title;
|
|
if (!tree[parentPageId]) {
|
|
tree[parentPageId] = [];
|
|
}
|
|
tree[parentPageId].push(page);
|
|
}
|
|
return tree;
|
|
}
|
|
|
|
export function computeLocalPath(
|
|
tree: PageExportTree,
|
|
format: string,
|
|
parentPageId: string | null,
|
|
currentPath: string,
|
|
slugIdToPath: Record<string, string>,
|
|
) {
|
|
const children = tree[parentPageId] || [];
|
|
|
|
for (const page of children) {
|
|
const title = encodeURIComponent(getPageTitle(page.title));
|
|
const localPath = `${currentPath}${title}`;
|
|
slugIdToPath[page.slugId] = `${localPath}${getExportExtension(format)}`;
|
|
|
|
computeLocalPath(tree, format, page.id, `${localPath}/`, slugIdToPath);
|
|
}
|
|
}
|
|
|
|
function computeRelativePath(from: string, to: string) {
|
|
return path.relative(path.dirname(from), to);
|
|
}
|