diff --git a/app/javascript/dashboard/components/Modal.vue b/app/javascript/dashboard/components/Modal.vue index c8a23bf95..48d4e8d6b 100644 --- a/app/javascript/dashboard/components/Modal.vue +++ b/app/javascript/dashboard/components/Modal.vue @@ -123,6 +123,10 @@ onMounted(() => { a { @apply p-4; } + + .ProseMirror a { + @apply p-0; + } } } } diff --git a/app/javascript/dashboard/constants/editor.js b/app/javascript/dashboard/constants/editor.js index cb0996136..8af56c5e2 100644 --- a/app/javascript/dashboard/constants/editor.js +++ b/app/javascript/dashboard/constants/editor.js @@ -188,6 +188,16 @@ export const ARTICLE_EDITOR_MENU_OPTIONS = [ 'insertTable', ]; +// [text](url) -> "text: url" (drop label if it equals the URL). Keep serializer +// escapes; the re-parse renders them literally, unescaping would crash it. +const flattenLink = (_match, text, url) => { + const cleanUrl = url + .trim() + .replace(/\s+["'(].*$/, '') + .replace(/^<|>$/g, ''); + return text === cleanUrl ? cleanUrl : `${text}: ${cleanUrl}`; +}; + /** * Markdown formatting patterns for stripping unsupported formatting. * @@ -264,7 +274,12 @@ export const MARKDOWN_PATTERNS = [ { type: 'link', // PM: link patterns: [ - { pattern: /\[([^\]]+)\]\([^)]+\)/g, replacement: '$1' }, // [text](url) -> text + // Escape-aware label + URL captures so a \] or \) can't cut the match + // short and leave link markup that crashes the re-parse. + { + pattern: /\[((?:\\.|[^\]\\])*)\]\(((?:\\.|[^)\\])*)\)/g, + replacement: flattenLink, + }, { pattern: /<([a-zA-Z][a-zA-Z0-9+.-]*:[^\s>]+)>/g, replacement: '$1' }, // , , , , etc { pattern: /<([^\s@]+@[^\s@>]+)>/g, replacement: '$1' }, // -> user@example.com ], diff --git a/app/javascript/dashboard/helper/specs/editorHelper.spec.js b/app/javascript/dashboard/helper/specs/editorHelper.spec.js index 7d32f63e0..220b9903e 100644 --- a/app/javascript/dashboard/helper/specs/editorHelper.spec.js +++ b/app/javascript/dashboard/helper/specs/editorHelper.spec.js @@ -1,4 +1,9 @@ -import { EditorState, EditorView } from '@chatwoot/prosemirror-schema'; +import { + EditorState, + EditorView, + buildMessageSchema, + MessageMarkdownTransformer, +} from '@chatwoot/prosemirror-schema'; import { FORMATTING } from 'dashboard/constants/editor'; import { Schema } from 'prosemirror-model'; import { @@ -978,13 +983,77 @@ describe('stripUnsupportedFormatting', () => { ); }); - it('strips links but keeps text', () => { + it('keeps link text and URL when schema does not support links', () => { expect( stripUnsupportedFormatting( 'Check [this link](https://example.com)', emptySchema ) - ).toBe('Check this link'); + ).toBe('Check this link: https://example.com'); + }); + + it('drops the hidden link title when preserving the URL', () => { + expect( + stripUnsupportedFormatting( + 'Check [docs](https://example.com "Docs")', + emptySchema + ) + ).toBe('Check docs: https://example.com'); + + expect( + stripUnsupportedFormatting( + 'Check [docs]( "Docs")', + emptySchema + ) + ).toBe('Check docs: https://example.com'); + }); + + // Output is re-parsed before sending, so assert the final text + // (strip + re-parse); the re-parse turns serializer escapes into literals. + describe('links round-trip through re-parse without crashing', () => { + const smsSchema = buildMessageSchema([], []); // no marks, no nodes + const sendAs = md => + new MessageMarkdownTransformer(smsSchema).parse( + stripUnsupportedFormatting(md, smsSchema) + ).textContent; + + it('keeps escaped parens/underscores anywhere in the URL', () => { + expect( + sendAs('See [wiki](https://en.wikipedia.org/wiki/Foo\\_\\(bar\\))') + ).toBe('See wiki: https://en.wikipedia.org/wiki/Foo_(bar)'); + expect(sendAs('See [wiki](https://host/a\\_\\(b\\)c)')).toBe( + 'See wiki: https://host/a_(b)c' + ); + }); + + it('drops the label when it equals the URL even when escaped', () => { + expect( + sendAs( + '[www.example.com/Foo\\_\\(bar\\)](www.example.com/Foo\\_\\(bar\\))' + ) + ).toBe('www.example.com/Foo_(bar)'); + }); + + it('does not reintroduce emphasis from an escaped label', () => { + expect(sendAs('[Use \\_id\\_](https://example.com)')).toBe( + 'Use _id_: https://example.com' + ); + }); + + it('flattens a label containing an escaped closing bracket', () => { + expect(sendAs('[FAQ \\[v2\\]](https://example.com)')).toBe( + 'FAQ [v2]: https://example.com' + ); + }); + }); + + it('leaves bare URLs untouched so channels can auto-link them', () => { + expect( + stripUnsupportedFormatting('Visit www.example.com now', emptySchema) + ).toBe('Visit www.example.com now'); + expect( + stripUnsupportedFormatting('Visit ', emptySchema) + ).toBe('Visit https://example.com'); }); it('converts autolinks to plain URLs when schema does not support links', () => { @@ -1049,7 +1118,7 @@ describe('stripUnsupportedFormatting', () => { it('handles complex content with multiple formatting types', () => { const content = '**Bold** and *italic* with `code` and [link](url)\n- list item'; - const expected = 'Bold and italic with code and link\nlist item'; + const expected = 'Bold and italic with code and link: url\nlist item'; expect(stripUnsupportedFormatting(content, emptySchema)).toBe(expected); }); }); diff --git a/app/javascript/dashboard/routes/dashboard/settings/canned/AddCanned.vue b/app/javascript/dashboard/routes/dashboard/settings/canned/AddCanned.vue index 7fd23ebb1..b2d0dae69 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/canned/AddCanned.vue +++ b/app/javascript/dashboard/routes/dashboard/settings/canned/AddCanned.vue @@ -149,9 +149,5 @@ export default { :deep(.ProseMirror-woot-style) { @apply min-h-[12.5rem]; - - p { - @apply text-base; - } } diff --git a/app/javascript/dashboard/routes/dashboard/settings/canned/EditCanned.vue b/app/javascript/dashboard/routes/dashboard/settings/canned/EditCanned.vue index 7a570a300..9980db58b 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/canned/EditCanned.vue +++ b/app/javascript/dashboard/routes/dashboard/settings/canned/EditCanned.vue @@ -153,9 +153,5 @@ export default { :deep(.ProseMirror-woot-style) { @apply min-h-[12.5rem]; - - p { - @apply text-base; - } }