Block Editor 集成官方Markdown扩展@tiptap/markdown

This commit is contained in:
jingrow 2026-06-22 02:17:18 +08:00
parent ce0831b7ec
commit 23e1a8d720
3 changed files with 8 additions and 100 deletions

View File

@ -45,6 +45,7 @@
"@tiptap/extension-unique-id": "^3.24.0",
"@tiptap/extensions": "^3.0.0",
"@tiptap/html": "^3.24.0",
"@tiptap/markdown": "^3.27.1",
"@tiptap/starter-kit": "^3.0.0",
"@tiptap/vue-3": "^3.0.0",
"@univerjs/preset-sheets-core": "^0.23.0",

View File

@ -746,15 +746,15 @@ export async function callInlineAI(options: InlineAICallOptions) {
/^#{1,6}\s/.test(line) || /^[-*]\s/.test(line) || /^\d+\.\s/.test(line) || /^>\s/.test(line)
)
if (needsRestructure) {
const structuredNodes = parseStructuredContent(accumulated)
if (structuredNodes.length > 0) {
const parsed = editor.markdown!.parse(accumulated)
if (parsed.content && parsed.content.length > 0) {
try {
editor.chain().focus()
.deleteRange({ from: startPos, to: endPos })
.insertContentAt(startPos, structuredNodes)
.insertContentAt(startPos, parsed.content)
.run()
} catch (e) {
console.warn('[AI Command] structured parsing failed, keeping paragraph format', e)
console.warn('[AI Command] markdown parsing failed, keeping paragraph format', e)
}
}
}
@ -803,99 +803,3 @@ export async function callInlineAI(options: InlineAICallOptions) {
callbacks?.onError?.(e.message || 'AI call exception')
}
}
// ── 文本到 ProseMirror 结构化节点的解析器 ─────────────────────────────────────
/**
* AI Markdown ProseMirror JSON
* heading (h1-h6), bulletList, orderedList, blockquote, paragraph
*/
export function parseStructuredContent(text: string): Record<string, any>[] {
const lines = text.split('\n')
const nodes: Record<string, any>[] = []
let listItems: Record<string, any>[] | null = null
let listOrdered = false
for (const line of lines) {
if (!line.trim()) {
flushList()
continue
}
// Heading: # ~ ######
const headingMatch = line.match(/^(#{1,6})\s+(.+)$/)
if (headingMatch) {
flushList()
nodes.push({
type: 'heading',
attrs: { level: headingMatch[1].length },
content: [{ type: 'text', text: headingMatch[2] }],
})
continue
}
// Ordered list: 1. ...
const orderedMatch = line.match(/^(\d+)\.\s+(.+)$/)
if (orderedMatch) {
if (!listItems || !listOrdered) {
flushList()
listItems = []
listOrdered = true
}
listItems.push({
type: 'listItem',
content: [{ type: 'paragraph', content: [{ type: 'text', text: orderedMatch[2] }] }],
})
continue
}
// Unordered list: - / * ...
const unorderedMatch = line.match(/^[-*]\s+(.+)$/)
if (unorderedMatch) {
if (!listItems || listOrdered) {
flushList()
listItems = []
listOrdered = false
}
listItems.push({
type: 'listItem',
content: [{ type: 'paragraph', content: [{ type: 'text', text: unorderedMatch[1] }] }],
})
continue
}
// Blockquote: > ...
const quoteMatch = line.match(/^>\s+(.+)$/)
if (quoteMatch) {
flushList()
nodes.push({
type: 'blockquote',
content: [{ type: 'paragraph', content: [{ type: 'text', text: quoteMatch[1] }] }],
})
continue
}
// 普通段落
flushList()
nodes.push({
type: 'paragraph',
content: [{ type: 'text', text: line }],
})
}
flushList()
function flushList() {
if (listItems && listItems.length > 0) {
if (listOrdered) {
nodes.push({ type: 'orderedList', content: listItems })
} else {
nodes.push({ type: 'bulletList', content: listItems })
}
listItems = null
listOrdered = false
}
}
return nodes
}

View File

@ -20,6 +20,7 @@ import { TaskList, TaskItem } from '@tiptap/extension-list'
import { Subscript } from '@tiptap/extension-subscript'
import { Superscript } from '@tiptap/extension-superscript'
import Typography from '@tiptap/extension-typography'
import { Markdown } from '@tiptap/markdown'
import Link from '@tiptap/extension-link'
import { createLowlight, all } from 'lowlight'
import CodeBlockLowlight from '@tiptap/extension-code-block-lowlight'
@ -149,6 +150,8 @@ export function buildExtensions(options: EditorSetupOptions): Extensions {
AIReviewExtension,
// ── AI 流式写入 Ghost Text流式输出期间的幽灵文本 + 打字光标) ──
AIStreamingExtension,
// ── Markdown 解析/序列化parse/serialize不开启全局 contentType: 'markdown' ──
Markdown,
// ── ★ 从 Registry 自动收集的块扩展 ──
// 新增块只需在 blocks/<name>/index.ts 中声明,无需改此文件
...blockRegistry.getAllExtensions(),