关于 word-processor-expert
Word Processor Expert 是面向 id8composer 的文字处理功能开发指南,基于 Tiptap 和 ProseMirror。它提供格式设置、文档导出、打印、快捷键及性能优化的实现范例。
主要功能
- 01Tiptap 扩展实现示例
- 02格式设置与编辑器控件模式
- 03DOCX 和 PDF 导出指南
- 04打印布局 CSS 与快捷键
- 05大型文档性能建议
适用场景
- 01添加文本对齐或字体控制
- 02实现文档导出
- 03构建适合打印的页面布局
- 04优化大型文档的性能
安装
复制下面的命令,在终端中运行。
$
npx skills add https:/ / github.com/ jmsktm/ claude-settings --skill word-processor-expert安装后在 Claude Code 中输入/word-processor-expert确认已加载。
详细介绍
文字处理器专家技能
概述
此技能为使用 Tiptap(基于 ProseMirror)在 id8composer 中实现专业文字处理器功能提供快速指导。借助现代 Web 技术,助你达到 Microsoft Word、Apple Pages 和 Google Docs 的品质。
当前技术栈(id8composer)
- 编辑器:Tiptap v3.10.7(基于 ProseMirror)
- 框架:Next.js 15.5.6 + React 19
- 状态管理:Zustand
- 导出:
docxv9.5.1、jspdfv3.0.3
快速参考:缺失的功能
❌ 尚未实现
- 文本对齐(左对齐/居中/右对齐/两端对齐)
- 字体和字号控制
- 标题样式(带样式的 H1-H6)
- 行距(1.0、1.5、2.0)
- 段落间距
- 缩进控制
- 查找和替换
- 分页符
- 页眉/页脚
- 页面设置(页边距、方向)
- 正确的 DOCX/PDF 导出
- 样式/模板
- 评论/修订
✅ 已可使用
- 粗体、斜体、下划线
- 列表(项目符号、有序列表)
- 表格
- 图片、链接
- 颜色和高亮
- 撤销/重做
- 自动保存
- 字符数/字数统计
必备 Tiptap 扩展
安装缺失的扩展
npm install @tiptap/extension-text-align
npm install @tiptap/extension-font-family
npm install @tiptap/extension-heading
npm install @tiptap/extension-hard-break
文本对齐
// Add to editor extensions
import { TextAlign } from '@tiptap/extension-text-align'
const editor = useEditor({
extensions: [
TextAlign.configure({
types: ['heading', 'paragraph'],
alignments: ['left', 'center', 'right', 'justify'],
defaultAlignment: 'left',
}),
// ... other extensions
],
})
// Toolbar buttons
<button onClick={() => editor.chain().focus().setTextAlign('left').run()}>
<AlignLeft />
</button>
<button onClick={() => editor.chain().focus().setTextAlign('center').run()}>
<AlignCenter />
</button>
<button onClick={() => editor.chain().focus().setTextAlign('right').run()}>
<AlignRight />
</button>
<button onClick={() => editor.chain().focus().setTextAlign('justify').run()}>
<AlignJustify />
</button>
字体和字号
import { FontFamily } from '@tiptap/extension-font-family'
import { TextStyle } from '@tiptap/extension-text-style' // Already installed
// Custom Font Size extension
import { Extension } from '@tiptap/core'
export const FontSize = Extension.create({
name: 'fontSize',
addOptions() {
return {
types: ['textStyle'],
}
},
addGlobalAttributes() {
return [
{
types: this.options.types,
attributes: {
fontSize: {
default: null,
parseHTML: element => element.style.fontSize.replace('px', ''),
renderHTML: attributes => {
if (!attributes.fontSize) return {}
return {
style: `font-size: ${attributes.fontSize}px`,
}
},
},
},
},
]
},
addCommands() {
return {
setFontSize: (fontSize: string) => ({ chain }) => {
return chain().setMark('textStyle', { fontSize }).run()
},
unsetFontSize: () => ({ chain }) => {
return chain().setMark('textStyle', { fontSize: null }).run()
},
}
},
})
// Usage in editor
const editor = useEditor({
extensions: [
TextStyle, // Required
FontFamily.configure({
types: ['textStyle'],
}),
FontSize,
// ...
],
})
// Dropdowns in toolbar
<select onChange={(e) => editor.chain().focus().setFontFamily(e.target.value).run()}>
<option value="Arial">Arial</option>
<option value="Times New Roman">Times New Roman</option>
<option value="Courier New">Courier New</option>
<option value="Georgia">Georgia</option>
</select>
<select onChange={(e) => editor.chain().focus().setFontSize(e.target.value).run()}>
<option value="12">12pt</option>
<option value="14">14pt</option>
<option value="16">16pt</option>
<option value="18">18pt</option>
<option value="24">24pt</option>
</select>
行距
// Custom Line Height extension
import { Extension } from '@tiptap/core'
export const LineHeight = Extension.create({
name: 'lineHeight',
addOptions() {
return {
types: ['paragraph', 'heading'],
defaultLineHeight: '1.5',
}
},
addGlobalAttributes() {
return [
{
types: this.options.types,
attributes: {
lineHeight: {
default: this.options.defaultLineHeight,
parseHTML: element => element.style.lineHeight || this.options.defaultLineHeight,
renderHTML: attributes => {
if (!attributes.lineHeight) return {}
return { style: `line-height: ${attributes.lineHeight}` }
},
},
},
},
]
},
addCommands() {
return {
setLineHeight: (lineHeight: string) => ({ commands }) => {
return this.options.types.every((type: string) =>
commands.updateAttributes(type, { lineHeight })
)
},
}
},
})
// Toolbar dropdown
<select onChange={(e) => editor.chain().focus().setLineHeight(e.target.value).run()}>
<option value="1.0">Single</option>
<option value="1.15">1.15</option>
<option value="1.5">1.5</option>
<option value="2.0">Double</option>
</select>
缩进
// Install @tiptap/extension-indent if available, or create custom
export const Indent = Extension.create({
name: 'indent',
addOptions() {
return {
types: ['paragraph', 'heading'],
minIndent: 0,
maxIndent: 10,
}
},
addGlobalAttributes() {
return [
{
types: this.options.types,
attributes: {
indent: {
default: 0,
parseHTML: element => {
const indent = element.style.paddingLeft
return indent ? parseInt(indent) / 40 : 0
},
renderHTML: attributes => {
if (!attributes.indent) return {}
return { style: `padding-left: ${attributes.indent * 40}px` }
},
},
},
},
]
},
addCommands() {
return {
indent: () => ({ commands, state }) => {
const { indent = 0 } = state.selection.$from.node().attrs
if (indent >= this.options.maxIndent) return false
return this.options.types.every((type: string) =>
commands.updateAttributes(type, { indent: indent + 1 })
)
},
outdent: () => ({ commands, state }) => {
const { indent = 0 } = state.selection.$from.node().attrs
if (indent <= this.options.minIndent) return false
return this.options.types.every((type: string) =>
commands.updateAttributes(type, { indent: indent - 1 })
)
},
}
},
addKeyboardShortcuts() {
return {
Tab: () => this.editor.commands.indent(),
'Shift-Tab': () => this.editor.commands.outdent(),
}
},
})
// Toolbar buttons
<button onClick={() => editor.chain().focus().indent().run()}>
<IndentIncrease />
</button>
<button onClick={() => editor.chain().focus().outdent().run()}>
<IndentDecrease />
</button>
分页符
// Custom Page Break node
import { Node, mergeAttributes } from '@tiptap/core'
export const PageBreak = Node.create({
name: 'pageBreak',
group: 'block',
parseHTML() {
return [{ tag: 'div.page-break' }]
},
renderHTML({ HTMLAttributes }) {
return ['div', mergeAttributes(HTMLAttributes, { class: 'page-break' }), ['hr']]
},
addCommands() {
return {
setPageBreak: () => ({ commands }) => {
return commands.insertContent({ type: this.name })
},
}
},
})
// CSS for page breaks
/* styles/editor.css */
.page-break {
page-break-after: always;
break-after: page;
margin: 2rem 0;
border: none;
border-top: 2px dashed #ccc;
text-align: center;
}
.page-break::after {
content: "Page Break";
display: inline-block;
position: relative;
top: -0.7em;
padding: 0 1em;
background: white;
color: #999;
font-size: 0.8em;
}
// Toolbar button
<button onClick={() => editor.chain().focus().setPageBreak().run()}>
Insert Page Break
</button>
查找和替换
'use client'
import { useState } from 'react'
export function FindReplace({ editor }: { editor: Editor }) {
const [searchTerm, setSearchTerm] = useState('')
const [replaceTerm, setReplaceTerm] = useState('')
const [caseSensitive, setCaseSensitive] = useState(false)
const findNext = () => {
const content = editor.getText()
const flags = caseSensitive ? 'g' : 'gi'
const regex = new RegExp(searchTerm, flags)
const matches = [...content.matchAll(regex)]
if (matches.length > 0) {
// Highlight first match
const match = matches[0]
// Implementation: Use Tiptap's TextSelection to highlight
}
}
const replaceNext = () => {
const { from, to } = editor.state.selection
const selectedText = editor.state.doc.textBetween(from, to)
if (selectedText === searchTerm || (!caseSensitive && selectedText.toLowerCase() === searchTerm.toLowerCase())) {
editor.chain().focus().insertContentAt({ from, to }, replaceTerm).run()
findNext()
}
}
const replaceAll = () => {
const content = editor.getHTML()
const flags = caseSensitive ? 'g' : 'gi'
const regex = new RegExp(searchTerm, flags)
const newContent = content.replace(regex, replaceTerm)
editor.commands.setContent(newContent)
}
return (
<div className="flex gap-2 p-4 border rounded">
<input
type="text"
placeholder="Find"
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="border px-2 py-1"
/>
<input
type="text"
placeholder="Replace"
value={replaceTerm}
onChange={(e) => setReplaceTerm(e.target.value)}
className="border px-2 py-1"
/>
<label className="flex items-center gap-1">
<input
type="checkbox"
checked={caseSensitive}
onChange={(e) => setCaseSensitive(e.target.checked)}
/>
Case sensitive
</label>
<button onClick={findNext} className="px-3 py-1 bg-blue-500 text-white rounded">
Find Next
</button>
<button onClick={replaceNext} className="px-3 py-1 bg-blue-500 text-white rounded">
Replace
</button>
<button onClick={replaceAll} className="px-3 py-1 bg-red-500 text-white rounded">
Replace All
</button>
</div>
)
}
专业 DOCX 导出
import { Document, Paragraph, TextRun, HeadingLevel, AlignmentType, Packer } from 'docx'
import { saveAs } from 'file-saver'
export async function exportToDocx(editor: Editor, filename: string) {
// Convert Tiptap JSON to DOCX structure
const doc = new Document({
sections: [{
properties: {},
children: convertTiptapToDocx(editor.getJSON()),
}],
})
const blob = await Packer.toBlob(doc)
saveAs(blob, `${filename}.docx`)
}
function convertTiptapToDocx(tiptapJson: any): Paragraph[] {
const paragraphs: Paragraph[] = []
tiptapJson.content?.forEach((node: any) => {
if (node.type === 'paragraph') {
const runs: TextRun[] = []
node.content?.forEach((inline: any) => {
if (inline.type === 'text') {
runs.push(new TextRun({
text: inline.text,
bold: inline.marks?.some((m: any) => m.type === 'bold'),
italics: inline.marks?.some((m: any) => m.type === 'italic'),
underline: inline.marks?.some((m: any) => m.type === 'underline') ? {} : undefined,
color: inline.marks?.find((m: any) => m.type === 'textStyle')?.attrs?.color?.replace('#', ''),
size: parseInt(inline.marks?.find((m: any) => m.type === 'textStyle')?.attrs?.fontSize || '24') * 2, // Half-points
}))
}
})
paragraphs.push(new Paragraph({
children: runs,
alignment: getAlignment(node.attrs?.textAlign),
spacing: {
before: 120,
after: 120,
line: parseInt(node.attrs?.lineHeight || '1.5') * 240,
},
indent: {
left: (node.attrs?.indent || 0) * 720, // Twips (1/20th of a point)
},
}))
} else if (node.type === 'heading') {
paragraphs.push(new Paragraph({
text: node.content?.[0]?.text || '',
heading: getHeadingLevel(node.attrs?.level),
alignment: getAlignment(node.attrs?.textAlign),
}))
}
})
return paragraphs
}
function getAlignment(align: string): AlignmentType {
switch (align) {
case 'left': return AlignmentType.LEFT
case 'center': return AlignmentType.CENTER
case 'right': return AlignmentType.RIGHT
case 'justify': return AlignmentType.JUSTIFIED
default: return AlignmentType.LEFT
}
}
function getHeadingLevel(level: number): HeadingLevel {
const levels = [
HeadingLevel.HEADING_1,
HeadingLevel.HEADING_2,
HeadingLevel.HEADING_3,
HeadingLevel.HEADING_4,
HeadingLevel.HEADING_5,
HeadingLevel.HEADING_6,
]
return levels[level - 1] || HeadingLevel.HEADING_1
}
专业 PDF 导出
import jsPDF from 'jspdf'
export function exportToPdf(editor: Editor, filename: string) {
const doc = new jsPDF({
orientation: 'portrait',
unit: 'pt',
format: 'letter',
})
const content = editor.getHTML()
// Convert HTML to PDF (basic approach)
// For production, consider using html2pdf or server-side rendering
doc.html(content, {
callback: (doc) => {
doc.save(`${filename}.pdf`)
},
margin: [72, 72, 72, 72], // 1 inch margins
x: 72,
y: 72,
width: 468, // 6.5 inches at 72 DPI
windowWidth: 816, // 8.5 inches at 96 DPI
})
}
// Better approach: Server-side with Puppeteer
// app/api/export-pdf/route.ts
import puppeteer from 'puppeteer'
export async function POST(req: Request) {
const { html } = await req.json()
const browser = await puppeteer.launch()
const page = await browser.newPage()
await page.setContent(html, { waitUntil: 'networkidle0' })
const pdf = await page.pdf({
format: 'letter',
margin: {
top: '1in',
right: '1in',
bottom: '1in',
left: '1in',
},
printBackground: true,
})
await browser.close()
return new Response(pdf, {
headers: {
'Content-Type': 'application/pdf',
'Content-Disposition': 'attachment; filename=document.pdf',
},
})
}
打印版式视图
// Print-friendly CSS
/* styles/print-layout.css */
@media print {
@page {
size: letter;
margin: 1in;
}
.editor-content {
font-family: 'Times New Roman', serif;
font-size: 12pt;
line-height: 1.5;
}
.page-break {
page-break-after: always;
}
.no-print {
display: none;
}
}
/* Print preview mode */
.print-preview .editor-content {
width: 8.5in;
min-height: 11in;
margin: 0 auto;
padding: 1in;
background: white;
box-shadow: 0 0 10px rgba(0,0,0,0.1);
}
.print-preview .page {
width: 8.5in;
height: 11in;
margin-bottom: 0.5in;
background: white;
box-shadow: 0 0 10px rgba(0,0,0,0.1);
page-break-after: always;
}
键盘快捷键参考
// Add to editor configuration
const editor = useEditor({
editorProps: {
handleKeyDown: (view, event) => {
// Cmd/Ctrl + B: Bold
// Cmd/Ctrl + I: Italic
// Cmd/Ctrl + U: Underline
// Cmd/Ctrl + E: Center align
// Cmd/Ctrl + L: Left align
// Cmd/Ctrl + R: Right align
// Cmd/Ctrl + J: Justify
// Cmd/Ctrl + F: Find
// Cmd/Ctrl + H: Replace
// Cmd/Ctrl + S: Save
// Cmd/Ctrl + P: Print
// Cmd/Ctrl + Z: Undo
// Cmd/Ctrl + Y: Redo
// Tab: Increase indent
// Shift+Tab: Decrease indent
return false // Let Tiptap handle defaults
},
},
})
性能优化
// Virtual scrolling for large documents
import { FixedSizeList } from 'react-window'
// Debounced auto-save
import { useDebouncedCallback } from 'use-debounce'
const debouncedSave = useDebouncedCallback(
(content) => {
// Save to backend
saveDocument(content)
},
2000 // 2 second delay
)
// Lazy load heavy extensions
const editor = useEditor({
extensions: [
StarterKit,
// Conditionally load based on feature flags
...(features.tables ? [Table, TableRow, TableCell] : []),
...(features.images ? [Image] : []),
],
})
何时使用此技能
在需要以下内容时调用此技能:
- 快速实现文本编辑器功能
- Tiptap 扩展示例
- 文档导出功能(DOCX/PDF)
- 格式工具栏模式
- 键盘快捷键
- 打印版式 CSS
- 大型文档的性能技巧
- 实现与文字处理器相当的功能
如需深入的架构工作,请改用 text-editor-architect Agent。
评论
支持 Markdown