Block Editor 集成官方Markdown扩展@tiptap/markdown
This commit is contained in:
parent
ce0831b7ec
commit
23e1a8d720
@ -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",
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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(),
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user