diff options
author | Greta <gretadoci@gmail.com> | 2020-04-02 17:48:51 +0300 |
---|---|---|
committer | GitHub <noreply@github.com> | 2020-04-02 17:48:51 +0300 |
commit | 659914c7217d9c82e67d849759e831b53878013b (patch) | |
tree | bf0bfbaa4ca63106ee39ea49cd443cd15dd887f3 | |
parent | 89b6ac576b7edef0e4af98cc90f095f00fc00cd2 (diff) | |
parent | 69c9a4da0059bdcba0b12737ee67e688d457fda5 (diff) |
Merge pull request #2838 from nextcloud/refactor/text-objectv1.3.0-beta3
Abstract text into an object with known format
-rw-r--r-- | src/ReplyBuilder.js | 45 | ||||
-rw-r--r-- | src/components/Composer.vue | 29 | ||||
-rw-r--r-- | src/components/NewMessageDetail.vue | 90 | ||||
-rw-r--r-- | src/components/SignatureSettings.vue | 12 | ||||
-rw-r--r-- | src/components/TextEditor.vue | 1 | ||||
-rw-r--r-- | src/service/MessageService.js | 4 | ||||
-rw-r--r-- | src/tests/unit/ReplyBuilder.spec.js | 13 | ||||
-rw-r--r-- | src/tests/unit/util/HtmlHelper.spec.js | 133 | ||||
-rw-r--r-- | src/tests/unit/util/text.spec.js | 156 | ||||
-rw-r--r-- | src/util/HtmlHelper.js | 41 | ||||
-rw-r--r-- | src/util/text.js | 149 |
11 files changed, 401 insertions, 272 deletions
diff --git a/src/ReplyBuilder.js b/src/ReplyBuilder.js index de477e9d7..4422f709e 100644 --- a/src/ReplyBuilder.js +++ b/src/ReplyBuilder.js @@ -23,28 +23,39 @@ import moment from '@nextcloud/moment' import negate from 'lodash/fp/negate' +import {html} from './util/text' + +/** + * @param {Text} original + * @param {object} from + * @param {Number} date + * @return {Text} + */ export const buildReplyBody = (original, from, date) => { const start = '<p></p><p></p>' - const body = '<br>> ' + original.replace(/\n/g, '<br>> ') - if (from) { - const dateString = moment.unix(date).format('LLL') - return `${start}"${from.label}" ${from.email} – ${dateString}` + body - } else { - return `${start}${body}` + switch (original.format) { + case 'plain': + const plainBody = '<br>> ' + original.value.replace(/\n/g, '<br>> ') + + if (from) { + const dateString = moment.unix(date).format('LLL') + return html(`${start}"${from.label}" ${from.email} – ${dateString}` + plainBody) + } else { + return html(`${start}${plainBody}`) + } + case 'html': + const htmlBody = `<blockquote>${original.value}</blockquote>` + + if (from) { + const dateString = moment.unix(date).format('LLL') + return html(`${start}"${from.label}" ${from.email} – ${dateString}<br>${htmlBody}`) + } else { + return html(`${start}${htmlBody}`) + } } -} - -export const buildHtmlReplyBody = (original, from, date) => { - const start = `<p></p><p></p>` - const body = `<blockquote>${original}</blockquote>` - if (from) { - const dateString = moment.unix(date).format('LLL') - return `${start}"${from.label}" ${from.email} – ${dateString}<br>${body}` - } else { - return `${start}${body}` - } + throw new Error(`can't build a reply for the format ${original.format}`) } const RecipientType = Object.seal({ diff --git a/src/components/Composer.vue b/src/components/Composer.vue index 00ee4b2b1..f8a4b8687 100644 --- a/src/components/Composer.vue +++ b/src/components/Composer.vue @@ -199,7 +199,7 @@ import Vue from 'vue' import ComposerAttachments from './ComposerAttachments' import {findRecipient} from '../service/AutocompleteService' -import {htmlToText, textToSimpleHtml} from '../util/HtmlHelper' +import {detect, html, toHtml} from '../util/text' import Loading from './Loading' import logger from '../logger' import TextEditor from './TextEditor' @@ -249,8 +249,8 @@ export default { default: '', }, body: { - type: String, - default: '', + type: Object, + default: () => html(''), }, draft: { type: Function, @@ -260,10 +260,6 @@ export default { type: Function, required: true, }, - isPlainText: { - type: Boolean, - default: true, - }, }, data() { return { @@ -272,21 +268,21 @@ export default { autocompleteRecipients: this.to.concat(this.cc).concat(this.bcc), newRecipients: [], subjectVal: this.subject, - bodyVal: this.isPlainText ? textToSimpleHtml(this.body) : this.body, + bodyVal: toHtml(this.body).value, attachments: [], noReply: this.to.some((to) => to.email.startsWith('noreply@') || to.email.startsWith('no-reply@')), submitButtonTitle: t('mail', 'Send'), draftsPromise: Promise.resolve(), attachmentsPromise: Promise.resolve(), savingDraft: undefined, - saveDraftDebounced: debounce(700)(this.saveDraft), + saveDraftDebounced: debounce(700, this.saveDraft), state: STATES.EDITING, errorText: undefined, STATES, selectTo: this.to, selectCc: this.cc, selectBcc: this.bcc, - editorPlainText: this.isPlainText, + editorPlainText: false, bus: new Vue(), } }, @@ -302,9 +298,6 @@ export default { isReply() { return this.to.length > 0 }, - noSubject() { - return this.subjectVal === '' && this.bodyVal !== '' - }, canSend() { return this.selectTo.length > 0 || this.selectCc.length > 0 || this.selectBcc.length > 0 }, @@ -350,7 +343,7 @@ export default { } }, initBody() { - this.bodyVal = this.bodyWithSignature(this.selectedAlias, this.bodyVal) + this.bodyVal = this.bodyWithSignature(this.selectedAlias, this.bodyVal).value }, recipientToRfc822(recipient) { if (recipient.email === recipient.label) { @@ -375,7 +368,7 @@ export default { bcc: this.selectBcc.map(this.recipientToRfc822).join(', '), draftUID: uid, subject: this.subjectVal, - body: this.editorPlainText ? htmlToText(this.bodyVal) : this.bodyVal, + body: html(this.bodyVal), attachments: this.attachments, folderId: this.replyTo ? this.replyTo.folderId : undefined, messageId: this.replyTo ? this.replyTo.messageId : undefined, @@ -513,10 +506,12 @@ export default { }, bodyWithSignature(alias, body) { if (!alias || !alias.signature) { - return body + return html(body) } - return `${body} <br>--<br> ${textToSimpleHtml(alias.signature)}` + return html(body) + .append('<br>--<br>') + .append(toHtml(detect(alias.signature))) }, }, } diff --git a/src/components/NewMessageDetail.vue b/src/components/NewMessageDetail.vue index 39be8ae6c..cc0f55b3c 100644 --- a/src/components/NewMessageDetail.vue +++ b/src/components/NewMessageDetail.vue @@ -18,7 +18,6 @@ :body="composerData.body" :draft="saveDraft" :send="sendMessage" - :is-plain-text="composerData.isPlainText" /> </AppContentDetails> </template> @@ -30,14 +29,14 @@ import {generateUrl} from '@nextcloud/router' import { buildForwardSubject, - buildHtmlReplyBody, buildReplyBody, buildReplySubject, buildRecipients as buildReplyRecipients, } from '../ReplyBuilder' import Composer from './Composer' -import {getRandomMessageErrorMessage} from '../util/ErrorMessageFactory' import Error from './Error' +import {getRandomMessageErrorMessage} from '../util/ErrorMessageFactory' +import {detect, html, plain, toPlain} from '../util/text' import Loading from './Loading' import Logger from '../logger' import {saveDraft, sendMessage} from '../service/MessageService' @@ -69,8 +68,7 @@ export default { cc: this.draft.cc, bcc: this.draft.bcc, subject: this.draft.subject, - body: this.draft.body, - isPlainText: !this.draft.hasHtmlBody, + body: this.draft.hasHtmlBody ? html(this.draft.body) : plain(this.draft.body), } } else if (this.$route.query.uid !== undefined) { // Forward or reply to a message @@ -118,7 +116,7 @@ export default { to: this.stringToRecipients(this.$route.query.to), cc: this.stringToRecipients(this.$route.query.cc), subject: this.$route.query.subject || '', - body: this.$route.query.body || '', + body: this.$route.query.body ? detect(this.$route.query.body) : html(''), } } }, @@ -193,53 +191,47 @@ export default { } }) }, - fetchOriginalMessage(uid) { + async fetchOriginalMessage(uid) { this.loading = true this.error = undefined this.errorMessage = '' - this.$store - .dispatch('fetchMessage', uid) - .then((message) => { - if (message.uid !== this.$route.query.uid) { - Logger.debug("User navigated away, loaded original message won't be used") - return - } + try { + const message = await this.$store.dispatch('fetchMessage', uid) + if (message.uid !== this.$route.query.uid) { + Logger.debug("User navigated away, loaded original message won't be used") + return + } - Logger.debug('original message fetched', {message}) - this.original = message + Logger.debug('original message fetched', {message}) + this.original = message - if (message.hasHtmlBody) { - Logger.debug('original message has HTML body') - return Axios.get( - generateUrl('/apps/mail/api/accounts/{accountId}/folders/{folderId}/messages/{id}/html', { - accountId: message.accountId, - folderId: message.folderId, - id: message.id, - }) - ).then((resp) => resp.data) - } else { - return message.body - } - }) - .then((body) => { - this.originalBody = body + let body = plain(message.body || '') + if (message.hasHtmlBody) { + Logger.debug('original message has HTML body') + const resp = await Axios.get( + generateUrl('/apps/mail/api/accounts/{accountId}/folders/{folderId}/messages/{id}/html', { + accountId: message.accountId, + folderId: message.folderId, + id: message.id, + }) + ) + body = html(resp.data) + } + this.originalBody = body + } catch (error) { + Logger.error('could not load original message ' + uid, {error}) + if (error.isError) { + this.errorMessage = t('mail', 'Could not load original message') + this.error = error this.loading = false - }) - .catch((error) => { - Logger.error('could not load original message ' + uid, {error}) - if (error.isError) { - this.errorMessage = t('mail', 'Could not load original message') - this.error = error - this.loading = false - } - }) + } + } finally { + this.loading = false + } }, getForwardReplyBody() { - if (this.original.hasHtmlBody) { - return buildHtmlReplyBody(this.originalBody, this.original.from[0], this.original.dateInt) - } return buildReplyBody(this.originalBody, this.original.from[0], this.original.dateInt) }, saveDraft(data) { @@ -247,7 +239,11 @@ export default { Logger.debug('draft data does not have a draftUID, adding one') data.draftUID = this.draft.id } - return saveDraft(data.account, data).then(({uid}) => { + const dataForServer = { + ...data, + body: data.isHtml ? data.body.value : toPlain(data.body).value, + } + return saveDraft(data.account, dataForServer).then(({uid}) => { if (this.draft === undefined) { return uid } @@ -275,7 +271,11 @@ export default { }) }, sendMessage(data) { - return sendMessage(data.account, data) + const dataForServer = { + ...data, + body: data.isHtml ? data.body.value : toPlain(data.body).value, + } + return sendMessage(data.account, dataForServer) }, }, } diff --git a/src/components/SignatureSettings.vue b/src/components/SignatureSettings.vue index fe0f2e4eb..0df62c1c3 100644 --- a/src/components/SignatureSettings.vue +++ b/src/components/SignatureSettings.vue @@ -47,7 +47,7 @@ <script> import logger from '../logger' import TextEditor from './TextEditor' -import {textToSimpleHtml} from '../util/HtmlHelper' +import {detect, html, toHtml} from '../util/text' import Vue from 'vue' export default { @@ -62,17 +62,9 @@ export default { }, }, data() { - let signature = this.account.signature || '' - - if ((signature.includes('\n') || signature.includes('\r')) && !signature.includes('>')) { - // Looks like a plain text signature -> convert to HTML - signature = textToSimpleHtml(signature) - logger.info('Converted plain text signature to HTML') - } - return { loading: false, - signature, + signature: this.account.signature ? toHtml(detect(this.account.signature)) : html(''), bus: new Vue(), } }, diff --git a/src/components/TextEditor.vue b/src/components/TextEditor.vue index 5cc5a79b9..af79f7c2d 100644 --- a/src/components/TextEditor.vue +++ b/src/components/TextEditor.vue @@ -45,7 +45,6 @@ import ParagraphPlugin from '@ckeditor/ckeditor5-paragraph/src/paragraph' import {getLanguage} from '@nextcloud/l10n' import logger from '../logger' -import {htmlToText} from '../util/HtmlHelper' export default { name: 'TextEditor', diff --git a/src/service/MessageService.js b/src/service/MessageService.js index 0be155b33..f9cb85ddc 100644 --- a/src/service/MessageService.js +++ b/src/service/MessageService.js @@ -121,12 +121,12 @@ export function fetchMessage(accountId, folderId, id) { }) } -export function saveDraft(accountId, data) { +export async function saveDraft(accountId, data) { const url = generateUrl('/apps/mail/api/accounts/{accountId}/draft', { accountId, }) - return axios.post(url, data).then((resp) => resp.data) + return (await axios.post(url, data)).data } export function sendMessage(accountId, data) { diff --git a/src/tests/unit/ReplyBuilder.spec.js b/src/tests/unit/ReplyBuilder.spec.js index 7191dc309..1534826b9 100644 --- a/src/tests/unit/ReplyBuilder.spec.js +++ b/src/tests/unit/ReplyBuilder.spec.js @@ -21,20 +21,21 @@ */ import {buildReplyBody, buildRecipients, buildReplySubject} from '../../ReplyBuilder' +import {html, plain} from '../../util/text' describe('ReplyBuilder', () => { it('creates a reply body without any sender', () => { - const body = 'Newsletter\nhello\ncheers' - const expectedReply = '<p></p><p></p><br>> Newsletter<br>> hello<br>> cheers' + const body = plain('Newsletter\nhello\ncheers') + const expectedReply = html('<p></p><p></p><br>> Newsletter<br>> hello<br>> cheers') const replyBody = buildReplyBody(body) - expect(replyBody).to.equal(expectedReply) + expect(replyBody).to.deep.equal(expectedReply) }) it('creates a reply body', () => { - const body = 'Newsletter\nhello' - const expectedReply = '<p></p><p></p>"Test User" test@user.ru – November 5, 2018 ' + const body = plain('Newsletter\nhello') + const expectedReply = html('<p></p><p></p>"Test User" test@user.ru – November 5, 2018 ') const replyBody = buildReplyBody( body, @@ -45,7 +46,7 @@ describe('ReplyBuilder', () => { 1541426237 ) - expect(replyBody.startsWith(expectedReply)).to.be.true + expect(replyBody.value.startsWith(expectedReply.value)).to.be.true }) let envelope diff --git a/src/tests/unit/util/HtmlHelper.spec.js b/src/tests/unit/util/HtmlHelper.spec.js deleted file mode 100644 index f3bafd93a..000000000 --- a/src/tests/unit/util/HtmlHelper.spec.js +++ /dev/null @@ -1,133 +0,0 @@ -/** - * @copyright 2017 Christoph Wurst <christoph@winzerhof-wurst.at> - * - * @author 2017 Christoph Wurst <christoph@winzerhof-wurst.at> - * - * @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 {htmlToText} from '../../../util/HtmlHelper' - -describe('HtmlHelper', () => { - it('preserves breaks', () => { - const html = 'line1<br>line2' - const expected = 'line1\nline2' - - const actual = htmlToText(html) - - expect(actual).to.equal(expected) - }) - - it('breaks on divs', () => { - const html = '<div>one</div><div>two</div>' - const expected = 'one\ntwo' - - const actual = htmlToText(html) - - expect(actual).to.equal(expected) - }) - - it('produces a line break for each ending div element', () => { - const html = '<div>' + ' <div>' + ' line1' + ' </div>' + '</div>' + '<div>line2</div>' - const expected = ' line1\n\nline2' - - const actual = htmlToText(html) - - expect(actual).to.equal(expected) - }) - - it('converts blocks to text', () => { - const html = '<div>hello</div>' - const expected = 'hello' - - const actual = htmlToText(html) - - expect(actual).to.equal(expected) - }) - - it('converts paragraph to text', () => { - const html = '<p>hello</p>' - const expected = 'hello' - - const actual = htmlToText(html) - - expect(actual).to.equal(expected) - }) - - it('converts paragraphs to text', () => { - const html = '<p>hello</p><p>world</p>' - const expected = 'hello\n\nworld' - - const actual = htmlToText(html) - - expect(actual).to.equal(expected) - }) - - it('converts lists to text', () => { - const html = '<ul><li>one</li><li>two</li><li>three</li></ul>' - const expected = ' * one\n * two\n * three' - - const actual = htmlToText(html) - - expect(actual).to.equal(expected) - }) - - it('converts deeply nested elements to text', () => { - const html = - '<html>' + - '<body><p>Hello!</p><p>this <i>is</i> <b>some</b> random <strong>text</strong></p></body>' + - '</html>' - const expected = 'Hello!\n\nthis is some random text' - - const actual = htmlToText(html) - - expect(actual).to.equal(expected) - }) - - it('does not leak internal redirection URLs', () => { - const html = '<a href="https://localhost/apps/mail/redirect?src=domain.tld">domain.tld</a>' - const expected = 'domain.tld' - - const actual = htmlToText(html) - - expect(actual).to.equal(expected) - }) - - it('preserves quotes', () => { - const html = `<blockquote><div><b>yes.</b></div><div><br /></div><div>Am Montag, den 21.10.2019, 16:51 +0200 schrieb Christoph Wurst:</div><blockquote style="margin:0 0 0 .8ex;border-left:2px #729fcf solid;padding-left:1ex;"><div>ok cool</div><div><br /></div><div>Am Montag, den 21.10.2019, 16:51 +0200 schrieb Christoph Wurst:</div><blockquote style="margin:0 0 0 .8ex;border-left:2px #729fcf solid;padding-left:1ex;"><div>Hello</div><div><br /></div><div>this is some t<i>e</i>xt</div><div><br /></div><div>yes</div><div><br /></div><div>cheers</div><br></blockquote><br></blockquote></blockquote>` - const expected = `> yes. -> -> Am Montag, den 21.10.2019, 16:51 +0200 schrieb Christoph Wurst: -> > ok cool -> > -> > Am Montag, den 21.10.2019, 16:51 +0200 schrieb Christoph Wurst: -> > > Hello -> > > -> > > this is some text -> > > -> > > yes -> > > -> > > cheers -> > > -> > > -> >` - - const actual = htmlToText(html) - - expect(actual).to.equal(expected) - }) -}) diff --git a/src/tests/unit/util/text.spec.js b/src/tests/unit/util/text.spec.js new file mode 100644 index 000000000..97e1c4a39 --- /dev/null +++ b/src/tests/unit/util/text.spec.js @@ -0,0 +1,156 @@ +/** + * @copyright 2017 Christoph Wurst <christoph@winzerhof-wurst.at> + * + * @author 2017 Christoph Wurst <christoph@winzerhof-wurst.at> + * + * @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 {html, toPlain, plain, detect} from '../../../util/text' + +describe('text', () => { + describe('toPlain', () => { + it('preserves breaks', () => { + const source = html('line1<br>line2') + const expected = plain('line1\nline2') + + const actual = toPlain(source) + + expect(actual).to.deep.equal(expected) + }) + + it('breaks on divs', () => { + const source = html('<div>one</div><div>two</div>') + const expected = plain('one\ntwo') + + const actual = toPlain(source) + + expect(actual).to.deep.equal(expected) + }) + + it('produces a line break for each ending div element', () => { + const source = html('<div>' + ' <div>' + ' line1' + ' </div>' + '</div>' + '<div>line2</div>') + const expected = plain(' line1\n\nline2') + + const actual = toPlain(source) + + expect(actual).to.deep.equal(expected) + }) + + it('converts blocks to text', () => { + const source = html('<div>hello</div>') + const expected = plain('hello') + + const actual = toPlain(source) + + expect(actual).to.deep.equal(expected) + }) + + it('converts paragraph to text', () => { + const source = html('<p>hello</p>') + const expected = plain('hello') + + const actual = toPlain(source) + + expect(actual).to.deep.equal(expected) + }) + + it('converts paragraphs to text', () => { + const source = html('<p>hello</p><p>world</p>') + const expected = plain('hello\n\nworld') + + const actual = toPlain(source) + + expect(actual).to.deep.equal(expected) + }) + + it('converts lists to text', () => { + const source = html('<ul><li>one</li><li>two</li><li>three</li></ul>') + const expected = plain(' * one\n * two\n * three') + + const actual = toPlain(source) + + expect(actual).to.deep.equal(expected) + }) + + it('converts deeply nested elements to text', () => { + const source = html( + '<html>' + + '<body><p>Hello!</p><p>this <i>is</i> <b>some</b> random <strong>text</strong></p></body>' + + '</html>' + ) + const expected = plain('Hello!\n\nthis is some random text') + + const actual = toPlain(source) + + expect(actual).to.deep.equal(expected) + }) + + it('does not leak internal redirection URLs', () => { + const source = html('<a href="https://localhost/apps/mail/redirect?src=domain.tld">domain.tld</a>') + const expected = plain('domain.tld') + + const actual = toPlain(source) + + expect(actual).to.deep.equal(expected) + }) + + it('preserves quotes', () => { + const source = html( + `<blockquote><div><b>yes.</b></div><div><br /></div><div>Am Montag, den 21.10.2019, 16:51 +0200 schrieb Christoph Wurst:</div><blockquote style="margin:0 0 0 .8ex;border-left:2px #729fcf solid;padding-left:1ex;"><div>ok cool</div><div><br /></div><div>Am Montag, den 21.10.2019, 16:51 +0200 schrieb Christoph Wurst:</div><blockquote style="margin:0 0 0 .8ex;border-left:2px #729fcf solid;padding-left:1ex;"><div>Hello</div><div><br /></div><div>this is some t<i>e</i>xt</div><div><br /></div><div>yes</div><div><br /></div><div>cheers</div><br></blockquote><br></blockquote></blockquote>` + ) + const expected = plain(`> yes. +> +> Am Montag, den 21.10.2019, 16:51 +0200 schrieb Christoph Wurst: +> > ok cool +> > +> > Am Montag, den 21.10.2019, 16:51 +0200 schrieb Christoph Wurst: +> > > Hello +> > > +> > > this is some text +> > > +> > > yes +> > > +> > > cheers +> > > +> > > +> >`) + + const actual = toPlain(source) + + expect(actual).to.deep.equal(expected) + }) + }) + + describe('detect', () => { + it('detects plain text', () => { + const text = 'hello world\nsecond line' + + const detected = detect(text) + + expect(detected).to.deep.equal(plain(text)) + }) + + it('detects html', () => { + const text = '<p>hello world</p><p>second line</p>' + + const detected = detect(text) + + expect(detected).to.deep.equal(html(text)) + }) + }) +}) diff --git a/src/util/HtmlHelper.js b/src/util/HtmlHelper.js deleted file mode 100644 index a1206d421..000000000 --- a/src/util/HtmlHelper.js +++ /dev/null @@ -1,41 +0,0 @@ -/** - * Mail - * - * This file is licensed under the Affero General Public License version 3 or - * later. See the COPYING file. - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @copyright Christoph Wurst 2016 - */ - -import {fromString} from 'html-to-text' - -export const htmlToText = (html) => { - const withBlockBreaks = html.replace(/<\/div>/gi, '</div><br>') - - const text = fromString(withBlockBreaks, { - noLinkBrackets: true, - ignoreHref: true, - ignoreImage: true, - wordwrap: false, - format: { - blockquote: function (element, fn, options) { - return fn(element.children, options) - .replace(/\n\n\n/g, '\n\n') // remove triple line breaks - .replace(/^/gm, '> ') // add > quotation to each line - }, - paragraph: function (element, fn, options) { - return fn(element.children, options) + '\n' - }, - }, - }) - - return text - .replace(/\n\n\n/g, '\n\n') // remove triple line breaks - .replace(/^[\n\r]+/g, '') // trim line breaks at beginning and end - .replace(/ $/gm, '') // trim white space at end of each line -} - -export const textToSimpleHtml = (text) => { - return text.replace(/([^>\r\n]?)(\r\n|\n\r|\r|\n)/g, '$1<br>$2') -} diff --git a/src/util/text.js b/src/util/text.js new file mode 100644 index 000000000..f1b7f245a --- /dev/null +++ b/src/util/text.js @@ -0,0 +1,149 @@ +/* + * @copyright 2020 Christoph Wurst <christoph@winzerhof-wurst.at> + * + * @author 2020 Christoph Wurst <christoph@winzerhof-wurst.at> + * + * @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 {curry} from 'ramda' +import {fromString} from 'html-to-text' + +/** + * @type {Text} + */ +class Text { + constructor(format, value) { + this.format = format + this.value = value + } + + /** + * @param {Text} other + * @return {Text} + */ + append(other) { + if (this.format !== other.format) { + throw new Error("can't append two different formats") + } + + return new Text(this.format, this.value + other.value) + } +} + +/** + * @param {string} format + * @param {string} value + * + * @return {object} + */ +const wrap = curry((format, value) => { + return new Text(format, value) +}) + +/** + * @param {string} value + * @return {Text} + */ +export const plain = wrap('plain') + +/** + * @function + * @param {string} value + * @return {Text} + */ +export const html = wrap('html') + +export const detect = (str) => { + if (!str.includes('>')) { + return plain(str) + } else { + return html(str) + } +} + +/** + * @function + * @param {string} format + * @param {Text} text + */ +const isFormat = curry((format, text) => { + return text.format === format +}) + +/** + * @function + * @param {Text} text + * @return {bool} + */ +export const isPlain = isFormat('plain') + +/** + * @function + * @param {Text} text + * @return {bool} + */ +export const isHtml = isFormat('html') + +/** + * @param {Text} text + * @return {Text} + */ +export const toPlain = (text) => { + if (text.format === 'plain') { + return text + } + const withBlockBreaks = text.value.replace(/<\/div>/gi, '</div><br>') + + const converted = fromString(withBlockBreaks, { + noLinkBrackets: true, + ignoreHref: true, + ignoreImage: true, + wordwrap: false, + format: { + blockquote: function (element, fn, options) { + return fn(element.children, options) + .replace(/\n\n\n/g, '\n\n') // remove triple line breaks + .replace(/^/gm, '> ') // add > quotation to each line + }, + paragraph: function (element, fn, options) { + return fn(element.children, options) + '\n\n' + }, + }, + }) + + return plain( + converted + .replace(/\n\n\n/g, '\n\n') // remove triple line breaks + .replace(/^[\n\r]+/g, '') // trim line breaks at beginning and end + .replace(/ $/gm, '') // trim white space at end of each line + ) +} + +/** + * @param {Text} text + * @return {Text} + */ +export const toHtml = (text) => { + if (text.format === 'html') { + return text + } + if (text.format === 'plain') { + return html(text.value.replace(/([^>\r\n]?)(\r\n|\n\r|\r|\n)/g, '$1<br>$2')) + } + + throw new Error(`Unknown format ${text.format}`) +} |