diff --git a/cypress/component/richtext.cy.ts b/cypress/component/richtext.cy.ts index 519068d7b9..28ae7e6f77 100644 --- a/cypress/component/richtext.cy.ts +++ b/cypress/component/richtext.cy.ts @@ -6,6 +6,19 @@ import NcRichText from '../../src/components/NcRichText/NcRichText.vue' describe('NcRichText', () => { describe('renders with markdown', () => { + describe('normal text', () => { + it('XML-like text (escaped and unescaped)', () => { + mount(NcRichText, { + propsData: { + text: 'text</span>', + useMarkdown: true, + }, + }) + + cy.get('p').should('have.text', 'text') + }) + }) + describe('headings', () => { it('heading (with hash (#) syntax divided with space from text)', () => { const testCases = [ @@ -274,6 +287,17 @@ describe('NcRichText', () => { cy.get('code').should('have.text', 'inline code') }) + + it('inline code (with ignored bold, italic, XML-like syntax))', () => { + mount(NcRichText, { + propsData: { + text: '`inline code **bold text** _italic text_ text</span>`', + useMarkdown: true, + }, + }) + + cy.get('code').should('have.text', 'inline code **bold text** _italic text_ text') + }) }) describe('multiline code', () => { @@ -333,20 +357,20 @@ describe('NcRichText', () => { cy.get('code').should('have.text', 'line 1\nline 2\nline 3\n') }) - it('multiline code (with ignored bold, italic, inline code syntax)', () => { + it('multiline code (with ignored bold, italic, inline code, XML-like syntax)', () => { mount(NcRichText, { propsData: { - text: '```\n**bold text**\n_italic text_\n`inline code`\n```', + text: '```\n**bold text**\n_italic text_\n`inline code`\ntext</span>\n```', useMarkdown: true, }, }) - cy.get('pre').should('have.text', '**bold text**\n_italic text_\n`inline code`\n') + cy.get('pre').should('have.text', '**bold text**\n_italic text_\n`inline code`\ntext\n') }) }) describe('blockquote', () => { - it('blockquote (with greater then (gt >) syntax)', () => { + it('blockquote (with greater then (>) syntax - normal)', () => { mount(NcRichText, { propsData: { text: '> blockquote', @@ -357,6 +381,17 @@ describe('NcRichText', () => { cy.get('blockquote').should('have.text', '\nblockquote\n') }) + it('blockquote (with greater then (>) syntax - escaped)', () => { + mount(NcRichText, { + propsData: { + text: '> blockquote', + useMarkdown: true, + }, + }) + + cy.get('blockquote').should('have.text', '\nblockquote\n') + }) + it('blockquote (with bold, italic text, inline code)', () => { mount(NcRichText, { propsData: { @@ -401,7 +436,7 @@ describe('NcRichText', () => { }, }) - cy.get('blockquote').should('have.text', '\nline 1\n\nline 3\n') + cy.get('blockquote').should('have.text', '\nline 1\nline 3\n') }) it('blockquote (with nested blockquote)', () => { diff --git a/package-lock.json b/package-lock.json index e4ab46f06b..f2c35fec1e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -34,9 +34,9 @@ "linkify-string": "^4.0.0", "md5": "^2.3.0", "node-polyfill-webpack-plugin": "^2.0.1", + "rehype-external-links": "^3.0.0", "rehype-react": "^7.1.2", "remark-breaks": "^3.0.2", - "remark-external-links": "^9.0.1", "remark-parse": "^10.0.1", "remark-rehype": "^10.1.0", "splitpanes": "^2.4.1", @@ -4814,6 +4814,11 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/@ungap/structured-clone": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", + "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==" + }, "node_modules/@vitejs/plugin-vue2": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue2/-/plugin-vue2-2.2.0.tgz", @@ -13442,6 +13447,26 @@ "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.6.tgz", "integrity": "sha512-PBjIUxZHOuj0R15/xuwJYjFi+KZdNFrehocChv4g5hu6aFroHue8m0lBP0POdK2nKzbw0cgV1mws8+V/JAcEkQ==" }, + "node_modules/hast-util-is-element": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hast-util-is-element/-/hast-util-is-element-3.0.0.tgz", + "integrity": "sha512-Val9mnv2IWpLbNPqc/pUem+a7Ipj2aHacCwgNfTiK0vJKl0LF+4Ba4+v1oPHFpf3bLYmreq0/l3Gud9S5OH42g==", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-is-element/node_modules/@types/hast": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.0.tgz", + "integrity": "sha512-SoytUJRuf68HXYqcXicQIhCrLQjqeYU2anikr4G3p3Iz+OZO5QDQpDj++gv+RenHsnUBwNZ2dumBArF8VLSk2Q==", + "dependencies": { + "@types/unist": "*" + } + }, "node_modules/hast-util-whitespace": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-2.0.1.tgz", @@ -22089,6 +22114,31 @@ "jsesc": "bin/jsesc" } }, + "node_modules/rehype-external-links": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/rehype-external-links/-/rehype-external-links-3.0.0.tgz", + "integrity": "sha512-yp+e5N9V3C6bwBeAC4n796kc86M4gJCdlVhiMTxIrJG5UHDMh+PJANf9heqORJbt1nrCbDwIlAZKjANIaVBbvw==", + "dependencies": { + "@types/hast": "^3.0.0", + "@ungap/structured-clone": "^1.0.0", + "hast-util-is-element": "^3.0.0", + "is-absolute-url": "^4.0.0", + "space-separated-tokens": "^2.0.0", + "unist-util-visit": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/rehype-external-links/node_modules/@types/hast": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.0.tgz", + "integrity": "sha512-SoytUJRuf68HXYqcXicQIhCrLQjqeYU2anikr4G3p3Iz+OZO5QDQpDj++gv+RenHsnUBwNZ2dumBArF8VLSk2Q==", + "dependencies": { + "@types/unist": "*" + } + }, "node_modules/rehype-react": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/rehype-react/-/rehype-react-7.2.0.tgz", @@ -22305,123 +22355,6 @@ "url": "https://opencollective.com/unified" } }, - "node_modules/remark-external-links": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/remark-external-links/-/remark-external-links-9.0.1.tgz", - "integrity": "sha512-EYw+p8Zqy5oT5+W8iSKzInfRLY+zeKWHCf0ut+Q5SwnaSIDGXd2zzvp4SWqyAuVbinNmZ0zjMrDKaExWZnTYqQ==", - "dependencies": { - "@types/hast": "^2.3.2", - "@types/mdast": "^3.0.0", - "extend": "^3.0.0", - "is-absolute-url": "^4.0.0", - "mdast-util-definitions": "^5.0.0", - "space-separated-tokens": "^2.0.0", - "unified": "^10.0.0", - "unist-util-visit": "^4.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-external-links/node_modules/@types/unist": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.7.tgz", - "integrity": "sha512-cputDpIbFgLUaGQn6Vqg3/YsJwxUwHLO13v3i5ouxT4lat0khip9AEWxtERujXV9wxIB1EyF97BSJFt6vpdI8g==" - }, - "node_modules/remark-external-links/node_modules/is-buffer": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.5.tgz", - "integrity": "sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "engines": { - "node": ">=4" - } - }, - "node_modules/remark-external-links/node_modules/is-plain-obj": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", - "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/remark-external-links/node_modules/unified": { - "version": "10.1.2", - "resolved": "https://registry.npmjs.org/unified/-/unified-10.1.2.tgz", - "integrity": "sha512-pUSWAi/RAnVy1Pif2kAoeWNBa3JVrx0MId2LASj8G+7AiHWoKZNTomq6LG326T68U7/e263X6fTdcXIy7XnF7Q==", - "dependencies": { - "@types/unist": "^2.0.0", - "bail": "^2.0.0", - "extend": "^3.0.0", - "is-buffer": "^2.0.0", - "is-plain-obj": "^4.0.0", - "trough": "^2.0.0", - "vfile": "^5.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-external-links/node_modules/unist-util-visit": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-4.1.2.tgz", - "integrity": "sha512-MSd8OUGISqHdVvfY9TPhyK2VdUrPgxkUtWSuMHF6XAAFuL4LokseigBnZtPnJMu+FbynTkFNnFlyjxpVKujMRg==", - "dependencies": { - "@types/unist": "^2.0.0", - "unist-util-is": "^5.0.0", - "unist-util-visit-parents": "^5.1.1" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-external-links/node_modules/vfile": { - "version": "5.3.7", - "resolved": "https://registry.npmjs.org/vfile/-/vfile-5.3.7.tgz", - "integrity": "sha512-r7qlzkgErKjobAmyNIkkSpizsFPYiUPuJb5pNW1RB4JcYVZhs4lIbVqk8XPk033CV/1z8ss5pkax8SuhGpcG8g==", - "dependencies": { - "@types/unist": "^2.0.0", - "is-buffer": "^2.0.0", - "unist-util-stringify-position": "^3.0.0", - "vfile-message": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-external-links/node_modules/vfile-message": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-3.1.4.tgz", - "integrity": "sha512-fa0Z6P8HUrQN4BZaX05SIVXic+7kE3b05PWAtPuYP9QLHsLKYR7/AlLW3NtOrpXRLeawpDLMsVkmk5DG0NXgWw==", - "dependencies": { - "@types/unist": "^2.0.0", - "unist-util-stringify-position": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, "node_modules/remark-parse": { "version": "10.0.2", "resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-10.0.2.tgz", diff --git a/package.json b/package.json index 4aa6cd0b76..0f8f47886c 100644 --- a/package.json +++ b/package.json @@ -84,9 +84,9 @@ "linkify-string": "^4.0.0", "md5": "^2.3.0", "node-polyfill-webpack-plugin": "^2.0.1", + "rehype-external-links": "^3.0.0", "rehype-react": "^7.1.2", "remark-breaks": "^3.0.2", - "remark-external-links": "^9.0.1", "remark-parse": "^10.0.1", "remark-rehype": "^10.1.0", "splitpanes": "^2.4.1", diff --git a/src/components/NcAvatar/NcAvatar.vue b/src/components/NcAvatar/NcAvatar.vue index 3a9b235f4c..e88c402c14 100644 --- a/src/components/NcAvatar/NcAvatar.vue +++ b/src/components/NcAvatar/NcAvatar.vue @@ -697,6 +697,7 @@ export default { &--unknown { position: relative; background-color: var(--color-main-background); + white-space: normal; } &:not(&--unknown) { diff --git a/src/components/NcRichText/NcRichText.vue b/src/components/NcRichText/NcRichText.vue index 99ea79d1af..348065bf3c 100644 --- a/src/components/NcRichText/NcRichText.vue +++ b/src/components/NcRichText/NcRichText.vue @@ -76,7 +76,7 @@ import markdown from 'remark-parse' import breaks from 'remark-breaks' import remark2rehype from 'remark-rehype' import rehype2react from 'rehype-react' -import remarkExternalLinks from 'remark-external-links' +import rehypeExternalLinks from 'rehype-external-links' export default { name: 'NcRichText', @@ -176,10 +176,6 @@ export default { autolink: this.autolink, useMarkdown: this.useMarkdown, }) - .use(remarkExternalLinks, { - target: '_blank', - rel: ['noopener noreferrer'], - }) .use(breaks) .use(remark2rehype, { handlers: { @@ -190,8 +186,18 @@ export default { }) // .use(rehypeAddClasses, this.markdownCssClasses) .use(remarkPlaceholder) + .use(rehypeExternalLinks, { + target: '_blank', + rel: ['noopener noreferrer'], + }) .use(rehype2react, { createElement: (tag, attrs, children) => { + // unescape special symbol "<" for simple text nodes + children = children?.map(child => typeof child === 'string' + ? child.replace(/</gmi, '<') + : child, + ) + if (!tag.startsWith('#')) { return h(tag, attrs, children) } @@ -217,18 +223,15 @@ export default { }, prefix: false, }) - .processSync(this.useMarkdown - // In order to correctly show newlines in Markdown, - // each newline contains a non-breaking space - ? this.text.slice() - .replace(/\n>\n/g, '\n>\u00A0\n') - .replace(/\n{2,}/g, (match) => { - return '\n' + '\n\u00A0\n'.repeat(match.length - 1) - }) - : this.text) + .processSync(this.text + // escape special symbol "<" to not treat text as HTML + .replace(/" to parse blockquotes + .replace(/>/gmi, '>') + ) .result - return h('div', { class: 'rich-text--wrapper' }, [ + return h('div', { class: 'rich-text--wrapper rich-text--wrapper-markdown' }, [ renderedMarkdown, this.referenceLimit > 0 ? h('div', { class: 'rich-text--reference-widget' }, [ @@ -239,11 +242,9 @@ export default { }, }, render(h) { - if (!this.useMarkdown) { - return this.renderPlaintext(h) - } - - return this.renderMarkdown(h) + return this.useMarkdown + ? this.renderMarkdown(h) + : this.renderPlaintext(h) }, } diff --git a/src/components/NcRichText/placeholder.js b/src/components/NcRichText/placeholder.js index f2ee5b4f91..accc69048c 100644 --- a/src/components/NcRichText/placeholder.js +++ b/src/components/NcRichText/placeholder.js @@ -26,10 +26,7 @@ export const remarkPlaceholder = function() { }) }) - node = u('element', { tagName: 'span' }, [ - ...placeholders, - ]) - parent.children[index] = node + parent.children.splice(index, 1, ...placeholders) } } } diff --git a/src/components/NcRichText/richtext.scss b/src/components/NcRichText/richtext.scss index 2d85ff4095..97c8994791 100644 --- a/src/components/NcRichText/richtext.scss +++ b/src/components/NcRichText/richtext.scss @@ -132,3 +132,41 @@ } } } + +.rich-text--wrapper-markdown { + div > *:first-child, + blockquote > *:first-child{ + margin-top: 0 !important; + } + div > *:last-child , + blockquote > *:last-child { + margin-bottom: 0 !important; + } + + h1, h2, h3, h4, h5, h6, p, ul, ol, blockquote, pre { + margin-top: 0; + margin-bottom: 1em; + } + + h1, h2, h3, h4, h5, h6 { + font-weight: bold; + } + + h1 { + font-size: 30px; + } + + ul, ol { + padding-left: 15px; + } + + ul { + list-style-type: disc; + } + + blockquote { + padding-left: 13px; + border-left: 2px solid var(--color-border-dark); + color: var(--color-text-lighter); + } +}