fix: handle links with spaces in markdown (#9164)

Signed-off-by: Alexander Onnikov <Alexander.Onnikov@xored.com>
This commit is contained in:
Alexander Onnikov 2025-06-04 11:38:12 +07:00 committed by GitHub
parent 14310dfa6f
commit 4c24bd5db9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 78 additions and 5 deletions

View File

@ -13,6 +13,7 @@
// limitations under the License.
//
import { MarkupNode } from '@hcengineering/text-core'
import { markdownToMarkup, markupToMarkdown } from '..'
import { isMarkdownsEquals } from '../compare'
@ -855,8 +856,65 @@ Lorem ipsum dolor sit amet.
})
})
describe('markupToMarkdown', () => {
const tests: Array<{ name: string, markdown: string, markup: object }> = [
{
name: 'links',
markdown: `[Link](https://example.com)
[Link with spaces](<https://example.com/with spaces>)
[Link with spaces and braces](<https://example.com/\\<with spaces\\>>)`,
markup: {
type: 'doc',
content: [
{
type: 'paragraph',
content: [
{
type: 'text',
text: 'Link',
marks: [{ type: 'link', attrs: { href: 'https://example.com' } }]
}
]
},
{
type: 'paragraph',
content: [
{
type: 'text',
text: 'Link with spaces',
marks: [{ type: 'link', attrs: { href: 'https://example.com/with spaces' } }]
}
]
},
{
type: 'paragraph',
content: [
{
type: 'text',
text: 'Link with spaces and braces',
marks: [{ type: 'link', attrs: { href: 'https://example.com/<with spaces>' } }]
}
]
}
]
}
}
]
describe('to markdown', () => {
tests.forEach(({ name, markdown, markup }) => {
it(name, () => {
const result = markupToMarkdown(markup as MarkupNode, options)
expect(result).toEqual(markdown)
})
})
})
})
describe('markdownToMarkup -> markupToMarkdown', () => {
const tests: Array<{ name: string, markdown: string }> = [
const tests: Array<{ name: string, markdown: string, alternate?: string }> = [
{ name: 'Italic', markdown: '*Asteriscs* and _Underscores_' },
{ name: 'Bold', markdown: '**Asteriscs** and __Underscores__' },
{ name: 'Bullet list with asteriscs', markdown: 'Asterisks :\r\n* Firstly\r\n* Secondly' },
@ -878,6 +936,16 @@ describe('markdownToMarkup -> markupToMarkdown', () => {
name: 'Link',
markdown: 'See [link](https://example.com)'
},
{
name: 'Link with spaces',
markdown: 'See [link](<https://example.com/with spaces>)',
alternate: 'See [link](https://example.com/with%20spaces)'
},
{
name: 'Link with spaces and braces',
markdown: 'See [link](<https://example.com/\\<with spaces\\>>)',
alternate: 'See [link](https://example.com/%3Cwith%20spaces%3E)'
},
{
name: 'Codeblock',
markdown: '```typescript\nconst x: number = 42;\n```'
@ -901,11 +969,11 @@ describe('markdownToMarkup -> markupToMarkdown', () => {
// }
]
tests.forEach(({ name, markdown }) => {
tests.forEach(({ name, markdown, alternate }) => {
it(name, () => {
const json = markdownToMarkup(markdown, options)
const serialized = markupToMarkdown(json, options)
expect(serialized).toEqualMarkdown(markdown)
expect(serialized).toEqualMarkdown(alternate ?? markdown)
})
})
})

View File

@ -351,11 +351,16 @@ export const storeMarks: Record<string, MarkProcessor> = {
} else {
const { inAutolink } = state
state.inAutolink = undefined
const href = (mark.attrs?.href as string) ?? ''
// eslint-disable-next-line
const url = href.replace(/[\(\)"\\<>]/g, '\\$&')
const hasSpaces = url.includes(' ')
return inAutolink === true
? '>'
: '](' +
// eslint-disable-next-line
(mark.attrs?.href as string).replace(/[\(\)"]/g, '\\$&') +
(hasSpaces ? `<${url}>` : url) +
(mark.attrs?.title !== undefined ? ` "${(mark.attrs?.title as string).replace(/"/g, '\\"')}"` : '') +
')'
}