Skip to content

Commit

Permalink
Merge pull request #6507 from nextcloud/fix/plaintext-linebreaks
Browse files Browse the repository at this point in the history
Fix(plaintext): multiple linebreaks at end
  • Loading branch information
max-nextcloud authored Oct 9, 2024
2 parents a1b8e20 + 995642e commit ab40a49
Show file tree
Hide file tree
Showing 10 changed files with 121 additions and 106 deletions.
2 changes: 1 addition & 1 deletion cypress/e2e/nodes/Links.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ describe('test link marks', function() {

describe('link bubble', function() {

function clickLink(link, options = {}) {
const clickLink = (link, options = {}) => {
cy.getContent()
.find(`a[href*="${link}"]`)
.click(options)
Expand Down
42 changes: 25 additions & 17 deletions src/EditorFactory.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,15 @@ const loadSyntaxHighlight = async (language) => {
}
}

const createEditor = ({ language, onCreate = () => {}, onUpdate = () => {}, extensions, enableRichEditing, session, relativePath, isEmbedded = false }) => {
let defaultExtensions
if (enableRichEditing) {
defaultExtensions = [
const editorProps = {
scrollMargin: 50,
scrollThreshold: 50,
}

const createRichEditor = ({ extensions = [], session, relativePath, isEmbedded = false } = {}) => {
return new Editor({
editorProps,
extensions: [
RichText.configure({
relativePath,
isEmbedded,
Expand All @@ -49,25 +54,28 @@ const createEditor = ({ language, onCreate = () => {}, onUpdate = () => {}, exte
],
}),
FocusTrap,
]
} else {
defaultExtensions = [PlainText, CodeBlockLowlight.configure({ lowlight, defaultLanguage: language })]
}
...extensions,
],
})
}

const createPlainEditor = ({ language, extensions = [] } = {}) => {
return new Editor({
onCreate,
onUpdate,
editorProps: {
scrollMargin: 50,
scrollThreshold: 50,
},
extensions: defaultExtensions.concat(extensions || []),
editorProps,
extensions: [
PlainText,
CodeBlockLowlight.configure({
lowlight,
defaultLanguage: language,
exitOnTripleEnter: false,
}),
...extensions,
],
})
}

const serializePlainText = (doc) => {
return doc.textContent
}

export default createEditor
export { createEditor, serializePlainText, loadSyntaxHighlight }
export { createRichEditor, createPlainEditor, serializePlainText, loadSyntaxHighlight }
117 changes: 65 additions & 52 deletions src/components/Editor.vue
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,12 @@ import { SyncService, ERROR_TYPE, IDLE_TIMEOUT } from './../services/SyncService
import createSyncServiceProvider from './../services/SyncServiceProvider.js'
import AttachmentResolver from './../services/AttachmentResolver.js'
import { extensionHighlight } from '../helpers/mappings.js'
import { createEditor, serializePlainText, loadSyntaxHighlight } from './../EditorFactory.js'
import {
createRichEditor,
createPlainEditor,
serializePlainText,
loadSyntaxHighlight,
} from './../EditorFactory.js'
import { createMarkdownSerializer } from './../extensions/Markdown.js'
import markdownit from './../markdownit/index.js'
Expand Down Expand Up @@ -401,10 +406,15 @@ export default {
listenEditorEvents() {
this.$editor.on('focus', this.onFocus)
this.$editor.on('blur', this.onBlur)
this.$editor.on('create', this.onCreate)
this.$editor.on('update', this.onUpdate)
},
unlistenEditorEvents() {
this.$editor.off('focus', this.onFocus)
this.$editor.off('blur', this.onBlur)
this.$editor.off('create', this.onCreate)
this.$editor.off('update', this.onUpdate)
},
listenSyncServiceEvents() {
Expand Down Expand Up @@ -504,59 +514,46 @@ export default {
this.$baseVersionEtag = document.baseVersionEtag
this.hasConnectionIssue = false
const language = extensionHighlight[this.fileExtension] || this.fileExtension;
(this.isRichEditor ? Promise.resolve() : loadSyntaxHighlight(language))
.then(() => {
const session = this.currentSession
if (!this.$editor) {
this.$editor = createEditor({
language,
relativePath: this.relativePath,
session,
onCreate: ({ editor }) => {
this.$syncService.startSync()
const proseMirrorMarkdown = this.$syncService.serialize(editor.state.doc)
this.emit('create:content', {
markdown: proseMirrorMarkdown,
})
},
onUpdate: ({ editor }) => {
// this.debugContent(editor)
const proseMirrorMarkdown = this.$syncService.serialize(editor.state.doc)
this.emit('update:content', {
markdown: proseMirrorMarkdown,
})
},
extensions: [
Autofocus.configure({
fileId: this.fileId,
}),
Collaboration.configure({
document: this.$ydoc,
}),
CollaborationCursor.configure({
provider: this.$providers[0],
user: {
name: session?.userId
? session.displayName
: (session?.guestName || t('text', 'Guest')),
color: session?.color,
clientId: this.$ydoc.clientID,
},
}),
],
enableRichEditing: this.isRichEditor,
isEmbedded: this.isEmbedded,
})
this.hasEditor = true
this.listenEditorEvents()
} else {
// $editor already existed. So this is a reconnect.
this.$syncService.startSync()
}
if (this.$editor) {
// $editor already existed. So this is a reconnect.
this.$syncService.startSync()
return
}
const session = this.currentSession
const extensions = [
Autofocus.configure({ fileId: this.fileId }),
Collaboration.configure({ document: this.$ydoc }),
CollaborationCursor.configure({
provider: this.$providers[0],
user: {
name: session?.userId
? session.displayName
: (session?.guestName || t('text', 'Guest')),
color: session?.color,
clientId: this.$ydoc.clientID,
},
}),
]
if (this.isRichEditor) {
this.$editor = createRichEditor({
relativePath: this.relativePath,
session,
extensions,
isEmbedded: this.isEmbedded,
})
this.hasEditor = true
this.listenEditorEvents()
} else {
const language = extensionHighlight[this.fileExtension]
|| this.fileExtension
loadSyntaxHighlight(language)
.then(() => {
this.$editor = createPlainEditor({ language, extensions })
this.hasEditor = true
this.listenEditorEvents()
})
}
},
Expand All @@ -571,6 +568,22 @@ export default {
}
},
onCreate({ editor }) {
this.$syncService.startSync()
const proseMirrorMarkdown = this.$syncService.serialize(editor.state.doc)
this.emit('create:content', {
markdown: proseMirrorMarkdown,
})
},
onUpdate({ editor }) {
// this.debugContent(editor)
const proseMirrorMarkdown = this.$syncService.serialize(editor.state.doc)
this.emit('update:content', {
markdown: proseMirrorMarkdown,
})
},
onSync({ steps, document }) {
this.hasConnectionIssue = this.$syncService.backend.fetcher === 0 || !this.$providers[0].wsconnected || this.$syncService.pushError > 0
this.$nextTick(() => {
Expand Down
9 changes: 5 additions & 4 deletions src/mixins/setContent.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { Doc, encodeStateAsUpdate, XmlFragment, applyUpdate } from 'yjs'
import { generateJSON } from '@tiptap/core'
import { prosemirrorToYXmlFragment } from 'y-prosemirror'
import { Node } from '@tiptap/pm/model'
import { createEditor } from '../EditorFactory.js'
import { createRichEditor, createPlainEditor } from '../EditorFactory.js'

export default {
methods: {
Expand All @@ -31,9 +31,10 @@ export default {
? markdownit.render(content) + '<p/>'
: `<pre>${escapeHtml(content)}</pre>`

const editor = createEditor({
enableRichEditing: isRichEditor,
})
const editor = isRichEditor
? createRichEditor()
: createPlainEditor()

const json = generateJSON(html, editor.extensionManager.extensions)

const doc = Node.fromJSON(editor.schema, json)
Expand Down
9 changes: 3 additions & 6 deletions src/tests/builders.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,11 @@
import { expect } from '@jest/globals';
import { Mark, Node } from '@tiptap/pm/model'
import { builders } from 'prosemirror-test-builder'
import createEditor from '../EditorFactory'
import { createRichEditor } from '../EditorFactory'


export function getBuilders() {
const editor = createEditor({
content: '',
enableRichEditing: true
})
const editor = createRichEditor()
return builders(editor.schema, {
tr: { nodeType: 'tableRow' },
td: { nodeType: 'tableCell' },
Expand Down Expand Up @@ -84,7 +81,7 @@ function createDocumentString(node) {
* @param {Node} subject The editor document
* @param {Node} expected The expected document
* @example
* const editor = createEditor()
* const editor = createRichEditor()
* expectDocument(editor.state.doc, table(
* tr(
* td('foo')
Expand Down
14 changes: 4 additions & 10 deletions src/tests/helpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import Document from '@tiptap/extension-document'
import Paragraph from '../nodes/Paragraph'
import Text from '@tiptap/extension-text'

import createEditor from '../EditorFactory'
import { createRichEditor } from '../EditorFactory'
import markdownit from '../markdownit'

export function createCustomEditor({ content, extensions }) {
Expand All @@ -32,9 +32,7 @@ export function createCustomEditor({ content, extensions }) {
* @returns {string}
*/
export function markdownThroughEditor(markdown) {
const tiptap = createEditor({
enableRichEditing: true
})
const tiptap = createRichEditor()
tiptap.commands.setContent(markdownit.render(markdown))
const serializer = createMarkdownSerializer(tiptap.schema)
return serializer.serialize(tiptap.state.doc)
Expand All @@ -47,9 +45,7 @@ export function markdownThroughEditor(markdown) {
* @returns {string}
*/
export function markdownThroughEditorHtml(html) {
const tiptap = createEditor({
enableRichEditing: true
})
const tiptap = createRichEditor()
tiptap.commands.setContent(html)
const serializer = createMarkdownSerializer(tiptap.schema)
return serializer.serialize(tiptap.state.doc)
Expand All @@ -62,9 +58,7 @@ export function markdownThroughEditorHtml(html) {
* @returns {string}
*/
export function markdownFromPaste(html) {
const tiptap = createEditor({
enableRichEditing: true
})
const tiptap = createRichEditor()
tiptap.commands.insertContent(html)
const serializer = createMarkdownSerializer(tiptap.schema)
return serializer.serialize(tiptap.state.doc)
Expand Down
6 changes: 2 additions & 4 deletions src/tests/markdown.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import {
markdownFromPaste
} from './helpers.js'
import { createMarkdownSerializer } from "../extensions/Markdown";
import createEditor from "../EditorFactory";
import { createRichEditor } from "../EditorFactory";

/*
* This file is for various markdown tests, mainly testing if input and output stays the same.
Expand Down Expand Up @@ -212,9 +212,7 @@ describe('Markdown serializer from html', () => {
describe('Trailing nodes', () => {
test('No extra transaction is added after loading', () => {
const source = "# My heading\n\n* test\n* test2"
const tiptap = createEditor({
enableRichEditing: true,
})
const tiptap = createRichEditor()
tiptap.commands.setContent(markdownit.render(source))

const jsonBefore = tiptap.getJSON()
Expand Down
6 changes: 2 additions & 4 deletions src/tests/nodes/Table.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

import { createEditor } from '../../EditorFactory'
import { createRichEditor } from '../../EditorFactory'
import { createMarkdownSerializer } from '../../extensions/Markdown'
import { builders } from 'prosemirror-test-builder'

Expand Down Expand Up @@ -75,9 +75,7 @@ describe('Table', () => {
})

function editorWithContent(content) {
const editor = createEditor({
enableRichEditing: true,
})
const editor = createRichEditor()
editor.commands.setContent(content)
return editor
}
Expand Down
16 changes: 12 additions & 4 deletions src/tests/plaintext.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

import { createEditor, serializePlainText } from './../EditorFactory';
import { createPlainEditor, serializePlainText } from './../EditorFactory';
import spec from "./fixtures/spec"
import xssFuzzVectors from './fixtures/xssFuzzVectors';

Expand All @@ -18,9 +18,7 @@ const escapeHTML = (s) => {

const plaintextThroughEditor = (markdown) => {
const content = '<pre>' + escapeHTML(markdown) + '</pre>'
const tiptap = createEditor({
enableRichEditing: false
})
const tiptap = createPlainEditor()
tiptap.commands.setContent(content)
return serializePlainText(tiptap.state.doc) || 'failed'
}
Expand Down Expand Up @@ -84,4 +82,14 @@ describe('html as plain text', () => {
expect(plaintextThroughEditor('"\';&.-#><')).toBe('"\';&.-#><')
expect(plaintextThroughEditor(xssFuzzVectors)).toBe(xssFuzzVectors)
})
} )

describe('regression tests', () => {
test('tripple enter creates new lines at end (#6507)', () => {
const tiptap = createPlainEditor()
tiptap.commands.enter()
tiptap.commands.enter()
tiptap.commands.enter()
expect(serializePlainText(tiptap.state.doc)).toEqual("\n\n\n")
})
})
Loading

0 comments on commit ab40a49

Please sign in to comment.