diff --git a/CHANGELOG.md b/CHANGELOG.md index 4090519..a7b979f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,10 @@ All notable changes to easymde will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). - +## [Unreleased] +### Fixed +- URLs with certain characters entered through prompts causing invalid markdown (Thanks to [@Zignature], [#393]). + ## [2.16.1] - 2022-01-14 ### Fixed - Incorrect initial line and column count in status bar. @@ -240,6 +243,7 @@ Project forked from [SimpleMDE](https://github.com/sparksuite/simplemde-markdown [#9]: https://github.com/Ionaru/easy-markdown-editor/issues/9 +[#393]: https://github.com/Ionaru/easy-markdown-editor/pull/393 [#389]: https://github.com/Ionaru/easy-markdown-editor/pull/389 [#388]: https://github.com/Ionaru/easy-markdown-editor/pull/388 [#384]: https://github.com/Ionaru/easy-markdown-editor/pull/384 diff --git a/cypress/integration/1-default-editor/default.html b/cypress/integration/1-default-editor/index.html similarity index 100% rename from cypress/integration/1-default-editor/default.html rename to cypress/integration/1-default-editor/index.html diff --git a/cypress/integration/1-default-editor/preview.spec.js b/cypress/integration/1-default-editor/preview.spec.js index ced582c..e437bbe 100644 --- a/cypress/integration/1-default-editor/preview.spec.js +++ b/cypress/integration/1-default-editor/preview.spec.js @@ -2,7 +2,7 @@ describe('Preview', () => { beforeEach(() => { - cy.visit(__dirname + '/default.html'); + cy.visit(__dirname + '/index.html'); }); it('can show a preview of markdown text', () => { @@ -22,9 +22,7 @@ describe('Preview', () => { cy.get('.EasyMDEContainer .cm-strong').should('contain', '**'); cy.get('.EasyMDEContainer .cm-strong').should('contain', 'important'); - // Toggle preview. - cy.get('.EasyMDEContainer .editor-toolbar button.preview').click(); - cy.get('.EasyMDEContainer .editor-preview').should('be.visible'); + cy.previewOn(); // Check preview window for rendered markdown. cy.get('.EasyMDEContainer .editor-preview').should('contain.html', '

My Big Title

'); diff --git a/cypress/integration/1-default-editor/statusbar.spec.js b/cypress/integration/1-default-editor/statusbar.spec.js index 12e8276..9b96735 100644 --- a/cypress/integration/1-default-editor/statusbar.spec.js +++ b/cypress/integration/1-default-editor/statusbar.spec.js @@ -2,7 +2,7 @@ describe('Default statusbar', () => { beforeEach(() => { - cy.visit(__dirname + '/default.html'); + cy.visit(__dirname + '/index.html'); }); it('loads the editor with default statusbar', () => { diff --git a/cypress/integration/1-default-editor/visual.spec.js b/cypress/integration/1-default-editor/visual.spec.js index 0ba1afc..18b7be2 100644 --- a/cypress/integration/1-default-editor/visual.spec.js +++ b/cypress/integration/1-default-editor/visual.spec.js @@ -2,10 +2,10 @@ describe('Default editor', () => { beforeEach(() => { - cy.visit(__dirname + '/default.html'); + cy.visit(__dirname + '/index.html'); }); - it('Loads the editor with default settings', () => { + it('loads the editor with default settings', () => { cy.get('.EasyMDEContainer').should('be.visible'); cy.get('#textarea').should('not.be.visible'); diff --git a/cypress/integration/2-url-prompt/index.html b/cypress/integration/2-url-prompt/index.html new file mode 100644 index 0000000..aa5f9e5 --- /dev/null +++ b/cypress/integration/2-url-prompt/index.html @@ -0,0 +1,20 @@ + + + + + + Default + + + + + + + + + + diff --git a/cypress/integration/2-url-prompt/url-prompt.spec.js b/cypress/integration/2-url-prompt/url-prompt.spec.js new file mode 100644 index 0000000..1436b12 --- /dev/null +++ b/cypress/integration/2-url-prompt/url-prompt.spec.js @@ -0,0 +1,228 @@ +/// + +describe('URL prompts', () => { + beforeEach(() => { + cy.visit(__dirname + '/index.html'); + }); + + it('must show the correct text for a link prompt', () => { + cy.get('.EasyMDEContainer').should('be.visible'); + cy.get('#textarea').should('not.be.visible'); + + cy.window().then(($win) => { + const stub = cy.stub($win, 'prompt'); + cy.get('button.link').click().then(() => { + expect(stub).to.be.calledWith('URL for the link:', 'https://'); + }); + }); + }); + + it('must show the correct text for an image prompt', () => { + cy.get('.EasyMDEContainer').should('be.visible'); + cy.get('#textarea').should('not.be.visible'); + + cy.window().then(($win) => { + const stub = cy.stub($win, 'prompt'); + cy.get('button.image').click().then(() => { + expect(stub).to.be.calledWith('URL of the image:', 'https://'); + }); + }); + }); + + it('must enter a link correctly through a prompt', () => { + cy.get('.EasyMDEContainer').should('be.visible'); + cy.get('#textarea').should('not.be.visible'); + + cy.window().then(($win) => { + cy.stub($win, 'prompt').returns('https://example.com'); + cy.get('button.link').click(); + }); + cy.get('.EasyMDEContainer .CodeMirror').contains('[](https://example.com)'); + cy.get('.EasyMDEContainer .CodeMirror').type('{home}{rightarrow}Link to a website!'); + + cy.previewOn(); + + cy.get('.EasyMDEContainer .editor-preview').should('contain.html', '

