Skip to content

Commit

Permalink
Merge pull request #6499 from nextcloud-libraries/fix/noid/rich-escape
Browse files Browse the repository at this point in the history
fix: extract un-escaping of text/code nodes with XML-like content
  • Loading branch information
Antreesy authored Feb 7, 2025
2 parents af857d1 + c290a62 commit d5043f6
Show file tree
Hide file tree
Showing 3 changed files with 69 additions and 21 deletions.
59 changes: 47 additions & 12 deletions cypress/component/richtext.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,20 +10,55 @@ import { mount } from 'cypress/vue2'
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: '<span>text&lt;/span&gt;',
useMarkdown: true,
},
})

cy.get('p').should('have.text', '<span>text</span>')
})
describe.only('XML-like text (escaped and unescaped)', () => {
const TEST = '<span>text</span> &lt;span&gt;text&lt;/span&gt;'
it('renders normal text as passed', () => {
mount(NcRichText, {
propsData: {
text: TEST,
},
})
cy.get('div.rich-text--wrapper').should('have.text', TEST)
})
it('renders with Markdown, escaping XML', () => {
mount(NcRichText, {
propsData: {
text: TEST,
useMarkdown: true,
},
})
cy.get('p').should('have.text', '<span>text</span> <span>text</span>')
})
it('renders with Markdown, escaping XML in code', () => {
mount(NcRichText, {
propsData: {
text: '```\n' + TEST + '\n```',
useMarkdown: true,
},
})
cy.get('code').should('have.text', '<span>text</span> <span>text</span>' + '\n')
})
it('renders with Flavored Markdown, escaping XML', () => {
mount(NcRichText, {
propsData: {
text: TEST,
useExtendedMarkdown: true,
},
})
cy.get('p').should('have.text', '<span>text</span> <span>text</span>')
})
it('renders with Flavored Markdown, escaping XML in code', () => {
mount(NcRichText, {
propsData: {
text: '```\n' + TEST + '\n```',
useExtendedMarkdown: true,
},
})
cy.get('code').should('have.text', '<span>text</span> <span>text</span>' + '\n')
})
})

describe('renders with markdown', () => {
describe('headings', () => {
it('heading (with hash (#) syntax divided with space from text)', () => {
const testCases = [
Expand Down
12 changes: 3 additions & 9 deletions src/components/NcRichText/NcRichText.vue
Original file line number Diff line number Diff line change
Expand Up @@ -308,6 +308,7 @@ import NcCheckboxRadioSwitch from '../NcCheckboxRadioSwitch/NcCheckboxRadioSwitc
import NcLoadingIcon from '../NcLoadingIcon/NcLoadingIcon.vue'
import { getRoute, remarkAutolink } from './autolink.js'
import { remarkPlaceholder, prepareTextNode } from './placeholder.js'
import { remarkUnescape } from './remarkUnescape.js'
import GenRandomId from '../../utils/GenRandomId.js'

import { unified } from 'unified'
Expand Down Expand Up @@ -459,6 +460,7 @@ export default {
useMarkdown: this.useMarkdown,
useExtendedMarkdown: this.useExtendedMarkdown,
})
.use(remarkUnescape)
.use(this.useExtendedMarkdown ? remarkGfm.value : undefined)
.use(breaks)
.use(remark2rehype, {
Expand All @@ -476,12 +478,6 @@ export default {
})
.use(rehype2react, {
createElement: (tag, attrs, children) => {
// unescape special symbol "<" for simple text nodes
children = children?.map(child => typeof child === 'string'
? child.replace(/&lt;/gmi, '<')
: child,
)

if (!tag.startsWith('#')) {
if (this.useExtendedMarkdown && remarkGfm.value) {
if (tag === 'code' && !rehypeHighlight.value
Expand Down Expand Up @@ -559,9 +555,7 @@ export default {
})
.processSync(this.text
// escape special symbol "<" to not treat text as HTML
.replace(/</gmi, '&lt;')
// unescape special symbol ">" to parse blockquotes
.replace(/&gt;/gmi, '>'),
.replace(/<[^>]+>/g, (match) => match.replace(/</g, '&lt;')),
)
.result

Expand Down
19 changes: 19 additions & 0 deletions src/components/NcRichText/remarkUnescape.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/**
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

import { visit, SKIP } from 'unist-util-visit'

export const remarkUnescape = function() {
return function(tree) {
visit(tree, (node) => node.type === 'text' || node.type === 'code',
(node, index, parent) => {
parent.children.splice(index, 1, {
...node,
value: node.value.replace(/&lt;/gmi, '<').replace(/&gt;/gmi, '>'),
})
return [SKIP, index + 1]
})
}
}

0 comments on commit d5043f6

Please sign in to comment.