Welcome to mirror list, hosted at ThFree Co, Russian Federation.

github.com/nextcloud/text.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--css/prosemirror.scss64
-rw-r--r--package.json1
-rw-r--r--src/EditorFactory.js31
-rw-r--r--src/components/EditorWrapper.vue28
-rw-r--r--src/components/MenuBar.vue6
-rw-r--r--src/files.js16
-rw-r--r--src/nodes/PlainTextDocument.js36
-rw-r--r--src/nodes/index.js4
-rw-r--r--src/tests/markdown.spec.js (renamed from src/tests/index.spec.js)0
-rw-r--r--src/tests/plaintext.spec.js78
10 files changed, 245 insertions, 19 deletions
diff --git a/css/prosemirror.scss b/css/prosemirror.scss
index 69d3f71da..6d6f45423 100644
--- a/css/prosemirror.scss
+++ b/css/prosemirror.scss
@@ -1,5 +1,5 @@
/* Document rendering styles */
-.ProseMirror {
+#editor-wrapper .ProseMirror {
margin-top: 44px;
height: 100%;
position: relative;
@@ -133,3 +133,65 @@
.ProseMirror-focused .ProseMirror-gapcursor {
display: block;
}
+#editor-wrapper:not(.richEditor) .ProseMirror {
+ pre {
+ background-color: var(--color-main-background);
+
+ &::before {
+ content: attr(data-language);
+ text-transform: uppercase;
+ display: block;
+ text-align: right;
+ font-weight: bold;
+ font-size: 0.6rem;
+ }
+ code {
+ .hljs-comment,
+ .hljs-quote {
+ color: #999999;
+ }
+ .hljs-variable,
+ .hljs-template-variable,
+ .hljs-attribute,
+ .hljs-tag,
+ .hljs-name,
+ .hljs-regexp,
+ .hljs-link,
+ .hljs-name,
+ .hljs-selector-id,
+ .hljs-selector-class {
+ color: #f2777a;
+ }
+ .hljs-number,
+ .hljs-meta,
+ .hljs-built_in,
+ .hljs-builtin-name,
+ .hljs-literal,
+ .hljs-type,
+ .hljs-params {
+ color: #f99157;
+ }
+ .hljs-string,
+ .hljs-symbol,
+ .hljs-bullet {
+ color: #99cc99;
+ }
+ .hljs-title,
+ .hljs-section {
+ color: #ffcc66;
+ }
+ .hljs-keyword,
+ .hljs-selector-tag {
+ color: #6699cc;
+ }
+ .hljs-emphasis {
+ font-style: italic;
+ }
+ .hljs-strong {
+ font-weight: 700;
+ }
+ }
+ }
+}
+
+
diff --git a/package.json b/package.json
index 528610549..6f224a804 100644
--- a/package.json
+++ b/package.json
@@ -23,6 +23,7 @@
"test:coverage": "NODE_ENV=test jest --coverage"
},
"dependencies": {
+ "highlight.js": "^9.15.8",
"nextcloud-axios": "^0.2.0",
"nextcloud-server": "^0.15.10",
"nextcloud-vue": "^0.11.4",
diff --git a/src/EditorFactory.js b/src/EditorFactory.js
index 3bd7b27fe..2026a56c8 100644
--- a/src/EditorFactory.js
+++ b/src/EditorFactory.js
@@ -19,7 +19,7 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
-import { Editor } from 'tiptap'
+import { Editor, Text } from 'tiptap'
import {
HardBreak,
Heading,
@@ -30,15 +30,18 @@ import {
ListItem,
Blockquote,
CodeBlock,
+ CodeBlockHighlight,
HorizontalRule,
History
} from 'tiptap-extensions'
import { Strong, Italic, Strike } from './marks'
-import { Image } from './nodes'
+import { Image, PlainTextDocument } from './nodes'
import MarkdownIt from 'markdown-it'
import { MarkdownSerializer, defaultMarkdownSerializer } from 'prosemirror-markdown'
-
+import cpp from 'highlight.js/lib/languages/cpp'
+import javascript from 'highlight.js/lib/languages/javascript'
+import css from 'highlight.js/lib/languages/css'
const createEditor = ({ content, onUpdate, extensions, enableRichEditing }) => {
let richEditingExtensions = []
if (enableRichEditing) {
@@ -48,6 +51,7 @@ const createEditor = ({ content, onUpdate, extensions, enableRichEditing }) => {
new Strong(),
new Italic(),
new Strike(),
+ new HardBreak(),
new HorizontalRule(),
new BulletList(),
new OrderedList(),
@@ -57,16 +61,26 @@ const createEditor = ({ content, onUpdate, extensions, enableRichEditing }) => {
new Link(),
new Image()
]
+ } else {
+ richEditingExtensions = [
+ new PlainTextDocument(),
+ new Text(),
+ new CodeBlockHighlight({
+ languages: {
+ cpp, css, javascript
+ }
+ })
+ ]
}
extensions = extensions || []
return new Editor({
content: content,
onUpdate: onUpdate,
extensions: [
- new HardBreak(),
...richEditingExtensions,
new History()
- ].concat(extensions)
+ ].concat(extensions),
+ useBuiltInExtensions: enableRichEditing
})
}
@@ -95,5 +109,10 @@ const createMarkdownSerializer = (_nodes, _marks) => {
)
}
+const serializePlainText = (tiptap) => {
+ const doc = tiptap.getHTML().replace(/<[^>]*>?/gm, '')
+ return new DOMParser().parseFromString(doc, 'text/html').body.textContent
+}
+
export default createEditor
-export { markdownit, createEditor, createMarkdownSerializer }
+export { markdownit, createEditor, createMarkdownSerializer, serializePlainText }
diff --git a/src/components/EditorWrapper.vue b/src/components/EditorWrapper.vue
index dcbe57b18..21b68a0ee 100644
--- a/src/components/EditorWrapper.vue
+++ b/src/components/EditorWrapper.vue
@@ -27,9 +27,10 @@
{{ t('text', 'The document has been changed outside of the editor. The changes cannot be applied.') }}
</p>
</div>
- <div v-if="currentSession && active" id="editor-wrapper" :class="{'has-conflicts': hasSyncCollission, 'icon-loading': !initialLoading}">
+ <div v-if="currentSession && active" id="editor-wrapper" :class="{'has-conflicts': hasSyncCollission, 'icon-loading': !initialLoading, 'richEditor': isRichEditor}">
<div id="editor">
- <menu-bar v-if="!syncError && !readOnly" ref="menubar" :editor="tiptap">
+ <menu-bar v-if="!syncError && !readOnly" ref="menubar" :editor="tiptap"
+ :is-rich-editor="isRichEditor">
<div v-if="currentSession && active" id="editor-session-list">
<div v-tooltip="lastSavedStatusTooltip" class="save-status" :class="lastSavedStatusClass">
{{ lastSavedStatus }}
@@ -39,7 +40,7 @@
</session-list>
</div>
</menu-bar>
- <menu-bubble v-if="!readOnly" :editor="tiptap" />
+ <menu-bubble v-if="!readOnly && isRichEditor" :editor="tiptap" />
<editor-content v-show="initialLoading" class="editor__content" :editor="tiptap" />
</div>
<read-only-editor v-if="hasSyncCollission" :content="syncError.data.outsideChange" />
@@ -54,7 +55,7 @@ import Vue from 'vue'
import { SyncService, ERROR_TYPE } from './../services/SyncService'
import { endpointUrl, getRandomGuestName } from './../helpers'
-import { createEditor, markdownit, createMarkdownSerializer } from './../EditorFactory'
+import { createEditor, markdownit, createMarkdownSerializer, serializePlainText } from './../EditorFactory'
import { EditorContent } from 'tiptap'
import { Collaboration } from 'tiptap-extensions'
@@ -172,6 +173,9 @@ export default {
},
isPublic() {
return document.getElementById('isPublic') && document.getElementById('isPublic').value === '1'
+ },
+ isRichEditor() {
+ return this.mime === 'text/markdown'
}
},
watch: {
@@ -218,9 +222,15 @@ export default {
guestName,
forceRecreate: this.forceRecreate,
serialize: (document) => {
- const markdown = (createMarkdownSerializer(this.tiptap.nodes, this.tiptap.marks)).serialize(document)
- console.debug('serialized document', { markdown })
- return markdown
+ if (this.isRichEditor) {
+ const markdown = (createMarkdownSerializer(this.tiptap.nodes, this.tiptap.marks)).serialize(document)
+ console.debug('serialized document', { markdown })
+ return markdown
+ }
+ const file = serializePlainText(this.tipta)
+ console.debug('serialized document', { file })
+ return file
+
}
})
.on('opened', ({ document, session }) => {
@@ -243,7 +253,7 @@ export default {
})
.on('loaded', ({ documentSource }) => {
this.tiptap = createEditor({
- content: markdownit.render(documentSource),
+ content: this.isRichEditor ? markdownit.render(documentSource) : '<pre>' + window.escapeHTML(documentSource) + '</pre>',
onUpdate: ({ state }) => {
this.syncService.state = state
},
@@ -268,7 +278,7 @@ export default {
}
})
],
- enableRichEditing: true
+ enableRichEditing: this.isRichEditor
})
this.syncService.state = this.tiptap.state
})
diff --git a/src/components/MenuBar.vue b/src/components/MenuBar.vue
index de29da886..798921977 100644
--- a/src/components/MenuBar.vue
+++ b/src/components/MenuBar.vue
@@ -23,7 +23,7 @@
<template>
<editor-menu-bar v-slot="{ commands, isActive }" :editor="editor">
<div class="menubar">
- <div ref="menubar" class="menubar-icons">
+ <div v-if="isRichEditor" ref="menubar" class="menubar-icons">
<template v-for="(icon, $index) in allIcons">
<button v-if="icon.class" v-show="$index < iconCount" :key="icon.label"
:title="icon.label"
@@ -91,6 +91,10 @@ export default {
type: Object,
required: false,
default: null
+ },
+ isRichEditor: {
+ type: Boolean,
+ default: true
}
},
data: () => {
diff --git a/src/files.js b/src/files.js
index 8d0fc2f93..a25cc8791 100644
--- a/src/files.js
+++ b/src/files.js
@@ -26,6 +26,20 @@ import { registerFileActionFallback, registerFileCreate } from './helpers'
__webpack_nonce__ = btoa(OC.requestToken) // eslint-disable-line
__webpack_public_path__ = OC.linkTo('text', 'js/') // eslint-disable-line
+const supportedTextMimeTypes = [
+ 'text/plain',
+ 'application/cmd',
+ 'application/javascript',
+ 'application/json',
+ 'application/xml',
+ 'application/x-empty',
+ 'application/x-msdos-program',
+ 'application/x-php',
+ 'application/x-pearl',
+ 'application/x-text',
+ 'application/yaml'
+]
+
document.addEventListener('DOMContentLoaded', () => {
if (typeof OCA.Viewer === 'undefined') {
console.error('Viewer app is not installed')
@@ -34,7 +48,7 @@ document.addEventListener('DOMContentLoaded', () => {
}
OCA.Viewer.registerHandler({
id: 'text',
- mimes: ['text/markdown', 'text/plain'],
+ mimes: ['text/markdown', ...supportedTextMimeTypes],
component: FilesEditor,
group: null
})
diff --git a/src/nodes/PlainTextDocument.js b/src/nodes/PlainTextDocument.js
new file mode 100644
index 000000000..aa169d0d4
--- /dev/null
+++ b/src/nodes/PlainTextDocument.js
@@ -0,0 +1,36 @@
+/*
+ * @copyright Copyright (c) 2019 Julius Härtl <jus@bitgrid.net>
+ *
+ * @author Julius Härtl <jus@bitgrid.net>
+ *
+ * @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 { Node } from 'tiptap'
+export default class PlainTextDocument extends Node {
+
+ get name() {
+ return 'doc'
+ }
+
+ get schema() {
+ return {
+ content: 'block'
+ }
+ }
+
+}
diff --git a/src/nodes/index.js b/src/nodes/index.js
index b51a01d03..79b5d4ebd 100644
--- a/src/nodes/index.js
+++ b/src/nodes/index.js
@@ -21,7 +21,9 @@
*/
import Image from './Image'
+import PlainTextDocument from './PlainTextDocument'
export {
- Image
+ Image,
+ PlainTextDocument
}
diff --git a/src/tests/index.spec.js b/src/tests/markdown.spec.js
index b0d33e341..b0d33e341 100644
--- a/src/tests/index.spec.js
+++ b/src/tests/markdown.spec.js
diff --git a/src/tests/plaintext.spec.js b/src/tests/plaintext.spec.js
new file mode 100644
index 000000000..6b9b54a63
--- /dev/null
+++ b/src/tests/plaintext.spec.js
@@ -0,0 +1,78 @@
+import { markdownit, createEditor, createMarkdownSerializer, serializePlainText } from './../EditorFactory';
+import spec from "./fixtures/spec"
+
+const markdownToDocument = (markdown) => {
+ const tiptap = createEditor({
+ content: markdownit.render(markdown),
+ enableRichEditing: false
+ })
+ const serializer = createMarkdownSerializer(tiptap.nodes, tiptap.marks)
+ return tiptap.state.doc
+}
+
+const plaintextThroughEditor = (markdown) => {
+ const content = `<pre>${markdown}</pre>`
+ const tiptap = createEditor({
+ content: content,
+ enableRichEditing: false
+ })
+ return serializePlainText(tiptap) || ''
+}
+
+describe('Commonmark', () => {
+ beforeAll(() => {
+ // Make sure html tests pass
+ // entry.section === 'HTML blocks' || entry.section === 'Raw HTML'
+ markdownit.set({ html: true})
+ })
+ afterAll(() => {
+ markdownit.set({ html: false})
+ })
+
+ // failures because of some additional newline in markdownit
+ const skippedMarkdownTests = [
+ 181, 202, 203
+ ];
+
+ spec.forEach((entry) => {
+ if (skippedMarkdownTests.indexOf(entry.example) !== -1) {
+ return
+ }
+ test('commonmark ' + entry.example, () => {
+ expect(markdownit.render(entry.markdown)).toBe(entry.html, entry)
+ })
+ })
+})
+
+describe('Markdown though editor', () => {
+ test('headlines', () => {
+ expect(plaintextThroughEditor('# Test')).toBe('# Test')
+ expect(plaintextThroughEditor('## Test')).toBe('## Test')
+ expect(plaintextThroughEditor('### Test')).toBe('### Test')
+ expect(plaintextThroughEditor('#### Test')).toBe('#### Test')
+ expect(plaintextThroughEditor('##### Test')).toBe('##### Test')
+ })
+ test('inline format', () => {
+ expect(plaintextThroughEditor('**Test**')).toBe('**Test**')
+ expect(plaintextThroughEditor('__Test__')).toBe('__Test__')
+ expect(plaintextThroughEditor('_Test_')).toBe('_Test_')
+ expect(plaintextThroughEditor('~~Test~~')).toBe('~~Test~~')
+ })
+ test('ul', () => {
+ expect(plaintextThroughEditor('- foo\n- bar')).toBe('- foo\n- bar')
+ expect(plaintextThroughEditor('- foo\n\n- bar')).toBe('- foo\n\n- bar')
+ expect(plaintextThroughEditor('- foo\n\n\n- bar')).toBe('- foo\n\n\n- bar')
+ })
+ test('ol', () => {
+ expect(plaintextThroughEditor('1. foo\n2. bar')).toBe('1. foo\n2. bar')
+ })
+ test('paragraph', () => {
+ expect(plaintextThroughEditor('foo\nbar\n\nfoobar\n\tfoobar')).toBe('foo\nbar\n\nfoobar\n\tfoobar')
+ })
+ test('links', () => {
+ expect(plaintextThroughEditor('[test](foo)')).toBe('[test](foo)')
+ })
+ test('images', () => {
+ expect(plaintextThroughEditor('![test](foo)')).toBe('![test](foo)')
+ })
+})