diff --git a/services/web/frontend/js/features/source-editor/components/command-tooltip/href-tooltip.tsx b/services/web/frontend/js/features/source-editor/components/command-tooltip/href-tooltip.tsx index 8c394611ca..88b05b4623 100644 --- a/services/web/frontend/js/features/source-editor/components/command-tooltip/href-tooltip.tsx +++ b/services/web/frontend/js/features/source-editor/components/command-tooltip/href-tooltip.tsx @@ -102,6 +102,7 @@ export const HrefTooltipContent: FC = () => { bsStyle="link" className="ol-cm-command-tooltip-link" onClick={() => { + // TODO: unescape content window.open(url, '_blank') }} > diff --git a/services/web/frontend/js/features/source-editor/extensions/visual/paste-html.ts b/services/web/frontend/js/features/source-editor/extensions/visual/paste-html.ts index 0c167f8fa6..7ed6dd1fc8 100644 --- a/services/web/frontend/js/features/source-editor/extensions/visual/paste-html.ts +++ b/services/web/frontend/js/features/source-editor/extensions/visual/paste-html.ts @@ -217,6 +217,25 @@ const matchingParents = (element: HTMLElement, selector: string) => { return matches } +const urlCharacterReplacements = new Map([ + ['\\', '\\\\'], + ['#', '\\#'], + ['%', '\\%'], + ['{', '%7B'], + ['}', '%7D'], +]) + +const protectUrlCharacters = (url: string) => { + // NOTE: add new characters to both this regex and urlCharacterReplacements + return url.replaceAll(/[\\#%{}]/g, match => { + const replacement = urlCharacterReplacements.get(match) + if (!replacement) { + throw new Error(`No replacement found for ${match}`) + } + return replacement + }) +} + const processLists = (element: HTMLElement) => { for (const list of element.querySelectorAll('ol,ul')) { // if the list has only one item, replace the list with an element containing the contents of the item @@ -519,8 +538,11 @@ const selectors = [ createSelector({ selector: 'a', match: element => !!element.href && hasContent(element), - start: (element: HTMLAnchorElement) => `\\href{${element.href}}{`, - end: element => `}`, + start: (element: HTMLAnchorElement) => { + const url = protectUrlCharacters(element.href) + return `\\href{${url}}{` + }, + end: () => `}`, }), createSelector({ selector: 'h1', diff --git a/services/web/test/frontend/features/source-editor/components/codemirror-editor-visual-paste-html.spec.tsx b/services/web/test/frontend/features/source-editor/components/codemirror-editor-visual-paste-html.spec.tsx index 7e9024a3f4..12fc98bcaf 100644 --- a/services/web/test/frontend/features/source-editor/components/codemirror-editor-visual-paste-html.spec.tsx +++ b/services/web/test/frontend/features/source-editor/components/codemirror-editor-visual-paste-html.spec.tsx @@ -277,7 +277,8 @@ describe(' paste HTML in Visual mode', function () { it('handles a pasted link', function () { mountEditor() - const data = 'foo' + const data = + 'foo' const clipboardData = new DataTransfer() clipboardData.setData('text/html', data) @@ -285,6 +286,13 @@ describe(' paste HTML in Visual mode', function () { cy.get('@content').should('have.text', '{foo}') cy.get('.ol-cm-command-href').should('have.length', 1) + + cy.get('.cm-line').eq(0).type('{leftArrow}') + cy.findByLabelText('URL').should( + 'have.value', + 'https://example.com/?q=$foo_~bar&x=\\\\bar\\#fragment%7By%7D\\%2' + ) + // TODO: assert that the "Go to page" link has been unescaped }) it('handles a pasted code block', function () {