mirror of
https://github.com/stablyai/orca.git
synced 2026-10-31 00:57:45 +00:00
fix(editor): synchronize rendered image metadata (#35028)
* fix(editor): synchronize rendered image metadata * fix(editor): resolve image attribute definitions once and trim metadata test --------- Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
This commit is contained in:
@@ -1,5 +1,10 @@
|
||||
import { createMarkdownTokenizerStart } from './markdown-tokenizer-start'
|
||||
import type { AnyExtension } from '@tiptap/core'
|
||||
import {
|
||||
getRenderedAttributes,
|
||||
mergeAttributes,
|
||||
type AnyExtension,
|
||||
type Editor
|
||||
} from '@tiptap/core'
|
||||
import StarterKit from '@tiptap/starter-kit'
|
||||
import { RichMarkdownTrailingParagraph } from './rich-markdown-trailing-paragraph'
|
||||
import Link from '@tiptap/extension-link'
|
||||
@@ -108,7 +113,9 @@ export function createRichMarkdownExtensions({
|
||||
}
|
||||
},
|
||||
addNodeView() {
|
||||
return ({ node, HTMLAttributes }) => {
|
||||
// The extensionManager.attributes getter recomputes across all extensions; resolve it once per editor.
|
||||
let imageAttributes: Editor['extensionManager']['attributes'] | undefined
|
||||
return ({ node, HTMLAttributes, editor }) => {
|
||||
// A wrapper prevents native image-byte dragging from conflicting with ProseMirror’s schema-node relocation.
|
||||
const dom = document.createElement('span')
|
||||
// Keep the image wrapper inline so it cannot break surrounding text onto separate lines.
|
||||
@@ -118,11 +125,27 @@ export function createRichMarkdownExtensions({
|
||||
|
||||
const img = document.createElement('img')
|
||||
img.draggable = false
|
||||
for (const [key, value] of Object.entries(HTMLAttributes)) {
|
||||
if (key !== 'src' && value != null && value !== false) {
|
||||
img.setAttribute(key, String(value))
|
||||
let renderedAttributeNames = new Set<string>()
|
||||
const syncImageAttributes = (attributes: Record<string, unknown>): void => {
|
||||
const nextNames = new Set<string>()
|
||||
for (const [key, value] of Object.entries(attributes)) {
|
||||
if (key === 'src' || key === 'draggable' || value == null || value === false) {
|
||||
continue
|
||||
}
|
||||
nextNames.add(key)
|
||||
const renderedValue = String(value)
|
||||
if (img.getAttribute(key) !== renderedValue) {
|
||||
img.setAttribute(key, renderedValue)
|
||||
}
|
||||
}
|
||||
for (const key of renderedAttributeNames) {
|
||||
if (!nextNames.has(key)) {
|
||||
img.removeAttribute(key)
|
||||
}
|
||||
}
|
||||
renderedAttributeNames = nextNames
|
||||
}
|
||||
syncImageAttributes(mergeAttributes(this.options.HTMLAttributes, HTMLAttributes))
|
||||
dom.appendChild(img)
|
||||
|
||||
let currentSrc = node.attrs.src as string | undefined
|
||||
@@ -185,6 +208,17 @@ export function createRichMarkdownExtensions({
|
||||
if (updatedNode.type.name !== 'image') {
|
||||
return false
|
||||
}
|
||||
syncImageAttributes(
|
||||
mergeAttributes(
|
||||
this.options.HTMLAttributes,
|
||||
getRenderedAttributes(
|
||||
updatedNode,
|
||||
(imageAttributes ??= editor.extensionManager.attributes.filter(
|
||||
(attribute) => attribute.type === 'image'
|
||||
))
|
||||
)
|
||||
)
|
||||
)
|
||||
const newSrc = updatedNode.attrs.src as string | undefined
|
||||
const nextContextVersion = getImageContextVersion(this.storage)
|
||||
if (newSrc !== currentSrc || nextContextVersion !== currentContextVersion) {
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
// @vitest-environment happy-dom
|
||||
import { Editor } from '@tiptap/core'
|
||||
import { afterEach, beforeEach, expect, it, vi } from 'vitest'
|
||||
import { createRichMarkdownExtensions } from './rich-markdown-extensions'
|
||||
import { setRichMarkdownImageResolverContext } from './rich-markdown-image-context'
|
||||
import { createRichMarkdownEditorCodec } from './rich-markdown-source-transport'
|
||||
import { resetLocalImageSrcStateForTests } from './useLocalImageSrc'
|
||||
|
||||
const readFile = vi.fn<Window['api']['fs']['readFile']>()
|
||||
const originalApi = Object.getOwnPropertyDescriptor(window, 'api')
|
||||
let editor: Editor | undefined
|
||||
|
||||
function updateImage(attributes: Record<string, unknown>): void {
|
||||
const node = editor?.state.doc.nodeAt(1)
|
||||
if (!editor || !node) {
|
||||
throw new Error('Image node missing')
|
||||
}
|
||||
editor.view.dispatch(
|
||||
editor.state.tr.setNodeMarkup(1, undefined, { ...node.attrs, ...attributes })
|
||||
)
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
resetLocalImageSrcStateForTests()
|
||||
readFile.mockReset().mockResolvedValue({ content: 'AA==', isBinary: true, mimeType: 'image/png' })
|
||||
Object.defineProperty(window, 'api', { configurable: true, value: { fs: { readFile } } })
|
||||
vi.spyOn(URL, 'createObjectURL').mockReturnValue('blob:attributes')
|
||||
vi.spyOn(URL, 'revokeObjectURL').mockImplementation(() => {})
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
editor?.destroy()
|
||||
editor = undefined
|
||||
resetLocalImageSrcStateForTests()
|
||||
vi.restoreAllMocks()
|
||||
if (originalApi) {
|
||||
Object.defineProperty(window, 'api', originalApi)
|
||||
} else {
|
||||
Reflect.deleteProperty(window, 'api')
|
||||
}
|
||||
document.body.innerHTML = ''
|
||||
})
|
||||
|
||||
it('syncs changed and cleared metadata onto the mounted image without reloading it', async () => {
|
||||
const host = document.createElement('div')
|
||||
document.body.appendChild(host)
|
||||
editor = new Editor({
|
||||
element: host,
|
||||
extensions: createRichMarkdownExtensions({ codec: createRichMarkdownEditorCodec() }),
|
||||
content: '',
|
||||
contentType: 'markdown'
|
||||
})
|
||||
setRichMarkdownImageResolverContext(editor, { filePath: '/repo/docs/readme.md' })
|
||||
const image = host.querySelector('img')
|
||||
await vi.waitFor(() => expect(image?.getAttribute('src')).toBe('blob:attributes'))
|
||||
expect(image?.alt).toBe('Before')
|
||||
|
||||
updateImage({ alt: 'After', title: 'New title', width: 160, height: 90 })
|
||||
expect(host.querySelector('img')).toBe(image)
|
||||
expect(image?.alt).toBe('After')
|
||||
expect(image?.title).toBe('New title')
|
||||
expect(image?.width).toBe(160)
|
||||
expect(image?.height).toBe(90)
|
||||
expect(image?.draggable).toBe(false)
|
||||
|
||||
updateImage({ alt: null, title: null, width: null, height: null })
|
||||
expect(host.querySelector('img')).toBe(image)
|
||||
for (const name of ['alt', 'title', 'width', 'height']) {
|
||||
expect(image?.hasAttribute(name), name).toBe(false)
|
||||
}
|
||||
expect(image?.getAttribute('src')).toBe('blob:attributes')
|
||||
expect(readFile).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
Reference in New Issue
Block a user