import { StarterKit } from '@tiptap/starter-kit'; import { TextAlign } from '@tiptap/extension-text-align'; 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, Comment, CustomCodeBlock, Details, DetailsContent, DetailsSummary, LinkExtension, MathBlock, MathInline, TableHeader, TableCell, TableRow, CustomTable, TiptapImage, TiptapVideo, TrailingNode, Attachment, Drawio, Excalidraw, Embed, Mention, Subpages, Highlight, UniqueID, addUniqueIdsToDoc, } from '@docmost/editor-ext'; import { generateText, getSchema, JSONContent } from '@tiptap/core'; 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, Schema } from '@tiptap/pm/model'; import { Logger } from '@nestjs/common'; export const tiptapExtensions = [ StarterKit.configure({ codeBlock: false, link: false, trailingNode: false, heading: false, }), Heading, UniqueID.configure({ types: ['heading', 'paragraph'], }), Comment, TextAlign.configure({ types: ['heading', 'paragraph'] }), TaskList, TaskItem.configure({ nested: true, }), LinkExtension, Superscript, SubScript, Highlight, Typography, TrailingNode, TextStyle, Color, MathInline, MathBlock, Details, DetailsContent, DetailsSummary, CustomTable, TableCell, TableRow, TableHeader, Youtube, TiptapImage, TiptapVideo, Callout, Attachment, CustomCodeBlock, Drawio, Excalidraw, Embed, Mention, Subpages, ] as any; export function jsonToHtml(tiptapJson: any) { return generateHTML(tiptapJson, tiptapExtensions); } export function htmlToJson(html: string) { const pmJson = generateJSON(html, tiptapExtensions); try { return addUniqueIdsToDoc(pmJson, tiptapExtensions); } catch (error) { console.warn('failed to add unique ids to doc', error); return pmJson; } } export function jsonToText(tiptapJson: JSONContent) { return generateText(tiptapJson, tiptapExtensions); } export function jsonToNode(tiptapJson: JSONContent) { const schema = getSchema(tiptapExtensions); try { return Node.fromJSON(schema, tiptapJson); } catch (error) { if ( error instanceof RangeError && error.message.includes('Unknown node type') ) { Logger.warn('Stripping unknown node types from document:', error.message); const cleanedJson = stripUnknownNodes(tiptapJson, schema); return Node.fromJSON(schema, cleanedJson); } throw error; } } export function getPageId(documentName: string) { return documentName.split('.')[1]; } function stripUnknownNodes( json: JSONContent, schema: Schema, ): JSONContent | null { if (!json || typeof json !== 'object') return json; // Recursively clean children first, flattening any unwrapped content if (json.content && Array.isArray(json.content)) { const newContent: JSONContent[] = []; for (const child of json.content) { const cleaned = stripUnknownNodes(child, schema); if (Array.isArray(cleaned)) { newContent.push(...cleaned); } else if (cleaned) { newContent.push(cleaned); } } json.content = newContent; } // Check if this node is unknown AFTER processing children if (json.type && !schema.nodes[json.type]) { // Unwrap: return cleaned children directly instead of wrapping return ( json.content && json.content.length > 0 ? json.content : null ) as any; } return json; }