| import OrderedMap from 'orderedmap'; |
| import { |
| Schema, |
| type Node as ProsemirrorNode, |
| type MarkSpec, |
| DOMParser, |
| } from 'prosemirror-model'; |
| import { schema } from 'prosemirror-schema-basic'; |
| import { addListNodes } from 'prosemirror-schema-list'; |
| import { EditorState } from 'prosemirror-state'; |
| import { EditorView } from 'prosemirror-view'; |
| import React, { useEffect, useRef } from 'react'; |
| import { renderToString } from 'react-dom/server'; |
| import { Streamdown } from 'streamdown'; |
|
|
| import { diffEditor, DiffType } from '@/lib/editor/diff'; |
|
|
| const diffSchema = new Schema({ |
| nodes: addListNodes(schema.spec.nodes, 'paragraph block*', 'block'), |
| marks: OrderedMap.from({ |
| ...schema.spec.marks.toObject(), |
| diffMark: { |
| attrs: { type: { default: '' } }, |
| toDOM(mark) { |
| let className = ''; |
|
|
| switch (mark.attrs.type) { |
| case DiffType.Inserted: |
| className = |
| 'bg-green-100 text-green-700 dark:bg-green-500/70 dark:text-green-300'; |
| break; |
| case DiffType.Deleted: |
| className = |
| 'bg-red-100 line-through text-red-600 dark:bg-red-500/70 dark:text-red-300'; |
| break; |
| default: |
| className = ''; |
| } |
| return ['span', { class: className }, 0]; |
| }, |
| } as MarkSpec, |
| }), |
| }); |
|
|
| function computeDiff(oldDoc: ProsemirrorNode, newDoc: ProsemirrorNode) { |
| return diffEditor(diffSchema, oldDoc.toJSON(), newDoc.toJSON()); |
| } |
|
|
| type DiffEditorProps = { |
| oldContent: string; |
| newContent: string; |
| }; |
|
|
| export const DiffView = ({ oldContent, newContent }: DiffEditorProps) => { |
| const editorRef = useRef<HTMLDivElement>(null); |
| const viewRef = useRef<EditorView | null>(null); |
|
|
| useEffect(() => { |
| if (editorRef.current && !viewRef.current) { |
| const parser = DOMParser.fromSchema(diffSchema); |
|
|
| const oldHtmlContent = renderToString( |
| <Streamdown>{oldContent}</Streamdown>, |
| ); |
| const newHtmlContent = renderToString( |
| <Streamdown>{newContent}</Streamdown>, |
| ); |
|
|
| const oldContainer = document.createElement('div'); |
| oldContainer.innerHTML = oldHtmlContent; |
|
|
| const newContainer = document.createElement('div'); |
| newContainer.innerHTML = newHtmlContent; |
|
|
| const oldDoc = parser.parse(oldContainer); |
| const newDoc = parser.parse(newContainer); |
|
|
| const diffedDoc = computeDiff(oldDoc, newDoc); |
|
|
| const state = EditorState.create({ |
| doc: diffedDoc, |
| plugins: [], |
| }); |
|
|
| viewRef.current = new EditorView(editorRef.current, { |
| state, |
| editable: () => false, |
| }); |
| } |
|
|
| return () => { |
| if (viewRef.current) { |
| viewRef.current.destroy(); |
| viewRef.current = null; |
| } |
| }; |
| }, [oldContent, newContent]); |
|
|
| return <div className="diff-editor" ref={editorRef} />; |
| }; |
|
|