Skip to content

Commit 7f372fa

Browse files
authored
Merge pull request #8449 from nextcloud-libraries/fix/8397/nested-links
2 parents 80ddb29 + 7fda70f commit 7f372fa

7 files changed

Lines changed: 70 additions & 31 deletions

File tree

package-lock.json

Lines changed: 4 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,7 @@
116116
"ts-md5": "^2.0.1",
117117
"unified": "^11.0.5",
118118
"unist-builder": "^4.0.0",
119-
"unist-util-visit": "^5.1.0",
119+
"unist-util-visit-parents": "^6.0.2",
120120
"vue": "^3.5.18",
121121
"vue-router": "^5.0.6"
122122
},

src/components/NcRichText/autolink.ts

Lines changed: 19 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,12 @@
33
* SPDX-License-Identifier: AGPL-3.0-or-later
44
*/
55

6+
import type { Node, Parent } from 'unist'
67
import type { Router } from 'vue-router'
78

89
import { getBaseUrl, getRootUrl } from '@nextcloud/router'
910
import { u } from 'unist-builder'
10-
import { SKIP, visit } from 'unist-util-visit'
11+
import { SKIP, visitParents } from 'unist-util-visit-parents'
1112
import { defineComponent, h } from 'vue'
1213
import { logger } from '../../utils/logger.ts'
1314
import { URL_PATTERN_AUTOLINK } from './helpers.js'
@@ -45,13 +46,19 @@ export function remarkAutolink({ autolink, useMarkdown, useExtendedMarkdown }) {
4546
return
4647
}
4748

48-
visit(tree, (node) => node.type === 'text', (node, index, parent) => {
49-
let parsed = parseUrl(node.value)
50-
if (typeof parsed === 'string') {
51-
parsed = [u('text', parsed)]
52-
} else {
53-
parsed = parsed
54-
.map((n) => {
49+
visitParents(tree, (node) => node.type === 'text', (node, ancestors: Parent[]) => {
50+
// Do not autolink text already inside a link node
51+
if (ancestors.some((ancestor) => ancestor.type === 'link' || ancestor.type === 'linkReference')) {
52+
return
53+
}
54+
55+
const parent = ancestors.at(-1)
56+
const index = parent!.children.indexOf(node) ?? 0
57+
58+
const parsed = parseUrl(node.value)
59+
const parsedNodes: Node[] = (typeof parsed === 'string')
60+
? [u('text', parsed)]
61+
: parsed.map((n) => {
5562
if (typeof n === 'string') {
5663
return u('text', n)
5764
}
@@ -60,12 +67,11 @@ export function remarkAutolink({ autolink, useMarkdown, useExtendedMarkdown }) {
6067
url: n.props.href,
6168
}, [u('text', n.props.href)])
6269
})
63-
.filter((x) => x)
64-
.flat()
65-
}
70+
.filter((x) => x)
71+
.flat()
6672

67-
parent.children.splice(index, 1, ...parsed)
68-
return [SKIP, (index ?? 0) + parsed.length]
73+
parent!.children.splice(index, 1, ...parsedNodes)
74+
return [SKIP, index + parsedNodes.length]
6975
})
7076
}
7177
}

src/components/NcRichText/remarkPlaceholder.ts

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import type { Node, Parent } from 'unist'
88
import type { TextNode } from './helpers.ts'
99

1010
import { u } from 'unist-builder'
11-
import { visit } from 'unist-util-visit'
11+
import { visitParents } from 'unist-util-visit-parents'
1212

1313
/**
1414
* Check if the given node is a literal and specifically a text node
@@ -21,14 +21,10 @@ function isTextNode(node: Node): node is TextNode {
2121

2222
const transformPlaceholders: Transformer = function(ast: Node) {
2323
// Apply the visitor to all text nodes of the AST
24-
visit(ast, isTextNode, visitor)
24+
visitParents(ast, isTextNode, (node: TextNode, ancestors: Parent[]) => {
25+
const parent = ancestors.at(-1)
26+
const index = parent!.children.indexOf(node)
2527

26-
/**
27-
* @param node - The text node
28-
* @param index - The index of the node
29-
* @param parent - The parent node
30-
*/
31-
function visitor(node: TextNode, index?: number, parent?: Parent) {
3228
const placeholders = node.value.split(/(\{[a-z\-_.0-9]+\})/ig)
3329
.map((entry: string) => {
3430
const matches = entry.match(/^\{([a-z\-_.0-9]+)\}$/i)
@@ -43,7 +39,7 @@ const transformPlaceholders: Transformer = function(ast: Node) {
4339
})
4440

4541
parent!.children.splice(index!, 1, ...placeholders)
46-
}
42+
})
4743
}
4844