Link to a website!

'); + }); + + it('can use the prompt multiple times', () => { + cy.get('.EasyMDEContainer').should('be.visible'); + cy.get('#textarea').should('not.be.visible'); + + cy.window().then(($win) => { + const stub = cy.stub($win, 'prompt'); + stub.returns('https://example.com'); + cy.get('button.link').click().then(() => { + expect(stub).to.be.calledWith('URL for the link:', 'https://'); + stub.restore(); + }); + }); + cy.get('.EasyMDEContainer .CodeMirror').contains('[](https://example.com)'); + cy.get('.EasyMDEContainer .CodeMirror').type('{home}{rightarrow}Link to a website!{end}{enter}'); + + cy.window().then(($win) => { + const stub = cy.stub($win, 'prompt'); + stub.returns('https://example.eu'); + cy.get('button.link').click().then(() => { + expect(stub).to.be.calledWith('URL for the link:', 'https://'); + stub.restore(); + }); + }); + cy.get('.EasyMDEContainer .CodeMirror').contains('[](https://example.eu)'); + cy.get('.EasyMDEContainer .CodeMirror').type('{home}{rightarrow}Link to a second website!'); + + cy.get('.EasyMDEContainer .CodeMirror').contains('[Link to a website!](https://example.com)'); + cy.get('.EasyMDEContainer .CodeMirror').contains('[Link to a second website!](https://example.eu)'); + + cy.previewOn(); + + cy.get('.EasyMDEContainer .editor-preview').should( + 'contain.html', + '

Link to a website!
Link to a second website!

', + ); + }); + + it('must be able to deal with parameters in links', () => { + cy.get('.EasyMDEContainer').should('be.visible'); + cy.get('#textarea').should('not.be.visible'); + + cy.window().then(($win) => { + cy.stub($win, 'prompt').returns('https://example.com?some=param&moo=cow'); + cy.get('button.link').click(); + }); + cy.get('.EasyMDEContainer .CodeMirror').contains('[](https://example.com?some=param&moo=cow)'); + cy.get('.EasyMDEContainer .CodeMirror').type('{home}{rightarrow}Link to a website!'); + + cy.previewOn(); + + cy.get('.EasyMDEContainer .editor-preview').should('contain.html', '

Link to a website!

