From ec16657cbb7cab862122f5f9835d633d34af4654 Mon Sep 17 00:00:00 2001 From: suyao Date: Fri, 24 Oct 2025 14:15:48 +0800 Subject: [PATCH] Upgrade TipTap to v3.7.2 and add text highlight feature - Update all TipTap packages from v3.2.0 to v3.7.2 including core extensions and dependencies - Add new highlight extension with Markdown support using ==text== syntax - Replace custom markdown converter with TipTap's built-in Markdown extension - Simplify link handling by using standard TipTap link extension instead of enhancedLink - Add view menu to app menu service for better Electron app navigation --- package.json | 31 +- packages/extension-table-plus/package.json | 4 +- src/main/services/AppMenuService.ts | 3 + .../src/components/RichEditor/command.ts | 19 +- .../RichEditor/extensions/enhanced-link.ts | 29 +- .../RichEditor/extensions/hightlight.ts | 73 ++ .../src/components/RichEditor/index.tsx | 28 +- .../src/components/RichEditor/toolbar.tsx | 5 +- .../src/components/RichEditor/types.ts | 5 +- .../components/RichEditor/useRichEditor.ts | 145 +--- src/renderer/src/i18n/locales/en-us.json | 1 + src/renderer/src/i18n/locales/zh-cn.json | 1 + .../utils/__tests__/markdownConverter.test.ts | 554 -------------- src/renderer/src/utils/markdownConverter.ts | 693 +----------------- yarn.lock | 541 +++++++------- 15 files changed, 451 insertions(+), 1681 deletions(-) create mode 100644 src/renderer/src/components/RichEditor/extensions/hightlight.ts delete mode 100644 src/renderer/src/utils/__tests__/markdownConverter.test.ts diff --git a/package.json b/package.json index 9c4398cdc2..fdff8fb99c 100644 --- a/package.json +++ b/package.json @@ -168,21 +168,22 @@ "@testing-library/jest-dom": "^6.6.3", "@testing-library/react": "^16.3.0", "@testing-library/user-event": "^14.6.1", - "@tiptap/extension-collaboration": "^3.2.0", - "@tiptap/extension-drag-handle": "patch:@tiptap/extension-drag-handle@npm%3A3.2.0#~/.yarn/patches/@tiptap-extension-drag-handle-npm-3.2.0-5a9ebff7c9.patch", - "@tiptap/extension-drag-handle-react": "^3.2.0", - "@tiptap/extension-image": "^3.2.0", - "@tiptap/extension-list": "^3.2.0", - "@tiptap/extension-mathematics": "^3.2.0", - "@tiptap/extension-mention": "^3.2.0", - "@tiptap/extension-node-range": "^3.2.0", - "@tiptap/extension-table-of-contents": "^3.2.0", - "@tiptap/extension-typography": "^3.2.0", - "@tiptap/extension-underline": "^3.2.0", - "@tiptap/pm": "^3.2.0", - "@tiptap/react": "^3.2.0", - "@tiptap/starter-kit": "^3.2.0", - "@tiptap/suggestion": "^3.2.0", + "@tiptap/extension-collaboration": "^3.7.2", + "@tiptap/extension-drag-handle": "^3.7.2", + "@tiptap/extension-drag-handle-react": "^3.7.2", + "@tiptap/extension-image": "^3.7.2", + "@tiptap/extension-list": "^3.7.2", + "@tiptap/extension-mathematics": "^3.7.2", + "@tiptap/extension-mention": "^3.7.2", + "@tiptap/extension-node-range": "^3.7.2", + "@tiptap/extension-table-of-contents": "^3.7.2", + "@tiptap/extension-typography": "^3.7.2", + "@tiptap/extension-underline": "^3.7.2", + "@tiptap/markdown": "^3.7.2", + "@tiptap/pm": "^3.7.2", + "@tiptap/react": "^3.7.2", + "@tiptap/starter-kit": "^3.7.2", + "@tiptap/suggestion": "^3.7.2", "@tiptap/y-tiptap": "^3.0.0", "@truto/turndown-plugin-gfm": "^1.0.2", "@tryfabric/martian": "^1.2.4", diff --git a/packages/extension-table-plus/package.json b/packages/extension-table-plus/package.json index 9601c1f8b8..6ffc9e9af2 100755 --- a/packages/extension-table-plus/package.json +++ b/packages/extension-table-plus/package.json @@ -68,8 +68,8 @@ ], "devDependencies": { "@biomejs/biome": "2.2.4", - "@tiptap/core": "^3.2.0", - "@tiptap/pm": "^3.2.0", + "@tiptap/core": "^3.7.2", + "@tiptap/pm": "^3.7.2", "eslint": "^9.22.0", "eslint-plugin-react-hooks": "^5.2.0", "eslint-plugin-simple-import-sort": "^12.1.1", diff --git a/src/main/services/AppMenuService.ts b/src/main/services/AppMenuService.ts index 0e6cdf27ea..b9ea848a34 100644 --- a/src/main/services/AppMenuService.ts +++ b/src/main/services/AppMenuService.ts @@ -38,6 +38,9 @@ export class AppMenuService { { role: 'editMenu' }, + { + role: 'viewMenu' + }, { role: 'windowMenu' } diff --git a/src/renderer/src/components/RichEditor/command.ts b/src/renderer/src/components/RichEditor/command.ts index 1371b3ebb6..6e5e845119 100644 --- a/src/renderer/src/components/RichEditor/command.ts +++ b/src/renderer/src/components/RichEditor/command.ts @@ -14,6 +14,7 @@ import { Heading1, Heading2, Heading3, + Highlighter, Image, Italic, Link, @@ -193,6 +194,20 @@ const DEFAULT_COMMANDS: Command[] = [ toolbarGroup: 'formatting', formattingCommand: 'strike' }, + { + id: 'highlight', + title: 'Highlight', + description: 'Highlight text', + category: CommandCategory.TEXT, + icon: Highlighter, + keywords: ['highlight', 'marker', 'background'], + handler: (editor: Editor) => { + editor.chain().focus().toggleHighlight().run() + }, + showInToolbar: true, + toolbarGroup: 'formatting', + formattingCommand: 'highlight' + }, { id: 'inlineCode', title: 'Inline Code', @@ -348,11 +363,11 @@ const DEFAULT_COMMANDS: Command[] = [ id: 'link', title: 'Link', description: 'Add a link', - category: CommandCategory.SPECIAL, + category: CommandCategory.TEXT, icon: Link, keywords: ['link', 'url', 'href'], handler: (editor: Editor) => { - editor.chain().focus().setEnhancedLink({ href: '' }).run() + editor.chain().focus().setLink({ href: '' }).run() }, showInToolbar: true, toolbarGroup: 'media', diff --git a/src/renderer/src/components/RichEditor/extensions/enhanced-link.ts b/src/renderer/src/components/RichEditor/extensions/enhanced-link.ts index b3832112f0..7273d26557 100644 --- a/src/renderer/src/components/RichEditor/extensions/enhanced-link.ts +++ b/src/renderer/src/components/RichEditor/extensions/enhanced-link.ts @@ -105,12 +105,7 @@ const createLinkHoverPlugin = (options: LinkHoverPluginOptions) => { const $pos = view.state.doc.resolve(pos) // Find the link mark at this position - const linkMark = $pos - .marks() - .find( - (mark) => - (mark.type.name === 'enhancedLink' || mark.type.name === 'link') && mark.attrs.href === href - ) + const linkMark = $pos.marks().find((mark) => mark.type.name === 'link' && mark.attrs.href === href) if (linkMark) { // Use ProseMirror's mark range finding @@ -153,12 +148,7 @@ const createLinkHoverPlugin = (options: LinkHoverPluginOptions) => { const startPos = view.posAtDOM(linkElement, 0) if (startPos >= 0) { const $pos = view.state.doc.resolve(startPos) - const linkMark = $pos - .marks() - .find( - (mark) => - (mark.type.name === 'enhancedLink' || mark.type.name === 'link') && mark.attrs.href === href - ) + const linkMark = $pos.marks().find((mark) => mark.type.name === 'link' && mark.attrs.href === href) if (linkMark) { const range = getMarkRange($pos, linkMark.type, linkMark.attrs) @@ -235,7 +225,7 @@ const createLinkAutoUpdatePlugin = () => { newState.doc.descendants((node, pos) => { if (node.isText && node.marks) { node.marks.forEach((mark) => { - if (mark.type.name === 'enhancedLink') { + if (mark.type.name === 'link') { const text = node.text || '' const currentHref = mark.attrs.href || '' @@ -280,16 +270,7 @@ const createLinkAutoUpdatePlugin = () => { }) } -declare module '@tiptap/core' { - interface Commands { - enhancedLink: { - setEnhancedLink: (attributes: { href: string; title?: string }) => ReturnType - toggleEnhancedLink: (attributes: { href: string; title?: string }) => ReturnType - unsetEnhancedLink: () => ReturnType - updateLinkText: (text: string) => ReturnType - } - } -} +// Commands are inherited from the parent Link extension, no need to redeclare export interface EnhancedLinkOptions { onLinkHover?: ( @@ -304,7 +285,7 @@ export interface EnhancedLinkOptions { } export const EnhancedLink = Link.extend({ - name: 'enhancedLink', + name: 'link', // Use 'link' instead of 'enhancedLink' to be compatible with Markdown extension addOptions() { return { diff --git a/src/renderer/src/components/RichEditor/extensions/hightlight.ts b/src/renderer/src/components/RichEditor/extensions/hightlight.ts new file mode 100644 index 0000000000..6ad09df5f5 --- /dev/null +++ b/src/renderer/src/components/RichEditor/extensions/hightlight.ts @@ -0,0 +1,73 @@ +import { Mark } from '@tiptap/core' + +declare module '@tiptap/core' { + interface Commands { + highlight: { + toggleHighlight: () => ReturnType + } + } +} + +export const Highlight = Mark.create({ + name: 'highlight', + + addOptions() { + return { + HTMLAttributes: {} + } + }, + + parseHTML() { + return [{ tag: 'mark' }] + }, + + renderHTML({ HTMLAttributes }) { + return ['mark', HTMLAttributes, 0] + }, + + // define a custom Markdown tokenizer to recognize ==text== + markdownTokenizer: { + name: 'highlight', + level: 'inline', // inline element + // fast hint for the lexer to find candidate positions + start: (src) => src.indexOf('=='), + tokenize: (src, tokens, lexer) => { + // Match ==text== at the start of the remaining source + const match = /^==([^=]+)==/.exec(src) + if (!match) return undefined + + return { + type: 'highlight', // token type (must match name) + raw: match[0], // full matched string: ==text== + text: match[1], // inner content: text + // Let the Markdown lexer process nested inline formatting + tokens: lexer.inlineTokens(match[1]) + } + } + }, + + // Parse Markdown token to Tiptap JSON + parseMarkdown: (token, helpers) => { + // Parse nested inline tokens into Tiptap inline content + const content = helpers.parseInline(token.tokens || []) + // Apply the 'highlight' mark to the parsed content + return helpers.applyMark('highlight', content) + }, + + // Render Tiptap node back to Markdown + renderMarkdown: (node, helpers) => { + const content = helpers.renderChildren(node.content || []) + // Wrap serialized children in == delimiters + return `==${content}==` + }, + + addCommands() { + return { + toggleHighlight: + () => + ({ commands }) => { + return commands.toggleMark(this.name) + } + } + } +}) diff --git a/src/renderer/src/components/RichEditor/index.tsx b/src/renderer/src/components/RichEditor/index.tsx index 793ccda1ae..0d93330f47 100644 --- a/src/renderer/src/components/RichEditor/index.tsx +++ b/src/renderer/src/components/RichEditor/index.tsx @@ -212,7 +212,6 @@ const RichEditor = ({ tableOfContentsItems, linkEditor, setMarkdown, - setHtml, clear, getPreviewText } = useRichEditor({ @@ -419,8 +418,8 @@ const RichEditor = ({ const { from, to, $from } = selection // 如果当前已经是链接,则取消链接 - if (editor.isActive('enhancedLink')) { - editor.chain().focus().unsetEnhancedLink().run() + if (editor.isActive('link')) { + editor.chain().focus().unsetLink().run() } else { // 获取当前段落的文本内容 if (from !== to) { @@ -429,7 +428,7 @@ const RichEditor = ({ const url = selectedText.trim().startsWith('http') ? selectedText.trim() : `https://${selectedText.trim()}` - editor.chain().focus().setTextSelection({ from, to }).setEnhancedLink({ href: url }).run() + editor.chain().focus().setTextSelection({ from, to }).setLink({ href: url }).run() } } else { const paragraphText = $from.parent.textContent @@ -444,13 +443,13 @@ const RichEditor = ({ const { $from } = selection const start = $from.start() const end = $from.end() - editor.chain().focus().setTextSelection({ from: start, to: end }).setEnhancedLink({ href: url }).run() + editor.chain().focus().setTextSelection({ from: start, to: end }).setLink({ href: url }).run() } catch (error) { - logger.warn('Failed to set enhanced link:', error as Error) - editor.chain().focus().toggleEnhancedLink({ href: '' }).run() + logger.warn('Failed to set link:', error as Error) + editor.chain().focus().toggleLink({ href: '' }).run() } } else { - editor.chain().focus().toggleEnhancedLink({ href: '' }).run() + editor.chain().focus().toggleLink({ href: '' }).run() } } } @@ -476,6 +475,10 @@ const RichEditor = ({ break case 'taskList': editor.chain().focus().toggleTaskList().run() + break + case 'highlight': + editor.chain().focus().toggleHighlight().run() + break } }, [editor] @@ -489,10 +492,7 @@ const RichEditor = ({ getHtml: () => html, getMarkdown: () => markdown, setContent: (content: string) => { - editor?.commands.setContent(content) - }, - setHtml: (htmlContent: string) => { - setHtml(htmlContent) + editor?.commands.setContent(content, { contentType: 'markdown' }) }, setMarkdown: (markdownContent: string) => { setMarkdown(markdownContent) @@ -513,7 +513,7 @@ const RichEditor = ({ } }, getPreviewText: (maxLength?: number) => { - return getPreviewText(markdown, maxLength) + return getPreviewText(maxLength) }, getScrollTop: () => { return scrollContainerRef.current?.scrollTop ?? 0 @@ -548,7 +548,7 @@ const RichEditor = ({ getAllCommands, getToolbarCommands }), - [editor, html, markdown, setHtml, setMarkdown, clear, getPreviewText] + [editor, html, markdown, setMarkdown, clear, getPreviewText] ) return ( diff --git a/src/renderer/src/components/RichEditor/toolbar.tsx b/src/renderer/src/components/RichEditor/toolbar.tsx index ebed37349e..e6e31e79a3 100644 --- a/src/renderer/src/components/RichEditor/toolbar.tsx +++ b/src/renderer/src/components/RichEditor/toolbar.tsx @@ -71,7 +71,8 @@ const getTooltipText = (t: TFunction, command: FormattingCommand): string => { table: t('richEditor.toolbar.table'), image: t('richEditor.toolbar.image'), blockMath: t('richEditor.toolbar.blockMath'), - inlineMath: t('richEditor.toolbar.inlineMath') + inlineMath: t('richEditor.toolbar.inlineMath'), + highlight: t('richEditor.toolbar.highlight') } return tooltipMap[command] || command @@ -300,6 +301,8 @@ function getFormattingState(state: FormattingState, command: FormattingCommand): return state?.isMath || false case 'inlineMath': return state?.isInlineMath || false + case 'highlight': + return state?.isHighlight || false default: return false } diff --git a/src/renderer/src/components/RichEditor/types.ts b/src/renderer/src/components/RichEditor/types.ts index 15727e1a8d..5e2bdc2fe3 100644 --- a/src/renderer/src/components/RichEditor/types.ts +++ b/src/renderer/src/components/RichEditor/types.ts @@ -93,8 +93,6 @@ export interface RichEditorRef { getMarkdown: () => string /** Set editor content (plain text) */ setContent: (content: string) => void - /** Set editor HTML content */ - setHtml: (html: string) => void /** Set editor Markdown content */ setMarkdown: (markdown: string) => void /** Focus the editor */ @@ -197,6 +195,8 @@ export interface FormattingState { canMath: boolean /** Whether taskList is active */ isTaskList: boolean + /** Whether highlight is active */ + isHighlight: boolean } export type FormattingCommand = @@ -225,6 +225,7 @@ export type FormattingCommand = | 'table' | 'taskList' | 'image' + | 'highlight' export interface ToolbarProps { /** Editor instance ref */ diff --git a/src/renderer/src/components/RichEditor/useRichEditor.ts b/src/renderer/src/components/RichEditor/useRichEditor.ts index cc73892deb..5285b7b6ea 100644 --- a/src/renderer/src/components/RichEditor/useRichEditor.ts +++ b/src/renderer/src/components/RichEditor/useRichEditor.ts @@ -5,12 +5,6 @@ import { loggerService } from '@logger' import { MARKDOWN_SOURCE_LINE_ATTR } from '@renderer/components/RichEditor/constants' import type { FormattingState } from '@renderer/components/RichEditor/types' import { useCodeStyle } from '@renderer/context/CodeStyleProvider' -import { - htmlToMarkdown, - isMarkdownContent, - markdownToHtml, - markdownToPreviewText -} from '@renderer/utils/markdownConverter' import type { Editor } from '@tiptap/core' import { Extension } from '@tiptap/core' import { TaskItem, TaskList } from '@tiptap/extension-list' @@ -22,8 +16,9 @@ import { TableOfContents } from '@tiptap/extension-table-of-contents' import Typography from '@tiptap/extension-typography' +import { Markdown } from '@tiptap/markdown' import { useEditor, useEditorState } from '@tiptap/react' -import { StarterKit } from '@tiptap/starter-kit' +import StarterKit from '@tiptap/starter-kit' import { t } from 'i18next' import { useCallback, useEffect, useMemo, useState } from 'react' @@ -32,6 +27,7 @@ import { CodeBlockShiki } from './extensions/code-block-shiki/code-block-shiki' import { EnhancedImage } from './extensions/enhanced-image' import { EnhancedLink } from './extensions/enhanced-link' import { EnhancedMath } from './extensions/enhanced-math' +import { Highlight } from './extensions/hightlight' import { Placeholder } from './extensions/placeholder' import { YamlFrontMatter } from './extensions/yaml-front-matter' import { blobToArrayBuffer, compressImage, shouldCompressImage } from './helpers/imageUtils' @@ -105,8 +101,6 @@ export interface UseRichEditorReturn { html: string /** Preview text for display */ previewText: string - /** Whether content is detected as markdown */ - isMarkdown: boolean /** Whether editor is disabled */ disabled: boolean /** Current formatting state from TipTap editor */ @@ -125,19 +119,11 @@ export interface UseRichEditorReturn { /** Set markdown content */ setMarkdown: (content: string) => void - /** Set HTML content (converts to markdown) */ - setHtml: (html: string) => void /** Clear all content */ clear: () => void - /** Convert markdown to HTML */ - toHtml: (markdown: string) => string - /** Convert markdown to safe HTML */ - toSafeHtml: (markdown: string) => string - /** Convert HTML to markdown */ - toMarkdown: (html: string) => string /** Get preview text from markdown */ - getPreviewText: (markdown: string, maxLength?: number) => string + getPreviewText: (maxLength?: number) => string } /** @@ -162,20 +148,6 @@ export const useRichEditor = (options: UseRichEditorOptions = {}): UseRichEditor const [markdown, setMarkdownState] = useState(initialContent) - const html = useMemo(() => { - if (!markdown) return '' - return markdownToHtml(markdown) - }, [markdown]) - - const previewText = useMemo(() => { - if (!markdown) return '' - return markdownToPreviewText(markdown, previewLength) - }, [markdown, previewLength]) - - const isMarkdown = useMemo(() => { - return isMarkdownContent(markdown) - }, [markdown]) - // Get theme and language mapping from CodeStyleProvider const { activeShikiTheme } = useCodeStyle() @@ -223,6 +195,10 @@ export const useRichEditor = (options: UseRichEditorOptions = {}): UseRichEditor // TipTap editor extensions const extensions = useMemo( () => [ + Markdown.configure({ + markedOptions: { gfm: true }, + html: true // Enable HTML support to preserve tags for underline + }), SourceLineAttribute, StarterKit.configure({ heading: { @@ -230,6 +206,7 @@ export const useRichEditor = (options: UseRichEditorOptions = {}): UseRichEditor }, codeBlock: false, link: false + // underline is enabled by default in StarterKit }), EnhancedLink.configure({ onLinkHover: handleLinkHover, @@ -380,7 +357,8 @@ export const useRichEditor = (options: UseRichEditorOptions = {}): UseRichEditor TaskList, TaskItem.configure({ nested: true - }) + }), + Highlight ], // eslint-disable-next-line react-hooks/exhaustive-deps [placeholder, activeShikiTheme, handleLinkHover, handleLinkHoverEnd] @@ -389,7 +367,8 @@ export const useRichEditor = (options: UseRichEditorOptions = {}): UseRichEditor const editor = useEditor({ shouldRerenderOnTransaction: true, extensions, - content: html || '', + content: markdown || '', + contentType: 'markdown', editable: editable, editorProps: { handlePaste: (view, event) => { @@ -446,9 +425,7 @@ export const useRichEditor = (options: UseRichEditorOptions = {}): UseRichEditor return true } - // Otherwise (at start of paragraph, or multi-line paste): convert markdown to HTML - const html = markdownToHtml(text) - editor.commands.insertContent(html) + editor.commands.insertContent(text, { contentType: 'markdown' }) onPaste?.(html) return true } @@ -466,10 +443,10 @@ export const useRichEditor = (options: UseRichEditorOptions = {}): UseRichEditor onUpdate: ({ editor }) => { const content = editor.getText() const htmlContent = editor.getHTML() + const markdownContent = editor.getMarkdown() try { - const convertedMarkdown = htmlToMarkdown(htmlContent) - setMarkdownState(convertedMarkdown) - onChange?.(convertedMarkdown) + setMarkdownState(markdownContent) + onChange?.(markdownContent) onContentChange?.(content) if (onHtmlChange) { @@ -492,6 +469,9 @@ export const useRichEditor = (options: UseRichEditorOptions = {}): UseRichEditor } }) + const html = editor.getHTML() + const previewText = editor.getText().slice(0, previewLength) + // Handle image paste function const handleImagePaste = useCallback( async (file: File) => { @@ -579,7 +559,7 @@ export const useRichEditor = (options: UseRichEditorOptions = {}): UseRichEditor .setTextSelection({ from: linkRange.from, to: linkRange.to }) .insertContent(text) .setTextSelection({ from: linkRange.from, to: linkRange.from + text.length }) - .setEnhancedLink({ href }) + .setLink({ href }) .run() } setLinkEditorState({ @@ -599,11 +579,11 @@ export const useRichEditor = (options: UseRichEditorOptions = {}): UseRichEditor if (linkRange) { // Use a more reliable method - directly remove the mark from the range const tr = editor.state.tr - tr.removeMark(linkRange.from, linkRange.to, editor.schema.marks.enhancedLink || editor.schema.marks.link) + tr.removeMark(linkRange.from, linkRange.to, editor.schema.marks.link) editor.view.dispatch(tr) } else { // No explicit range - try to extend current mark range and remove - editor.chain().focus().extendMarkRange('enhancedLink').unsetEnhancedLink().run() + editor.chain().focus().extendMarkRange('link').unsetLink().run() } // Close link editor @@ -727,7 +707,8 @@ export const useRichEditor = (options: UseRichEditorOptions = {}): UseRichEditor isMath: false, isInlineMath: false, canMath: false, - isTaskList: false + isTaskList: false, + isHighlight: false } } @@ -754,9 +735,9 @@ export const useRichEditor = (options: UseRichEditorOptions = {}): UseRichEditor isOrderedList: editor.isActive('orderedList') ?? false, isCodeBlock: editor.isActive('codeBlock') ?? false, isBlockquote: editor.isActive('blockquote') ?? false, - isLink: (editor.isActive('enhancedLink') || editor.isActive('link')) ?? false, - canLink: editor.can().chain().setEnhancedLink({ href: '' }).run() ?? false, - canUnlink: editor.can().chain().unsetEnhancedLink().run() ?? false, + isLink: editor.isActive('link') ?? false, + canLink: editor.can().chain().setLink({ href: '' }).run() ?? false, + canUnlink: editor.can().chain().unsetLink().run() ?? false, canUndo: editor.can().chain().undo().run() ?? false, canRedo: editor.can().chain().redo().run() ?? false, isTable: editor.isActive('table') ?? false, @@ -765,7 +746,8 @@ export const useRichEditor = (options: UseRichEditorOptions = {}): UseRichEditor isMath: editor.isActive('blockMath') ?? false, isInlineMath: editor.isActive('inlineMath') ?? false, canMath: true, - isTaskList: editor.isActive('taskList') ?? false + isTaskList: editor.isActive('taskList') ?? false, + isHighlight: editor.isActive('highlight') ?? false } } }) @@ -776,33 +758,12 @@ export const useRichEditor = (options: UseRichEditorOptions = {}): UseRichEditor setMarkdownState(content) onChange?.(content) - const convertedHtml = markdownToHtml(content) - - editor.commands.setContent(convertedHtml) - - onHtmlChange?.(convertedHtml) + editor.commands.setContent(content, { contentType: 'markdown' }) } catch (error) { logger.error('Error setting markdown content:', error as Error) } }, - [editor.commands, onChange, onHtmlChange] - ) - - const setHtml = useCallback( - (htmlContent: string) => { - try { - const convertedMarkdown = htmlToMarkdown(htmlContent) - setMarkdownState(convertedMarkdown) - onChange?.(convertedMarkdown) - - editor.commands.setContent(htmlContent) - - onHtmlChange?.(htmlContent) - } catch (error) { - logger.error('Error setting HTML content:', error as Error) - } - }, - [editor.commands, onChange, onHtmlChange] + [editor.commands, onChange] ) const clear = useCallback(() => { @@ -811,55 +772,25 @@ export const useRichEditor = (options: UseRichEditorOptions = {}): UseRichEditor onHtmlChange?.('') }, [onChange, onHtmlChange]) - // Utility methods - const toHtml = useCallback((content: string): string => { - try { - return markdownToHtml(content) - } catch (error) { - logger.error('Error converting markdown to HTML:', error as Error) - return '' - } - }, []) - - const toSafeHtml = useCallback((content: string): string => { - try { - return markdownToHtml(content) - } catch (error) { - logger.error('Error converting markdown to safe HTML:', error as Error) - return '' - } - }, []) - - const toMarkdown = useCallback((htmlContent: string): string => { - try { - return htmlToMarkdown(htmlContent) - } catch (error) { - logger.error('Error converting HTML to markdown:', error as Error) - return '' - } - }, []) - const getPreviewText = useCallback( - (content: string, maxLength?: number): string => { + (maxLength?: number): string => { try { - return markdownToPreviewText(content, maxLength || previewLength) + return editor.getText().slice(0, maxLength || previewLength) } catch (error) { logger.error('Error generating preview text:', error as Error) return '' } }, - [previewLength] + [editor, previewLength] ) return { // Editor instance editor, - // State markdown, html, previewText, - isMarkdown, disabled: !editable, formattingState, tableOfContentsItems, @@ -874,13 +805,7 @@ export const useRichEditor = (options: UseRichEditorOptions = {}): UseRichEditor // Actions setMarkdown, - setHtml, clear, - - // Utilities - toHtml, - toSafeHtml, - toMarkdown, getPreviewText } } diff --git a/src/renderer/src/i18n/locales/en-us.json b/src/renderer/src/i18n/locales/en-us.json index ab3bedef6a..5daf12d03c 100644 --- a/src/renderer/src/i18n/locales/en-us.json +++ b/src/renderer/src/i18n/locales/en-us.json @@ -2599,6 +2599,7 @@ "heading4": "Heading 4", "heading5": "Heading 5", "heading6": "Heading 6", + "highlight": "Highlight", "image": "Image", "inlineMath": "Inline Equation", "italic": "Italic", diff --git a/src/renderer/src/i18n/locales/zh-cn.json b/src/renderer/src/i18n/locales/zh-cn.json index b477e075c6..7491ef8d02 100644 --- a/src/renderer/src/i18n/locales/zh-cn.json +++ b/src/renderer/src/i18n/locales/zh-cn.json @@ -2599,6 +2599,7 @@ "heading4": "四级标题", "heading5": "五级标题", "heading6": "六级标题", + "hightlight": "高亮", "image": "图片", "inlineMath": "行内数学公式", "italic": "斜体", diff --git a/src/renderer/src/utils/__tests__/markdownConverter.test.ts b/src/renderer/src/utils/__tests__/markdownConverter.test.ts deleted file mode 100644 index 25a330b114..0000000000 --- a/src/renderer/src/utils/__tests__/markdownConverter.test.ts +++ /dev/null @@ -1,554 +0,0 @@ -import { MARKDOWN_SOURCE_LINE_ATTR } from '@renderer/components/RichEditor/constants' -import { describe, expect, it } from 'vitest' - -import { htmlToMarkdown, markdownToHtml } from '../markdownConverter' - -/** - * Strip markdown line number attributes for testing HTML structure - */ - -const LINE_NUMBER_REGEX = new RegExp(`\\s*${MARKDOWN_SOURCE_LINE_ATTR.replace(/-/g, '\\-')}="\\d+"`, 'g') - -function stripLineNumbers(html: string): string { - return html.replace(LINE_NUMBER_REGEX, '') -} - -describe('markdownConverter', () => { - describe('htmlToMarkdown', () => { - it('should convert HTML to Markdown', () => { - const html = '

Hello World

' - const result = htmlToMarkdown(html) - expect(result).toBe('# Hello World') - }) - - it('should keep
to
', () => { - const html = '

Text with
\nindentation
\nand without indentation

' - const result = htmlToMarkdown(html) - expect(result).toBe('Text with
indentation
and without indentation') - }) - - it('should convert task list HTML back to Markdown', () => { - const html = - '
  • abcd
  • efgh
' - const result = htmlToMarkdown(html) - expect(result).toContain('- [ ] abcd') - expect(result).toContain('- [x] efgh') - }) - - it('should convert task list HTML back to Markdown with label', () => { - const html = - '
' - const result = htmlToMarkdown(html) - expect(result).toBe('- [ ] abcd\n\n- [x] efgh') - }) - - it('should handle empty HTML', () => { - const result = htmlToMarkdown('') - expect(result).toBe('') - }) - - it('should handle null/undefined input', () => { - expect(htmlToMarkdown(null as any)).toBe('') - expect(htmlToMarkdown(undefined as any)).toBe('') - }) - - it('should keep math block containers intact', () => { - const html = '
' - const result = htmlToMarkdown(html) - expect(result).toBe('$$a+b+c$$') - }) - - it('should convert multiple math blocks to Markdown', () => { - const html = - '
' - const result = htmlToMarkdown(html) - expect(result).toBe( - '$$\\begin{array}{c}\n\\nabla \\times \\vec{\\mathbf{B}} -\\, \\frac1c\\, \\frac{\\partial\\vec{\\mathbf{E}}}{\\partial t} &\n= \\frac{4\\pi}{c}\\vec{\\mathbf{j}} \\nabla \\cdot \\vec{\\mathbf{E}} & = 4 \\pi \\rho \\\\\n\n\\nabla \\times \\vec{\\mathbf{E}}\\, +\\, \\frac1c\\, \\frac{\\partial\\vec{\\mathbf{B}}}{\\partial t} & = \\vec{\\mathbf{0}} \\\\\n\n\\nabla \\cdot \\vec{\\mathbf{B}} & = 0\n\n\\end{array}$$' - ) - }) - - it('should convert math inline syntax to Markdown', () => { - const html = '' - const result = htmlToMarkdown(html) - expect(result).toBe('$a+b+c$') - }) - - it('shoud convert multiple math blocks and inline math to Markdown', () => { - const html = - '

' - const result = htmlToMarkdown(html) - expect(result).toBe('$$a+b+c$$\n\n$d+e+f$') - }) - - it('should convert heading and img to Markdown', () => { - const html = '

Hello

\n

alt text

\n' - const result = htmlToMarkdown(html) - expect(result).toBe('# Hello\n\n![alt text](https://example.com/image.png)') - }) - - it('should convert heading and paragraph to Markdown', () => { - const html = '

Hello

\n

Hello

\n' - const result = htmlToMarkdown(html) - expect(result).toBe('# Hello\n\nHello') - }) - - it('should convert code block to Markdown', () => { - const html = '
console.log("Hello, world!");
' - const result = htmlToMarkdown(html) - expect(result).toBe('```\nconsole.log("Hello, world!");\n```') - }) - - it('should convert code block with language to Markdown', () => { - const html = '
console.log("Hello, world!");
' - const result = htmlToMarkdown(html) - expect(result).toBe('```javascript\nconsole.log("Hello, world!");\n```') - }) - - it('should convert table to Markdown', () => { - const html = - '

f

f

f

' - const result = htmlToMarkdown(html) - expect(result).toBe('| f | | |\n| --- | --- | --- |\n| | f | |\n| | | f |') - }) - }) - - describe('markdownToHtml', () => { - it('should convert
to
', () => { - const markdown = 'Text with
\nindentation
\nand without indentation' - const result = stripLineNumbers(markdownToHtml(markdown)) - expect(result).toBe('

Text with
\nindentation
\nand without indentation

\n') - }) - - it('should handle indentation in blockquotes', () => { - const markdown = '> Quote line 1\n> Quote line 2 with indentation' - const result = stripLineNumbers(markdownToHtml(markdown)) - // This should preserve indentation within the blockquote - expect(result).toContain('Quote line 1') - expect(result).toContain('Quote line 2 with indentation') - }) - - it('should preserve indentation in nested lists', () => { - const markdown = '- Item 1\n - Nested item\n - Double nested\n with continued line' - const result = stripLineNumbers(markdownToHtml(markdown)) - // Should create proper nested list structure - expect(result).toContain('
    ') - expect(result).toContain('
  • ') - }) - - it('should handle poetry or formatted text with indentation', () => { - const markdown = 'Roses are red\n Violets are blue\n Sugar is sweet\n And so are you' - const result = stripLineNumbers(markdownToHtml(markdown)) - expect(result).toBe('

    Roses are red\nViolets are blue\nSugar is sweet\nAnd so are you

    \n') - }) - - it('should preserve indentation after line breaks with multiple paragraphs', () => { - const markdown = 'First paragraph\n\n with indentation\n\n Second paragraph\n\nwith different indentation' - const result = stripLineNumbers(markdownToHtml(markdown)) - expect(result).toBe( - '

    First paragraph

    \n
    with indentation\n\nSecond paragraph\n

    with different indentation

    \n' - ) - }) - - it('should handle zero-width indentation (just line break)', () => { - const markdown = 'Hello\n\nWorld' - const result = stripLineNumbers(markdownToHtml(markdown)) - expect(result).toBe('

    Hello

    \n

    World

    \n') - }) - - it('should preserve indentation in mixed content', () => { - const markdown = - 'Normal text\n Indented continuation\n\n- List item\n List continuation\n\n> Quote\n> Indented quote' - const result = stripLineNumbers(markdownToHtml(markdown)) - expect(result).toBe( - '

    Normal text\nIndented continuation

    \n
      \n
    • List item\nList continuation
    • \n
    \n
    \n

    Quote\nIndented quote

    \n
    \n' - ) - }) - - it('should convert Markdown to HTML', () => { - const markdown = '# Hello World' - const result = stripLineNumbers(markdownToHtml(markdown)) - expect(result).toContain('

    Hello World

    ') - }) - - it('should convert math block syntax to HTML', () => { - const markdown = '$$a+b+c$$' - const result = stripLineNumbers(markdownToHtml(markdown)) - expect(result).toContain('
    ') - }) - - it('should convert math inline syntax to HTML', () => { - const markdown = '$a+b+c$' - const result = stripLineNumbers(markdownToHtml(markdown)) - expect(result).toContain('') - }) - - it('should convert multiple math blocks to HTML', () => { - const markdown = `$$\\begin{array}{c} -\\nabla \\times \\vec{\\mathbf{B}} -\\, \\frac1c\\, \\frac{\\partial\\vec{\\mathbf{E}}}{\\partial t} & -= \\frac{4\\pi}{c}\\vec{\\mathbf{j}} \\nabla \\cdot \\vec{\\mathbf{E}} & = 4 \\pi \\rho \\\\ - -\\nabla \\times \\vec{\\mathbf{E}}\\, +\\, \\frac1c\\, \\frac{\\partial\\vec{\\mathbf{B}}}{\\partial t} & = \\vec{\\mathbf{0}} \\\\ - -\\nabla \\cdot \\vec{\\mathbf{B}} & = 0 - -\\end{array}$$` - const result = stripLineNumbers(markdownToHtml(markdown)) - expect(result).toContain( - '
    ' - ) - }) - - it('should convert task list syntax to proper HTML', () => { - const markdown = '- [ ] abcd\n\n- [x] efgh\n\n' - const result = stripLineNumbers(markdownToHtml(markdown)) - expect(result).toContain('data-type="taskList"') - expect(result).toContain('data-type="taskItem"') - expect(result).toContain('data-checked="false"') - expect(result).toContain('data-checked="true"') - expect(result).toContain('') - expect(result).toContain('') - expect(result).toContain('abcd') - expect(result).toContain('efgh') - }) - - it('should convert mixed task list with checked and unchecked items', () => { - const markdown = '- [ ] First task\n\n- [x] Second task\n\n- [ ] Third task' - const result = stripLineNumbers(markdownToHtml(markdown)) - expect(result).toContain('data-type="taskList"') - expect(result).toContain('First task') - expect(result).toContain('Second task') - expect(result).toContain('Third task') - expect(result.match(/data-checked="false"/g)).toHaveLength(2) - expect(result.match(/data-checked="true"/g)).toHaveLength(1) - }) - - it('should NOT convert standalone task syntax to task list', () => { - const markdown = '[x] abcd' - const result = stripLineNumbers(markdownToHtml(markdown)) - expect(result).toContain('

    [x] abcd

    ') - expect(result).not.toContain('data-type="taskList"') - }) - - it('should handle regular list items alongside task lists', () => { - const markdown = '- Regular item\n\n- [ ] Task item\n\n- Another regular item' - const result = stripLineNumbers(markdownToHtml(markdown)) - expect(result).toContain('data-type="taskList"') - expect(result).toContain('Regular item') - expect(result).toContain('Task item') - expect(result).toContain('Another regular item') - }) - - it('should handle empty Markdown', () => { - const result = markdownToHtml('') - expect(result).toBe('') - }) - - it('should handle null/undefined input', () => { - expect(markdownToHtml(null as any)).toBe('') - expect(markdownToHtml(undefined as any)).toBe('') - }) - - it('should handle heading and img', () => { - const markdown = `# 🌠 Screenshot - -![](https://example.com/image.png)` - const result = stripLineNumbers(markdownToHtml(markdown)) - expect(result).toBe('

    🌠 Screenshot

    \n

    \n') - }) - - it('should handle heading and paragraph', () => { - const markdown = '# Hello\n\nHello' - const result = stripLineNumbers(markdownToHtml(markdown)) - expect(result).toBe('

    Hello

    \n

    Hello

    \n') - }) - - it('should convert code block to HTML', () => { - const markdown = '```\nconsole.log("Hello, world!");\n```' - const result = stripLineNumbers(markdownToHtml(markdown)) - expect(result).toBe('
    console.log("Hello, world!");\n
    ') - }) - - it('should convert code block with language to HTML', () => { - const markdown = '```javascript\nconsole.log("Hello, world!");\n```' - const result = stripLineNumbers(markdownToHtml(markdown)) - expect(result).toBe( - '
    console.log("Hello, world!");\n
    ' - ) - }) - - it('should convert table to HTML', () => { - const markdown = '| f | | |\n| --- | --- | --- |\n| | f | |\n| | | f |' - const result = stripLineNumbers(markdownToHtml(markdown)) - expect(result).toBe( - '\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
    f
    f
    f
    \n' - ) - }) - - it('should escape XML-like tags in code blocks', () => { - const markdown = '```jsx\nconst component = <>
    content
    \n```' - const result = stripLineNumbers(markdownToHtml(markdown)) - expect(result).toBe( - '
    const component = <><div>content</div></>\n
    ' - ) - }) - - it('should escape XML-like tags in inline code', () => { - const markdown = 'Use `<>` for fragments' - const result = stripLineNumbers(markdownToHtml(markdown)) - expect(result).toBe('

    Use <> for fragments

    \n') - }) - - it('shoud convert XML-like tags in paragraph', () => { - const markdown = '' - const result = stripLineNumbers(markdownToHtml(markdown)) - expect(result).toBe('

    \n') - }) - }) - - describe('Task List with Labels', () => { - it('should wrap task items with labels when label option is true', () => { - const markdown = '- [ ] abcd\n\n- [x] efgh' - const result = stripLineNumbers(markdownToHtml(markdown)) - expect(result).toBe( - '
      \n
    • \n

      \n
    • \n
    • \n

      \n
    • \n
    \n' - ) - }) - }) - - describe('Task List Round Trip', () => { - it('should maintain task list structure through markdown → html → markdown conversion', () => { - const originalMarkdown = '- [ ] abcd\n\n- [x] efgh' - const html = markdownToHtml(originalMarkdown) - const backToMarkdown = htmlToMarkdown(html) - - expect(backToMarkdown).toBe(originalMarkdown) - }) - - it('should maintain task list structure through html → markdown → html conversion', () => { - const originalHtml = - '
    ' - const markdown = htmlToMarkdown(originalHtml) - const html = stripLineNumbers(markdownToHtml(markdown)) - - expect(html).toBe( - '
      \n
    • \n
    \n' - ) - }) - - it('should maintain task list structure through html → markdown → html conversion2', () => { - const originalHtml = - '
      \n
    • \n

      123

      \n
    • \n
    • \n

      \n
    • \n
    \n' - const markdown = htmlToMarkdown(originalHtml) - const html = stripLineNumbers(markdownToHtml(markdown)) - - expect(html).toBe(originalHtml) - }) - - it('should handle complex task lists with multiple items', () => { - const originalMarkdown = - '- [ ] First unchecked task\n\n- [x] First checked task\n\n- [ ] Second unchecked task\n\n- [x] Second checked task' - const html = markdownToHtml(originalMarkdown) - const backToMarkdown = htmlToMarkdown(html) - - expect(backToMarkdown).toBe(originalMarkdown) - }) - }) - - describe('LaTeX Escaping in Tables', () => { - it('should test simple inline math with backslashes', () => { - const html = '' - const result = htmlToMarkdown(html) - expect(result).toBe('$\\int_{-\\infty}^{\\infty}$') - }) - - it('should test inline math within table structure', () => { - const tableHtml = - '
    FormulaDescription
    Gaussian integral
    ' - const result = htmlToMarkdown(tableHtml) - expect(result).toContain('$\\int_{-\\infty}^{\\infty} e^{-x²} dx = \\sqrt{\\pi}$') - }) - - it('should preserve LaTeX backslashes in table cells during round trip conversion', () => { - const tableWithLatex = - '| Formula | Description |\n| --- | --- |\n| $\\int_{-\\infty}^{\\infty} e^{-x²} dx = \\sqrt{\\pi}$ | Gaussian integral |' - const html = markdownToHtml(tableWithLatex) - const backToMarkdown = htmlToMarkdown(html) - - // The LaTeX formula should preserve its backslashes - expect(backToMarkdown).toContain('$\\int_{-\\infty}^{\\infty} e^{-x²} dx = \\sqrt{\\pi}$') - expect(backToMarkdown).not.toContain('$\\\\int_{-\\\\infty}^{\\\\infty} e^{-x²} dx = \\\\sqrt{\\\\pi}$') - }) - - it('should handle LaTeX in table cells without double escaping', () => { - const markdown = - '| Math | Result |\n| --- | --- |\n| $E = mc^2$ | Energy-mass equivalence |\n| $\\sum_{i=1}^{n} i = \\frac{n(n+1)}{2}$ | Sum formula |' - const html = markdownToHtml(markdown) - const result = htmlToMarkdown(html) - - expect(result).toContain('$E = mc^2$') - expect(result).toContain('$\\sum_{i=1}^{n} i = \\frac{n(n+1)}{2}$') - expect(result).not.toContain('$\\\\sum_{i=1}^{n} i = \\\\frac{n(n+1)}{2}$') - }) - }) - - describe('markdown image', () => { - it('should convert markdown image to HTML img tag', () => { - const markdown = '![foo](train.jpg)' - const result = stripLineNumbers(markdownToHtml(markdown)) - expect(result).toBe('

    foo

    \n') - }) - it('should convert markdown image with file:// protocol to HTML img tag', () => { - const markdown = - '![pasted_image_45285c9c-a7cd-4c3d-a9b6-6854c3bbe479.png](file:///Users/xxxx/Library/Application Support/CherryStudioDev/Data/Files/45285c9c-a7cd-4c3d-a9b6-6854c3bbe479.png)' - const result = markdownToHtml(markdown) - expect(result).toContain( - 'pasted_image_45285c9c-a7cd-4c3d-a9b6-6854c3bbe479.png' - ) - }) - - it('should handle file:// protocol images differently from http images', () => { - const markdown = - 'Local: ![Local image](file:///path/to/local.png)\\n\\nRemote: ![Remote image](https://example.com/remote.png)' - const result = markdownToHtml(markdown) - // file:// should be converted to HTML img tag - expect(result).toContain('Local image') - // https:// should be processed by markdown-it normally - expect(result).toContain('Remote image') - }) - - it('should handle images with spaces in file:// protocol paths', () => { - const markdown = '![My Image](file:///path/to/my image with spaces.png)' - const result = htmlToMarkdown(markdownToHtml(markdown)) - expect(result).toBe(markdown) - }) - - it('shoud img label to markdown', () => { - const html = 'My Image' - const result = htmlToMarkdown(html) - expect(result).toBe('![My Image](file:///path/to/my image with spaces.png)') - }) - }) - - it('should handle hardbreak with backslash followed by indented text', () => { - const markdown = 'Text with \\\n indentation \\\nand without indentation' - const result = stripLineNumbers(markdownToHtml(markdown)) - expect(result).toBe('

    Text with
    \nindentation
    \nand without indentation

    \n') - }) - - describe('Custom XML Tags Preservation', () => { - it('should preserve custom XML tags through markdown-to-HTML and HTML-to-markdown conversion', () => { - const markdown = 'Some text with content and more text' - const html = markdownToHtml(markdown) - const backToMarkdown = htmlToMarkdown(html) - - expect(html).toContain('Some text with content and more text') - expect(backToMarkdown).toBe('Some text with content and more text') - }) - - it('should preserve single custom XML tags', () => { - const markdown = '' - const html = markdownToHtml(markdown) - const backToMarkdown = htmlToMarkdown(html) - - expect(html).toBe('

    ') - expect(backToMarkdown).toBe('') - }) - - it('should preserve single custom XML tags in html', () => { - const html = '

    ' - const markdown = htmlToMarkdown(html) - const backToHtml = markdownToHtml(markdown) - - expect(markdown).toBe('') - expect(backToHtml).toBe('

    ') - }) - - it('should preserve custom XML tags mixed with regular markdown', () => { - const markdown = '# Heading\n\nWidget content\n\n**Bold text**' - const html = stripLineNumbers(markdownToHtml(markdown)) - const backToMarkdown = htmlToMarkdown(html) - - expect(html).toContain('

    Heading

    ') - expect(html).toContain('Widget content') - expect(html).toContain('Bold text') - expect(backToMarkdown).toContain('# Heading') - expect(backToMarkdown).toContain('Widget content') - expect(backToMarkdown).toContain('**Bold text**') - }) - }) - - describe('Typing behavior issues', () => { - it('should not add unwanted line breaks during simple text typing', () => { - const html = '

    Hello world

    ' - const markdown = htmlToMarkdown(html) - const backToHtml = stripLineNumbers(markdownToHtml(markdown)) - - expect(markdown).toBe('Hello world') - expect(backToHtml).toBe('

    Hello world

    \n') - }) - - it('should preserve simple paragraph structure during round-trip conversion', () => { - const originalHtml = '

    This is a simple paragraph being typed

    ' - const markdown = htmlToMarkdown(originalHtml) - const backToHtml = stripLineNumbers(markdownToHtml(markdown)) - expect(markdown).toBe('This is a simple paragraph being typed') - expect(backToHtml).toBe('

    This is a simple paragraph being typed

    \n') - }) - }) - - describe('should keep YAML front matter', () => { - it('should keep YAML front matter', () => { - const markdown = `--- -tags: - - 你好 -aliases: - - "1111" - - "222" - - "333" - - "3333" -cssclasses: - - fffff - - ssss - - s12 ----` - const result = markdownToHtml(markdown) - const backToMarkdown = htmlToMarkdown(result) - expect(backToMarkdown).toBe(markdown) - }) - }) - - describe('should keep []', () => { - it('should keep [[foo]]', () => { - const markdown = `[[foo]]` - const result = markdownToHtml(markdown) - const backToMarkdown = htmlToMarkdown(result) - expect(backToMarkdown).toBe(markdown) - }) - it('should keep []', () => { - const markdown = `[foo]` - const result = markdownToHtml(markdown) - const backToMarkdown = htmlToMarkdown(result) - expect(backToMarkdown).toBe(markdown) - }) - }) - - describe('should have markdown line number injected in HTML', () => { - it('should inject line numbers into paragraphs', () => { - const markdown = 'First paragraph\n\nSecond paragraph\n\nThird paragraph' - const result = markdownToHtml(markdown) - expect(result).toContain(`

    First paragraph

    `) - expect(result).toContain(`

    Second paragraph

    `) - expect(result).toContain(`

    Third paragraph

    `) - }) - - it('should inject line numbers into mixed content', () => { - const markdown = 'Text\n\n- List\n\n> Quote' - const result = markdownToHtml(markdown) - expect(result).toContain(`

    Text

    `) - expect(result).toContain(`
      `) - expect(result).toContain(`
    • List
    • `) - expect(result).toContain(`
      `) - expect(result).toContain(`

      Quote

      `) - }) - }) -}) diff --git a/src/renderer/src/utils/markdownConverter.ts b/src/renderer/src/utils/markdownConverter.ts index 51d684612d..3ccfb1fb74 100644 --- a/src/renderer/src/utils/markdownConverter.ts +++ b/src/renderer/src/utils/markdownConverter.ts @@ -1,82 +1,11 @@ import { loggerService } from '@logger' import { MARKDOWN_SOURCE_LINE_ATTR } from '@renderer/components/RichEditor/constants' -import { TurndownPlugin } from '@truto/turndown-plugin-gfm' import he from 'he' -import htmlTags, { type HtmlTags } from 'html-tags' -import * as htmlparser2 from 'htmlparser2' import MarkdownIt from 'markdown-it' -import striptags from 'striptags' import TurndownService from 'turndown' const logger = loggerService.withContext('markdownConverter') -function escapeCustomTags(html: string) { - let result = '' - let currentPos = 0 - const processedPositions = new Set() - - const parser = new htmlparser2.Parser({ - onopentagname(tagname) { - const startPos = parser.startIndex - const endPos = parser.endIndex - - // Add content before this tag - result += html.slice(currentPos, startPos) - - if (!htmlTags.includes(tagname as HtmlTags)) { - // This is a custom tag, escape it - const tagHtml = html.slice(startPos, endPos + 1) - result += tagHtml.replace(//g, '>') - } else { - // This is a standard HTML tag, keep it as-is - result += html.slice(startPos, endPos + 1) - } - - currentPos = endPos + 1 - }, - - onclosetag(tagname) { - const startPos = parser.startIndex - const endPos = parser.endIndex - - // Skip if we've already processed this position (handles malformed HTML) - if (processedPositions.has(endPos) || endPos + 1 <= currentPos) { - return - } - - processedPositions.add(endPos) - - // Get the actual HTML content at this position to verify what tag it really is - const actualTagHtml = html.slice(startPos, endPos + 1) - const actualTagMatch = actualTagHtml.match(/<\/([^>]+)>/) - const actualTagName = actualTagMatch ? actualTagMatch[1] : tagname - - if (!htmlTags.includes(actualTagName as HtmlTags)) { - // This is a custom tag, escape it - result += html.slice(currentPos, startPos) - result += actualTagHtml.replace(//g, '>') - currentPos = endPos + 1 - } else { - // This is a standard HTML tag, add content up to and including the closing tag - result += html.slice(currentPos, endPos + 1) - currentPos = endPos + 1 - } - }, - - onend() { - result += html.slice(currentPos) - } - }) - - parser.write(html) - parser.end() - return result -} - -export interface TaskListOptions { - label?: boolean -} - // Create markdown-it instance with task list plugin const md = new MarkdownIt({ html: true, // Enable HTML tags in source @@ -154,105 +83,6 @@ md.renderer.rules.fence = function (tokens, idx) { return html } -// Custom task list plugin for markdown-it -function taskListPlugin(md: MarkdownIt, options: TaskListOptions = {}) { - const { label = false } = options - md.core.ruler.after('inline', 'task_list', (state) => { - const tokens = state.tokens - let inside_task_list = false - - for (let i = 0; i < tokens.length; i++) { - const token = tokens[i] - - if (token.type === 'bullet_list_open') { - // Check if this list contains task items - let hasTaskItems = false - for (let j = i + 1; j < tokens.length && tokens[j].type !== 'bullet_list_close'; j++) { - if (tokens[j].type === 'inline' && /^\s*\[[ x]\](\s|$)/.test(tokens[j].content)) { - hasTaskItems = true - break - } - } - - if (hasTaskItems) { - inside_task_list = true - token.attrSet('data-type', 'taskList') - token.attrSet('class', 'task-list') - } - } else if (token.type === 'bullet_list_close' && inside_task_list) { - inside_task_list = false - } else if (token.type === 'list_item_open' && inside_task_list) { - token.attrSet('data-type', 'taskItem') - token.attrSet('class', 'task-list-item') - } else if (token.type === 'inline' && inside_task_list) { - const match = token.content.match(/^(\s*)\[([x ])\](\s+(.*))?$/) - if (match) { - const [, , check, , content] = match - const isChecked = check.toLowerCase() === 'x' - - // Find the parent list item token - for (let j = i - 1; j >= 0; j--) { - if (tokens[j].type === 'list_item_open') { - tokens[j].attrSet('data-checked', isChecked.toString()) - break - } - } - - // Find the parent paragraph token and replace it entirely - let paragraphTokenIndex = -1 - for (let k = i - 1; k >= 0; k--) { - if (tokens[k].type === 'paragraph_open') { - paragraphTokenIndex = k - break - } - } - - // Check if this came from HTML with

      structure - // Empty content typically indicates it came from

      structure - const shouldUseDivFormat = token.content === '' || state.src.includes('') - - if (paragraphTokenIndex >= 0 && label && shouldUseDivFormat) { - // Replace the entire paragraph structure with raw HTML for div format - const htmlToken = new state.Token('html_inline', '', 0) - if (content) { - htmlToken.content = `

      ${content}

      ` - } else { - htmlToken.content = `

      ` - } - - // Remove the paragraph tokens and replace with our HTML token - tokens.splice(paragraphTokenIndex, 3, htmlToken) // Remove paragraph_open, inline, paragraph_close - i = paragraphTokenIndex // Adjust index after splice - } else { - // Use the standard label format - token.content = content || '' - const checkboxToken = new state.Token('html_inline', '', 0) - - if (label) { - if (content) { - checkboxToken.content = `` - } else { - checkboxToken.content = `` - } - token.children = [checkboxToken] - } else { - checkboxToken.content = `` - - if (content) { - const textToken = new state.Token('text', '', 0) - textToken.content = ' ' + content - token.children = [checkboxToken, textToken] - } else { - token.children = [checkboxToken] - } - } - } - } - } - } - }) -} - interface TokenLike { content: string block?: boolean @@ -361,159 +191,8 @@ function yamlFrontMatterPlugin(md: MarkdownIt) { } } -function tipTapKatexPlugin(md: MarkdownIt) { - // 1) Parser: recognize $$ ... $$ as a block math token - md.block.ruler.before( - 'fence', - 'math_block', - (stateLike: unknown, startLine: number, endLine: number, silent: boolean): boolean => { - const state = stateLike as BlockStateLike - - const startPos = state.bMarks[startLine] + state.tShift[startLine] - const maxPos = state.eMarks[startLine] - - // Must begin with $$ at line start (after indentation) - if (startPos + 2 > maxPos) return false - if (state.src.charCodeAt(startPos) !== 0x24 /* $ */ || state.src.charCodeAt(startPos + 1) !== 0x24 /* $ */) { - return false - } - - // If requested only to validate existence - if (silent) return true - - // Search for closing $$ - let nextLine = startLine - let content = '' - - // Same-line closing? $$ ... $$ - const sameLineClose = state.src.indexOf('$$', startPos + 2) - if (sameLineClose !== -1 && sameLineClose <= maxPos - 2) { - content = state.src.slice(startPos + 2, sameLineClose).trim() - nextLine = startLine - } else { - // Multiline: look for closing $$ anywhere - for (nextLine = startLine + 1; nextLine < endLine; nextLine++) { - const lineStart = state.bMarks[nextLine] + state.tShift[nextLine] - const lineEnd = state.eMarks[nextLine] - const line = state.src.slice(lineStart, lineEnd) - - // Check if this line contains closing $$ - const closingPos = line.indexOf('$$') - if (closingPos !== -1) { - // Found closing $$; extract content between opening and closing - const allLines: string[] = [] - - // First line: content after opening $$ - const firstLineStart = state.bMarks[startLine] + state.tShift[startLine] + 2 - const firstLineEnd = state.eMarks[startLine] - const firstLineContent = state.src.slice(firstLineStart, firstLineEnd) - if (firstLineContent.trim()) { - allLines.push(firstLineContent) - } - - // Middle lines: full content - for (let lineIdx = startLine + 1; lineIdx < nextLine; lineIdx++) { - const midLineStart = state.bMarks[lineIdx] + state.tShift[lineIdx] - const midLineEnd = state.eMarks[lineIdx] - allLines.push(state.src.slice(midLineStart, midLineEnd)) - } - - // Last line: content before closing $$ - const lastLineContent = line.slice(0, closingPos) - if (lastLineContent.trim()) { - allLines.push(lastLineContent) - } - - content = allLines.join('\n').trim() - break - } - - // Check if line starts with $$ (alternative closing pattern) - if ( - lineStart + 2 <= lineEnd && - state.src.charCodeAt(lineStart) === 0x24 && - state.src.charCodeAt(lineStart + 1) === 0x24 - ) { - // Extract content between start and this line - const firstContentLineStart = state.bMarks[startLine] + state.tShift[startLine] + 2 - const lastContentLineEnd = state.bMarks[nextLine] - content = state.src.slice(firstContentLineStart, lastContentLineEnd).trim() - break - } - } - if (nextLine >= endLine) { - // No closing fence -> not a valid block - return false - } - } - - const token = state.push('math_block', 'div', 0) - token.block = true - token.map = [startLine, nextLine] - token.content = content - - state.line = nextLine + 1 - return true - } - ) - - // 2) Renderer: output TipTap-friendly container - md.renderer.rules.math_block = (tokens: Array<{ content?: string }>, idx: number): string => { - const token = tokens[idx] - const content = token?.content ?? '' - const latexEscaped = he.encode(content, { useNamedReferences: true }) - let html = `
      ` - html = injectLineNumber(token, html) - return html - } - - // 3) Inline parser: recognize $...$ on a single line as inline math - md.inline.ruler.before('emphasis', 'math_inline', (stateLike: unknown, silent: boolean): boolean => { - const state = stateLike as InlineStateLike - const start = state.pos - - // Need starting $ - if (start >= state.posMax || state.src.charCodeAt(start) !== 0x24 /* $ */) { - return false - } - - // Find the next $ after start+1 - const close = state.src.indexOf('$', start + 1) - if (close === -1 || close > state.posMax) { - return false - } - - const content = state.src.slice(start + 1, close) - // Inline variant must not contain a newline - if (content.indexOf('\n') !== -1) { - return false - } - - if (!silent) { - const token = state.push('math_inline', 'span', 0) - token.content = content.trim() - } - - state.pos = close + 1 - return true - }) - - // 4) Inline renderer: output TipTap-friendly inline container - md.renderer.rules.math_inline = (tokens: Array<{ content?: string }>, idx: number): string => { - const content = tokens[idx]?.content ?? '' - const latexEscaped = he.encode(content, { useNamedReferences: true }) - return `` - } -} - md.use(yamlFrontMatterPlugin) -md.use(taskListPlugin, { - label: true -}) - -md.use(tipTapKatexPlugin) - // Initialize turndown service const turndownService = new TurndownService({ headingStyle: 'atx', // Use # for headings @@ -522,56 +201,7 @@ const turndownService = new TurndownService({ codeBlockStyle: 'fenced', // Use ``` for code blocks fence: '```', // Use ``` for code blocks emDelimiter: '*', // Use * for emphasis - strongDelimiter: '**', // Use ** for strong - blankReplacement: (_content, node) => { - const el = node as any as HTMLElement - if (el.nodeName === 'DIV' && el.getAttribute?.('data-type') === 'block-math') { - const latex = el.getAttribute?.('data-latex') || '' - const decodedLatex = he.decode(latex, { - isAttributeValue: false, - strict: false - }) - return `$$${decodedLatex}$$\n\n` - } - if (el.nodeName === 'SPAN' && el.getAttribute?.('data-type') === 'inline-math') { - const latex = el.getAttribute?.('data-latex') || '' - const decodedLatex = he.decode(latex, { - isAttributeValue: false, - strict: false - }) - return `$${decodedLatex}$` - } - // Handle paragraphs containing only math spans - if (el.nodeName === 'P' && el.querySelector?.('[data-type="inline-math"]')) { - const mathSpans = el.querySelectorAll('[data-type="inline-math"]') - if (mathSpans.length === 1 && el.children.length === 1) { - const span = mathSpans[0] - const latex = span.getAttribute('data-latex') || '' - const decodedLatex = he.decode(latex, { - isAttributeValue: false, - strict: false - }) - return `$${decodedLatex}$` - } - } - return (node as any).isBlock ? '\n\n' : '' - } -}) - -turndownService.addRule('strikethrough', { - filter: ['del', 's'], - replacement: (content) => `~~${content}~~` -}) - -turndownService.addRule('underline', { - filter: ['u'], - replacement: (content) => `${content}` -}) - -// Custom rule to preserve
      tags as literal text -turndownService.addRule('br', { - filter: 'br', - replacement: () => '
      ' + strongDelimiter: '**' // Use ** for strong }) // Custom rule to preserve YAML front matter @@ -596,321 +226,6 @@ turndownService.addRule('yamlFrontMatter', { } }) -// Helper function to safely get text content and clean it with LaTeX support -function cleanCellContent(content: string, cellElement?: Element): string { - // First check for math elements in the cell - if (cellElement) { - const blockMath = cellElement.querySelector('[data-type="block-math"]') - if (blockMath) { - const latex = blockMath.getAttribute('data-latex') || '' - const decodedLatex = he.decode(latex, { isAttributeValue: false, strict: false }) - return `$$${decodedLatex}$$` - } - - const inlineMath = cellElement.querySelector('[data-type="inline-math"]') - if (inlineMath) { - const latex = inlineMath.getAttribute('data-latex') || '' - const decodedLatex = he.decode(latex, { isAttributeValue: false, strict: false }) - return `$${decodedLatex}$` - } - } - - if (!content) return ' ' // Default empty cell content - - // Clean and normalize content - let cleaned = content - .trim() - .replace(/\s+/g, ' ') // Normalize whitespace - .replace(/\|/g, '\\|') // Escape pipes - .replace(/\n+/g, ' ') // Convert newlines to spaces - .replace(/\r+/g, ' ') // Convert carriage returns to spaces - - // If content is still empty or only whitespace, provide default - if (!cleaned || cleaned.match(/^\s*$/)) { - return ' ' - } - - // Ensure minimum width for table readability - if (cleaned.length < 3) { - cleaned += ' '.repeat(3 - cleaned.length) - } - - return cleaned -} - -// Enhanced cell replacement with LaTeX support -function cellWithLatex(content: string, node: Element, index?: number | null): string { - if (index === null && node && node.parentNode) { - index = Array.prototype.indexOf.call(node.parentNode.childNodes, node) - } - if (index === null) index = 0 - - let prefix = ' ' - if (index === 0) prefix = '| ' - - const cellContent = cleanCellContent(content, node) - - // Handle colspan by adding extra empty cells - let colspan = 1 - if (node && node.getAttribute) { - colspan = parseInt(node.getAttribute('colspan') || '1', 10) - if (isNaN(colspan) || colspan < 1) colspan = 1 - } - - let result = prefix + cellContent + ' |' - - // Add empty cells for colspan - for (let i = 1; i < colspan; i++) { - result += ' |' - } - - return result -} - -const customTablesPlugin: TurndownPlugin = (turndownService) => { - turndownService.addRule('tableCell', { - filter: ['th', 'td'], - replacement: function (content: string, node: Element) { - return cellWithLatex(content, node, null) - } - }) - - turndownService.addRule('tableRow', { - filter: 'tr', - replacement: function (content: string, node: Element) { - // Skip empty rows - if (!content || !content.trim()) return '' - - let borderCells = '' - - // Add separator row for heading (simplified version) - const parentNode = node.parentNode - if (parentNode && parentNode.nodeName === 'THEAD') { - const table = node.closest('table') - if (table) { - // Count cells in this row - const cellNodes = Array.from(node.querySelectorAll('th, td')) - const colCount = cellNodes.length - - if (colCount > 0) { - for (let i = 0; i < colCount; i++) { - const prefix = i === 0 ? '| ' : ' ' - borderCells += prefix + '---' + ' |' - } - } - } - } - - return '\n' + content + (borderCells ? '\n' + borderCells : '') - } - }) - - turndownService.addRule('table', { - filter: 'table', - replacement: function (content: string) { - // Clean up content (remove extra newlines) - content = content.replace(/\n+/g, '\n').trim() - - // If no content after cleaning, return empty - if (!content) return '' - - // Split into lines and filter out empty lines - const lines = content.split('\n').filter((line) => line.trim()) - - if (lines.length === 0) return '' - - // Check if we need to add a header row - const hasHeaderSeparator = lines.length >= 2 && /\|\s*-+/.test(lines[1]) - - let result = lines.join('\n') - - // If no header separator exists, add a simple one - if (!hasHeaderSeparator && lines.length >= 1) { - const firstLine = lines[0] - const colCount = (firstLine.match(/\|/g) || []).length - 1 - - if (colCount > 0) { - let separator = '|' - for (let i = 0; i < colCount; i++) { - separator += ' --- |' - } - - // Insert separator after first line - const resultLines = [lines[0], separator, ...lines.slice(1)] - result = resultLines.join('\n') - } - } - - return '\n\n' + result + '\n\n' - } - }) - - // Remove table sections but keep content - turndownService.addRule('tableSection', { - filter: ['thead', 'tbody', 'tfoot'], - replacement: function (content: string) { - return content - } - }) -} - -const taskListItemsPlugin: TurndownPlugin = (turndownService) => { - turndownService.addRule('taskListItems', { - filter: (node: Element) => { - return node.nodeName === 'LI' && node.getAttribute && node.getAttribute('data-type') === 'taskItem' - }, - replacement: (_content: string, node: Element) => { - const checkbox = node.querySelector('input[type="checkbox"]') as HTMLInputElement | null - const isChecked = checkbox?.checked || node.getAttribute('data-checked') === 'true' - - // Check if this task item uses the div format - const hasDiv = node.querySelector('div p') !== null - const divContent = node.querySelector('div p')?.textContent?.trim() || '' - - let textContent = '' - if (hasDiv) { - textContent = divContent - // Add a marker to indicate this came from div format - const marker = '' - return '- ' + (isChecked ? '[x]' : '[ ]') + ' ' + textContent + ' ' + marker + '\n\n' - } else { - textContent = node.textContent?.trim() || '' - return '- ' + (isChecked ? '[x]' : '[ ]') + ' ' + textContent + '\n\n' - } - } - }) - turndownService.addRule('taskList', { - filter: (node: Element) => { - return node.nodeName === 'UL' && node.getAttribute && node.getAttribute('data-type') === 'taskList' - }, - replacement: (content: string) => { - return content - } - }) -} - -turndownService.use([customTablesPlugin, taskListItemsPlugin]) - -/** - * Converts HTML content to Markdown - * @param html - HTML string to convert - * @returns Markdown string - */ -export const htmlToMarkdown = (html: string | null | undefined): string => { - if (!html || typeof html !== 'string') { - return '' - } - - try { - const encodedHtml = escapeCustomTags(html) - const turndownResult = turndownService.turndown(encodedHtml) - let finalResult = he.decode(turndownResult) - - // Post-process to unescape square brackets that are not part of Markdown link syntax - // This preserves wiki-style double brackets [[foo]] and single brackets [foo] - // but keeps proper Markdown links [text](url) intact - - // Use a more sophisticated approach: check for the link pattern first, - // then unescape standalone brackets - - // First, protect actual Markdown links by temporarily replacing them - const linkPlaceholders: string[] = [] - let linkCounter = 0 - - // Find and replace all Markdown links with placeholders - finalResult = finalResult.replace(/\\\[([^\]]*)\\\]\([^)]*\)/g, (match) => { - const placeholder = `__MDLINK_${linkCounter++}__` - linkPlaceholders[linkCounter - 1] = match - return placeholder - }) - - // Now unescape all remaining square brackets - finalResult = finalResult.replace(/\\\[/g, '[').replace(/\\\]/g, ']') - - // Restore the Markdown links - for (let i = 0; i < linkPlaceholders.length; i++) { - const placeholder = `__MDLINK_${i}__` - finalResult = finalResult.replace(placeholder, linkPlaceholders[i]) - } - - return finalResult - } catch (error) { - logger.error('Error converting HTML to Markdown:', error as Error) - return '' - } -} - -/** - * Converts Markdown content to HTML - * @param markdown - Markdown string to convert - * @param options - Task list options - * @returns HTML string - */ -export const markdownToHtml = (markdown: string | null | undefined): string => { - if (!markdown || typeof markdown !== 'string') { - return '' - } - - try { - // First, convert any standalone markdown images to HTML img tags - // This handles cases where markdown images should be rendered as HTML instead of going through markdown-it - const processedMarkdown = markdown.replace( - /!\[([^\]]*)\]\(([^)]+?)(?:\s+"([^"]*)")?\)/g, - (match, alt, src, title) => { - // Only convert file:// protocol images to HTML img tags - if (src.startsWith('file://')) { - const altText = alt || '' - const srcUrl = src.trim() - const titleAttr = title ? ` title="${title}"` : '' - return `${altText}` - } - return match - } - ) - - let html = md.render(processedMarkdown) - const trimmedMarkdown = processedMarkdown.trim() - - if (html.trim() === trimmedMarkdown) { - const singleTagMatch = trimmedMarkdown.match(/^<([a-zA-Z][^>\s]*)\/?>$/) - if (singleTagMatch) { - const tagName = singleTagMatch[1] - if (!htmlTags.includes(tagName.toLowerCase() as any)) { - html = `

      ${html}

      ` - } - } - } - - // Normalize task list HTML to match expected format - if (html.includes('data-type="taskList"') && html.includes('data-type="taskItem"')) { - // Clean up any div-format markers that leaked through - html = html.replace(/\s*\s*/g, '') - - // Handle both empty and non-empty task items with

      content

      structure - if (html.includes('

      ') && html.includes('

      ')) { - // Both tests use the div format now, but with different formatting expectations - // conversion2 has multiple items and expects expanded format - // original conversion has single item and expects compact format - const hasMultipleItems = (html.match(/]*data-type="taskItem"/g) || []).length > 1 - - if (hasMultipleItems) { - // This is conversion2 format with multiple items - add proper newlines - html = html.replace(/(<\/div>)<\/li>/g, '$1\n') - } else { - // This is the original conversion format - compact inside li tags but keep list structure - // Keep newlines around list items but compact content within li tags - html = html.replace(/(]*>)\s+/g, '$1').replace(/\s+(<\/li>)/g, '$1') - } - } - } - - return html - } catch (error) { - logger.error('Error converting Markdown to HTML:', error as Error) - return '' - } -} - /** * Gets plain text preview from Markdown content * @param markdown - Markdown string @@ -919,11 +234,7 @@ export const markdownToHtml = (markdown: string | null | undefined): string => { */ export const markdownToPreviewText = (markdown: string, maxLength: number = 50): string => { if (!markdown) return '' - - // Convert to HTML first, then strip tags - const html = markdownToHtml(markdown) - const textContent = he.decode(striptags(html)).replace(/\s+/g, ' ').trim() - + const textContent = turndownService.turndown(markdown).replace(/\s+/g, ' ').trim() return textContent.length > maxLength ? `${textContent.slice(0, maxLength)}...` : textContent } diff --git a/yarn.lock b/yarn.lock index 672c4af35f..83bd2e58c0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2664,8 +2664,8 @@ __metadata: resolution: "@cherrystudio/extension-table-plus@workspace:packages/extension-table-plus" dependencies: "@biomejs/biome": "npm:2.2.4" - "@tiptap/core": "npm:^3.2.0" - "@tiptap/pm": "npm:^3.2.0" + "@tiptap/core": "npm:^3.7.2" + "@tiptap/pm": "npm:^3.7.2" eslint: "npm:^9.22.0" eslint-plugin-react-hooks: "npm:^5.2.0" eslint-plugin-simple-import-sort: "npm:^12.1.1" @@ -11516,363 +11516,360 @@ __metadata: languageName: node linkType: hard -"@tiptap/core@npm:^3.2.0": - version: 3.2.0 - resolution: "@tiptap/core@npm:3.2.0" +"@tiptap/core@npm:^3.7.2": + version: 3.7.2 + resolution: "@tiptap/core@npm:3.7.2" peerDependencies: - "@tiptap/pm": ^3.2.0 - checksum: 10c0/8f10d6dc9cc65f9bbec49a8643740819e73587d247782239dd02d386650e483f4bc221f883395c50be8c0b049a25f6138c8dd09ec205908e3bf2841d51533fd7 + "@tiptap/pm": ^3.7.2 + checksum: 10c0/53545b8117e3e68333a3d426b799c970689c02d8fa0749ccd1bff1b86ba8d2f1fdb6cfcfe3f232bf46341c46ffd4abfade554db472e08e7d0a51c3a15ae8f06d languageName: node linkType: hard -"@tiptap/extension-blockquote@npm:^3.2.0": - version: 3.2.0 - resolution: "@tiptap/extension-blockquote@npm:3.2.0" +"@tiptap/extension-blockquote@npm:^3.7.2": + version: 3.7.2 + resolution: "@tiptap/extension-blockquote@npm:3.7.2" peerDependencies: - "@tiptap/core": ^3.2.0 - checksum: 10c0/53d652ef56d688401e2691a1ef40a1ec8585d691baab8ba62800cfe12d32c0509ede887417dd38bef6e42c32a69172fcae88c50a510bc3afce08e4a5730a717d + "@tiptap/core": ^3.7.2 + checksum: 10c0/44bd70fea40fd1a925f1a7302c45bd25ffeeb5ad5efa44b7bf56c149e1a8d380df13f2d4e16482ba950e4e91dfe34a80d063f7086ab0ae6582da1a6c2bc99c6d languageName: node linkType: hard -"@tiptap/extension-bold@npm:^3.2.0": - version: 3.2.0 - resolution: "@tiptap/extension-bold@npm:3.2.0" +"@tiptap/extension-bold@npm:^3.7.2": + version: 3.7.2 + resolution: "@tiptap/extension-bold@npm:3.7.2" peerDependencies: - "@tiptap/core": ^3.2.0 - checksum: 10c0/233569d10be16cc89b4480d6385b7c7cd114d9b9427cf2493915082a779779b01c9ece5ece38965082c9eede5516da8a460bbfea7f5e16aa55e1b54fbfc95b5c + "@tiptap/core": ^3.7.2 + checksum: 10c0/2c97a1fcc9e54a6ac1c5a5be44467288cf77b8ca7c1b596b1c9e5901e26b05c05b16ce50fd7f76d5e664f2c3fecd3ef54699727baabdfc1f2a4e26408fafb114 languageName: node linkType: hard -"@tiptap/extension-bubble-menu@npm:^3.2.0": - version: 3.2.0 - resolution: "@tiptap/extension-bubble-menu@npm:3.2.0" +"@tiptap/extension-bubble-menu@npm:^3.7.2": + version: 3.7.2 + resolution: "@tiptap/extension-bubble-menu@npm:3.7.2" dependencies: "@floating-ui/dom": "npm:^1.0.0" peerDependencies: - "@tiptap/core": ^3.2.0 - "@tiptap/pm": ^3.2.0 - checksum: 10c0/42e17d8a4c192219ff9315595e0cfa39909de6812cd76a453151bb7ec4d0267eccf98b2ee346ce890a9ae4b83d0255b7b72d3fb48c5edbd4c33280f098e60e51 + "@tiptap/core": ^3.7.2 + "@tiptap/pm": ^3.7.2 + checksum: 10c0/91442f2db3124127eb7123d73d437c15cd5e594916bab69204f618e20d65bd4a8ecc9453065106adde4f6d28c47994c9aa82b5d0896470503f357731ae056ad0 languageName: node linkType: hard -"@tiptap/extension-bullet-list@npm:^3.2.0": - version: 3.2.0 - resolution: "@tiptap/extension-bullet-list@npm:3.2.0" +"@tiptap/extension-bullet-list@npm:^3.7.2": + version: 3.7.2 + resolution: "@tiptap/extension-bullet-list@npm:3.7.2" peerDependencies: - "@tiptap/extension-list": ^3.2.0 - checksum: 10c0/46f5d753585e2c3385fbfc691f992250be76534aec1c9cb6f4af1fde17ce4a373d2cffe50072a45cb90360236733316898df24feb23ac5e1bffe42ad3f55d1d2 + "@tiptap/extension-list": ^3.7.2 + checksum: 10c0/c1564f2898938a9a3c0d971ba00abcdb550b8e19a2c479903ae688c7097fed878ce3d2ab1187fd82fd486ea619d61c6a6035b6a8662d1031763d78250556f8a1 languageName: node linkType: hard -"@tiptap/extension-code-block@npm:^3.2.0": - version: 3.2.0 - resolution: "@tiptap/extension-code-block@npm:3.2.0" +"@tiptap/extension-code-block@npm:^3.7.2": + version: 3.7.2 + resolution: "@tiptap/extension-code-block@npm:3.7.2" peerDependencies: - "@tiptap/core": ^3.2.0 - "@tiptap/pm": ^3.2.0 - checksum: 10c0/26bd0867b3af8f59670dbc253ba5b41d1ef8bb31e4ed94c6e6dfe8ca9d0a9f52388191c33432f29c633e261dd8001fd9c3eb4aed0c57dc233008b246766fe448 + "@tiptap/core": ^3.7.2 + "@tiptap/pm": ^3.7.2 + checksum: 10c0/170e74d0e2f7f4e50fae4083255c86bf5824daf5734f2f99bc09f4f67038ef0405a91210b412c74ee07edd606cda60241ac1808eb8c9b954e050af55b7972fbb languageName: node linkType: hard -"@tiptap/extension-code@npm:^3.2.0": - version: 3.2.0 - resolution: "@tiptap/extension-code@npm:3.2.0" +"@tiptap/extension-code@npm:^3.7.2": + version: 3.7.2 + resolution: "@tiptap/extension-code@npm:3.7.2" peerDependencies: - "@tiptap/core": ^3.2.0 - checksum: 10c0/97c7d8f7d4081d9e26c86543d5152d2d014c7bf37edc2e150ba1c17cda5265a2e4d714b9bebe935cf2164b8c932393a39b8664205c9bc0106e185d8cb7c97f12 + "@tiptap/core": ^3.7.2 + checksum: 10c0/4452d0a3bd787307b071946aec09085864c4f291e362bae175c339aa7d5cc99d2cc15bfa58552d3da3d812e0e56dba2c9bdd0a0e2327f8ab35f744dbde623eb8 languageName: node linkType: hard -"@tiptap/extension-collaboration@npm:^3.2.0": - version: 3.2.0 - resolution: "@tiptap/extension-collaboration@npm:3.2.0" +"@tiptap/extension-collaboration@npm:^3.7.2": + version: 3.7.2 + resolution: "@tiptap/extension-collaboration@npm:3.7.2" peerDependencies: - "@tiptap/core": ^3.2.0 - "@tiptap/pm": ^3.2.0 + "@tiptap/core": ^3.7.2 + "@tiptap/pm": ^3.7.2 "@tiptap/y-tiptap": ^3.0.0-beta.3 yjs: ^13 - checksum: 10c0/ebc50b11c32f14aba648327fd0d7d0394d1bd5a164a05399b22683bab2f814b2d63efa3d38d11234b5cd801b3533ec5f15b0ef655954b571290daec62b4e8cab + checksum: 10c0/a8771a9c31b85344caf2d73396b8377e88d875d05dfc2acb128bc0cba377ee26d53b181cbd4802bdafb06ec8b3762cedca3b71c811030ce59ab479c53f51eb03 languageName: node linkType: hard -"@tiptap/extension-document@npm:^3.2.0": - version: 3.2.0 - resolution: "@tiptap/extension-document@npm:3.2.0" +"@tiptap/extension-document@npm:^3.7.2": + version: 3.7.2 + resolution: "@tiptap/extension-document@npm:3.7.2" peerDependencies: - "@tiptap/core": ^3.2.0 - checksum: 10c0/60d6108ab992a3c66995e1351a7da66b2bd0cfecd6cf0de1dc7e3fbe2b1da6205aa5e9afa4ea2bf5c477c164ea8760fff62786e7410e69af56b01cb5a45289f5 + "@tiptap/core": ^3.7.2 + checksum: 10c0/cb69a9a7dbfafdd8fc92d220a67332b66e4e83d3f93b777a51fd028705b8d55d12a4ebed8aa714588b0eb8d121011727d6127fa9e13efe6b4a09166b9f28c0ad languageName: node linkType: hard -"@tiptap/extension-drag-handle-react@npm:^3.2.0": - version: 3.2.0 - resolution: "@tiptap/extension-drag-handle-react@npm:3.2.0" +"@tiptap/extension-drag-handle-react@npm:^3.7.2": + version: 3.7.2 + resolution: "@tiptap/extension-drag-handle-react@npm:3.7.2" peerDependencies: - "@tiptap/extension-drag-handle": ^3.2.0 - "@tiptap/pm": ^3.2.0 - "@tiptap/react": ^3.2.0 + "@tiptap/extension-drag-handle": ^3.7.2 + "@tiptap/pm": ^3.7.2 + "@tiptap/react": ^3.7.2 react: ^16.8 || ^17 || ^18 || ^19 react-dom: ^16.8 || ^17 || ^18 || ^19 - checksum: 10c0/908acd7df808d06e5b7b5fdf68dbfeed41e588ec1d372854949cd059298bed3ef5faf0edbc14e75a2799f632ecae962ae38ccd9f2b1ff69d2fc7fabd15ffcd0b + checksum: 10c0/4003caef5cbdef0aa8278c32f8d607ebdacfbe1fc8057a43884e1247875bd8177e093b1591920d3101fb2fbcd719827ceedc4ade9e97b708445b14abe758ba94 languageName: node linkType: hard -"@tiptap/extension-drag-handle@npm:3.2.0": - version: 3.2.0 - resolution: "@tiptap/extension-drag-handle@npm:3.2.0" +"@tiptap/extension-drag-handle@npm:^3.7.2": + version: 3.7.2 + resolution: "@tiptap/extension-drag-handle@npm:3.7.2" dependencies: "@floating-ui/dom": "npm:^1.6.13" peerDependencies: - "@tiptap/core": ^3.2.0 - "@tiptap/extension-collaboration": ^3.2.0 - "@tiptap/extension-node-range": ^3.2.0 - "@tiptap/pm": ^3.2.0 + "@tiptap/core": ^3.7.2 + "@tiptap/extension-collaboration": ^3.7.2 + "@tiptap/extension-node-range": ^3.7.2 + "@tiptap/pm": ^3.7.2 "@tiptap/y-tiptap": ^3.0.0-beta.3 - checksum: 10c0/b1ad74b85332ae1b34c5b61190992d15c9c3718dc320a13e71edf4b56e5cfc9c3bec9039249217c60f744407fe68debd53abd588aa131ae76f662abae096ce55 + checksum: 10c0/1159fe071d75aed2af8755a2a00af1473f5f9144175b39a6081fd09951fc87aeabb76ffed4d6728882c9283a5ac94e1a32ce5c1cba53a4499e746f44495f8d39 languageName: node linkType: hard -"@tiptap/extension-drag-handle@patch:@tiptap/extension-drag-handle@npm%3A3.2.0#~/.yarn/patches/@tiptap-extension-drag-handle-npm-3.2.0-5a9ebff7c9.patch": - version: 3.2.0 - resolution: "@tiptap/extension-drag-handle@patch:@tiptap/extension-drag-handle@npm%3A3.2.0#~/.yarn/patches/@tiptap-extension-drag-handle-npm-3.2.0-5a9ebff7c9.patch::version=3.2.0&hash=2b492f" - dependencies: - "@floating-ui/dom": "npm:^1.6.13" +"@tiptap/extension-dropcursor@npm:^3.7.2": + version: 3.7.2 + resolution: "@tiptap/extension-dropcursor@npm:3.7.2" peerDependencies: - "@tiptap/core": ^3.2.0 - "@tiptap/extension-collaboration": ^3.2.0 - "@tiptap/extension-node-range": ^3.2.0 - "@tiptap/pm": ^3.2.0 - "@tiptap/y-tiptap": ^3.0.0-beta.3 - checksum: 10c0/09b0e68a58b8dae2f0425c893f17f85ed55b5f5698b4b3a811e7f74cc16e2edd2c25d1df767fe2ad2a543fc96659ed09a5ce0f9f2e16f1db2185c6c4d67f0e37 + "@tiptap/extensions": ^3.7.2 + checksum: 10c0/5a10154c6f259e9693285071ab5a774ed65ee70bbd20955a67e6c4e1a5344fb0f2d8d04deed966235ac17f1bd7105cc2679a1b715212c96ced940600712ebd69 languageName: node linkType: hard -"@tiptap/extension-dropcursor@npm:^3.2.0": - version: 3.2.0 - resolution: "@tiptap/extension-dropcursor@npm:3.2.0" - peerDependencies: - "@tiptap/extensions": ^3.2.0 - checksum: 10c0/db3eb4f656625675a9d2fb9467a71af647c1c9a0926a8513a87ce0ac10d5d5f0f18840e6d0eac724b542430712650bc3f566a999b6f45453525b2797bbb24cda - languageName: node - linkType: hard - -"@tiptap/extension-floating-menu@npm:^3.2.0": - version: 3.2.0 - resolution: "@tiptap/extension-floating-menu@npm:3.2.0" +"@tiptap/extension-floating-menu@npm:^3.7.2": + version: 3.7.2 + resolution: "@tiptap/extension-floating-menu@npm:3.7.2" peerDependencies: "@floating-ui/dom": ^1.0.0 - "@tiptap/core": ^3.2.0 - "@tiptap/pm": ^3.2.0 - checksum: 10c0/55b8c746cac277b05682ca3047131ba886ecb402ad88a1af2a4a7895535f769ec781c0c4a197e560201a2cc376f95683fb06c918f8058da267f567c213a61b6d + "@tiptap/core": ^3.7.2 + "@tiptap/pm": ^3.7.2 + checksum: 10c0/0e53ef9b41d119b031acc9ff1c5cdf6a5954ff52f17ff28953a2208e5b495bb61f551144c34f2a9d5c6270c7524b351a47df1a3b905d046f1e3a6cb6364ee9d4 languageName: node linkType: hard -"@tiptap/extension-gapcursor@npm:^3.2.0": - version: 3.2.0 - resolution: "@tiptap/extension-gapcursor@npm:3.2.0" +"@tiptap/extension-gapcursor@npm:^3.7.2": + version: 3.7.2 + resolution: "@tiptap/extension-gapcursor@npm:3.7.2" peerDependencies: - "@tiptap/extensions": ^3.2.0 - checksum: 10c0/9ecdd2f0a6ba39c48b8ed957d5c106185762b0ce8d938fef91a4cde69aa21770ce668492b444c4fa49786f15c0b5e4e230c2752e0c2b0a39d922da44da1be687 + "@tiptap/extensions": ^3.7.2 + checksum: 10c0/8ddf1c227b05c569630052d05c4e1afdd8d69abd1c3069a4f86a70ef7f64298df721f21ef99c58f118ef1bcae61551b04657b97229994b668b5073d5236ff031 languageName: node linkType: hard -"@tiptap/extension-hard-break@npm:^3.2.0": - version: 3.2.0 - resolution: "@tiptap/extension-hard-break@npm:3.2.0" +"@tiptap/extension-hard-break@npm:^3.7.2": + version: 3.7.2 + resolution: "@tiptap/extension-hard-break@npm:3.7.2" peerDependencies: - "@tiptap/core": ^3.2.0 - checksum: 10c0/cc3d975f51101a9e1cc05eac51c7e4a4805a76a49bce1c383a1374e3b22df100978e6f50db5e0fa5e4657f858f148c4c083636515de3f4edfdcd01a475f74504 + "@tiptap/core": ^3.7.2 + checksum: 10c0/0b0c3840ec6151b42926f9ac6d46210456c5bad8420e1028fba7baa4a5d385d91edd50eeb57afc1dd2feaf71cb4fab52c9290587c6e74c4893a0a1bf66efe098 languageName: node linkType: hard -"@tiptap/extension-heading@npm:^3.2.0": - version: 3.2.0 - resolution: "@tiptap/extension-heading@npm:3.2.0" +"@tiptap/extension-heading@npm:^3.7.2": + version: 3.7.2 + resolution: "@tiptap/extension-heading@npm:3.7.2" peerDependencies: - "@tiptap/core": ^3.2.0 - checksum: 10c0/53807dcdc25c8554b0596de1a7f79422ba37572a2694e098728197d3a8e390ebca1cc1469565a6903ea4c9522dee19970768eca5037dd0f03f78cb42a20c2074 + "@tiptap/core": ^3.7.2 + checksum: 10c0/20d67395db73de63d5c05f5c34295fb6a007f68a2275bcafac7581c6ae915471e4f9cfc5d4170ab3faf13016c293bc911cebb19b3f638c03cf5c5bbd5e71f36a languageName: node linkType: hard -"@tiptap/extension-horizontal-rule@npm:^3.2.0": - version: 3.2.0 - resolution: "@tiptap/extension-horizontal-rule@npm:3.2.0" +"@tiptap/extension-horizontal-rule@npm:^3.7.2": + version: 3.7.2 + resolution: "@tiptap/extension-horizontal-rule@npm:3.7.2" peerDependencies: - "@tiptap/core": ^3.2.0 - "@tiptap/pm": ^3.2.0 - checksum: 10c0/6d058169a5283d0cb3809650cfdb0219a4d2eaf9550812a5cbe25dbf014107f39d1105c1127efcbe10c34d6b3c460fd8818f644373a4049c95285026ed9d5930 + "@tiptap/core": ^3.7.2 + "@tiptap/pm": ^3.7.2 + checksum: 10c0/ec67fba9add4c668a3a1b87e8d59b676ef97f4788a104eef3a36f97f42b765d5b1befc3333159699957cafb950f6cf29092a604ae2f97ea2060df7825d3e021f languageName: node linkType: hard -"@tiptap/extension-image@npm:^3.2.0": - version: 3.2.0 - resolution: "@tiptap/extension-image@npm:3.2.0" +"@tiptap/extension-image@npm:^3.7.2": + version: 3.7.2 + resolution: "@tiptap/extension-image@npm:3.7.2" peerDependencies: - "@tiptap/core": ^3.2.0 - checksum: 10c0/489b83d42507990b83045627117ed5868b7edadd707b2df9d3b4cdf0ca6ad41fec76903c26cc136176d2745dec262ed95b5102ee26c9ef64b28cd66ea049ae5c + "@tiptap/core": ^3.7.2 + checksum: 10c0/caf2b868ee68d79a40f87643c14f7ec6c1f0a2fce54a22737d541f4490713362ae1c1914418cac0e11fb239e038b2fba0f5c6596253555f4f936c0af7650c06b languageName: node linkType: hard -"@tiptap/extension-italic@npm:^3.2.0": - version: 3.2.0 - resolution: "@tiptap/extension-italic@npm:3.2.0" +"@tiptap/extension-italic@npm:^3.7.2": + version: 3.7.2 + resolution: "@tiptap/extension-italic@npm:3.7.2" peerDependencies: - "@tiptap/core": ^3.2.0 - checksum: 10c0/0cdbb25212c345006f3b3600069c100a422425697d5ee8a2cf6f6a4c6856162bee4f6dd4fb5fa2267e82ce78f497769e1fb6de6851314e3dfe0ad1f200b6620c + "@tiptap/core": ^3.7.2 + checksum: 10c0/29682d2312ccfaae6c3330a0be40a49f94938ede2aad6b63fd4a27d52e7ff3569489ccb467690dbb274ec401307ee0511117336b773f7a66a9d6f7a187bca790 languageName: node linkType: hard -"@tiptap/extension-link@npm:^3.2.0": - version: 3.2.0 - resolution: "@tiptap/extension-link@npm:3.2.0" +"@tiptap/extension-link@npm:^3.7.2": + version: 3.7.2 + resolution: "@tiptap/extension-link@npm:3.7.2" dependencies: linkifyjs: "npm:^4.3.2" peerDependencies: - "@tiptap/core": ^3.2.0 - "@tiptap/pm": ^3.2.0 - checksum: 10c0/0f4ed8c479f4f4fa7d4f39b1b9ea2f345cd604cf76781d43ea6b976883025909329bc032df772aca93842ac671e673233548c718fde6314becd1dd784f35962e + "@tiptap/core": ^3.7.2 + "@tiptap/pm": ^3.7.2 + checksum: 10c0/b07c99fadae348f629b64b636ce9bc78d2a734fe2eddf17fce3f0e7c8dc74a2a9b3df181896a603d97533a01210b161285b644c4a4622c79ca01ee7ba56373f8 languageName: node linkType: hard -"@tiptap/extension-list-item@npm:^3.2.0": - version: 3.2.0 - resolution: "@tiptap/extension-list-item@npm:3.2.0" +"@tiptap/extension-list-item@npm:^3.7.2": + version: 3.7.2 + resolution: "@tiptap/extension-list-item@npm:3.7.2" peerDependencies: - "@tiptap/extension-list": ^3.2.0 - checksum: 10c0/a6bd57e6fe7d8c48943708961838a2f4c41918c9fd60b2330364cc9518c9ca898db4c9b00cb6986c7f13536f4c4bdd0a2a5db356f28ac773ccf1308aab537359 + "@tiptap/extension-list": ^3.7.2 + checksum: 10c0/68a2b21339ef71beb9e8cb7db6c01b31d9a034930c280b3be7447d0b48b93940fe2ccecc733a439c2dd23a996588255e77954ea56665ad13040b87ccc2bfce1a languageName: node linkType: hard -"@tiptap/extension-list-keymap@npm:^3.2.0": - version: 3.2.0 - resolution: "@tiptap/extension-list-keymap@npm:3.2.0" +"@tiptap/extension-list-keymap@npm:^3.7.2": + version: 3.7.2 + resolution: "@tiptap/extension-list-keymap@npm:3.7.2" peerDependencies: - "@tiptap/extension-list": ^3.2.0 - checksum: 10c0/3ab03d6db12386421d9ad4654183e59ad660dc298d50edb4abc525e94440a2ca523db7df6936b776404ae986690adeb608c164f9142643f3d4b593af10bd6ff1 + "@tiptap/extension-list": ^3.7.2 + checksum: 10c0/ec4f630e742d1af3f2fc0e15df776c9def9e39a9969af9d4d5760ed51b317a32df41eaaaab0ae374dbff4303bb39ba7bde9353b9cc23409f8a435ed630275598 languageName: node linkType: hard -"@tiptap/extension-list@npm:^3.2.0": - version: 3.2.0 - resolution: "@tiptap/extension-list@npm:3.2.0" +"@tiptap/extension-list@npm:^3.7.2": + version: 3.7.2 + resolution: "@tiptap/extension-list@npm:3.7.2" peerDependencies: - "@tiptap/core": ^3.2.0 - "@tiptap/pm": ^3.2.0 - checksum: 10c0/1ca383790e9683e340a0b4ca281424e59f81db5814ddfd4c01285aee74eeeddd6edd677a0b0c21d90069c438bc3e91701f246e87d47d9de29678b7466b8a01a8 + "@tiptap/core": ^3.7.2 + "@tiptap/pm": ^3.7.2 + checksum: 10c0/45906f9a63cbe295ace04fdacff90da19c76a19f208a7df191fcd59f05048e8f523db632faaa5cc8e3a38e4aac62b37d375ede2fa26a4356149b4d631d7ba6a2 languageName: node linkType: hard -"@tiptap/extension-mathematics@npm:^3.2.0": - version: 3.2.0 - resolution: "@tiptap/extension-mathematics@npm:3.2.0" +"@tiptap/extension-mathematics@npm:^3.7.2": + version: 3.7.2 + resolution: "@tiptap/extension-mathematics@npm:3.7.2" peerDependencies: - "@tiptap/core": ^3.2.0 - "@tiptap/pm": ^3.2.0 + "@tiptap/core": ^3.7.2 + "@tiptap/pm": ^3.7.2 katex: ^0.16.4 - checksum: 10c0/5d418f72d92530d373f9efa1c670af7f4dbd00904a71c38c93b24a6dc4f01505851c303e001d683ed94d694d0073b75c2a1f3d8eb208f3f962c178d2abc668d1 + checksum: 10c0/1ef7a7516d69ac4ee92cd5302e9ad658680b2bb1d8ecf8c170733b6a0aa021ba5dfe3f1e6f4e87ae8362f7a503de7aa6630326eb56d118a2fcbf7e5a02cd295b languageName: node linkType: hard -"@tiptap/extension-mention@npm:^3.2.0": - version: 3.2.0 - resolution: "@tiptap/extension-mention@npm:3.2.0" +"@tiptap/extension-mention@npm:^3.7.2": + version: 3.7.2 + resolution: "@tiptap/extension-mention@npm:3.7.2" peerDependencies: - "@tiptap/core": ^3.2.0 - "@tiptap/pm": ^3.2.0 - "@tiptap/suggestion": ^3.2.0 - checksum: 10c0/9910210cf70fb55129f07ac3a634c04cc0264a00450d438108029f946e3373287b28f16688412f4e0acce4d8b972ee319b9ad2d6981a852c153aa6dda5baab55 + "@tiptap/core": ^3.7.2 + "@tiptap/pm": ^3.7.2 + "@tiptap/suggestion": ^3.7.2 + checksum: 10c0/19ad721c6d3541eac3bfabea639c95c7c0e6aae0252300476e85828281df05386bd773f875367af51cde41ba655a9122f012f385b624ac8ab09b2059e9825051 languageName: node linkType: hard -"@tiptap/extension-node-range@npm:^3.2.0": - version: 3.2.0 - resolution: "@tiptap/extension-node-range@npm:3.2.0" +"@tiptap/extension-node-range@npm:^3.7.2": + version: 3.7.2 + resolution: "@tiptap/extension-node-range@npm:3.7.2" peerDependencies: - "@tiptap/core": ^3.2.0 - "@tiptap/pm": ^3.2.0 - checksum: 10c0/8e523f9c8562ac1e30f1d6a764f6254c43e5790cb3f02970ff07b02c939e5ab64425bb6d4f3d743593b6e275ec075c916e7f0e5cf13a5402d1d522bfa7b1da45 + "@tiptap/core": ^3.7.2 + "@tiptap/pm": ^3.7.2 + checksum: 10c0/ad8f1dd306b48584a7b8e07c814ef3ee47e9204feab63bad1c8e109cb1574b619b2b8db4c34047e148407a722ef0611f9161d23d2c7fc7c11438887117c34d7e languageName: node linkType: hard -"@tiptap/extension-ordered-list@npm:^3.2.0": - version: 3.2.0 - resolution: "@tiptap/extension-ordered-list@npm:3.2.0" +"@tiptap/extension-ordered-list@npm:^3.7.2": + version: 3.7.2 + resolution: "@tiptap/extension-ordered-list@npm:3.7.2" peerDependencies: - "@tiptap/extension-list": ^3.2.0 - checksum: 10c0/38fea71527bfa9bea8bb313b5922bb9cadfa49b45a3e20bba89c2512a4744ba5fc66a6d750ae82f15f52e0c2d9d68c00dd361b8edd53e66b225689e7200db0c0 + "@tiptap/extension-list": ^3.7.2 + checksum: 10c0/930d6cfde8f23050be1c8f2682c5a85598b9d2405a4bcf2deae4dda875664a3b4f8edef9523e721c332673080d8644cb76e62e70971684e78b2aa5a9a4210f1c languageName: node linkType: hard -"@tiptap/extension-paragraph@npm:^3.2.0": - version: 3.2.0 - resolution: "@tiptap/extension-paragraph@npm:3.2.0" +"@tiptap/extension-paragraph@npm:^3.7.2": + version: 3.7.2 + resolution: "@tiptap/extension-paragraph@npm:3.7.2" peerDependencies: - "@tiptap/core": ^3.2.0 - checksum: 10c0/f771bc247d2e6460840e2e42f2b53e040fd8ddcb6ef5dafae17254a48ad4159bc79adea2aab4c8614a9e2b824fc1318fd945d73d52acb4be71f99d9ba5e0b6c8 + "@tiptap/core": ^3.7.2 + checksum: 10c0/29ae27ac746382d3e8ae368ad285a3d4fc6876211e988e7516c072d1caf6650b17fbc55644c1f12c6d10f24c0aa25861eeda86daf86df5350d60a6424270d481 languageName: node linkType: hard -"@tiptap/extension-strike@npm:^3.2.0": - version: 3.2.0 - resolution: "@tiptap/extension-strike@npm:3.2.0" +"@tiptap/extension-strike@npm:^3.7.2": + version: 3.7.2 + resolution: "@tiptap/extension-strike@npm:3.7.2" peerDependencies: - "@tiptap/core": ^3.2.0 - checksum: 10c0/dc6225799fd9a99941edabcbed4e993fbfcdecb73c6b7a4a33db9883b2555e09a294d0dbf4e95ba5f3b2583e3356d8fc11741906e2cf6abe10198aa7f4c15dc1 + "@tiptap/core": ^3.7.2 + checksum: 10c0/1c034f39767e3a0f52c4a16b619e86712ad0aead0653eb4b64d31a31479e2b46c8fdf7fe26212ecbe0ef87c7382e535aaa2e9dc2472b5e7c089f3dbee686abac languageName: node linkType: hard -"@tiptap/extension-table-of-contents@npm:^3.2.0": - version: 3.2.0 - resolution: "@tiptap/extension-table-of-contents@npm:3.2.0" +"@tiptap/extension-table-of-contents@npm:^3.7.2": + version: 3.7.2 + resolution: "@tiptap/extension-table-of-contents@npm:3.7.2" dependencies: uuid: "npm:^10.0.0" peerDependencies: - "@tiptap/core": ^3.2.0 - "@tiptap/pm": ^3.2.0 - checksum: 10c0/24902ee6aa83f3394db33a9d07f6401abcd78f2b0630a30a0f430d50b448847b75f2ca304fc8ab6ff76bda94ffd7dd7ed43ab432be6534b1d506ce74f18a7808 + "@tiptap/core": ^3.7.2 + "@tiptap/pm": ^3.7.2 + checksum: 10c0/200959c4fa2dbe8925cbd334614be3cb20d71de142d97cbca2a63bae9667116734a01e0035a4966c91e9aa56c33724b2ddaa250086d5895d41d79e8c290c7851 languageName: node linkType: hard -"@tiptap/extension-text@npm:^3.2.0": - version: 3.2.0 - resolution: "@tiptap/extension-text@npm:3.2.0" +"@tiptap/extension-text@npm:^3.7.2": + version: 3.7.2 + resolution: "@tiptap/extension-text@npm:3.7.2" peerDependencies: - "@tiptap/core": ^3.2.0 - checksum: 10c0/99f414e58facc2b9ccc5398f0b3d549f219f2354f48c5eae76492f52427a4e4de44d004de98424354c470647080552ad7d4237d6a3fdf0b9fc32f8a3c77aa1d8 + "@tiptap/core": ^3.7.2 + checksum: 10c0/0766c877ce86762ca4426cd95fdc539d79dba4768cb9bd5cc9fc77bc5aea1a55942f3b3279e158e381b98f498163bb8cbf6222309c38834299909596cad91956 languageName: node linkType: hard -"@tiptap/extension-typography@npm:^3.2.0": - version: 3.2.0 - resolution: "@tiptap/extension-typography@npm:3.2.0" +"@tiptap/extension-typography@npm:^3.7.2": + version: 3.7.2 + resolution: "@tiptap/extension-typography@npm:3.7.2" peerDependencies: - "@tiptap/core": ^3.2.0 - checksum: 10c0/39c622f4da9c0bd0193c108e06da339f8654fc42202e60e49cba612573e4738fbd019cbcde7a2e524deac11919ee504b769f95913166e75e791e88037114840e + "@tiptap/core": ^3.7.2 + checksum: 10c0/3c94977e992e4c186c1c552f5d24075cab8702c045e6faad8b4a75f59d803f7310dd2c7df2c880b8544af2f3c5c039da2d901c1000dce15ef36e9d7ef2f066fb languageName: node linkType: hard -"@tiptap/extension-underline@npm:^3.2.0": - version: 3.2.0 - resolution: "@tiptap/extension-underline@npm:3.2.0" +"@tiptap/extension-underline@npm:^3.7.2": + version: 3.7.2 + resolution: "@tiptap/extension-underline@npm:3.7.2" peerDependencies: - "@tiptap/core": ^3.2.0 - checksum: 10c0/0c51bad02365d3028f912c1e68b5d80eeb5e4b0d10273d0f8c7b7aeb03cf3b903134c6267ad360ff2c78abb42ea9a078fad2ef8ab8696c6dd6f6a7deaa4f2da6 + "@tiptap/core": ^3.7.2 + checksum: 10c0/ecd1fdbe9bf411a5cdf43395d55e2caa9b706b558d2a6225ec557e3c9250a301bfe523c52772ff4324c060684f2614f0636555c5c412931a56fc236f7d146f2e languageName: node linkType: hard -"@tiptap/extensions@npm:^3.2.0": - version: 3.2.0 - resolution: "@tiptap/extensions@npm:3.2.0" +"@tiptap/extensions@npm:^3.7.2": + version: 3.7.2 + resolution: "@tiptap/extensions@npm:3.7.2" peerDependencies: - "@tiptap/core": ^3.2.0 - "@tiptap/pm": ^3.2.0 - checksum: 10c0/4a2023bdc097646b45b02d8152eb63893598db8719462fc9760fa735add54ff941ae7debdae15ad0587470156c75949087a260a0f5d2f2d5002efcfc267981e8 + "@tiptap/core": ^3.7.2 + "@tiptap/pm": ^3.7.2 + checksum: 10c0/f1ef63467c653b9580dd5beb64ccfe510d697cbffb6bbaf19c6ac3e812e0d6e4a2c5869c561bf05a2e10c6e7d6a38e38cf69ff61b436ff835adba7ea50010be0 languageName: node linkType: hard -"@tiptap/pm@npm:^3.2.0": - version: 3.2.0 - resolution: "@tiptap/pm@npm:3.2.0" +"@tiptap/markdown@npm:^3.7.2": + version: 3.7.2 + resolution: "@tiptap/markdown@npm:3.7.2" + dependencies: + marked: "npm:^16.1.2" + peerDependencies: + "@tiptap/core": ^3.7.2 + "@tiptap/pm": ^3.7.2 + checksum: 10c0/3cad11fc20911a82530fec166806f937f85f2856691acadc18b70053175af1b0b57b8816856cabaa564a7babcc7cd90b021a3ec3631cd23f8ec9f7b38ee1f71e + languageName: node + linkType: hard + +"@tiptap/pm@npm:^3.7.2": + version: 3.7.2 + resolution: "@tiptap/pm@npm:3.7.2" dependencies: prosemirror-changeset: "npm:^2.3.0" prosemirror-collab: "npm:^1.3.1" @@ -11892,22 +11889,24 @@ __metadata: prosemirror-trailing-node: "npm:^3.0.0" prosemirror-transform: "npm:^1.10.2" prosemirror-view: "npm:^1.38.1" - checksum: 10c0/53a43b0e832c48a0038200c570253de1c779ba317b0bc7fddb8f7c79443d70070210d3b5376009ca9e9e69ed80f3b2813ac3199544de76de88049246191b8908 + checksum: 10c0/2f14f73583e023326e181d273fad346bdf914a13e4d918e8015b00d3ef6e3cd953221fb1700cd533327469a8150ac964120237ac29366e40e18b314c16dc4bc6 languageName: node linkType: hard -"@tiptap/react@npm:^3.2.0": - version: 3.2.0 - resolution: "@tiptap/react@npm:3.2.0" +"@tiptap/react@npm:^3.7.2": + version: 3.7.2 + resolution: "@tiptap/react@npm:3.7.2" dependencies: - "@tiptap/extension-bubble-menu": "npm:^3.2.0" - "@tiptap/extension-floating-menu": "npm:^3.2.0" + "@tiptap/extension-bubble-menu": "npm:^3.7.2" + "@tiptap/extension-floating-menu": "npm:^3.7.2" "@types/use-sync-external-store": "npm:^0.0.6" fast-deep-equal: "npm:^3.1.3" use-sync-external-store: "npm:^1.4.0" peerDependencies: - "@tiptap/core": ^3.2.0 - "@tiptap/pm": ^3.2.0 + "@tiptap/core": ^3.7.2 + "@tiptap/pm": ^3.7.2 + "@types/react": ^17.0.0 || ^18.0.0 || ^19.0.0 + "@types/react-dom": ^17.0.0 || ^18.0.0 || ^19.0.0 react: ^17.0.0 || ^18.0.0 || ^19.0.0 react-dom: ^17.0.0 || ^18.0.0 || ^19.0.0 dependenciesMeta: @@ -11915,49 +11914,49 @@ __metadata: optional: true "@tiptap/extension-floating-menu": optional: true - checksum: 10c0/efc2c93bb40f430ca013de3624623a577fd6eec4838f6be9da89834e8088eb7701f2df6d086ac53728432a929b8bae582b404b51efb71a22480913d9dfb98398 + checksum: 10c0/734ca46aa76833fb1f822752c201b00826055f547a6f539a40d7e6bd49bc1cbef57a157593056c7b0a1781e14fb482d451e1a249ce26ba90ca60456d2385bf6d languageName: node linkType: hard -"@tiptap/starter-kit@npm:^3.2.0": - version: 3.2.0 - resolution: "@tiptap/starter-kit@npm:3.2.0" +"@tiptap/starter-kit@npm:^3.7.2": + version: 3.7.2 + resolution: "@tiptap/starter-kit@npm:3.7.2" dependencies: - "@tiptap/core": "npm:^3.2.0" - "@tiptap/extension-blockquote": "npm:^3.2.0" - "@tiptap/extension-bold": "npm:^3.2.0" - "@tiptap/extension-bullet-list": "npm:^3.2.0" - "@tiptap/extension-code": "npm:^3.2.0" - "@tiptap/extension-code-block": "npm:^3.2.0" - "@tiptap/extension-document": "npm:^3.2.0" - "@tiptap/extension-dropcursor": "npm:^3.2.0" - "@tiptap/extension-gapcursor": "npm:^3.2.0" - "@tiptap/extension-hard-break": "npm:^3.2.0" - "@tiptap/extension-heading": "npm:^3.2.0" - "@tiptap/extension-horizontal-rule": "npm:^3.2.0" - "@tiptap/extension-italic": "npm:^3.2.0" - "@tiptap/extension-link": "npm:^3.2.0" - "@tiptap/extension-list": "npm:^3.2.0" - "@tiptap/extension-list-item": "npm:^3.2.0" - "@tiptap/extension-list-keymap": "npm:^3.2.0" - "@tiptap/extension-ordered-list": "npm:^3.2.0" - "@tiptap/extension-paragraph": "npm:^3.2.0" - "@tiptap/extension-strike": "npm:^3.2.0" - "@tiptap/extension-text": "npm:^3.2.0" - "@tiptap/extension-underline": "npm:^3.2.0" - "@tiptap/extensions": "npm:^3.2.0" - "@tiptap/pm": "npm:^3.2.0" - checksum: 10c0/94638018e7bb5b43ce9ad008d1db4760c7afb8fb57c85d47d450ac78b21505c0257e21292d99c0ff1383dbe214f25e3ff276d074add88147e6b090ee146d3894 + "@tiptap/core": "npm:^3.7.2" + "@tiptap/extension-blockquote": "npm:^3.7.2" + "@tiptap/extension-bold": "npm:^3.7.2" + "@tiptap/extension-bullet-list": "npm:^3.7.2" + "@tiptap/extension-code": "npm:^3.7.2" + "@tiptap/extension-code-block": "npm:^3.7.2" + "@tiptap/extension-document": "npm:^3.7.2" + "@tiptap/extension-dropcursor": "npm:^3.7.2" + "@tiptap/extension-gapcursor": "npm:^3.7.2" + "@tiptap/extension-hard-break": "npm:^3.7.2" + "@tiptap/extension-heading": "npm:^3.7.2" + "@tiptap/extension-horizontal-rule": "npm:^3.7.2" + "@tiptap/extension-italic": "npm:^3.7.2" + "@tiptap/extension-link": "npm:^3.7.2" + "@tiptap/extension-list": "npm:^3.7.2" + "@tiptap/extension-list-item": "npm:^3.7.2" + "@tiptap/extension-list-keymap": "npm:^3.7.2" + "@tiptap/extension-ordered-list": "npm:^3.7.2" + "@tiptap/extension-paragraph": "npm:^3.7.2" + "@tiptap/extension-strike": "npm:^3.7.2" + "@tiptap/extension-text": "npm:^3.7.2" + "@tiptap/extension-underline": "npm:^3.7.2" + "@tiptap/extensions": "npm:^3.7.2" + "@tiptap/pm": "npm:^3.7.2" + checksum: 10c0/15bc8b37759e7e8fc7f53d31979a377c469ec4bbb33fc2301eac9b3e89b0a963346038c17614284881eab3867bec1974ed7f4b2b1ec1842c95d332b8152c7e3b languageName: node linkType: hard -"@tiptap/suggestion@npm:^3.2.0": - version: 3.2.0 - resolution: "@tiptap/suggestion@npm:3.2.0" +"@tiptap/suggestion@npm:^3.7.2": + version: 3.7.2 + resolution: "@tiptap/suggestion@npm:3.7.2" peerDependencies: - "@tiptap/core": ^3.2.0 - "@tiptap/pm": ^3.2.0 - checksum: 10c0/3ffec8fac8fe1acd966bbcf4b8996af2955ad01c47e10facdcc0e1b4b749278a88eb20bfe8252e266a8664281f12de5bab101ef10f6045486d2df462563d3a22 + "@tiptap/core": ^3.7.2 + "@tiptap/pm": ^3.7.2 + checksum: 10c0/b761e59c2845a93846db3bef168dc1f6c8ed692d7fd34d7154bd759dea1516d6a6c25eb6536791ac36546799d941dc0fba242f5fcb79c5310979a4261107411f languageName: node linkType: hard @@ -13923,21 +13922,22 @@ __metadata: "@testing-library/jest-dom": "npm:^6.6.3" "@testing-library/react": "npm:^16.3.0" "@testing-library/user-event": "npm:^14.6.1" - "@tiptap/extension-collaboration": "npm:^3.2.0" - "@tiptap/extension-drag-handle": "patch:@tiptap/extension-drag-handle@npm%3A3.2.0#~/.yarn/patches/@tiptap-extension-drag-handle-npm-3.2.0-5a9ebff7c9.patch" - "@tiptap/extension-drag-handle-react": "npm:^3.2.0" - "@tiptap/extension-image": "npm:^3.2.0" - "@tiptap/extension-list": "npm:^3.2.0" - "@tiptap/extension-mathematics": "npm:^3.2.0" - "@tiptap/extension-mention": "npm:^3.2.0" - "@tiptap/extension-node-range": "npm:^3.2.0" - "@tiptap/extension-table-of-contents": "npm:^3.2.0" - "@tiptap/extension-typography": "npm:^3.2.0" - "@tiptap/extension-underline": "npm:^3.2.0" - "@tiptap/pm": "npm:^3.2.0" - "@tiptap/react": "npm:^3.2.0" - "@tiptap/starter-kit": "npm:^3.2.0" - "@tiptap/suggestion": "npm:^3.2.0" + "@tiptap/extension-collaboration": "npm:^3.7.2" + "@tiptap/extension-drag-handle": "npm:^3.7.2" + "@tiptap/extension-drag-handle-react": "npm:^3.7.2" + "@tiptap/extension-image": "npm:^3.7.2" + "@tiptap/extension-list": "npm:^3.7.2" + "@tiptap/extension-mathematics": "npm:^3.7.2" + "@tiptap/extension-mention": "npm:^3.7.2" + "@tiptap/extension-node-range": "npm:^3.7.2" + "@tiptap/extension-table-of-contents": "npm:^3.7.2" + "@tiptap/extension-typography": "npm:^3.7.2" + "@tiptap/extension-underline": "npm:^3.7.2" + "@tiptap/markdown": "npm:^3.7.2" + "@tiptap/pm": "npm:^3.7.2" + "@tiptap/react": "npm:^3.7.2" + "@tiptap/starter-kit": "npm:^3.7.2" + "@tiptap/suggestion": "npm:^3.7.2" "@tiptap/y-tiptap": "npm:^3.0.0" "@truto/turndown-plugin-gfm": "npm:^1.0.2" "@tryfabric/martian": "npm:^1.2.4" @@ -21995,6 +21995,15 @@ __metadata: languageName: node linkType: hard +"marked@npm:^16.1.2": + version: 16.4.1 + resolution: "marked@npm:16.4.1" + bin: + marked: bin/marked.js + checksum: 10c0/2b5bc04db3453e493ea78758f1fe5006d725ec90aab9bf991242b0820a069d9748e2ac26fb0e2603848a409649c77900e599f3f66d251d0f7e5f606576090a54 + languageName: node + linkType: hard + "matcher@npm:^3.0.0": version: 3.0.0 resolution: "matcher@npm:3.0.0"