4945
export const remarkPlaceholder: Plugin = () => transformPlaceholders

src/components/NcRichText/remarkStripCode.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import type { Plugin } from 'unified'
77
import type { Node, Parent } from 'unist'
88
import type { TextNode } from './helpers.ts'
99

10-
import { SKIP, visit } from 'unist-util-visit'
10+
import { SKIP, visitParents } from 'unist-util-visit-parents'
1111

1212
/**
1313
* Check if the given node is a literal and specifically a fenced node (inline code or code block)
@@ -20,7 +20,10 @@ function isCodeNode(node: Node): node is TextNode {
2020

2121
export const remarkStripCode: Plugin = function() {
2222
return function(tree: Node) {
23-
visit(tree, isCodeNode, (node: TextNode, index?: number, parent?: Parent) => {
23+
visitParents(tree, isCodeNode, (node: TextNode, ancestors: Parent[]) => {
24+
const parent = ancestors.at(-1)
25+
const index = parent!.children.indexOf(node)
26+
2427
parent!.children.splice(index!, 1, {
2528
...node,
2629
value: '',

src/components/NcRichText/remarkUnescape.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import type { Plugin } from 'unified'
77
import type { Node, Parent } from 'unist'
88
import type { TextNode } from './helpers.ts'
99

10-
import { SKIP, visit } from 'unist-util-visit'
10+
import { SKIP, visitParents } from 'unist-util-visit-parents'
1111

1212
/**
1313
* Check if the given node is a literal and specifically a text node
@@ -20,7 +20,10 @@ function isTextNode(node: Node): node is TextNode {
2020

2121
export const remarkUnescape: Plugin = function() {
2222
return function(tree: Node) {
23-
visit(tree, isTextNode, (node: TextNode, index?: number, parent?: Parent) => {
23+
visitParents(tree, isTextNode, (node: TextNode, ancestors: Parent[]) => {
24+
const parent = ancestors.at(-1)
25+
const index = parent!.children.indexOf(node)
26+
2427
parent!.children.splice(index!, 1, {
2528
...node,
2629
value: node.value.replace(/&lt;/gmi, '<').replace(/&gt;/gmi, '>'),

tests/unit/components/NcRichText/NcRichText.spec.js

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,37 @@ describe('Foo', () => {
192192
expect(wrapper.find('em').text()).toEqual('to')
193193
})
194194

195+
it('does not autolink markdown link text that is already inside a link', async () => {
196+
const wrapper = mount(NcRichText, {
197+
props: {
198+
text: '[https://example-nested.org](https://example.com)',
199+
autolink: true,
200+
useMarkdown: true,
201+
},
202+
})
203+
204+
const links = wrapper.findAll('a')
205+
expect(links).toHaveLength(1)
206+
expect(links[0].attributes('href')).toEqual('https://example.com')
207+
expect(links[0].text()).toEqual('https://example-nested.org')
208+
})
209+
210+
it('does not autolink deeply nested markdown link text that is already inside a link', async () => {
211+
const wrapper = mount(NcRichText, {
212+
props: {
213+
text: '[**https://example-nested.org**](https://example.com)',
214+
autolink: true,
215+
useMarkdown: true,
216+
},
217+
})
218+
219+
const links = wrapper.findAll('a')
220+
expect(links).toHaveLength(1)
221+
expect(links[0].attributes('href')).toEqual('https://example.com')
222+
expect(links[0].text()).toEqual('https://example-nested.org')
223+
expect(wrapper.find('strong').text()).toEqual('https://example-nested.org')
224+
})
225+
195226
it('formats markdown is disabled', async () => {
196227
const wrapper = mount(NcRichText, {
197228
props: {

0 commit comments

Comments
 (0)