word-processor-expert について
Word Processor Expert は、Tiptap と ProseMirror を使った id8composer の文書編集機能を実装するためのガイドスキルです。書式設定、文書エクスポート、印刷、ショートカット、パフォーマンスの実装例を紹介します。
主な機能
- 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 にプロフェッショナルなワープロ機能を実装するための迅速なガイダンスを提供します。最新のウェブテクノロジーを使って、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 エージェントを使用してください。
コメント
Markdown 対応