import { Plugin } from 'prosemirror-state' import type { Node, Schema } from 'prosemirror-model' import type { EditorView } from 'prosemirror-view' // import { convertFileSrc } from '@tauri-apps/api/tauri' // import { resolvePath, dirname } from '../../remote' // import { isTauri } from '../../env' import type { ProseMirrorExtension } from '../state' const REGEX = /^!\[([^[\]]*)]\((.+?)\)\s+/ const MAX_MATCH = 500 const isUrl = (str: string) => { try { const url = new URL(str) return url.protocol === 'http:' || url.protocol === 'https:' } catch { return false } } const isBlank = (text: string) => text === ' ' || text === '\u00A0' /* export const getImagePath = async (src: string, path?: string) => { let paths = [src] if (path) paths = [await dirname(path), src] const absolutePath = await resolvePath(paths) return convertFileSrc(absolutePath) } */ const imageInput = (schema: Schema, _path?: string) => new Plugin({ props: { handleTextInput(view, from, to, text) { if (view.composing || !isBlank(text)) return false const $from = view.state.doc.resolve(from) if ($from.parent.type.spec.code) return false const textBefore = $from.parent.textBetween( Math.max(0, $from.parentOffset - MAX_MATCH), $from.parentOffset, undefined, '\uFFFC' ) + text const match = REGEX.exec(textBefore) if (match) { const [, title, src] = match if (isUrl(src)) { const node = schema.node('image', { src, title }) const start = from - (match[0].length - text.length) const tr = view.state.tr tr.delete(start, to) tr.insert(start, node) view.dispatch(tr) return true } // if (!isTauri) return false /* getImagePath(src, path).then((p) => { const node = schema.node('image', { src: p, title, path: src }) const start = from - (match[0].length - text.length) const tr = view.state.tr tr.delete(start, to) tr.insert(start, node) view.dispatch(tr) }) */ return false } } } }) const imageSchema = { inline: true, attrs: { src: {}, alt: { default: null }, title: { default: null }, path: { default: null }, width: { default: null } }, group: 'inline', draggable: true, parseDOM: [ { tag: 'img[src]', getAttrs: (dom: HTMLElement) => ({ src: dom.getAttribute('src'), title: dom.getAttribute('title'), alt: dom.getAttribute('alt'), path: dom.dataset.path }) } ], toDOM: (node: Node) => [ 'img', { src: node.attrs.src, title: node.attrs.title, alt: node.attrs.alt, 'data-path': node.attrs.path } ] } export const insertImage = (view: EditorView, src: string, left: number, top: number) => { const state = view.state const tr = state.tr const node = state.schema.nodes.image.create({ src }) if (view) { const pos = view.posAtCoords({ left, top }).pos tr.insert(pos, node) view.dispatch(tr) } } class ImageView { node: Node view: EditorView getPos: () => number schema: Schema dom: Element contentDOM: Element container: HTMLElement handle: HTMLElement onResizeFn: any onResizeEndFn: any width: number updating: number constructor(node: Node, view: EditorView, getPos: () => number, schema: Schema, _path: string) { this.node = node this.view = view this.getPos = getPos this.schema = schema this.onResizeFn = this.onResize.bind(this) this.onResizeEndFn = this.onResizeEnd.bind(this) this.container = document.createElement('span') this.container.className = 'image-container' if (node.attrs.width) this.setWidth(node.attrs.width) const image = document.createElement('img') image.setAttribute('title', node.attrs.title ?? '') if ( // isTauri && !node.attrs.src.startsWith('asset:') && !node.attrs.src.startsWith('data:') && !isUrl(node.attrs.src) ) { // getImagePath(node.attrs.src, path).then((p) => image.setAttribute('src', p)) } else { image.setAttribute('src', node.attrs.src) } this.handle = document.createElement('span') this.handle.className = 'resize-handle' this.handle.addEventListener('mousedown', (e) => { e.preventDefault() window.addEventListener('mousemove', this.onResizeFn) window.addEventListener('mouseup', this.onResizeEndFn) }) this.container.append(image) this.container.append(this.handle) this.dom = this.container } onResize(e: MouseEvent) { this.width = e.pageX - this.container.getBoundingClientRect().left this.setWidth(this.width) } onResizeEnd() { window.removeEventListener('mousemove', this.onResizeFn) if (this.updating === this.width) return this.updating = this.width const tr = this.view.state.tr tr.setNodeMarkup(this.getPos(), undefined, { ...this.node.attrs, width: this.width }) this.view.dispatch(tr) } setWidth(width: number) { this.container.style.width = `${width}px` } } export default (path?: string): ProseMirrorExtension => ({ schema: (prev) => ({ ...prev, nodes: (prev.nodes as any).update('image', imageSchema) }), plugins: (prev, schema) => [...prev, imageInput(schema, path)], nodeViews: { // FIXME something is not right // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore image: (node, view, getPos) => { return new ImageView(node, view, getPos, view.state.schema, path) } } })