'); + }); + + it('must be able to deal with brackets in links', () => { + cy.get('.EasyMDEContainer').should('be.visible'); + cy.get('#textarea').should('not.be.visible'); + + cy.window().then(($win) => { + cy.stub($win, 'prompt').returns('https://example.com?some=[]param'); + cy.get('button.link').click(); + }); + cy.get('.EasyMDEContainer .CodeMirror').contains('[](https://example.com?some=%5B%5Dparam)'); + cy.get('.EasyMDEContainer .CodeMirror').type('{home}{rightarrow}Link to a website!'); + + cy.previewOn(); + + cy.get('.EasyMDEContainer .editor-preview').should('contain.html', '

Link to a website!

'); + }); + + it('must be able to deal with parentheses in links', () => { + cy.get('.EasyMDEContainer').should('be.visible'); + cy.get('#textarea').should('not.be.visible'); + + cy.window().then(($win) => { + cy.stub($win, 'prompt').returns('https://example.com?some=(param)'); + cy.get('button.link').click(); + }); + cy.get('.EasyMDEContainer .CodeMirror').contains('[](https://example.com?some=\\(param\\))'); + cy.get('.EasyMDEContainer .CodeMirror').type('{home}{rightarrow}Link to a website!'); + + cy.previewOn(); + + cy.get('.EasyMDEContainer .editor-preview').should('contain.html', '

Link to a website!

'); + }); + + it('must be able to deal with parentheses in links (multiple)', () => { + cy.get('.EasyMDEContainer').should('be.visible'); + cy.get('#textarea').should('not.be.visible'); + + cy.window().then(($win) => { + cy.stub($win, 'prompt').returns('https://example.com?some=(param1,param2)&more=(param3,param4)&end=true'); + cy.get('button.link').click(); + }); + cy.get('.EasyMDEContainer .CodeMirror').contains('[](https://example.com?some=\\(param1,param2\\)&more=\\(param3,param4\\)&end=true)'); + cy.get('.EasyMDEContainer .CodeMirror').type('{home}{rightarrow}Link to a website!'); + + cy.previewOn(); + + cy.get('.EasyMDEContainer .editor-preview').should('contain.html', '

Link to a website!

'); + }); + + it('must be able to deal with unbalanced parentheses in links (opening)', () => { + cy.get('.EasyMDEContainer').should('be.visible'); + cy.get('#textarea').should('not.be.visible'); + + cy.window().then(($win) => { + cy.stub($win, 'prompt').returns('https://example.com?some=(param'); + cy.get('button.link').click(); + }); + cy.get('.EasyMDEContainer .CodeMirror').contains('[](https://example.com?some=\\(param)'); + cy.get('.EasyMDEContainer .CodeMirror').type('{home}{rightarrow}Link to a website!'); + + cy.previewOn(); + + cy.get('.EasyMDEContainer .editor-preview').should('contain.html', '

Link to a website!

'); + }); + + it('must be able to deal with unbalanced parentheses in links (closing)', () => { + cy.get('.EasyMDEContainer').should('be.visible'); + cy.get('#textarea').should('not.be.visible'); + + cy.window().then(($win) => { + cy.stub($win, 'prompt').returns('https://example.com?some=)param'); + cy.get('button.link').click(); + }); + cy.get('.EasyMDEContainer .CodeMirror').contains('[](https://example.com?some=\\)param)'); + cy.get('.EasyMDEContainer .CodeMirror').type('{home}{rightarrow}Link to a website!'); + + cy.previewOn(); + + cy.get('.EasyMDEContainer .editor-preview').should('contain.html', '

Link to a website!

'); + }); + + it('must be able to deal with inequality symbols in links', () => { + cy.get('.EasyMDEContainer').should('be.visible'); + cy.get('#textarea').should('not.be.visible'); + + cy.window().then(($win) => { + cy.stub($win, 'prompt').returns('https://example.com?some=Link to a website!

