From c9b01f32179c1403a40b976f83eb07feed367a9d Mon Sep 17 00:00:00 2001 From: Jonas Date: Wed, 19 Oct 2022 12:57:31 +0200 Subject: [PATCH] Make images block elements Use markdown-it-image-figures to render standalone images inside `
` elements instead of `

` when markdown source gets parsed into HTML. This allows to treat images as block elements by TipTap. Also keep support for inline images to not break markdown files that have inline images. Fixes: #2873 Signed-off-by: Jonas --- src/extensions/RichText.js | 6 ++-- src/markdownit/index.js | 2 ++ src/nodes/Image.js | 10 ++++++ src/nodes/ImageInline.js | 74 ++++++++++++++++++++++++++++++++++++++ 4 files changed, 89 insertions(+), 3 deletions(-) create mode 100644 src/nodes/ImageInline.js diff --git a/src/extensions/RichText.js b/src/extensions/RichText.js index c19d09bc859..5ec6ecad10b 100644 --- a/src/extensions/RichText.js +++ b/src/extensions/RichText.js @@ -37,6 +37,7 @@ import HardBreak from './HardBreak.js' import Heading from '../nodes/Heading/index.js' import HorizontalRule from '@tiptap/extension-horizontal-rule' import Image from './../nodes/Image.js' +import ImageInline from './../nodes/ImageInline.js' import KeepSyntax from './KeepSyntax.js' import ListItem from '@tiptap/extension-list-item' import Mention from './../extensions/Mention.js' @@ -82,9 +83,8 @@ export default Extension.create({ TaskItem, Callout, Underline, - Image.configure({ - inline: true, - }), + Image, + ImageInline, Dropcursor, KeepSyntax, FrontMatter, diff --git a/src/markdownit/index.js b/src/markdownit/index.js index 71c36cc9280..545f076fe4e 100644 --- a/src/markdownit/index.js +++ b/src/markdownit/index.js @@ -5,6 +5,7 @@ import underline from './underline.js' import splitMixedLists from './splitMixedLists.js' import callouts from './callouts.js' import keepSyntax from './keepSyntax.js' +import implicitFigures from 'markdown-it-image-figures' const markdownit = MarkdownIt('commonmark', { html: false, breaks: false }) .enable('strikethrough') @@ -15,5 +16,6 @@ const markdownit = MarkdownIt('commonmark', { html: false, breaks: false }) .use(callouts) .use(keepSyntax) .use(markdownitMentions) + .use(implicitFigures) export default markdownit diff --git a/src/nodes/Image.js b/src/nodes/Image.js index 3660c04e72e..137bd7715f3 100644 --- a/src/nodes/Image.js +++ b/src/nodes/Image.js @@ -29,6 +29,16 @@ const Image = TiptapImage.extend({ selectable: false, + parseHTML() { + return [ + { + tag: this.options.allowBase64 + ? 'figure img[src]' + : 'figure img[src]:not([src^="data:"])', + }, + ] + }, + renderHTML() { // Avoid the prosemirror node creation to trigger image loading as we use a custom node view anyways // Otherwise it would attempt to load the image from the current location before the node view is even initialized diff --git a/src/nodes/ImageInline.js b/src/nodes/ImageInline.js new file mode 100644 index 00000000000..fc82eb71385 --- /dev/null +++ b/src/nodes/ImageInline.js @@ -0,0 +1,74 @@ +/* + * @copyright Copyright (c) 2022 Jonas + * + * @author Jonas + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +import TiptapImage from '@tiptap/extension-image' +import ImageView from './ImageView.vue' +import { VueNodeViewRenderer } from '@tiptap/vue-2' + +// Inline image extension. Needed if markdown contains inline images. +// Not supported to be created from our UI (we default to block images). +const ImageInline = TiptapImage.extend({ + name: 'image-inline', + + // Lower priority than (block) Image extension + priority: 99, + + selectable: false, + + parseHTML() { + return [ + { + tag: this.options.allowBase64 + ? 'img[src]' + : 'img[src]:not([src^="data:"])', + }, + ] + }, + + addOptions() { + return { + ...this.parent?.(), + inline: true, + } + }, + + // Empty commands, we want only those from (block) Image extension + addCommands() { + return {} + }, + + // Empty input rules, we want only those from (block) Image extension + addInputRules() { + return [] + }, + + addNodeView() { + return VueNodeViewRenderer(ImageView) + }, + + toMarkdown(state, node) { + state.write('![' + state.esc(node.attrs.alt || '') + '](' + node.attrs.src.replace(/[()]/g, '\\$&') + + (node.attrs.title ? ' "' + node.attrs.title.replace(/"/g, '\\"') + '"' : '') + ')') + }, +}) + +export default ImageInline