Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@
"markdown-it": "^13.0.1",
"markdown-it-container": "^3.0.0",
"markdown-it-front-matter": "^0.2.3",
"markdown-it-image-figures": "^2.1.0",
"mitt": "^3.0.0",
"path-normalize": "^6.0.7",
"prosemirror-collab": "^1.3.0",
Expand Down
20 changes: 17 additions & 3 deletions src/components/Editor/MediaHandler.vue
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@
-->

<template>
<div class="editor editor-midia-handler"
data-text-el="editor-midia-handler"
<div class="editor editor-media-handler"
data-text-el="editor-media-handler"
:class="{ draggedOver }"
@image-paste="onPaste"
@dragover.prevent.stop="setDraggedOver(true)"
Expand Down Expand Up @@ -194,7 +194,21 @@ export default {
? this.$editor.chain().focus(position)
: this.$editor.chain()

chain.setImage({ src, alt }).insertContent('<br />').focus().run()
chain.setImage({ src, alt }).run()

const selection = this.$editor.view.state.selection
if (!selection.empty) {
// If inserted image is first element, it is selected and would get overwritten by
// subsequent editor inserts (see tiptap#3355). So unselect the image by placing
// the cursor at the end of the selection.
this.$editor.commands.focus(selection.to)
} else {
// Place the cursor after the inserted image node
this.$editor.commands.focus(selection.to + 2)
}

// Insert a newline to allow placing the cursor in between subsequent images
this.$editor.chain().insertContent('<br />').focus().run()
},
},
}
Expand Down
6 changes: 3 additions & 3 deletions src/extensions/RichText.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -82,9 +83,8 @@ export default Extension.create({
TaskItem,
Callout,
Underline,
Image.configure({
inline: true,
}),
Image,
ImageInline,
Dropcursor,
KeepSyntax,
FrontMatter,
Expand Down
2 changes: 2 additions & 0 deletions src/markdownit/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand All @@ -15,6 +16,7 @@ const markdownit = MarkdownIt('commonmark', { html: false, breaks: false })
.use(callouts)
.use(keepSyntax)
.use(markdownitMentions)
.use(implicitFigures)

// Issue #3370: To preserve softbreaks within md files we preserve all whitespaces, so we must not introduce additional new lines after a <br> element
markdownit.renderer.rules.hardbreak = (tokens, idx, options) => (options.xhtmlOut ? '<br />' : '<br>')
Expand Down
16 changes: 16 additions & 0 deletions src/nodes/Image.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -83,6 +93,12 @@ const Image = TiptapImage.extend({
]
},

// Append two newlines after image to make it a block image
toMarkdown(state, node) {
state.write('![' + state.esc(node.attrs.alt || '') + '](' + node.attrs.src.replace(/[()]/g, '\\$&')
+ (node.attrs.title ? ' "' + node.attrs.title.replace(/"/g, '\\"') + '"' : '') + ')\n\n')
},

})

export default Image
74 changes: 74 additions & 0 deletions src/nodes/ImageInline.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
/*
* @copyright Copyright (c) 2022 Jonas <[email protected]>
*
* @author Jonas <[email protected]>
*
* @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 <http://www.gnu.org/licenses/>.
*
*/

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)
},
Comment on lines +64 to +66
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Somehow the image does not get rendered for me.
I'll add a failing test.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay... turns out i did not have the file in place. But there also was no placeholder icon. We might want to add that.


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
42 changes: 31 additions & 11 deletions src/tests/extensions/Markdown.spec.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import { Markdown } from './../../extensions';
import { createMarkdownSerializer } from './../../extensions/Markdown';
import Underline from './../../marks/Underline';
import TaskList from './../../nodes/TaskList';
import TaskItem from './../../nodes/TaskItem'
import Image from '@tiptap/extension-image'
import { Markdown } from './../../extensions/index.js'
import { createMarkdownSerializer } from './../../extensions/Markdown.js'
import Image from './../../nodes/Image.js'
import ImageInline from './../../nodes/ImageInline.js'
import TaskList from './../../nodes/TaskList.js'
import TaskItem from './../../nodes/TaskItem.js'
import Underline from './../../marks/Underline.js'
import TiptapImage from '@tiptap/extension-image'
import { getExtensionField } from '@tiptap/core'
import createEditor from './../createEditor'
import createEditor from './../createEditor.js'

describe('Markdown extension unit', () => {
it('has a config', () => {
Expand All @@ -19,7 +21,7 @@ describe('Markdown extension unit', () => {

it('makes toMarkdown available in prose mirror schema', () => {
const editor = createEditor({
extensions: [Markdown, Underline]
extensions: [Markdown, Underline],
})
const serializer = createMarkdownSerializer(editor.schema)
const underline = serializer.serializer.marks.underline
Expand Down Expand Up @@ -48,13 +50,31 @@ describe('Markdown extension integrated in the editor', () => {
expect(serializer.serialize(editor.state.doc)).toBe('\n* [ ] Hello')
})

it('serializes nodes with the default prosemirror way', () => {
it('serializes images with the default prosemirror way', () => {
const editor = createEditor({
content: `<p><img alt="Hello" src="test" /></p>`,
extensions: [Markdown, Image.configure({inline: true})],
content: '<p><img alt="Hello" src="test"></p>',
extensions: [Markdown, TiptapImage.configure({ inline: true })],
})
const serializer = createMarkdownSerializer(editor.schema)
expect(serializer.serialize(editor.state.doc)).toBe('![Hello](test)')
})

it('serializes block images with the default prosemirror way', () => {
const editor = createEditor({
content: '<figure><img alt="Hello" src="test"></figure>',
extensions: [Markdown, Image, ImageInline],
})
const serializer = createMarkdownSerializer(editor.schema)
expect(serializer.serialize(editor.state.doc)).toBe('![Hello](test)\n\n')
})

it('serializes inline images with the default prosemirror way', () => {
const editor = createEditor({
content: '<p>inline image <img alt="Hello" src="test"> inside text</p>',
extensions: [Markdown, Image, ImageInline],
})
const serializer = createMarkdownSerializer(editor.schema)
expect(serializer.serialize(editor.state.doc)).toBe('inline image ![Hello](test) inside text')
})

})
32 changes: 30 additions & 2 deletions src/tests/markdown.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,11 @@ describe('Commonmark', () => {
.replace(/<br \/>/, '<br />\n')
}

// special treatment because we use markdown-it-image-figures
const figureImageMarkdownTests = [
516, 519, 530, 571, 572, 573, 574, 575, 576, 577, 579, 580, 581, 582, 583, 584, 585, 587, 588, 590
]

spec.forEach((entry) => {
// We do not support HTML
if (entry.section === 'HTML blocks' || entry.section === 'Raw HTML') return;
Expand All @@ -46,16 +51,36 @@ describe('Commonmark', () => {
}

test('commonmark parsing ' + entry.example, () => {
const expected = entry.markdown.includes('__')
let expected = entry.markdown.includes('__')
? entry.html.replace(/<strong>/g, '<u>').replace(/<\/strong>/g, '</u>')
: entry.html
if (figureImageMarkdownTests.indexOf(entry.example) !== -1) {
expected = expected.replace(/<p>/g, '<figure>').replace(/<\/p>/g, '</figure>')
}

const rendered = markdownit.render(entry.markdown)

// Ignore special markup for untouched markdown
expect(normalize(rendered)).toBe(expected)
})
})
})

describe('Commonmark images', () => {
beforeAll(() => {
// Make sure html tests pass
// entry.section === 'HTML blocks' || entry.section === 'Raw HTML'
markdownit.set({ html: true})
})
afterAll(() => {
markdownit.set({ html: false})
})

test('commonmark 513', () => {
expect(markdownit.render('[![moon](moon.jpg)](/uri)\n')).toBe('<figure><a href=\"/uri\"><img src=\"moon.jpg\" alt=\"moon\" /></a></figure>\n')
})
})

describe('Markdown though editor', () => {
test('headlines', () => {
expect(markdownThroughEditor('# Test')).toBe('# Test')
Expand Down Expand Up @@ -95,7 +120,9 @@ describe('Markdown though editor', () => {
expect(markdownThroughEditor('[bar\\\\]: /uri\n\n[bar\\\\]')).toBe('[bar\\\\](/uri)')
})
test('images', () => {
expect(markdownThroughEditor('![test](foo)')).toBe('![test](foo)')
expect(markdownThroughEditor('text ![test](foo) moretext')).toBe('text ![test](foo) moretext')
// regression introduced in #3282. To be fixed in #3428.
expect(markdownThroughEditor('![test](foo)')).toBe('![test](foo)\n\n')
})
test('special characters', () => {
expect(markdownThroughEditor('"\';&.-#><')).toBe('"\';&.-#><')
Expand Down Expand Up @@ -180,6 +207,7 @@ describe('Markdown serializer from html', () => {
test('images', () => {
expect(markdownThroughEditorHtml('<img src="image" alt="description" />')).toBe('![description](image)')
expect(markdownThroughEditorHtml('<p><img src="image" alt="description" /></p>')).toBe('![description](image)')
expect(markdownThroughEditorHtml('<p>text<img src="image" alt="description" />moretext</p>')).toBe('text![description](image)moretext')
})
test('checkboxes', () => {
expect(markdownThroughEditorHtml('<ul class="contains-task-list"><li><input type="checkbox" checked /><label>foo</label></li></ul>')).toBe('* [x] foo')
Expand Down