Skip to content

Commit

Permalink
Merge pull request #6500 from nextcloud-libraries/backport/6499/next
Browse files Browse the repository at this point in the history
[next] fix: extract un-escaping of text/code nodes with XML-like content
  • Loading branch information
Antreesy authored Feb 10, 2025
2 parents 7033776 + 67d8c5d commit f25a75e
Show file tree
Hide file tree
Showing 3 changed files with 74 additions and 18 deletions.
11 changes: 4 additions & 7 deletions src/components/NcRichText/NcRichText.vue
Original file line number Diff line number Diff line change
Expand Up @@ -307,6 +307,7 @@ import NcCheckboxRadioSwitch from '../NcCheckboxRadioSwitch/NcCheckboxRadioSwitc
import NcLoadingIcon from '../NcLoadingIcon/NcLoadingIcon.vue'
import { getRoute, remarkAutolink } from './autolink.ts'
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 @@ -456,6 +457,7 @@ export default {
useMarkdown: this.useMarkdown,
useExtendedMarkdown: this.useExtendedMarkdown,
})
.use(remarkUnescape)
.use(this.useExtendedMarkdown ? remarkGfm.value : undefined)
.use(breaks)
.use(remark2rehype, {
Expand All @@ -480,7 +482,7 @@ export default {
})
.processSync(this.text
// escape special symbol "<" to not treat text as HTML
.replace(/</gmi, '&lt;')
.replace(/<[^>]+>/g, (match) => match.replace(/</g, '&lt;'))
// unescape special symbol ">" to parse blockquotes
.replace(/&gt;/gmi, '>'),
)
Expand All @@ -506,14 +508,9 @@ export default {
props.key = key
}
// Children should be always an array
let children = props.children ?? []
const children = props.children ?? []
delete props.children

// unescape special symbol "<" for simple text nodes
if (typeof children === 'string') {
children = children.replace(/&lt;/gmi, '<')
}

if (!String(type).startsWith('#')) {
let nestedNode = null
if (this.useExtendedMarkdown && remarkGfm.value) {
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) => ['text', 'code', 'inlineCode'].includes(node.type),
(node, index, parent) => {
parent.children.splice(index, 1, {
...node,
value: node.value.replace(/&lt;/gmi, '<').replace(/&gt;/gmi, '>'),
})
return [SKIP, index + 1]
})
}
}
62 changes: 51 additions & 11 deletions tests/component/components/NcRichText/markown-rendering.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,57 @@
// Markdown guide: https://www.markdownguide.org/basic-syntax/
// Reference tests: https://github.com/nextcloud-deps/CDMarkdownKit/tree/master/CDMarkdownKitTests

import { expect, test } from '@playwright/experimental-ct-vue';
import { expect, test } from '@playwright/experimental-ct-vue'
import NcRichText from '../../../../src/components/NcRichText/NcRichText.vue'

test.describe('XML-like text (escaped and unescaped)', () => {
const TEST = '<span>text</span> &lt;span&gt;text&lt;/span&gt;'
test('renders normal text as passed', async ({ mount }) => {
const component = await mount(NcRichText, {
props: {
text: TEST,
},
})
await expect(component.getByText(TEST)).toBeVisible()
})
test('renders with Markdown, escaping XML', async ({ mount }) => {
const component = await mount(NcRichText, {
props: {
text: TEST,
useMarkdown: true,
},
})
await expect(component.getByRole('paragraph')).toContainText('<span>text</span> <span>text</span>')
})
test('renders with Markdown, escaping XML in code', async ({ mount }) => {
const component = await mount(NcRichText, {
props: {
text: '```\n' + TEST + '\n```',
useMarkdown: true,
},
})
await expect(component.getByRole('code')).toContainText('<span>text</span> <span>text</span>' + '\n')
})
test('renders with Flavored Markdown, escaping XML', async ({ mount }) => {
const component = await mount(NcRichText, {
props: {
text: TEST,
useExtendedMarkdown: true,
},
})
await expect(component.getByRole('paragraph')).toContainText('<span>text</span> <span>text</span>')
})
test('renders with Flavored Markdown, escaping XML in code', async ({ mount }) => {
const component = await mount(NcRichText, {
props: {
text: '```\n' + TEST + '\n```',
useExtendedMarkdown: true,
},
})
await expect(component.getByRole('code')).toContainText('<span>text</span> <span>text</span>' + '\n')
})
})

test.describe('dividers', () => {
test('dividers with asterisks', async ({ mount }) => {
const component = await mount(NcRichText, {
Expand Down Expand Up @@ -179,7 +227,6 @@ test.describe('bold text', () => {
})
})


test.describe('italic text', () => {
test('italic text (single with asterisk syntax)', async ({ mount }) => {
const component = await mount(NcRichText, {
Expand Down Expand Up @@ -253,7 +300,6 @@ test.describe('italic text', () => {
})
})


test.describe('strikethrough text', () => {
test('strikethrough text (with single tilda syntax)', async ({ mount }) => {
const component = await mount(NcRichText, {
Expand Down Expand Up @@ -303,7 +349,6 @@ test.describe('strikethrough text', () => {
})
})


test.describe('inline code', () => {
test('inline code (single with backticks syntax)', async ({ mount }) => {
const component = await mount(NcRichText, {
Expand Down Expand Up @@ -374,7 +419,6 @@ test.describe('inline code', () => {
})
})


test.describe('multiline code', () => {
test('multiline code (with triple backticks syntax)', async ({ mount }) => {
const component = await mount(NcRichText, {
Expand Down Expand Up @@ -444,7 +488,6 @@ test.describe('multiline code', () => {
})
})


test.describe('blockquote', () => {
test('blockquote (with greater then (>) syntax - normal)', async ({ mount }) => {
const component = await mount(NcRichText, {
Expand Down Expand Up @@ -527,7 +570,6 @@ test.describe('blockquote', () => {
})
})


test.describe('lists', () => {
test('ordered list (with number + `.` syntax divided with space from text)', async ({ mount }) => {
const testCases = [
Expand Down Expand Up @@ -586,7 +628,6 @@ test.describe('lists', () => {
})
})


test.describe('task lists', () => {
test('task list (with `- [ ]` and `- [x]` syntax divided with space from text)', async ({ mount }) => {
const testCases = [
Expand All @@ -606,14 +647,13 @@ test.describe('task lists', () => {
await expect(component.locator('ul')).toHaveCount(1)
await expect(component.getByRole('listitem')).toHaveCount(testCases.length)

for(const [index, testcase] of testCases.entries()) {
for (const [index, testcase] of testCases.entries()) {
await expect(component.getByRole('listitem').nth(index)).toHaveText(testcase.output)
await expect(component.getByRole('listitem').nth(index).getByRole('checkbox')).toBeChecked({ checked: testcase.checked})
await expect(component.getByRole('listitem').nth(index).getByRole('checkbox')).toBeChecked({ checked: testcase.checked })
}
})
})


test.describe('tables', () => {
test('table (with `-- | --` syntax)', async ({ mount }) => {
const component = await mount(NcRichText, {
Expand Down

0 comments on commit f25a75e

Please sign in to comment.