'); + }); + + it('must be able to deal with emoji in links', () => { + cy.get('.EasyMDEContainer').should('be.visible'); + cy.get('#textarea').should('not.be.visible'); + + cy.window().then(($win) => { + cy.stub($win, 'prompt').returns('https://example.com?some=👷‍♂️'); + cy.get('button.link').click(); + }); + cy.get('.EasyMDEContainer .CodeMirror').contains('[](https://example.com?some=%F0%9F%91%B7%E2%80%8D%E2%99%82%EF%B8%8F'); + cy.get('.EasyMDEContainer .CodeMirror').type('{home}{rightarrow}Link to a 👌 website!'); + + cy.previewOn(); + + cy.get('.EasyMDEContainer .editor-preview').should('contain.html', '

Link to a 👌 website!

'); + }); + + it('must be able to deal with spaces in links', () => { + cy.get('.EasyMDEContainer').should('be.visible'); + cy.get('#textarea').should('not.be.visible'); + + cy.window().then(($win) => { + cy.stub($win, 'prompt').returns('https://example.com?some=very special param'); + cy.get('button.link').click(); + }); + cy.get('.EasyMDEContainer .CodeMirror').contains('[](https://example.com?some=very%20special%20param'); + cy.get('.EasyMDEContainer .CodeMirror').type('{home}{rightarrow}Link to a website!'); + + cy.previewOn(); + + cy.get('.EasyMDEContainer .editor-preview').should('contain.html', '

Link to a website!

'); + }); +}); diff --git a/cypress/support/commands.js b/cypress/support/commands.js index d7e51ad..4927ac8 100644 --- a/cypress/support/commands.js +++ b/cypress/support/commands.js @@ -13,3 +13,19 @@ Cypress.Commands.add( return unquote(before.getPropertyValue(property)); }, ); + +Cypress.Commands.add('previewOn' , () => { + cy.get('.EasyMDEContainer .editor-preview').should('not.be.visible'); + cy.get('.EasyMDEContainer .editor-toolbar button.preview').should('not.have.class', 'active'); + cy.get('.EasyMDEContainer .editor-toolbar button.preview').click(); + cy.get('.EasyMDEContainer .editor-toolbar button.preview').should('have.class', 'active'); + cy.get('.EasyMDEContainer .editor-preview').should('be.visible'); +}); + +Cypress.Commands.add('previewOff' , () => { + cy.get('.EasyMDEContainer .editor-preview').should('be.visible'); + cy.get('.EasyMDEContainer .editor-toolbar button.preview').should('have.class', 'active'); + cy.get('.EasyMDEContainer .editor-toolbar button.preview').click(); + cy.get('.EasyMDEContainer .editor-toolbar button.preview').should('not.have.class', 'active'); + cy.get('.EasyMDEContainer .editor-preview').should('not.be.visible'); +}); diff --git a/src/js/easymde.js b/src/js/easymde.js index cece3b0..dbb0135 100644 --- a/src/js/easymde.js +++ b/src/js/easymde.js @@ -844,10 +844,11 @@ function drawLink(editor) { var options = editor.options; var url = 'https://'; if (options.promptURLs) { - url = prompt(options.promptTexts.link, 'https://'); + url = prompt(options.promptTexts.link, url); if (!url) { return false; } + url = escapePromptURL(url); } _replaceSelection(cm, stat.link, options.insertTexts.link, url); } @@ -861,14 +862,23 @@ function drawImage(editor) { var options = editor.options; var url = 'https://'; if (options.promptURLs) { - url = prompt(options.promptTexts.image, 'https://'); + url = prompt(options.promptTexts.image, url); if (!url) { return false; } + url = escapePromptURL(url); } _replaceSelection(cm, stat.image, options.insertTexts.image, url); } +/** + * Encode and escape URLs to prevent breaking up rendered Markdown links. + * @param url {string} The url of the link or image + */ +function escapePromptURL(url) { + return encodeURI(url).replace(/([\\()])/g, '\\$1'); +} + /** * Action for opening the browse-file window to upload an image to a server. * @param editor {EasyMDE} The EasyMDE object