From 4dce8d9ab44bc71d6ea9f87a0bcd1ea6e3022846 Mon Sep 17 00:00:00 2001 From: Marcin Panek Date: Fri, 20 Oct 2023 12:58:13 +0200 Subject: [PATCH 01/33] Feature: Paste from markdown PoC. --- .../ckeditor5-markdown-gfm/src/markdown.ts | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/packages/ckeditor5-markdown-gfm/src/markdown.ts b/packages/ckeditor5-markdown-gfm/src/markdown.ts index 82d4da205eb..e58ea8e5512 100644 --- a/packages/ckeditor5-markdown-gfm/src/markdown.ts +++ b/packages/ckeditor5-markdown-gfm/src/markdown.ts @@ -9,6 +9,8 @@ import { Plugin, type Editor } from 'ckeditor5/src/core'; import GFMDataProcessor from './gfmdataprocessor'; +import type { ClipboardPipeline, ClipboardInputTransformationEvent } from 'ckeditor5/src/clipboard'; +import type { ViewDocumentKeyDownEvent } from 'ckeditor5/src/engine'; /** * The GitHub Flavored Markdown (GFM) plugin. @@ -31,4 +33,31 @@ export default class Markdown extends Plugin { public static get pluginName() { return 'Markdown' as const; } + + /** + * @inheritDoc + */ + public init(): void { + const editor = this.editor; + const view = editor.editing.view; + const viewDocument = view.document; + + const clipboardPipeline: ClipboardPipeline = editor.plugins.get( 'ClipboardPipeline' ); + + let shiftPressed = false; + + this.listenTo( viewDocument, 'keydown', ( evt, data ) => { + shiftPressed = data.shiftKey; + } ); + + this.listenTo( clipboardPipeline, 'inputTransformation', ( evt, data ) => { + if ( data.dataTransfer.getData( 'text/html' ) || shiftPressed ) { + return; + } + + const dataStr = data.dataTransfer.getData( 'text/plain' ); + + data.content = editor.data.processor.toView( dataStr ); + } ); + } } From 229d8ca3cd4849838cc634fddc9f0d77440aedaa Mon Sep 17 00:00:00 2001 From: Marcin Panek Date: Mon, 23 Oct 2023 17:03:11 +0200 Subject: [PATCH 02/33] Added tests for paste from markdown. --- packages/ckeditor5-markdown-gfm/package.json | 4 + .../ckeditor5-markdown-gfm/src/markdown.ts | 1 + .../ckeditor5-markdown-gfm/tests/markdown.js | 3 +- .../tests/pastefrommarkdown.js | 141 ++++++++++++++++++ 4 files changed, 148 insertions(+), 1 deletion(-) create mode 100644 packages/ckeditor5-markdown-gfm/tests/pastefrommarkdown.js diff --git a/packages/ckeditor5-markdown-gfm/package.json b/packages/ckeditor5-markdown-gfm/package.json index 3f4142580ad..f22c5e8c43c 100644 --- a/packages/ckeditor5-markdown-gfm/package.json +++ b/packages/ckeditor5-markdown-gfm/package.json @@ -19,14 +19,18 @@ }, "devDependencies": { "@ckeditor/ckeditor5-basic-styles": "40.0.0", + "@ckeditor/ckeditor5-clipboard": "40.0.0", "@ckeditor/ckeditor5-code-block": "40.0.0", "@ckeditor/ckeditor5-core": "40.0.0", "@ckeditor/ckeditor5-dev-utils": "^39.0.0", "@ckeditor/ckeditor5-editor-classic": "40.0.0", "@ckeditor/ckeditor5-engine": "40.0.0", "@ckeditor/ckeditor5-list": "40.0.0", + "@ckeditor/ckeditor5-paragraph": "40.0.0", "@ckeditor/ckeditor5-table": "40.0.0", "@ckeditor/ckeditor5-theme-lark": "40.0.0", + "@ckeditor/ckeditor5-undo": "40.0.0", + "@ckeditor/ckeditor5-utils": "40.0.0", "@types/marked": "^4.0.8", "typescript": "^4.8.4", "webpack": "^5.58.1", diff --git a/packages/ckeditor5-markdown-gfm/src/markdown.ts b/packages/ckeditor5-markdown-gfm/src/markdown.ts index e58ea8e5512..1b20a3454aa 100644 --- a/packages/ckeditor5-markdown-gfm/src/markdown.ts +++ b/packages/ckeditor5-markdown-gfm/src/markdown.ts @@ -42,6 +42,7 @@ export default class Markdown extends Plugin { const view = editor.editing.view; const viewDocument = view.document; + // TODO think about adding the paste logic only if ClipboardPipeline pluggin is added or make it required const clipboardPipeline: ClipboardPipeline = editor.plugins.get( 'ClipboardPipeline' ); let shiftPressed = false; diff --git a/packages/ckeditor5-markdown-gfm/tests/markdown.js b/packages/ckeditor5-markdown-gfm/tests/markdown.js index 6e7867e20f8..a50845e8cb2 100644 --- a/packages/ckeditor5-markdown-gfm/tests/markdown.js +++ b/packages/ckeditor5-markdown-gfm/tests/markdown.js @@ -6,6 +6,7 @@ import Markdown from '../src/markdown'; import GFMDataProcessor from '../src/gfmdataprocessor'; import ClassicTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/classictesteditor'; +import { ClipboardPipeline } from '@ckeditor/ckeditor5-clipboard'; describe( 'Markdown', () => { it( 'has proper name', () => { @@ -15,7 +16,7 @@ describe( 'Markdown', () => { it( 'should set editor.data.processor', () => { return ClassicTestEditor .create( '', { - plugins: [ Markdown ] + plugins: [ Markdown, ClipboardPipeline ] } ) .then( editor => { expect( editor.data.processor ).to.be.an.instanceof( GFMDataProcessor ); diff --git a/packages/ckeditor5-markdown-gfm/tests/pastefrommarkdown.js b/packages/ckeditor5-markdown-gfm/tests/pastefrommarkdown.js new file mode 100644 index 00000000000..5e15f743953 --- /dev/null +++ b/packages/ckeditor5-markdown-gfm/tests/pastefrommarkdown.js @@ -0,0 +1,141 @@ +/** + * @license Copyright (c) 2003-2023, CKSource Holding sp. z o.o. All rights reserved. + * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license + */ + +import ClassicTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/classictesteditor'; +import global from '@ckeditor/ckeditor5-utils/src/dom/global'; +import { getData, setData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model'; +import Markdown from '../src/markdown'; +import { ClipboardPipeline } from '@ckeditor/ckeditor5-clipboard'; +import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph'; +import { keyCodes } from '@ckeditor/ckeditor5-utils'; +import Bold from '@ckeditor/ckeditor5-basic-styles/src/bold'; +import Italic from '@ckeditor/ckeditor5-basic-styles/src/italic'; +import Undo from '@ckeditor/ckeditor5-undo/src/undo'; +import DocumentFragment from '@ckeditor/ckeditor5-engine/src/view/documentfragment'; + +describe( 'PasteFromMarkdown', () => { + let editorElement, editor; + + beforeEach( () => { + editorElement = global.document.createElement( 'div' ); + global.document.body.appendChild( editorElement ); + + return ClassicTestEditor + .create( editorElement, { + plugins: [ ClipboardPipeline, Markdown, Paragraph, Bold, Italic, Undo ] + } ) + .then( newEditor => { + editor = newEditor; + } ); + } ); + + afterEach( () => { + editorElement.remove(); + + return editor.destroy(); + } ); + + it( 'should paste as markdown when type is text/plain', () => { + setData( editor.model, '[]' ); + pasteText( editor, 'foo **bar** [baz](https://ckeditor.com).' ); + + expect( getData( editor.model ) ).to.equal( 'foo <$text bold="true">bar baz.[]' ); + } ); + + it( 'should not paste as markdown if type is text/html', () => { + setData( editor.model, '[]' ); + pasteHtml( editor, 'foo **bar** [baz](https://ckeditor.com).' ); + + expect( getData( editor.model ) ).to.equal( 'foo **bar** [baz](https://ckeditor.com).[]' ); + } ); + + it( 'should not paste as markdown if shift key pressed', () => { + setData( editor.model, '[]' ); + pressShiftKey( editor ); + pasteText( editor, 'foo **bar** [baz](https://ckeditor.com).' ); + + expect( getData( editor.model ) ).to.equal( 'foo **bar** [baz](https://ckeditor.com).[]' ); + } ); + + it( 'should paste as markdown and remove it on undo', () => { + setData( editor.model, '[]' ); + pasteText( editor, 'foo **bar** [baz](https://ckeditor.com).' ); + + expect( getData( editor.model ) ).to.equal( 'foo <$text bold="true">bar baz.[]' ); + + editor.commands.execute( 'undo' ); + + expect( getData( editor.model ) ).to.equal( '[]' ); + } ); + + it( 'inserts markdown in-place (non-collapsed selection)', () => { + setData( editor.model, '[should override]' ); + pasteText( editor, 'foo **bar** [baz](https://ckeditor.com).' ); + + expect( getData( editor.model ) ).to.equal( 'foo <$text bold="true">bar baz.[]' ); + } ); + + it( 'inserts markdown in-place (collapsed selection)', () => { + setData( editor.model, 'Foo []Bar' ); + pasteText( editor, 'foo **bar** [baz](https://ckeditor.com).' ); + + expect( getData( editor.model ) ).to.equal( + 'Foo foo <$text bold="true">bar baz.[]Bar' + ); + } ); + + it( 'should call toView function once on markdown paste', () => { + const toViewStub = sinon.stub().returns( new DocumentFragment() ); + + editor.data.processor.toView = toViewStub; + setData( editor.model, '[]' ); + pasteText( editor, 'foo **bar** [baz](https://ckeditor.com).' ); + + expect( toViewStub.callCount ).to.equal( 1 ); + } ); + + it( 'should not call toView function html paste', () => { + const toViewStub = sinon.stub().returns( new DocumentFragment() ); + + editor.data.processor.toView = toViewStub; + setData( editor.model, '[]' ); + pasteHtml( editor, 'foo **bar** [baz](https://ckeditor.com).' ); + + expect( toViewStub.callCount ).to.equal( 0 ); + } ); + + function pressShiftKey( editor ) { + editor.editing.view.document.fire( 'keydown', { + keyCode: keyCodes.shift, + shiftKey: true, + preventDefault: () => {}, + domTarget: global.document.body + } ); + } + + function pasteHtml( editor, html ) { + editor.editing.view.document.fire( 'paste', { + dataTransfer: createDataTransfer( { 'text/html': html } ), + stopPropagation() {}, + preventDefault() {} + } ); + } + + function pasteText( editor, text ) { + editor.editing.view.document.fire( 'paste', { + dataTransfer: createDataTransfer( { 'text/plain': text } ), + stopPropagation() {}, + preventDefault() {} + } ); + } + + function createDataTransfer( data ) { + return { + getData( type ) { + return data[ type ]; + } + }; + } +} ); From 80464f75ecb613ffcf7822af07d979d9519c8106 Mon Sep 17 00:00:00 2001 From: Marcin Panek Date: Mon, 30 Oct 2023 10:42:03 +0100 Subject: [PATCH 03/33] Cleaned up tests for a previous version. --- packages/ckeditor5-markdown-gfm/package.json | 3 - .../ckeditor5-markdown-gfm/src/markdown.ts | 1 - .../tests/pastefrommarkdown.js | 141 ------------------ 3 files changed, 145 deletions(-) delete mode 100644 packages/ckeditor5-markdown-gfm/tests/pastefrommarkdown.js diff --git a/packages/ckeditor5-markdown-gfm/package.json b/packages/ckeditor5-markdown-gfm/package.json index f22c5e8c43c..c3971309d6e 100644 --- a/packages/ckeditor5-markdown-gfm/package.json +++ b/packages/ckeditor5-markdown-gfm/package.json @@ -26,11 +26,8 @@ "@ckeditor/ckeditor5-editor-classic": "40.0.0", "@ckeditor/ckeditor5-engine": "40.0.0", "@ckeditor/ckeditor5-list": "40.0.0", - "@ckeditor/ckeditor5-paragraph": "40.0.0", "@ckeditor/ckeditor5-table": "40.0.0", "@ckeditor/ckeditor5-theme-lark": "40.0.0", - "@ckeditor/ckeditor5-undo": "40.0.0", - "@ckeditor/ckeditor5-utils": "40.0.0", "@types/marked": "^4.0.8", "typescript": "^4.8.4", "webpack": "^5.58.1", diff --git a/packages/ckeditor5-markdown-gfm/src/markdown.ts b/packages/ckeditor5-markdown-gfm/src/markdown.ts index 1b20a3454aa..e58ea8e5512 100644 --- a/packages/ckeditor5-markdown-gfm/src/markdown.ts +++ b/packages/ckeditor5-markdown-gfm/src/markdown.ts @@ -42,7 +42,6 @@ export default class Markdown extends Plugin { const view = editor.editing.view; const viewDocument = view.document; - // TODO think about adding the paste logic only if ClipboardPipeline pluggin is added or make it required const clipboardPipeline: ClipboardPipeline = editor.plugins.get( 'ClipboardPipeline' ); let shiftPressed = false; diff --git a/packages/ckeditor5-markdown-gfm/tests/pastefrommarkdown.js b/packages/ckeditor5-markdown-gfm/tests/pastefrommarkdown.js deleted file mode 100644 index 5e15f743953..00000000000 --- a/packages/ckeditor5-markdown-gfm/tests/pastefrommarkdown.js +++ /dev/null @@ -1,141 +0,0 @@ -/** - * @license Copyright (c) 2003-2023, CKSource Holding sp. z o.o. All rights reserved. - * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license - */ - -import ClassicTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/classictesteditor'; -import global from '@ckeditor/ckeditor5-utils/src/dom/global'; -import { getData, setData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model'; -import Markdown from '../src/markdown'; -import { ClipboardPipeline } from '@ckeditor/ckeditor5-clipboard'; -import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph'; -import { keyCodes } from '@ckeditor/ckeditor5-utils'; -import Bold from '@ckeditor/ckeditor5-basic-styles/src/bold'; -import Italic from '@ckeditor/ckeditor5-basic-styles/src/italic'; -import Undo from '@ckeditor/ckeditor5-undo/src/undo'; -import DocumentFragment from '@ckeditor/ckeditor5-engine/src/view/documentfragment'; - -describe( 'PasteFromMarkdown', () => { - let editorElement, editor; - - beforeEach( () => { - editorElement = global.document.createElement( 'div' ); - global.document.body.appendChild( editorElement ); - - return ClassicTestEditor - .create( editorElement, { - plugins: [ ClipboardPipeline, Markdown, Paragraph, Bold, Italic, Undo ] - } ) - .then( newEditor => { - editor = newEditor; - } ); - } ); - - afterEach( () => { - editorElement.remove(); - - return editor.destroy(); - } ); - - it( 'should paste as markdown when type is text/plain', () => { - setData( editor.model, '[]' ); - pasteText( editor, 'foo **bar** [baz](https://ckeditor.com).' ); - - expect( getData( editor.model ) ).to.equal( 'foo <$text bold="true">bar baz.[]' ); - } ); - - it( 'should not paste as markdown if type is text/html', () => { - setData( editor.model, '[]' ); - pasteHtml( editor, 'foo **bar** [baz](https://ckeditor.com).' ); - - expect( getData( editor.model ) ).to.equal( 'foo **bar** [baz](https://ckeditor.com).[]' ); - } ); - - it( 'should not paste as markdown if shift key pressed', () => { - setData( editor.model, '[]' ); - pressShiftKey( editor ); - pasteText( editor, 'foo **bar** [baz](https://ckeditor.com).' ); - - expect( getData( editor.model ) ).to.equal( 'foo **bar** [baz](https://ckeditor.com).[]' ); - } ); - - it( 'should paste as markdown and remove it on undo', () => { - setData( editor.model, '[]' ); - pasteText( editor, 'foo **bar** [baz](https://ckeditor.com).' ); - - expect( getData( editor.model ) ).to.equal( 'foo <$text bold="true">bar baz.[]' ); - - editor.commands.execute( 'undo' ); - - expect( getData( editor.model ) ).to.equal( '[]' ); - } ); - - it( 'inserts markdown in-place (non-collapsed selection)', () => { - setData( editor.model, '[should override]' ); - pasteText( editor, 'foo **bar** [baz](https://ckeditor.com).' ); - - expect( getData( editor.model ) ).to.equal( 'foo <$text bold="true">bar baz.[]' ); - } ); - - it( 'inserts markdown in-place (collapsed selection)', () => { - setData( editor.model, 'Foo []Bar' ); - pasteText( editor, 'foo **bar** [baz](https://ckeditor.com).' ); - - expect( getData( editor.model ) ).to.equal( - 'Foo foo <$text bold="true">bar baz.[]Bar' - ); - } ); - - it( 'should call toView function once on markdown paste', () => { - const toViewStub = sinon.stub().returns( new DocumentFragment() ); - - editor.data.processor.toView = toViewStub; - setData( editor.model, '[]' ); - pasteText( editor, 'foo **bar** [baz](https://ckeditor.com).' ); - - expect( toViewStub.callCount ).to.equal( 1 ); - } ); - - it( 'should not call toView function html paste', () => { - const toViewStub = sinon.stub().returns( new DocumentFragment() ); - - editor.data.processor.toView = toViewStub; - setData( editor.model, '[]' ); - pasteHtml( editor, 'foo **bar** [baz](https://ckeditor.com).' ); - - expect( toViewStub.callCount ).to.equal( 0 ); - } ); - - function pressShiftKey( editor ) { - editor.editing.view.document.fire( 'keydown', { - keyCode: keyCodes.shift, - shiftKey: true, - preventDefault: () => {}, - domTarget: global.document.body - } ); - } - - function pasteHtml( editor, html ) { - editor.editing.view.document.fire( 'paste', { - dataTransfer: createDataTransfer( { 'text/html': html } ), - stopPropagation() {}, - preventDefault() {} - } ); - } - - function pasteText( editor, text ) { - editor.editing.view.document.fire( 'paste', { - dataTransfer: createDataTransfer( { 'text/plain': text } ), - stopPropagation() {}, - preventDefault() {} - } ); - } - - function createDataTransfer( data ) { - return { - getData( type ) { - return data[ type ]; - } - }; - } -} ); From aeb964e2761c1e3d7ba85a6fb1448a3a8a40d499 Mon Sep 17 00:00:00 2001 From: Marcin Panek Date: Mon, 30 Oct 2023 10:42:39 +0100 Subject: [PATCH 04/33] Improved paste from markdown to detect markdown in HTML clipboard. --- .../ckeditor5-markdown-gfm/src/markdown.ts | 36 +++++++++++++++++-- 1 file changed, 33 insertions(+), 3 deletions(-) diff --git a/packages/ckeditor5-markdown-gfm/src/markdown.ts b/packages/ckeditor5-markdown-gfm/src/markdown.ts index e58ea8e5512..c814e65d1f2 100644 --- a/packages/ckeditor5-markdown-gfm/src/markdown.ts +++ b/packages/ckeditor5-markdown-gfm/src/markdown.ts @@ -51,13 +51,43 @@ export default class Markdown extends Plugin { } ); this.listenTo( clipboardPipeline, 'inputTransformation', ( evt, data ) => { - if ( data.dataTransfer.getData( 'text/html' ) || shiftPressed ) { + const dataAsTextHtml = data.dataTransfer.getData( 'text/html' ); + const markdownFromHtml = this.parseMarkdownFromHtml( dataAsTextHtml ); + + if ( shiftPressed ) { return; } - const dataStr = data.dataTransfer.getData( 'text/plain' ); + if ( !dataAsTextHtml ) { + const dataAsTextPlain = data.dataTransfer.getData( 'text/plain' ); + data.content = editor.data.processor.toView( dataAsTextPlain ); - data.content = editor.data.processor.toView( dataStr ); + return; + } + + if ( markdownFromHtml ) { + data.content = editor.data.processor.toView( markdownFromHtml ); + } } ); } + + private parseMarkdownFromHtml( htmlString: string ) { + // Removing tag present on Mac. + const withoutMetaTag = htmlString.replace( /^]*>/, '' ).trim(); + // Removing tag present on Windows. + const withoutHtmlTag = withoutMetaTag.replace( /^/, '' ).replace( /<\/html>$/, '' ).trim(); + // Removing tag present on Windows. + const withoutBodyTag = withoutHtmlTag.replace( /^/, '' ).replace( /<\/body>$/, '' ).trim(); + // Removing tag present on Windows. + const withoutFragmentTag = withoutBodyTag.replace( /^/, '' ).replace( /$/, '' ).trim(); + // Removing a wrapper HTML tag if exists. + const withoutWrapperTag = withoutFragmentTag.replace( /^<(\w+)\b[^>]*>\s*([\s\S]*?)\s*<\/\1>/, '$2' ).trim(); + const containsAnyRemainingHtmlTags = /<[^>]+>[\s\S]*<[^>]+>/.test( withoutWrapperTag ); + + if ( containsAnyRemainingHtmlTags ) { + return null; + } + + return withoutWrapperTag; + } } From d97203df8877460843f90664cb988efe9fe16f3b Mon Sep 17 00:00:00 2001 From: Marcin Panek Date: Tue, 31 Oct 2023 13:22:07 +0100 Subject: [PATCH 05/33] Addded tests for paste from markdown. --- packages/ckeditor5-markdown-gfm/package.json | 3 + .../ckeditor5-markdown-gfm/src/markdown.ts | 2 +- .../tests/pastefrommarkdown.js | 193 ++++++++++++++++++ 3 files changed, 197 insertions(+), 1 deletion(-) create mode 100644 packages/ckeditor5-markdown-gfm/tests/pastefrommarkdown.js diff --git a/packages/ckeditor5-markdown-gfm/package.json b/packages/ckeditor5-markdown-gfm/package.json index c3971309d6e..f22c5e8c43c 100644 --- a/packages/ckeditor5-markdown-gfm/package.json +++ b/packages/ckeditor5-markdown-gfm/package.json @@ -26,8 +26,11 @@ "@ckeditor/ckeditor5-editor-classic": "40.0.0", "@ckeditor/ckeditor5-engine": "40.0.0", "@ckeditor/ckeditor5-list": "40.0.0", + "@ckeditor/ckeditor5-paragraph": "40.0.0", "@ckeditor/ckeditor5-table": "40.0.0", "@ckeditor/ckeditor5-theme-lark": "40.0.0", + "@ckeditor/ckeditor5-undo": "40.0.0", + "@ckeditor/ckeditor5-utils": "40.0.0", "@types/marked": "^4.0.8", "typescript": "^4.8.4", "webpack": "^5.58.1", diff --git a/packages/ckeditor5-markdown-gfm/src/markdown.ts b/packages/ckeditor5-markdown-gfm/src/markdown.ts index c814e65d1f2..e59836b0cce 100644 --- a/packages/ckeditor5-markdown-gfm/src/markdown.ts +++ b/packages/ckeditor5-markdown-gfm/src/markdown.ts @@ -71,7 +71,7 @@ export default class Markdown extends Plugin { } ); } - private parseMarkdownFromHtml( htmlString: string ) { + private parseMarkdownFromHtml( htmlString = '' ) { // Removing tag present on Mac. const withoutMetaTag = htmlString.replace( /^]*>/, '' ).trim(); // Removing tag present on Windows. diff --git a/packages/ckeditor5-markdown-gfm/tests/pastefrommarkdown.js b/packages/ckeditor5-markdown-gfm/tests/pastefrommarkdown.js new file mode 100644 index 00000000000..1cea7062814 --- /dev/null +++ b/packages/ckeditor5-markdown-gfm/tests/pastefrommarkdown.js @@ -0,0 +1,193 @@ +/** + * @license Copyright (c) 2003-2023, CKSource Holding sp. z o.o. All rights reserved. + * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license + */ + +import ClassicTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/classictesteditor'; +import global from '@ckeditor/ckeditor5-utils/src/dom/global'; +import { getData, setData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model'; +import Markdown from '../src/markdown'; +import { ClipboardPipeline } from '@ckeditor/ckeditor5-clipboard'; +import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph'; +import { keyCodes } from '@ckeditor/ckeditor5-utils'; +import Bold from '@ckeditor/ckeditor5-basic-styles/src/bold'; +import Italic from '@ckeditor/ckeditor5-basic-styles/src/italic'; +import Undo from '@ckeditor/ckeditor5-undo/src/undo'; +import DocumentFragment from '@ckeditor/ckeditor5-engine/src/view/documentfragment'; + +describe( 'PasteFromMarkdown', () => { + let editorElement, editor; + + beforeEach( () => { + editorElement = global.document.createElement( 'div' ); + global.document.body.appendChild( editorElement ); + + return ClassicTestEditor + .create( editorElement, { + plugins: [ ClipboardPipeline, Markdown, Paragraph, Bold, Italic, Undo ] + } ) + .then( newEditor => { + editor = newEditor; + } ); + } ); + + afterEach( () => { + editorElement.remove(); + + return editor.destroy(); + } ); + + it( 'should paste as markdown when type is text/plain', () => { + setData( editor.model, '[]' ); + pasteText( editor, 'foo **bar** [baz](https://ckeditor.com).' ); + + expect( getData( editor.model ) ).to.equal( 'foo <$text bold="true">bar baz.[]' ); + } ); + + it( 'should paste one level nested HTML as markdown if type is text/html', () => { + setData( editor.model, '[]' ); + pasteHtml( editor, 'foo **bar** [baz](https://ckeditor.com).' ); + + expect( getData( editor.model ) ).to.equal( 'foo <$text bold="true">bar baz.[]' ); + } ); + + it( 'should not two level nested HTML as markdown if type is text/html', () => { + setData( editor.model, '[]' ); + pasteHtml( editor, 'foo **bar** [baz](https://ckeditor.com).' ); + + expect( getData( editor.model ) ).to.equal( 'foo **bar** [baz](https://ckeditor.com).[]' ); + } ); + + it( 'should not paste as markdown if shift key pressed', () => { + setData( editor.model, '[]' ); + pressShiftKey( editor ); + pasteText( editor, 'foo **bar** [baz](https://ckeditor.com).' ); + + expect( getData( editor.model ) ).to.equal( 'foo **bar** [baz](https://ckeditor.com).[]' ); + } ); + + // TODO probably remove + it( 'should paste as markdown and remove it on undo', () => { + setData( editor.model, '[]' ); + pasteText( editor, 'foo **bar** [baz](https://ckeditor.com).' ); + + expect( getData( editor.model ) ).to.equal( 'foo <$text bold="true">bar baz.[]' ); + + editor.commands.execute( 'undo' ); + + expect( getData( editor.model ) ).to.equal( '[]' ); + } ); + + // TODO probably remove + it( 'inserts markdown in-place (non-collapsed selection)', () => { + setData( editor.model, '[should override]' ); + pasteText( editor, 'foo **bar** [baz](https://ckeditor.com).' ); + + expect( getData( editor.model ) ).to.equal( 'foo <$text bold="true">bar baz.[]' ); + } ); + + // TODO probably remove + it( 'inserts markdown in-place (collapsed selection)', () => { + setData( editor.model, 'Foo []Bar' ); + pasteText( editor, 'foo **bar** [baz](https://ckeditor.com).' ); + + expect( getData( editor.model ) ).to.equal( + 'Foo foo <$text bold="true">bar baz.[]Bar' + ); + } ); + + it( 'should call toView function once on markdown paste', () => { + const toViewStub = sinon.stub().returns( new DocumentFragment() ); + + editor.data.processor.toView = toViewStub; + setData( editor.model, '[]' ); + pasteText( editor, 'foo **bar** [baz](https://ckeditor.com).' ); + + expect( toViewStub.callCount ).to.equal( 1 ); + } ); + + it( 'should call toView function on one level nested HTML paste', () => { + const toViewStub = sinon.stub().returns( new DocumentFragment() ); + + editor.data.processor.toView = toViewStub; + setData( editor.model, '[]' ); + pasteHtml( editor, 'foo **bar** [baz](https://ckeditor.com).' ); + + expect( toViewStub.callCount ).to.equal( 1 ); + } ); + + it( 'should not call toView function on two level nested HTML paste', () => { + const toViewStub = sinon.stub().returns( new DocumentFragment() ); + + editor.data.processor.toView = toViewStub; + setData( editor.model, '[]' ); + pasteHtml( editor, 'foo **bar** [baz](https://ckeditor.com).' ); + + expect( toViewStub.callCount ).to.equal( 0 ); + } ); + + // TODO add Chrome, Firefox, Safari, Edge clipboard examples + it( 'should parse correctly Mac type clipboard', () => { + setData( editor.model, '[]' ); + pasteHtml( editor, 'foo **bar** [baz](https://ckeditor.com).' ); + + expect( getData( editor.model ) ).to.equal( 'foo <$text bold="true">bar baz.[]' ); + } ); + + it( 'should parse correctly Windows type clipboard', () => { + setData( editor.model, '[]' ); + pasteHtml( editor, + ` + + + + foo **bar** [baz](https://ckeditor.com). + + + + ` + ); + + expect( getData( editor.model ) ).to.equal( 'foo <$text bold="true">bar baz.[]' ); + } ); + + it( 'should parse correctly Linux type clipboard', () => { + setData( editor.model, '[]' ); + pasteHtml( editor, 'foo **bar** [baz](https://ckeditor.com).' ); + + expect( getData( editor.model ) ).to.equal( 'foo <$text bold="true">bar baz.[]' ); + } ); + + function pressShiftKey( editor ) { + editor.editing.view.document.fire( 'keydown', { + keyCode: keyCodes.shift, + shiftKey: true, + preventDefault: () => {}, + domTarget: global.document.body + } ); + } + + function pasteHtml( editor, html ) { + editor.editing.view.document.fire( 'paste', { + dataTransfer: createDataTransfer( { 'text/html': html } ), + stopPropagation() {}, + preventDefault() {} + } ); + } + + function pasteText( editor, text ) { + editor.editing.view.document.fire( 'paste', { + dataTransfer: createDataTransfer( { 'text/plain': text } ), + stopPropagation() {}, + preventDefault() {} + } ); + } + + function createDataTransfer( data ) { + return { + getData( type ) { + return data[ type ]; + } + }; + } +} ); From 0d94ac152b6f081a0fdb5c2916c34f9386e26bc8 Mon Sep 17 00:00:00 2001 From: Marcin Panek Date: Thu, 9 Nov 2023 13:15:47 +0100 Subject: [PATCH 06/33] Extracted paste from markdown to a separate plugin as PasteFromMarkdownExperimental. --- packages/ckeditor5-markdown-gfm/package.json | 9 ++ packages/ckeditor5-markdown-gfm/src/index.ts | 1 + .../ckeditor5-markdown-gfm/src/markdown.ts | 59 --------- .../src/pastefrommarkdownexperimental.ts | 115 ++++++++++++++++++ .../tests/pastefrommarkdown.js | 54 +------- 5 files changed, 127 insertions(+), 111 deletions(-) create mode 100644 packages/ckeditor5-markdown-gfm/src/pastefrommarkdownexperimental.ts diff --git a/packages/ckeditor5-markdown-gfm/package.json b/packages/ckeditor5-markdown-gfm/package.json index f22c5e8c43c..8c8322125f4 100644 --- a/packages/ckeditor5-markdown-gfm/package.json +++ b/packages/ckeditor5-markdown-gfm/package.json @@ -18,14 +18,23 @@ "turndown-plugin-gfm": "1.0.2" }, "devDependencies": { + "@ckeditor/ckeditor5-autoformat":"40.0.0", "@ckeditor/ckeditor5-basic-styles": "40.0.0", + "@ckeditor/ckeditor5-block-quote": "40.0.0", "@ckeditor/ckeditor5-clipboard": "40.0.0", "@ckeditor/ckeditor5-code-block": "40.0.0", "@ckeditor/ckeditor5-core": "40.0.0", "@ckeditor/ckeditor5-dev-utils": "^39.0.0", "@ckeditor/ckeditor5-editor-classic": "40.0.0", "@ckeditor/ckeditor5-engine": "40.0.0", + "@ckeditor/ckeditor5-essentials": "40.0.0", + "@ckeditor/ckeditor5-heading":"40.0.0", + "@ckeditor/ckeditor5-horizontal-line": "40.0.0", + "@ckeditor/ckeditor5-image": "40.0.0", + "@ckeditor/ckeditor5-indent":"40.0.0", + "@ckeditor/ckeditor5-link": "40.0.0", "@ckeditor/ckeditor5-list": "40.0.0", + "@ckeditor/ckeditor5-media-embed":"40.0.0", "@ckeditor/ckeditor5-paragraph": "40.0.0", "@ckeditor/ckeditor5-table": "40.0.0", "@ckeditor/ckeditor5-theme-lark": "40.0.0", diff --git a/packages/ckeditor5-markdown-gfm/src/index.ts b/packages/ckeditor5-markdown-gfm/src/index.ts index baec7372da6..5ab3e2740a9 100644 --- a/packages/ckeditor5-markdown-gfm/src/index.ts +++ b/packages/ckeditor5-markdown-gfm/src/index.ts @@ -8,5 +8,6 @@ */ export { default as Markdown } from './markdown'; +export { default as PasteFromMarkdownExperimental } from './pastefrommarkdownexperimental'; import './augmentation'; diff --git a/packages/ckeditor5-markdown-gfm/src/markdown.ts b/packages/ckeditor5-markdown-gfm/src/markdown.ts index e59836b0cce..82d4da205eb 100644 --- a/packages/ckeditor5-markdown-gfm/src/markdown.ts +++ b/packages/ckeditor5-markdown-gfm/src/markdown.ts @@ -9,8 +9,6 @@ import { Plugin, type Editor } from 'ckeditor5/src/core'; import GFMDataProcessor from './gfmdataprocessor'; -import type { ClipboardPipeline, ClipboardInputTransformationEvent } from 'ckeditor5/src/clipboard'; -import type { ViewDocumentKeyDownEvent } from 'ckeditor5/src/engine'; /** * The GitHub Flavored Markdown (GFM) plugin. @@ -33,61 +31,4 @@ export default class Markdown extends Plugin { public static get pluginName() { return 'Markdown' as const; } - - /** - * @inheritDoc - */ - public init(): void { - const editor = this.editor; - const view = editor.editing.view; - const viewDocument = view.document; - - const clipboardPipeline: ClipboardPipeline = editor.plugins.get( 'ClipboardPipeline' ); - - let shiftPressed = false; - - this.listenTo( viewDocument, 'keydown', ( evt, data ) => { - shiftPressed = data.shiftKey; - } ); - - this.listenTo( clipboardPipeline, 'inputTransformation', ( evt, data ) => { - const dataAsTextHtml = data.dataTransfer.getData( 'text/html' ); - const markdownFromHtml = this.parseMarkdownFromHtml( dataAsTextHtml ); - - if ( shiftPressed ) { - return; - } - - if ( !dataAsTextHtml ) { - const dataAsTextPlain = data.dataTransfer.getData( 'text/plain' ); - data.content = editor.data.processor.toView( dataAsTextPlain ); - - return; - } - - if ( markdownFromHtml ) { - data.content = editor.data.processor.toView( markdownFromHtml ); - } - } ); - } - - private parseMarkdownFromHtml( htmlString = '' ) { - // Removing tag present on Mac. - const withoutMetaTag = htmlString.replace( /^]*>/, '' ).trim(); - // Removing tag present on Windows. - const withoutHtmlTag = withoutMetaTag.replace( /^/, '' ).replace( /<\/html>$/, '' ).trim(); - // Removing tag present on Windows. - const withoutBodyTag = withoutHtmlTag.replace( /^/, '' ).replace( /<\/body>$/, '' ).trim(); - // Removing tag present on Windows. - const withoutFragmentTag = withoutBodyTag.replace( /^/, '' ).replace( /$/, '' ).trim(); - // Removing a wrapper HTML tag if exists. - const withoutWrapperTag = withoutFragmentTag.replace( /^<(\w+)\b[^>]*>\s*([\s\S]*?)\s*<\/\1>/, '$2' ).trim(); - const containsAnyRemainingHtmlTags = /<[^>]+>[\s\S]*<[^>]+>/.test( withoutWrapperTag ); - - if ( containsAnyRemainingHtmlTags ) { - return null; - } - - return withoutWrapperTag; - } } diff --git a/packages/ckeditor5-markdown-gfm/src/pastefrommarkdownexperimental.ts b/packages/ckeditor5-markdown-gfm/src/pastefrommarkdownexperimental.ts new file mode 100644 index 00000000000..732762b3194 --- /dev/null +++ b/packages/ckeditor5-markdown-gfm/src/pastefrommarkdownexperimental.ts @@ -0,0 +1,115 @@ +/** + * @license Copyright (c) 2003-2023, CKSource Holding sp. z o.o. All rights reserved. + * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license + */ + +/** + * @module markdown-gfm/markdown + */ + +import { Plugin, type Editor } from 'ckeditor5/src/core'; +import GFMDataProcessor from './gfmdataprocessor'; +import type { ClipboardPipeline, ClipboardInputTransformationEvent } from 'ckeditor5/src/clipboard'; +import type { ViewDocumentKeyDownEvent } from 'ckeditor5/src/engine'; + +/** + * The GitHub Flavored Markdown (GFM) paste plugin. + * + * // TDODO add correct link to the guide. + */ +export default class PasteFromMarkdownExperimental extends Plugin { + /** + * @internal + */ + private _gfmDataProcessor: GFMDataProcessor; + + /** + * @inheritDoc + */ + constructor( editor: Editor ) { + super( editor ); + + this._gfmDataProcessor = new GFMDataProcessor( editor.data.viewDocument ); + } + + /** + * @inheritDoc + */ + public static get pluginName() { + return 'PasteFromMarkdownExperimental' as const; + } + + /** + * @inheritDoc + */ + public static get requires() { + return [ 'ClipboardPipeline' ] as const; + } + + /** + * @inheritDoc + */ + public init(): void { + const editor = this.editor; + const view = editor.editing.view; + const viewDocument = view.document; + + const clipboardPipeline: ClipboardPipeline = editor.plugins.get( 'ClipboardPipeline' ); + + let shiftPressed = false; + + this.listenTo( viewDocument, 'keydown', ( evt, data ) => { + shiftPressed = data.shiftKey; + } ); + + this.listenTo( clipboardPipeline, 'inputTransformation', ( evt, data ) => { + const dataAsTextHtml = data.dataTransfer.getData( 'text/html' ); + const markdownFromHtml = this.parseMarkdownFromHtml( dataAsTextHtml ); + + if ( shiftPressed ) { + return; + } + + if ( !dataAsTextHtml ) { + const dataAsTextPlain = data.dataTransfer.getData( 'text/plain' ); + data.content = this._gfmDataProcessor.toView( dataAsTextPlain ); + + return; + } + + if ( markdownFromHtml ) { + data.content = this._gfmDataProcessor.toView( markdownFromHtml ); + } + } ); + } + + /** + * Determines if code copied from a website in `text/html` type can be parsed as markdown. + * It removes any OS specific HTML tags e.g. on Mac OS and on Windows. + * Then removes a single wrapper HTML tag, and if there are no more tags left, returns the remaining text. + * Returns null, if there are any remaining HTML tags detected. + * + * @param {String} htmlString Clipboard content in `text/html` type format. + * @private + * @returns String | null + */ + private parseMarkdownFromHtml( htmlString = '' ) { + // Removing tag present on Mac. + const withoutMetaTag = htmlString.replace( /^]*>/, '' ).trim(); + // Removing tag present on Windows. + const withoutHtmlTag = withoutMetaTag.replace( /^/, '' ).replace( /<\/html>$/, '' ).trim(); + // Removing tag present on Windows. + const withoutBodyTag = withoutHtmlTag.replace( /^/, '' ).replace( /<\/body>$/, '' ).trim(); + // Removing tag present on Windows. + const withoutFragmentTag = withoutBodyTag.replace( /^/, '' ).replace( /$/, '' ).trim(); + // Removing a wrapper HTML tag if exists. + const withoutWrapperTag = withoutFragmentTag.replace( /^<(\w+)\b[^>]*>\s*([\s\S]*?)\s*<\/\1>/, '$2' ).trim(); + const containsAnyRemainingHtmlTags = /<[^>]+>[\s\S]*<[^>]+>/.test( withoutWrapperTag ); + + if ( containsAnyRemainingHtmlTags ) { + return null; + } + + return withoutWrapperTag; + } +} diff --git a/packages/ckeditor5-markdown-gfm/tests/pastefrommarkdown.js b/packages/ckeditor5-markdown-gfm/tests/pastefrommarkdown.js index 1cea7062814..4649467ff80 100644 --- a/packages/ckeditor5-markdown-gfm/tests/pastefrommarkdown.js +++ b/packages/ckeditor5-markdown-gfm/tests/pastefrommarkdown.js @@ -6,7 +6,6 @@ import ClassicTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/classictesteditor'; import global from '@ckeditor/ckeditor5-utils/src/dom/global'; import { getData, setData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model'; -import Markdown from '../src/markdown'; import { ClipboardPipeline } from '@ckeditor/ckeditor5-clipboard'; import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph'; import { keyCodes } from '@ckeditor/ckeditor5-utils'; @@ -14,6 +13,7 @@ import Bold from '@ckeditor/ckeditor5-basic-styles/src/bold'; import Italic from '@ckeditor/ckeditor5-basic-styles/src/italic'; import Undo from '@ckeditor/ckeditor5-undo/src/undo'; import DocumentFragment from '@ckeditor/ckeditor5-engine/src/view/documentfragment'; +import PasteFromMarkdownExperimental from '../src/pastefrommarkdownexperimental'; describe( 'PasteFromMarkdown', () => { let editorElement, editor; @@ -24,7 +24,7 @@ describe( 'PasteFromMarkdown', () => { return ClassicTestEditor .create( editorElement, { - plugins: [ ClipboardPipeline, Markdown, Paragraph, Bold, Italic, Undo ] + plugins: [ ClipboardPipeline, PasteFromMarkdownExperimental, Paragraph, Bold, Italic, Undo ] } ) .then( newEditor => { editor = newEditor; @@ -66,36 +66,6 @@ describe( 'PasteFromMarkdown', () => { expect( getData( editor.model ) ).to.equal( 'foo **bar** [baz](https://ckeditor.com).[]' ); } ); - // TODO probably remove - it( 'should paste as markdown and remove it on undo', () => { - setData( editor.model, '[]' ); - pasteText( editor, 'foo **bar** [baz](https://ckeditor.com).' ); - - expect( getData( editor.model ) ).to.equal( 'foo <$text bold="true">bar baz.[]' ); - - editor.commands.execute( 'undo' ); - - expect( getData( editor.model ) ).to.equal( '[]' ); - } ); - - // TODO probably remove - it( 'inserts markdown in-place (non-collapsed selection)', () => { - setData( editor.model, '[should override]' ); - pasteText( editor, 'foo **bar** [baz](https://ckeditor.com).' ); - - expect( getData( editor.model ) ).to.equal( 'foo <$text bold="true">bar baz.[]' ); - } ); - - // TODO probably remove - it( 'inserts markdown in-place (collapsed selection)', () => { - setData( editor.model, 'Foo []Bar' ); - pasteText( editor, 'foo **bar** [baz](https://ckeditor.com).' ); - - expect( getData( editor.model ) ).to.equal( - 'Foo foo <$text bold="true">bar baz.[]Bar' - ); - } ); - it( 'should call toView function once on markdown paste', () => { const toViewStub = sinon.stub().returns( new DocumentFragment() ); @@ -106,26 +76,6 @@ describe( 'PasteFromMarkdown', () => { expect( toViewStub.callCount ).to.equal( 1 ); } ); - it( 'should call toView function on one level nested HTML paste', () => { - const toViewStub = sinon.stub().returns( new DocumentFragment() ); - - editor.data.processor.toView = toViewStub; - setData( editor.model, '[]' ); - pasteHtml( editor, 'foo **bar** [baz](https://ckeditor.com).' ); - - expect( toViewStub.callCount ).to.equal( 1 ); - } ); - - it( 'should not call toView function on two level nested HTML paste', () => { - const toViewStub = sinon.stub().returns( new DocumentFragment() ); - - editor.data.processor.toView = toViewStub; - setData( editor.model, '[]' ); - pasteHtml( editor, 'foo **bar** [baz](https://ckeditor.com).' ); - - expect( toViewStub.callCount ).to.equal( 0 ); - } ); - // TODO add Chrome, Firefox, Safari, Edge clipboard examples it( 'should parse correctly Mac type clipboard', () => { setData( editor.model, '[]' ); From 6b8b1abc323a76f8b925444e81fbbc78c389458f Mon Sep 17 00:00:00 2001 From: Marcin Panek Date: Thu, 9 Nov 2023 13:18:48 +0100 Subject: [PATCH 07/33] Added manual test for paste from markdown. --- .../pastefrommarkdown/pastefrommarkdown.html | 17 +++ .../pastefrommarkdown/pastefrommarkdown.js | 125 ++++++++++++++++++ .../pastefrommarkdown/pastefrommarkdown.md | 5 + 3 files changed, 147 insertions(+) create mode 100644 packages/ckeditor5-markdown-gfm/tests/manual/pastefrommarkdown/pastefrommarkdown.html create mode 100644 packages/ckeditor5-markdown-gfm/tests/manual/pastefrommarkdown/pastefrommarkdown.js create mode 100644 packages/ckeditor5-markdown-gfm/tests/manual/pastefrommarkdown/pastefrommarkdown.md diff --git a/packages/ckeditor5-markdown-gfm/tests/manual/pastefrommarkdown/pastefrommarkdown.html b/packages/ckeditor5-markdown-gfm/tests/manual/pastefrommarkdown/pastefrommarkdown.html new file mode 100644 index 00000000000..9848ec577e2 --- /dev/null +++ b/packages/ckeditor5-markdown-gfm/tests/manual/pastefrommarkdown/pastefrommarkdown.html @@ -0,0 +1,17 @@ + + +
Paste markdown code here +
+ +

Output:

+ +
diff --git a/packages/ckeditor5-markdown-gfm/tests/manual/pastefrommarkdown/pastefrommarkdown.js b/packages/ckeditor5-markdown-gfm/tests/manual/pastefrommarkdown/pastefrommarkdown.js new file mode 100644 index 00000000000..81c69385ced --- /dev/null +++ b/packages/ckeditor5-markdown-gfm/tests/manual/pastefrommarkdown/pastefrommarkdown.js @@ -0,0 +1,125 @@ +/** + * @license Copyright (c) 2003-2023, CKSource Holding sp. z o.o. All rights reserved. + * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license + */ + +/* globals console, window, document, setTimeout */ + +import ClassicEditor from '@ckeditor/ckeditor5-editor-classic/src/classiceditor'; + +import Strikethrough from '@ckeditor/ckeditor5-basic-styles/src/strikethrough'; +import TableProperties from '@ckeditor/ckeditor5-table/src/tableproperties'; +import TableCellProperties from '@ckeditor/ckeditor5-table/src/tablecellproperties'; +import CodeBlock from '@ckeditor/ckeditor5-code-block/src/codeblock'; +import Code from '@ckeditor/ckeditor5-basic-styles/src/code'; +import DocumentList from '@ckeditor/ckeditor5-list/src/documentlist'; +import DocumentListProperties from '@ckeditor/ckeditor5-list/src/documentlistproperties'; +import Essentials from '@ckeditor/ckeditor5-essentials/src/essentials'; +import AutoFormat from '@ckeditor/ckeditor5-autoformat/src/autoformat'; +import BlockQuote from '@ckeditor/ckeditor5-block-quote/src/blockquote'; +import Bold from '@ckeditor/ckeditor5-basic-styles/src/bold'; +import Italic from '@ckeditor/ckeditor5-basic-styles/src/italic'; +import Heading from '@ckeditor/ckeditor5-heading/src/heading'; +import Image from '@ckeditor/ckeditor5-image/src/image'; +import ImageCaption from '@ckeditor/ckeditor5-image/src/imagecaption'; +import ImageStyle from '@ckeditor/ckeditor5-image/src/imagestyle'; +import ImageToolbar from '@ckeditor/ckeditor5-image/src/imagetoolbar'; +import Indent from '@ckeditor/ckeditor5-indent/src/indent'; +import Link from '@ckeditor/ckeditor5-link/src/link'; +import MediaEmbed from '@ckeditor/ckeditor5-media-embed/src/mediaembed'; +import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph'; +import Table from '@ckeditor/ckeditor5-table/src/table'; +import TableToolbar from '@ckeditor/ckeditor5-table/src/tabletoolbar'; +import HorizontalLine from '@ckeditor/ckeditor5-horizontal-line/src/horizontalline'; +import Markdown from '../../../src/markdown'; +import PasteFromMarkdownExperimental from '../../../src/pastefrommarkdownexperimental'; + +ClassicEditor + .create( document.querySelector( '#editor' ), { + plugins: [ + PasteFromMarkdownExperimental, + Markdown, + Essentials, + AutoFormat, + BlockQuote, + Bold, + Heading, + Image, + ImageCaption, + ImageStyle, + ImageToolbar, + Indent, + Italic, + Link, + MediaEmbed, + Paragraph, + Table, + TableToolbar, + Code, + CodeBlock, + Strikethrough, + DocumentList, + DocumentListProperties, + TableProperties, + TableCellProperties, + HorizontalLine + ], + toolbar: [ + 'heading', + '|', + 'bold', + 'italic', + 'strikethrough', + 'link', + '|', + 'code', + 'codeBlock', + '|', + 'bulletedList', + 'numberedList', + '|', + 'outdent', + 'indent', + '|', + 'blockQuote', + 'insertTable', + '|', + 'undo', + 'redo', + 'horizontalLine' + ], + image: { + toolbar: [ 'imageStyle:inline', 'imageStyle:block', 'imageStyle:side', '|', 'imageTextAlternative' ] + }, + table: { + contentToolbar: [ 'tableColumn', 'tableRow', 'mergeTableCells', 'tableProperties', 'tableCellProperties' ], + tableToolbar: [ 'bold', 'italic' ] + }, + heading: { + options: [ + { model: 'heading1', view: 'h1', title: 'Heading 1', class: 'ck-heading_heading1' }, + { model: 'heading2', view: 'h2', title: 'Heading 2', class: 'ck-heading_heading2' }, + { model: 'heading3', view: 'h3', title: 'Heading 3', class: 'ck-heading_heading3' }, + { model: 'heading4', view: 'h4', title: 'Heading 4', class: 'ck-heading_heading4' }, + { model: 'heading5', view: 'h5', title: 'Heading 5', class: 'ck-heading_heading5' }, + { model: 'heading6', view: 'h6', title: 'Heading 6', class: 'ck-heading_heading6' } + ] + } + } ) + .then( editor => { + window.editor = editor; + + const outputElement = document.querySelector( '#markdown-output' ); + + editor.model.document.on( 'change', () => { + outputElement.innerText = editor.getData(); + } ); + + // Set the initial data with delay so hightlight.js doesn't catch them. + setTimeout( () => { + outputElement.innerText = editor.getData(); + }, 500 ); + } ) + .catch( err => { + console.error( err.stack ); + } ); diff --git a/packages/ckeditor5-markdown-gfm/tests/manual/pastefrommarkdown/pastefrommarkdown.md b/packages/ckeditor5-markdown-gfm/tests/manual/pastefrommarkdown/pastefrommarkdown.md new file mode 100644 index 00000000000..083055032f3 --- /dev/null +++ b/packages/ckeditor5-markdown-gfm/tests/manual/pastefrommarkdown/pastefrommarkdown.md @@ -0,0 +1,5 @@ +#### GitHub Flavored Markdown Editor + +- Play around with [GitHub Flavored Markdown](https://guides.github.com/features/mastering-markdown/). +- See the markdown generated in the "output" box. +- You can copy and paste markdown to set markdown content e.g. from here [dillinger.io](https://dillinger.io/) From 8ed8509390ee9445d2551f9220db82efc66d49ee Mon Sep 17 00:00:00 2001 From: Marcin Panek Date: Thu, 9 Nov 2023 13:29:05 +0100 Subject: [PATCH 08/33] Added missing type augmentation for paste from markdown. --- packages/ckeditor5-markdown-gfm/src/augmentation.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/ckeditor5-markdown-gfm/src/augmentation.ts b/packages/ckeditor5-markdown-gfm/src/augmentation.ts index cc1dc43f393..e5b1e834522 100644 --- a/packages/ckeditor5-markdown-gfm/src/augmentation.ts +++ b/packages/ckeditor5-markdown-gfm/src/augmentation.ts @@ -3,10 +3,11 @@ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license */ -import type { Markdown } from './index'; +import type { Markdown, PasteFromMarkdownExperimental } from './index'; declare module '@ckeditor/ckeditor5-core' { interface PluginsMap { [ Markdown.pluginName ]: Markdown; + [ PasteFromMarkdownExperimental.pluginName ]: PasteFromMarkdownExperimental; } } From d1787340baa53ff0bbd0e48f68cdcf1373e92262 Mon Sep 17 00:00:00 2001 From: Marcin Panek Date: Thu, 9 Nov 2023 17:57:21 +0100 Subject: [PATCH 09/33] Added docs snippets for paste from markdown. --- .../features/build-markdown-source.html | 7 ++ .../features/build-markdown-source.js | 70 +++++++++++++++++++ .../docs/_snippets/features/markdown.html | 1 - .../docs/_snippets/features/markdown.js | 22 +----- .../features/paste-from-markdown.html | 10 +++ .../_snippets/features/paste-from-markdown.js | 53 ++++++++++++++ .../docs/features/markdown.md | 8 +++ .../src/pastefrommarkdownexperimental.ts | 2 +- .../pastefrommarkdown/pastefrommarkdown.html | 1 + 9 files changed, 151 insertions(+), 23 deletions(-) create mode 100644 packages/ckeditor5-markdown-gfm/docs/_snippets/features/build-markdown-source.html create mode 100644 packages/ckeditor5-markdown-gfm/docs/_snippets/features/build-markdown-source.js create mode 100644 packages/ckeditor5-markdown-gfm/docs/_snippets/features/paste-from-markdown.html create mode 100644 packages/ckeditor5-markdown-gfm/docs/_snippets/features/paste-from-markdown.js diff --git a/packages/ckeditor5-markdown-gfm/docs/_snippets/features/build-markdown-source.html b/packages/ckeditor5-markdown-gfm/docs/_snippets/features/build-markdown-source.html new file mode 100644 index 00000000000..951498030b7 --- /dev/null +++ b/packages/ckeditor5-markdown-gfm/docs/_snippets/features/build-markdown-source.html @@ -0,0 +1,7 @@ + + + diff --git a/packages/ckeditor5-markdown-gfm/docs/_snippets/features/build-markdown-source.js b/packages/ckeditor5-markdown-gfm/docs/_snippets/features/build-markdown-source.js new file mode 100644 index 00000000000..3ae5e88ff88 --- /dev/null +++ b/packages/ckeditor5-markdown-gfm/docs/_snippets/features/build-markdown-source.js @@ -0,0 +1,70 @@ +/** + * @license Copyright (c) 2003-2023, CKSource Holding sp. z o.o. All rights reserved. + * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license + */ + +/* globals window */ + +import { Code, Strikethrough } from '@ckeditor/ckeditor5-basic-styles'; +import { CloudServices } from '@ckeditor/ckeditor5-cloud-services'; +import { CS_CONFIG } from '@ckeditor/ckeditor5-cloud-services/tests/_utils/cloud-services-config'; +import { CodeBlock } from '@ckeditor/ckeditor5-code-block'; +import ArticlePluginSet from '@ckeditor/ckeditor5-core/tests/_utils/articlepluginset'; +import { CKBox } from '@ckeditor/ckeditor5-ckbox'; +import { HorizontalLine } from '@ckeditor/ckeditor5-horizontal-line'; +import { ImageUpload, PictureEditing } from '@ckeditor/ckeditor5-image'; +import { TodoList } from '@ckeditor/ckeditor5-list'; +import { SourceEditing } from '@ckeditor/ckeditor5-source-editing'; + +import { Markdown, PasteFromMarkdownExperimental } from '@ckeditor/ckeditor5-markdown-gfm'; + +// Umberto combines all `packages/*/docs` into the `docs/` directory. The import path must be valid after merging all directories. +import ClassicEditor from '../build-classic'; + +ClassicEditor.builtinPlugins.push( ArticlePluginSet, SourceEditing, CKBox, ImageUpload, PictureEditing, CloudServices, Markdown, + Code, CodeBlock, TodoList, Strikethrough, HorizontalLine ); + +ClassicEditor.defaultConfig = { + cloudServices: CS_CONFIG, + toolbar: { + items: [ + 'undo', 'redo', '|', 'sourceEditing', '|', 'heading', + '|', 'bold', 'italic', 'strikethrough', 'code', + '-', 'link', 'uploadImage', 'insertTable', 'mediaEmbed', 'blockQuote', 'codeBlock', 'horizontalLine', + '|', 'bulletedList', 'numberedList', 'todoList', 'outdent', 'indent' + ] + }, + image: { + toolbar: [ 'imageStyle:inline', 'imageStyle:block', 'imageStyle:side', '|', 'toggleImageCaption', 'imageTextAlternative' ] + }, + codeBlock: { + languages: [ + { language: 'css', label: 'CSS' }, + { language: 'html', label: 'HTML' }, + { language: 'javascript', label: 'JavaScript' }, + { language: 'php', label: 'PHP' } + ] + }, + ui: { + viewportOffset: { + top: window.getViewportTopOffsetConfig() + } + } +}; + +window.ClassicEditor = ClassicEditor; +window.CKEditorPlugins = { + ArticlePluginSet, + SourceEditing, + CKBox, + ImageUpload, + PictureEditing, + CloudServices, + Markdown, + PasteFromMarkdownExperimental, + Code, + CodeBlock, + TodoList, + Strikethrough, + HorizontalLine +}; diff --git a/packages/ckeditor5-markdown-gfm/docs/_snippets/features/markdown.html b/packages/ckeditor5-markdown-gfm/docs/_snippets/features/markdown.html index a9aa8fef179..f2bd81886d1 100644 --- a/packages/ckeditor5-markdown-gfm/docs/_snippets/features/markdown.html +++ b/packages/ckeditor5-markdown-gfm/docs/_snippets/features/markdown.html @@ -1,4 +1,3 @@ - + +

Output:

+ +
diff --git a/packages/ckeditor5-markdown-gfm/docs/_snippets/features/paste-from-markdown.js b/packages/ckeditor5-markdown-gfm/docs/_snippets/features/paste-from-markdown.js new file mode 100644 index 00000000000..bae2da57a9e --- /dev/null +++ b/packages/ckeditor5-markdown-gfm/docs/_snippets/features/paste-from-markdown.js @@ -0,0 +1,53 @@ +/** + * @license Copyright (c) 2003-2023, CKSource Holding sp. z o.o. All rights reserved. + * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license + */ + +/* globals ClassicEditor, console, window, document, setTimeout */ + +ClassicEditor + .create( document.querySelector( '#snippet-paste-from-markdown' ), { + plugins: [ ...ClassicEditor.builtinPlugins, window.CKEditorPlugins.PasteFromMarkdownExperimental ], + toolbar: { + items: [ + 'undo', 'redo', '|', 'sourceEditing', '|', 'heading', + '|', 'bold', 'italic', 'strikethrough', 'code', + '-', 'link', 'uploadImage', 'insertTable', 'mediaEmbed', 'blockQuote', 'codeBlock', 'horizontalLine', + '|', 'bulletedList', 'numberedList', 'todoList', 'outdent', 'indent' + ], + shouldNotGroupWhenFull: true + }, + image: { + toolbar: [ 'imageStyle:inline', 'imageStyle:block', 'imageStyle:side', '|', 'toggleImageCaption', 'imageTextAlternative' ] + }, + codeBlock: { + languages: [ + { language: 'css', label: 'CSS' }, + { language: 'html', label: 'HTML' }, + { language: 'javascript', label: 'JavaScript' }, + { language: 'php', label: 'PHP' } + ] + }, + ui: { + viewportOffset: { + top: window.getViewportTopOffsetConfig() + } + } + } ) + .then( editor => { + window.editor = editor; + + const outputElement = document.querySelector( '#snippet-paste-from-markdown-output' ); + + editor.model.document.on( 'change', () => { + outputElement.innerText = editor.getData(); + } ); + + // Set the initial data with delay so hightlight.js doesn't catch it. + setTimeout( () => { + outputElement.innerText = editor.getData(); + }, 500 ); + } ) + .catch( err => { + console.error( err.stack ); + } ); diff --git a/packages/ckeditor5-markdown-gfm/docs/features/markdown.md b/packages/ckeditor5-markdown-gfm/docs/features/markdown.md index 461051b6e0a..4a6d93048d1 100644 --- a/packages/ckeditor5-markdown-gfm/docs/features/markdown.md +++ b/packages/ckeditor5-markdown-gfm/docs/features/markdown.md @@ -4,6 +4,8 @@ meta-title: Markdown output | CKEditor 5 Documentation category: features --- +{@snippet features/build-markdown-source} + The Markdown plugin lets you switch the default output from HTML to Markdown. This way you can produce lightweight text documents with a simple formatting syntax that is popular among developers. ## Demo @@ -49,6 +51,12 @@ When converting the output produced by this data processor, make sure to use a c And please do remember — using Markdown [does not automatically make your application or website secure](https://github.com/ckeditor/ckeditor5-markdown-gfm/issues/16#issuecomment-375752994). +## Paste from markdown + +[//]: # (TODO add description here) + +{@snippet features/paste-from-markdown} + ## Installation diff --git a/packages/ckeditor5-markdown-gfm/src/pastefrommarkdownexperimental.ts b/packages/ckeditor5-markdown-gfm/src/pastefrommarkdownexperimental.ts index 732762b3194..82d73fc008f 100644 --- a/packages/ckeditor5-markdown-gfm/src/pastefrommarkdownexperimental.ts +++ b/packages/ckeditor5-markdown-gfm/src/pastefrommarkdownexperimental.ts @@ -15,7 +15,7 @@ import type { ViewDocumentKeyDownEvent } from 'ckeditor5/src/engine'; /** * The GitHub Flavored Markdown (GFM) paste plugin. * - * // TDODO add correct link to the guide. + * // TODO add correct link to the guide. */ export default class PasteFromMarkdownExperimental extends Plugin { /** diff --git a/packages/ckeditor5-markdown-gfm/tests/manual/pastefrommarkdown/pastefrommarkdown.html b/packages/ckeditor5-markdown-gfm/tests/manual/pastefrommarkdown/pastefrommarkdown.html index 9848ec577e2..1b6536e659c 100644 --- a/packages/ckeditor5-markdown-gfm/tests/manual/pastefrommarkdown/pastefrommarkdown.html +++ b/packages/ckeditor5-markdown-gfm/tests/manual/pastefrommarkdown/pastefrommarkdown.html @@ -9,6 +9,7 @@ } +
Paste markdown code here
From be999a35152a7b62a5fffaca267af4a9a69c1aa2 Mon Sep 17 00:00:00 2001 From: Kamil Piechaczek Date: Fri, 10 Nov 2023 10:46:38 +0100 Subject: [PATCH 10/33] Review fixes. --- .../features/build-markdown-source.js | 47 ++++++++++--------- .../docs/_snippets/features/markdown.js | 29 ++---------- .../_snippets/features/paste-from-markdown.js | 32 +++---------- .../docs/features/markdown.md | 17 ++++--- .../src/pastefrommarkdownexperimental.ts | 18 +++---- ...wn.js => pastefrommarkdownexperimental.js} | 4 +- 6 files changed, 56 insertions(+), 91 deletions(-) rename packages/ckeditor5-markdown-gfm/tests/{pastefrommarkdown.js => pastefrommarkdownexperimental.js} (97%) diff --git a/packages/ckeditor5-markdown-gfm/docs/_snippets/features/build-markdown-source.js b/packages/ckeditor5-markdown-gfm/docs/_snippets/features/build-markdown-source.js index 3ae5e88ff88..5edafca3be1 100644 --- a/packages/ckeditor5-markdown-gfm/docs/_snippets/features/build-markdown-source.js +++ b/packages/ckeditor5-markdown-gfm/docs/_snippets/features/build-markdown-source.js @@ -6,34 +6,45 @@ /* globals window */ import { Code, Strikethrough } from '@ckeditor/ckeditor5-basic-styles'; -import { CloudServices } from '@ckeditor/ckeditor5-cloud-services'; import { CS_CONFIG } from '@ckeditor/ckeditor5-cloud-services/tests/_utils/cloud-services-config'; import { CodeBlock } from '@ckeditor/ckeditor5-code-block'; -import ArticlePluginSet from '@ckeditor/ckeditor5-core/tests/_utils/articlepluginset'; -import { CKBox } from '@ckeditor/ckeditor5-ckbox'; import { HorizontalLine } from '@ckeditor/ckeditor5-horizontal-line'; -import { ImageUpload, PictureEditing } from '@ckeditor/ckeditor5-image'; -import { TodoList } from '@ckeditor/ckeditor5-list'; import { SourceEditing } from '@ckeditor/ckeditor5-source-editing'; - +import { List, TodoList, DocumentList, TodoDocumentList, AdjacentListsSupport } from '@ckeditor/ckeditor5-list'; import { Markdown, PasteFromMarkdownExperimental } from '@ckeditor/ckeditor5-markdown-gfm'; // Umberto combines all `packages/*/docs` into the `docs/` directory. The import path must be valid after merging all directories. import ClassicEditor from '../build-classic'; -ClassicEditor.builtinPlugins.push( ArticlePluginSet, SourceEditing, CKBox, ImageUpload, PictureEditing, CloudServices, Markdown, - Code, CodeBlock, TodoList, Strikethrough, HorizontalLine ); +ClassicEditor.builtinPlugins = ClassicEditor.builtinPlugins + // Remove the `List` plugin as in a single demo we want to use the Document list feature. + .filter( pluginConstructor => { + if ( pluginConstructor.pluginName === 'List' ) { + return false; + } + + return true; + } ) + // Then, add Markdown-specific features. + .concat( [ + SourceEditing, + Code, Strikethrough, + Markdown, + CodeBlock, + HorizontalLine + ] ); ClassicEditor.defaultConfig = { - cloudServices: CS_CONFIG, toolbar: { items: [ 'undo', 'redo', '|', 'sourceEditing', '|', 'heading', '|', 'bold', 'italic', 'strikethrough', 'code', '-', 'link', 'uploadImage', 'insertTable', 'mediaEmbed', 'blockQuote', 'codeBlock', 'horizontalLine', '|', 'bulletedList', 'numberedList', 'todoList', 'outdent', 'indent' - ] + ], + shouldNotGroupWhenFull: true }, + cloudServices: CS_CONFIG, image: { toolbar: [ 'imageStyle:inline', 'imageStyle:block', 'imageStyle:side', '|', 'toggleImageCaption', 'imageTextAlternative' ] }, @@ -54,17 +65,7 @@ ClassicEditor.defaultConfig = { window.ClassicEditor = ClassicEditor; window.CKEditorPlugins = { - ArticlePluginSet, - SourceEditing, - CKBox, - ImageUpload, - PictureEditing, - CloudServices, - Markdown, - PasteFromMarkdownExperimental, - Code, - CodeBlock, - TodoList, - Strikethrough, - HorizontalLine + List, TodoList, + DocumentList, TodoDocumentList, AdjacentListsSupport, + PasteFromMarkdownExperimental }; diff --git a/packages/ckeditor5-markdown-gfm/docs/_snippets/features/markdown.js b/packages/ckeditor5-markdown-gfm/docs/_snippets/features/markdown.js index 7c07d80a932..3f22c204ac4 100644 --- a/packages/ckeditor5-markdown-gfm/docs/_snippets/features/markdown.js +++ b/packages/ckeditor5-markdown-gfm/docs/_snippets/features/markdown.js @@ -7,31 +7,10 @@ ClassicEditor .create( document.querySelector( '#snippet-markdown' ), { - toolbar: { - items: [ - 'undo', 'redo', '|', 'sourceEditing', '|', 'heading', - '|', 'bold', 'italic', 'strikethrough', 'code', - '-', 'link', 'uploadImage', 'insertTable', 'mediaEmbed', 'blockQuote', 'codeBlock', 'horizontalLine', - '|', 'bulletedList', 'numberedList', 'todoList', 'outdent', 'indent' - ], - shouldNotGroupWhenFull: true - }, - image: { - toolbar: [ 'imageStyle:inline', 'imageStyle:block', 'imageStyle:side', '|', 'toggleImageCaption', 'imageTextAlternative' ] - }, - codeBlock: { - languages: [ - { language: 'css', label: 'CSS' }, - { language: 'html', label: 'HTML' }, - { language: 'javascript', label: 'JavaScript' }, - { language: 'php', label: 'PHP' } - ] - }, - ui: { - viewportOffset: { - top: window.getViewportTopOffsetConfig() - } - } + extraPlugins: [ + window.CKEditorPlugins.List, + window.CKEditorPlugins.TodoList + ] } ) .then( editor => { window.editor = editor; diff --git a/packages/ckeditor5-markdown-gfm/docs/_snippets/features/paste-from-markdown.js b/packages/ckeditor5-markdown-gfm/docs/_snippets/features/paste-from-markdown.js index bae2da57a9e..7627b9ef89b 100644 --- a/packages/ckeditor5-markdown-gfm/docs/_snippets/features/paste-from-markdown.js +++ b/packages/ckeditor5-markdown-gfm/docs/_snippets/features/paste-from-markdown.js @@ -7,32 +7,12 @@ ClassicEditor .create( document.querySelector( '#snippet-paste-from-markdown' ), { - plugins: [ ...ClassicEditor.builtinPlugins, window.CKEditorPlugins.PasteFromMarkdownExperimental ], - toolbar: { - items: [ - 'undo', 'redo', '|', 'sourceEditing', '|', 'heading', - '|', 'bold', 'italic', 'strikethrough', 'code', - '-', 'link', 'uploadImage', 'insertTable', 'mediaEmbed', 'blockQuote', 'codeBlock', 'horizontalLine', - '|', 'bulletedList', 'numberedList', 'todoList', 'outdent', 'indent' - ], - shouldNotGroupWhenFull: true - }, - image: { - toolbar: [ 'imageStyle:inline', 'imageStyle:block', 'imageStyle:side', '|', 'toggleImageCaption', 'imageTextAlternative' ] - }, - codeBlock: { - languages: [ - { language: 'css', label: 'CSS' }, - { language: 'html', label: 'HTML' }, - { language: 'javascript', label: 'JavaScript' }, - { language: 'php', label: 'PHP' } - ] - }, - ui: { - viewportOffset: { - top: window.getViewportTopOffsetConfig() - } - } + extraPlugins: [ + window.CKEditorPlugins.DocumentList, + window.CKEditorPlugins.TodoDocumentList, + window.CKEditorPlugins.AdjacentListsSupport, + window.CKEditorPlugins.PasteFromMarkdownExperimental + ] } ) .then( editor => { window.editor = editor; diff --git a/packages/ckeditor5-markdown-gfm/docs/features/markdown.md b/packages/ckeditor5-markdown-gfm/docs/features/markdown.md index 4a6d93048d1..64c035147da 100644 --- a/packages/ckeditor5-markdown-gfm/docs/features/markdown.md +++ b/packages/ckeditor5-markdown-gfm/docs/features/markdown.md @@ -51,12 +51,6 @@ When converting the output produced by this data processor, make sure to use a c And please do remember — using Markdown [does not automatically make your application or website secure](https://github.com/ckeditor/ckeditor5-markdown-gfm/issues/16#issuecomment-375752994).
-## Paste from markdown - -[//]: # (TODO add description here) - -{@snippet features/paste-from-markdown} - ## Installation @@ -116,6 +110,17 @@ While the Markdown plugin is stable and ready to use, some issues are still bein * Pasting Markdown-formatted content does not automatically convert the pasted syntax markers into properly formatted content. GitHub issues: [#2321](https://github.com/ckeditor/ckeditor5/issues/2321), [#2322](https://github.com/ckeditor/ckeditor5/issues/2322). * The Markdown code generated with the Markdown output feature will not properly render {@link features/tables#nesting-tables nested tables}. GitHub issue: [#9475](https://github.com/ckeditor/ckeditor5/issues/9475). +### Paste from markdown + +TODOs: + +* Description what the PasteFromMarkdownExperimental is. What does it do, and why is it marked as experimental? +* How to use it? +* Demo (see below). It would be good to have some predefined content: “Copy and paste to see results.”. +* Please, share your feedback that may impact the final shape of the plugin. + +{@snippet features/paste-from-markdown} + ## Related features Some other ways to output the edited content include: diff --git a/packages/ckeditor5-markdown-gfm/src/pastefrommarkdownexperimental.ts b/packages/ckeditor5-markdown-gfm/src/pastefrommarkdownexperimental.ts index 82d73fc008f..f07da99fb02 100644 --- a/packages/ckeditor5-markdown-gfm/src/pastefrommarkdownexperimental.ts +++ b/packages/ckeditor5-markdown-gfm/src/pastefrommarkdownexperimental.ts @@ -8,14 +8,14 @@ */ import { Plugin, type Editor } from 'ckeditor5/src/core'; +import { Clipboard, type ClipboardPipeline, type ClipboardInputTransformationEvent } from 'ckeditor5/src/clipboard'; import GFMDataProcessor from './gfmdataprocessor'; -import type { ClipboardPipeline, ClipboardInputTransformationEvent } from 'ckeditor5/src/clipboard'; import type { ViewDocumentKeyDownEvent } from 'ckeditor5/src/engine'; /** * The GitHub Flavored Markdown (GFM) paste plugin. * - * // TODO add correct link to the guide. + * For a detailed overview, check the {@glink features/markdown##paste-from-markdown Markdown feature} guide. */ export default class PasteFromMarkdownExperimental extends Plugin { /** @@ -43,7 +43,7 @@ export default class PasteFromMarkdownExperimental extends Plugin { * @inheritDoc */ public static get requires() { - return [ 'ClipboardPipeline' ] as const; + return [ Clipboard ] as const; } /** @@ -63,13 +63,12 @@ export default class PasteFromMarkdownExperimental extends Plugin { } ); this.listenTo( clipboardPipeline, 'inputTransformation', ( evt, data ) => { - const dataAsTextHtml = data.dataTransfer.getData( 'text/html' ); - const markdownFromHtml = this.parseMarkdownFromHtml( dataAsTextHtml ); - if ( shiftPressed ) { return; } + const dataAsTextHtml = data.dataTransfer.getData( 'text/html' ); + if ( !dataAsTextHtml ) { const dataAsTextPlain = data.dataTransfer.getData( 'text/plain' ); data.content = this._gfmDataProcessor.toView( dataAsTextPlain ); @@ -77,6 +76,8 @@ export default class PasteFromMarkdownExperimental extends Plugin { return; } + const markdownFromHtml = this.parseMarkdownFromHtml( dataAsTextHtml ); + if ( markdownFromHtml ) { data.content = this._gfmDataProcessor.toView( markdownFromHtml ); } @@ -89,11 +90,10 @@ export default class PasteFromMarkdownExperimental extends Plugin { * Then removes a single wrapper HTML tag, and if there are no more tags left, returns the remaining text. * Returns null, if there are any remaining HTML tags detected. * - * @param {String} htmlString Clipboard content in `text/html` type format. * @private - * @returns String | null + * @param htmlString Clipboard content in `text/html` type format. */ - private parseMarkdownFromHtml( htmlString = '' ) { + private parseMarkdownFromHtml( htmlString: string ): string | null { // Removing tag present on Mac. const withoutMetaTag = htmlString.replace( /^]*>/, '' ).trim(); // Removing tag present on Windows. diff --git a/packages/ckeditor5-markdown-gfm/tests/pastefrommarkdown.js b/packages/ckeditor5-markdown-gfm/tests/pastefrommarkdownexperimental.js similarity index 97% rename from packages/ckeditor5-markdown-gfm/tests/pastefrommarkdown.js rename to packages/ckeditor5-markdown-gfm/tests/pastefrommarkdownexperimental.js index 4649467ff80..9cd9cee8c9b 100644 --- a/packages/ckeditor5-markdown-gfm/tests/pastefrommarkdown.js +++ b/packages/ckeditor5-markdown-gfm/tests/pastefrommarkdownexperimental.js @@ -15,7 +15,7 @@ import Undo from '@ckeditor/ckeditor5-undo/src/undo'; import DocumentFragment from '@ckeditor/ckeditor5-engine/src/view/documentfragment'; import PasteFromMarkdownExperimental from '../src/pastefrommarkdownexperimental'; -describe( 'PasteFromMarkdown', () => { +describe( 'PasteFromMarkdownExperimental', () => { let editorElement, editor; beforeEach( () => { @@ -76,7 +76,7 @@ describe( 'PasteFromMarkdown', () => { expect( toViewStub.callCount ).to.equal( 1 ); } ); - // TODO add Chrome, Firefox, Safari, Edge clipboard examples + // TODO add Chrome, Firefox, Safari, Edge clipboard examples. it( 'should parse correctly Mac type clipboard', () => { setData( editor.model, '[]' ); pasteHtml( editor, 'foo **bar** [baz](https://ckeditor.com).' ); From 27312f023f8ccb7d2aee5224d7590da38b15c155 Mon Sep 17 00:00:00 2001 From: Marcin Panek Date: Fri, 10 Nov 2023 13:42:14 +0100 Subject: [PATCH 11/33] Updated paste from markdown component to handle lists copied as `text/html`. --- .../src/pastefrommarkdownexperimental.ts | 92 ++++++++++++++++--- 1 file changed, 81 insertions(+), 11 deletions(-) diff --git a/packages/ckeditor5-markdown-gfm/src/pastefrommarkdownexperimental.ts b/packages/ckeditor5-markdown-gfm/src/pastefrommarkdownexperimental.ts index 934d138f1e6..b6b4ee80670 100644 --- a/packages/ckeditor5-markdown-gfm/src/pastefrommarkdownexperimental.ts +++ b/packages/ckeditor5-markdown-gfm/src/pastefrommarkdownexperimental.ts @@ -7,8 +7,8 @@ * @module markdown-gfm/pastefrommarkdownexperimental */ -import { Plugin, type Editor } from 'ckeditor5/src/core'; -import { Clipboard, type ClipboardPipeline, type ClipboardInputTransformationEvent } from 'ckeditor5/src/clipboard'; +import { type Editor, Plugin } from 'ckeditor5/src/core'; +import { Clipboard, type ClipboardInputTransformationEvent, type ClipboardPipeline } from 'ckeditor5/src/clipboard'; import GFMDataProcessor from './gfmdataprocessor'; import type { ViewDocumentKeyDownEvent } from 'ckeditor5/src/engine'; @@ -87,29 +87,99 @@ export default class PasteFromMarkdownExperimental extends Plugin { /** * Determines if code copied from a website in `text/html` type can be parsed as markdown. * It removes any OS specific HTML tags e.g. on Mac OS and on Windows. - * Then removes a single wrapper HTML tag, and if there are no more tags left, returns the remaining text. - * Returns null, if there are any remaining HTML tags detected. + * Then removes a single wrapper HTML tag or wrappers for sibling tags, and if there are no more tags left, + * returns the remaining text. Returns null, if there are any remaining HTML tags detected. * * @private * @param htmlString Clipboard content in `text/html` type format. */ private parseMarkdownFromHtml( htmlString: string ): string | null { + const withoutOsSpecificTags = this.removeOsSpecificTags( htmlString ); + + const withoutWrapperTag = this.isHtmlList( withoutOsSpecificTags ) ? + this.removeListWrapperTagsAndBrs( withoutOsSpecificTags ) : + this.removeWrapperTag( withoutOsSpecificTags ); + + if ( this.containsAnyRemainingHtmlTags( withoutWrapperTag ) ) { + return null; + } + + return withoutWrapperTag; + } + + /** + * Removes OS specific tags. + * + * @private + * @param htmlString Clipboard content in `text/html` type format. + */ + private removeOsSpecificTags( htmlString: string ): string { // Removing tag present on Mac. const withoutMetaTag = htmlString.replace( /^]*>/, '' ).trim(); // Removing tag present on Windows. const withoutHtmlTag = withoutMetaTag.replace( /^/, '' ).replace( /<\/html>$/, '' ).trim(); // Removing tag present on Windows. const withoutBodyTag = withoutHtmlTag.replace( /^/, '' ).replace( /<\/body>$/, '' ).trim(); + // Removing tag present on Windows. - const withoutFragmentTag = withoutBodyTag.replace( /^/, '' ).replace( /$/, '' ).trim(); - // Removing a wrapper HTML tag if exists. - const withoutWrapperTag = withoutFragmentTag.replace( /^<(\w+)\b[^>]*>\s*([\s\S]*?)\s*<\/\1>/, '$2' ).trim(); - const containsAnyRemainingHtmlTags = /<[^>]+>[\s\S]*<[^>]+>/.test( withoutWrapperTag ); + return withoutBodyTag.replace( /^/, '' ).replace( /$/, '' ).trim(); + } - if ( containsAnyRemainingHtmlTags ) { - return null; + /** + * Removes a single HTML wrapper tag from string. + * + * @private + * @param htmlString Clipboard content without any OS specific tags. + */ + private removeWrapperTag( htmlString: string ): string { + return htmlString.replace( /^<(\w+)\b[^>]*>\s*([\s\S]*?)\s*<\/\1>/, '$2' ).trim(); + } + + /** + * Removes multiple HTML wrapper tags from a list of sibling HTML tags. + * + * @private + * @param htmlString Clipboard content without any OS specific tags. + */ + private removeListWrapperTagsAndBrs( htmlString: string ): string { + const tempDiv = document.createElement( 'div' ); + tempDiv.innerHTML = htmlString; + + const outerElements = tempDiv.querySelectorAll( ':not(:empty)' ); + const brElements = tempDiv.querySelectorAll( 'br' ); + + for ( const element of outerElements ) { + const elementClone = element.cloneNode( true ); + element.replaceWith( ...elementClone.childNodes ); } - return withoutWrapperTag; + for ( const br of brElements ) { + br.replaceWith( '\n' ); + } + + return tempDiv.innerHTML; + } + + /** + * Determines if sting contains sibling HTML tags at root level. + * + * @private + * @param htmlString Clipboard content without any OS specific tags. + */ + private isHtmlList( htmlString: string ): boolean { + const tempDiv = document.createElement( 'div' ); + tempDiv.innerHTML = htmlString; + + return tempDiv.children.length > 1; + } + + /** + * Determines if string contains any HTML tags. + * + * @private + * @param str + */ + private containsAnyRemainingHtmlTags( str: string ): boolean { + return /<[^>]+>[\s\S]*<[^>]+>/.test( str ); } } From e588a2a6c0c76f74e07c4d489799fa6854e2b813 Mon Sep 17 00:00:00 2001 From: godai78 Date: Fri, 10 Nov 2023 14:42:37 +0100 Subject: [PATCH 12/33] Docs: guide placeholder. [short flow] --- .../docs/features/markdown.md | 12 +-- .../docs/features/paste-from-markdown.md | 89 +++++++++++++++++++ 2 files changed, 90 insertions(+), 11 deletions(-) create mode 100644 packages/ckeditor5-markdown-gfm/docs/features/paste-from-markdown.md diff --git a/packages/ckeditor5-markdown-gfm/docs/features/markdown.md b/packages/ckeditor5-markdown-gfm/docs/features/markdown.md index 64c035147da..7380577a8c3 100644 --- a/packages/ckeditor5-markdown-gfm/docs/features/markdown.md +++ b/packages/ckeditor5-markdown-gfm/docs/features/markdown.md @@ -110,17 +110,6 @@ While the Markdown plugin is stable and ready to use, some issues are still bein * Pasting Markdown-formatted content does not automatically convert the pasted syntax markers into properly formatted content. GitHub issues: [#2321](https://github.com/ckeditor/ckeditor5/issues/2321), [#2322](https://github.com/ckeditor/ckeditor5/issues/2322). * The Markdown code generated with the Markdown output feature will not properly render {@link features/tables#nesting-tables nested tables}. GitHub issue: [#9475](https://github.com/ckeditor/ckeditor5/issues/9475). -### Paste from markdown - -TODOs: - -* Description what the PasteFromMarkdownExperimental is. What does it do, and why is it marked as experimental? -* How to use it? -* Demo (see below). It would be good to have some predefined content: “Copy and paste to see results.”. -* Please, share your feedback that may impact the final shape of the plugin. - -{@snippet features/paste-from-markdown} - ## Related features Some other ways to output the edited content include: @@ -129,6 +118,7 @@ Some other ways to output the edited content include: * {@link features/export-word Export to Word} – Generate editable `.docx` files out of your editor-created content. * {@link features/export-pdf Export to PDF} – Generate portable PDF files out of your editor-created content. * {@link features/autoformat Autoformatting} – Use Markdown syntax shortcodes to automatically format your content as you type! +* {@link features/paste-from-markdown Paste from Markdown} – Paste Markdown formatted content straight into the editor! ## Contribute diff --git a/packages/ckeditor5-markdown-gfm/docs/features/paste-from-markdown.md b/packages/ckeditor5-markdown-gfm/docs/features/paste-from-markdown.md new file mode 100644 index 00000000000..69134485880 --- /dev/null +++ b/packages/ckeditor5-markdown-gfm/docs/features/paste-from-markdown.md @@ -0,0 +1,89 @@ +--- +menu-title: Paste from Markdown +meta-title: Paste from Markdown | CKEditor 5 Documentation +category: features-pasting +order: 40 +--- + +{@snippet features/build-markdown-source} + +# Paste from Markdown + +xxxTODOs: + +* Description what the PasteFromMarkdownExperimental is. What does it do, and why is it marked as experimental? +* How to use it? +* Demo (see below). It would be good to have some predefined content: “Copy and paste to see results.” +* Please, share your feedback that may impact the final shape of the plugin. + +## Demo + +xxx + +{@snippet features/paste-from-markdown} + + + This demo only presents a limited set of features. Visit the {@link examples/builds/full-featured-editor feature-rich editor example} to see more in action. + + +## Installation + + + This feature is not available in any of the {@link installation/getting-started/predefined-builds predefined builds}. + + +To enable this data processor in your editor, install the [`@ckeditor/ckeditor5-markdown-gfm`](https://www.npmjs.com/package/@ckeditor/ckeditor5-markdown-gfm) package: + +``` +npm install --save @ckeditor/ckeditor5-markdown-gfm +``` + +Then add the {@link module:markdown-gfm/markdown~Markdown} plugin to the editor configuration, which will change the default {@link module:engine/dataprocessor/dataprocessor~DataProcessor data processor} to the {@link module:markdown-gfm/gfmdataprocessor~GFMDataProcessor}: + +```js +import { ClassicEditor } from '@ckeditor/ckeditor5-editor-classic'; + +import { Bold, Italic } from '@ckeditor/ckeditor5-basic-styles'; +import { Essentials } from '@ckeditor/ckeditor5-essentials'; +// More imports. +// ... + +import { Markdown } from '@ckeditor/ckeditor5-markdown-gfm'; + +ClassicEditor + .create( document.querySelector( '#snippet-markdown' ), { + plugins: [ + Markdown, + Essentials, + Bold, + Italic, + // More plugins. + // ... + ], + // More of editor's config. + // ... + } ) + .then( /* ... */ ) + .catch( /* ... */ ); + +``` + + + Read more about {@link installation/plugins/installing-plugins installing plugins}. + + +## Known issues + +* list issues + +## Related features + +CKEditor 5 supports a wider range of paste features, including: +* {@link features/paste-from-office Paste from Office} – Paste content from Microsoft Word and maintain the original structure and formatting. +* {@link features/paste-from-google-docs Paste from Google Docs} – Paste content from Google Docs, maintaining the original formatting and structure. +* {@link features/paste-plain-text Paste plain text} – Paste text without formatting that will inherit the style of the content it was pasted into. +* {@link features/import-word Import from Word} – Convert Word files directly into HTML content. + +## Contribute + +The source code of the feature is available on GitHub at [https://github.com/ckeditor/ckeditor5/tree/master/packages/ckeditor5-clipboard](https://github.com/ckeditor/ckeditor5/tree/master/packages/ckeditor5-clipboard). From e925a0d05447175fcf9ff13279aee50ce67d12a7 Mon Sep 17 00:00:00 2001 From: godai78 Date: Mon, 13 Nov 2023 07:10:35 +0100 Subject: [PATCH 13/33] Docs: link fix. [short flow] --- .../ckeditor5-markdown-gfm/docs/features/paste-from-markdown.md | 1 + .../ckeditor5-markdown-gfm/src/pastefrommarkdownexperimental.ts | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/ckeditor5-markdown-gfm/docs/features/paste-from-markdown.md b/packages/ckeditor5-markdown-gfm/docs/features/paste-from-markdown.md index 69134485880..1ff8e1ba153 100644 --- a/packages/ckeditor5-markdown-gfm/docs/features/paste-from-markdown.md +++ b/packages/ckeditor5-markdown-gfm/docs/features/paste-from-markdown.md @@ -3,6 +3,7 @@ menu-title: Paste from Markdown meta-title: Paste from Markdown | CKEditor 5 Documentation category: features-pasting order: 40 +modified_at: 2023-11-10 --- {@snippet features/build-markdown-source} diff --git a/packages/ckeditor5-markdown-gfm/src/pastefrommarkdownexperimental.ts b/packages/ckeditor5-markdown-gfm/src/pastefrommarkdownexperimental.ts index b6b4ee80670..a16d1d53548 100644 --- a/packages/ckeditor5-markdown-gfm/src/pastefrommarkdownexperimental.ts +++ b/packages/ckeditor5-markdown-gfm/src/pastefrommarkdownexperimental.ts @@ -15,7 +15,7 @@ import type { ViewDocumentKeyDownEvent } from 'ckeditor5/src/engine'; /** * The GitHub Flavored Markdown (GFM) paste plugin. * - * For a detailed overview, check the {@glink features/markdown#paste-from-markdown Markdown feature} guide. + * For a detailed overview, check the {@glink features/pasting/paste-from-markdown Markdown feature} guide. */ export default class PasteFromMarkdownExperimental extends Plugin { /** From 01ec01892b2096c65fccc4f5533dde4444033571 Mon Sep 17 00:00:00 2001 From: godai78 Date: Mon, 13 Nov 2023 07:50:31 +0100 Subject: [PATCH 14/33] Docs: adjustments. [short flow] --- .../docs/features/paste-from-markdown.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/ckeditor5-markdown-gfm/docs/features/paste-from-markdown.md b/packages/ckeditor5-markdown-gfm/docs/features/paste-from-markdown.md index 1ff8e1ba153..0d98930314b 100644 --- a/packages/ckeditor5-markdown-gfm/docs/features/paste-from-markdown.md +++ b/packages/ckeditor5-markdown-gfm/docs/features/paste-from-markdown.md @@ -10,16 +10,15 @@ modified_at: 2023-11-10 # Paste from Markdown -xxxTODOs: +The paste from Markdown feature lets users paste Markdown-formatted content straight into the editor. It will be then converted into rich text on the fly. -* Description what the PasteFromMarkdownExperimental is. What does it do, and why is it marked as experimental? -* How to use it? -* Demo (see below). It would be good to have some predefined content: “Copy and paste to see results.” -* Please, share your feedback that may impact the final shape of the plugin. + + This feature is still experimental. See the [known issue](#known-issues) section to learn more. + ## Demo -xxx +Simply paste some Markdown-formatted content into the demo editor below and see it turn into rich text on the fly. You can copy [this document](#) for convenience. {@snippet features/paste-from-markdown} @@ -80,6 +79,7 @@ ClassicEditor ## Related features CKEditor 5 supports a wider range of paste features, including: +* {@link features/autoformat Autoformatting} – Format your content on the go with Markdown-like shortcodes * {@link features/paste-from-office Paste from Office} – Paste content from Microsoft Word and maintain the original structure and formatting. * {@link features/paste-from-google-docs Paste from Google Docs} – Paste content from Google Docs, maintaining the original formatting and structure. * {@link features/paste-plain-text Paste plain text} – Paste text without formatting that will inherit the style of the content it was pasted into. From e819d1c14df3f79b3fef2f422c08ba83bbc66fdc Mon Sep 17 00:00:00 2001 From: Kamil Piechaczek Date: Mon, 13 Nov 2023 14:10:32 +0100 Subject: [PATCH 15/33] Clean up. --- .../features/build-markdown-source.html | 7 -- .../features/build-markdown-source.js | 71 ------------------- .../docs/_snippets/features/markdown.html | 1 + .../docs/_snippets/features/markdown.js | 51 +++++++++++-- .../features/paste-from-markdown.html | 1 + .../_snippets/features/paste-from-markdown.js | 68 ++++++++++++++++-- .../docs/features/markdown.md | 2 - .../docs/features/paste-from-markdown.md | 6 +- 8 files changed, 111 insertions(+), 96 deletions(-) delete mode 100644 packages/ckeditor5-markdown-gfm/docs/_snippets/features/build-markdown-source.html delete mode 100644 packages/ckeditor5-markdown-gfm/docs/_snippets/features/build-markdown-source.js diff --git a/packages/ckeditor5-markdown-gfm/docs/_snippets/features/build-markdown-source.html b/packages/ckeditor5-markdown-gfm/docs/_snippets/features/build-markdown-source.html deleted file mode 100644 index 951498030b7..00000000000 --- a/packages/ckeditor5-markdown-gfm/docs/_snippets/features/build-markdown-source.html +++ /dev/null @@ -1,7 +0,0 @@ - - - diff --git a/packages/ckeditor5-markdown-gfm/docs/_snippets/features/build-markdown-source.js b/packages/ckeditor5-markdown-gfm/docs/_snippets/features/build-markdown-source.js deleted file mode 100644 index 5edafca3be1..00000000000 --- a/packages/ckeditor5-markdown-gfm/docs/_snippets/features/build-markdown-source.js +++ /dev/null @@ -1,71 +0,0 @@ -/** - * @license Copyright (c) 2003-2023, CKSource Holding sp. z o.o. All rights reserved. - * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license - */ - -/* globals window */ - -import { Code, Strikethrough } from '@ckeditor/ckeditor5-basic-styles'; -import { CS_CONFIG } from '@ckeditor/ckeditor5-cloud-services/tests/_utils/cloud-services-config'; -import { CodeBlock } from '@ckeditor/ckeditor5-code-block'; -import { HorizontalLine } from '@ckeditor/ckeditor5-horizontal-line'; -import { SourceEditing } from '@ckeditor/ckeditor5-source-editing'; -import { List, TodoList, DocumentList, TodoDocumentList, AdjacentListsSupport } from '@ckeditor/ckeditor5-list'; -import { Markdown, PasteFromMarkdownExperimental } from '@ckeditor/ckeditor5-markdown-gfm'; - -// Umberto combines all `packages/*/docs` into the `docs/` directory. The import path must be valid after merging all directories. -import ClassicEditor from '../build-classic'; - -ClassicEditor.builtinPlugins = ClassicEditor.builtinPlugins - // Remove the `List` plugin as in a single demo we want to use the Document list feature. - .filter( pluginConstructor => { - if ( pluginConstructor.pluginName === 'List' ) { - return false; - } - - return true; - } ) - // Then, add Markdown-specific features. - .concat( [ - SourceEditing, - Code, Strikethrough, - Markdown, - CodeBlock, - HorizontalLine - ] ); - -ClassicEditor.defaultConfig = { - toolbar: { - items: [ - 'undo', 'redo', '|', 'sourceEditing', '|', 'heading', - '|', 'bold', 'italic', 'strikethrough', 'code', - '-', 'link', 'uploadImage', 'insertTable', 'mediaEmbed', 'blockQuote', 'codeBlock', 'horizontalLine', - '|', 'bulletedList', 'numberedList', 'todoList', 'outdent', 'indent' - ], - shouldNotGroupWhenFull: true - }, - cloudServices: CS_CONFIG, - image: { - toolbar: [ 'imageStyle:inline', 'imageStyle:block', 'imageStyle:side', '|', 'toggleImageCaption', 'imageTextAlternative' ] - }, - codeBlock: { - languages: [ - { language: 'css', label: 'CSS' }, - { language: 'html', label: 'HTML' }, - { language: 'javascript', label: 'JavaScript' }, - { language: 'php', label: 'PHP' } - ] - }, - ui: { - viewportOffset: { - top: window.getViewportTopOffsetConfig() - } - } -}; - -window.ClassicEditor = ClassicEditor; -window.CKEditorPlugins = { - List, TodoList, - DocumentList, TodoDocumentList, AdjacentListsSupport, - PasteFromMarkdownExperimental -}; diff --git a/packages/ckeditor5-markdown-gfm/docs/_snippets/features/markdown.html b/packages/ckeditor5-markdown-gfm/docs/_snippets/features/markdown.html index f2bd81886d1..a9aa8fef179 100644 --- a/packages/ckeditor5-markdown-gfm/docs/_snippets/features/markdown.html +++ b/packages/ckeditor5-markdown-gfm/docs/_snippets/features/markdown.html @@ -1,3 +1,4 @@ + diff --git a/packages/ckeditor5-markdown-gfm/tests/manual/pastefrommarkdown/pastefrommarkdown.html b/packages/ckeditor5-markdown-gfm/tests/manual/pastefrommarkdown/pastefrommarkdown.html index 1b6536e659c..1bb87c650df 100644 --- a/packages/ckeditor5-markdown-gfm/tests/manual/pastefrommarkdown/pastefrommarkdown.html +++ b/packages/ckeditor5-markdown-gfm/tests/manual/pastefrommarkdown/pastefrommarkdown.html @@ -9,9 +9,7 @@ } - -
Paste markdown code here -
+

Markdown output 🛫

Output:

From 2890cba006edba2a91dcf96b7d2bf9b6aeff75f2 Mon Sep 17 00:00:00 2001 From: Kamil Piechaczek Date: Thu, 23 Nov 2023 09:59:12 +0100 Subject: [PATCH 22/33] Review fixes. --- .../ckeditor5-markdown-gfm/tests/markdown.js | 3 +- .../tests/pastefrommarkdownexperimental.js | 127 +++++++++--------- 2 files changed, 65 insertions(+), 65 deletions(-) diff --git a/packages/ckeditor5-markdown-gfm/tests/markdown.js b/packages/ckeditor5-markdown-gfm/tests/markdown.js index a50845e8cb2..6e7867e20f8 100644 --- a/packages/ckeditor5-markdown-gfm/tests/markdown.js +++ b/packages/ckeditor5-markdown-gfm/tests/markdown.js @@ -6,7 +6,6 @@ import Markdown from '../src/markdown'; import GFMDataProcessor from '../src/gfmdataprocessor'; import ClassicTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/classictesteditor'; -import { ClipboardPipeline } from '@ckeditor/ckeditor5-clipboard'; describe( 'Markdown', () => { it( 'has proper name', () => { @@ -16,7 +15,7 @@ describe( 'Markdown', () => { it( 'should set editor.data.processor', () => { return ClassicTestEditor .create( '', { - plugins: [ Markdown, ClipboardPipeline ] + plugins: [ Markdown ] } ) .then( editor => { expect( editor.data.processor ).to.be.an.instanceof( GFMDataProcessor ); diff --git a/packages/ckeditor5-markdown-gfm/tests/pastefrommarkdownexperimental.js b/packages/ckeditor5-markdown-gfm/tests/pastefrommarkdownexperimental.js index 9e349d0d36c..a736a316bc8 100644 --- a/packages/ckeditor5-markdown-gfm/tests/pastefrommarkdownexperimental.js +++ b/packages/ckeditor5-markdown-gfm/tests/pastefrommarkdownexperimental.js @@ -12,7 +12,6 @@ import { keyCodes } from '@ckeditor/ckeditor5-utils'; import Bold from '@ckeditor/ckeditor5-basic-styles/src/bold'; import Italic from '@ckeditor/ckeditor5-basic-styles/src/italic'; import Undo from '@ckeditor/ckeditor5-undo/src/undo'; -import DocumentFragment from '@ckeditor/ckeditor5-engine/src/view/documentfragment'; import PasteFromMarkdownExperimental from '../src/pastefrommarkdownexperimental'; describe( 'PasteFromMarkdownExperimental', () => { @@ -37,75 +36,73 @@ describe( 'PasteFromMarkdownExperimental', () => { return editor.destroy(); } ); - it( 'should paste as markdown when type is text/plain', () => { - setData( editor.model, '[]' ); - pasteText( editor, 'foo **bar** [baz](https://ckeditor.com).' ); + describe( 'text/plain', () => { + it( 'should convert to HTML the pasted markdown content', () => { + setData( editor.model, '[]' ); + pasteText( editor, 'foo **bar** [baz](https://ckeditor.com).' ); - expect( getData( editor.model ) ).to.equal( 'foo <$text bold="true">bar baz.[]' ); - } ); - - it( 'should paste one level nested HTML as markdown if type is text/html', () => { - setData( editor.model, '[]' ); - pasteHtml( editor, 'foo **bar** [baz](https://ckeditor.com).' ); - - expect( getData( editor.model ) ).to.equal( 'foo <$text bold="true">bar baz.[]' ); - } ); - - it( 'should not paste two level nested HTML as markdown if type is text/html', () => { - setData( editor.model, '[]' ); - pasteHtml( editor, 'foo **bar** [baz](https://ckeditor.com).' ); - - expect( getData( editor.model ) ).to.equal( 'foo **bar** [baz](https://ckeditor.com).[]' ); - } ); + expect( getData( editor.model ) ).to.equal( 'foo <$text bold="true">bar baz.[]' ); + } ); - it( 'should paste single level HTML list as markdown if type is text/html', () => { - setData( editor.model, '[]' ); - pasteHtml( editor, 'foo **bar** [baz](https://ckeditor.com).foo **bar** [baz](https://ckeditor.com).' ); + it( 'should paste as plain text when pasting with the Shift key pressed', () => { + setData( editor.model, '[]' ); + pressShiftKey( editor ); + pasteText( editor, 'foo **bar** [baz](https://ckeditor.com).' ); - expect( getData( editor.model ) ).to.equal( - 'foo <$text bold="true">bar baz.foo <$text bold="true">bar baz.[]' - ); + expect( getData( editor.model ) ).to.equal( 'foo **bar** [baz](https://ckeditor.com).[]' ); + } ); } ); + describe( 'text/html', () => { + it( 'should paste one level nested HTML as markdown if type is text/html', () => { + setData( editor.model, '[]' ); + pasteHtml( editor, 'foo **bar** [baz](https://ckeditor.com).' ); - it( 'should remove "br" tags in a HTML list', () => { - setData( editor.model, '[]' ); - pasteHtml( editor, 'foo **bar**
foo **bar**' ); + expect( getData( editor.model ) ).to.equal( 'foo <$text bold="true">bar baz.[]' ); + } ); - expect( getData( editor.model ) ).to.equal( - 'foo <$text bold="true">barfoo <$text bold="true">bar[]' - ); - } ); + it( 'should not paste two level nested HTML as markdown if type is text/html', () => { + setData( editor.model, '[]' ); + pasteHtml( editor, 'foo **bar** [baz](https://ckeditor.com).' ); - it( 'should not paste as markdown if shift key pressed', () => { - setData( editor.model, '[]' ); - pressShiftKey( editor ); - pasteText( editor, 'foo **bar** [baz](https://ckeditor.com).' ); + expect( getData( editor.model ) ).to.equal( 'foo **bar** [baz](https://ckeditor.com).[]' ); + } ); - expect( getData( editor.model ) ).to.equal( 'foo **bar** [baz](https://ckeditor.com).[]' ); - } ); + it( 'should paste single level HTML list as markdown if type is text/html', () => { + setData( editor.model, '[]' ); + pasteHtml( + editor, + 'foo **bar** [baz](https://ckeditor.com).foo **bar** [baz](https://ckeditor.com).' + ); - it( 'should call toView function once on markdown paste', () => { - const toViewStub = sinon.stub().returns( new DocumentFragment() ); + expect( getData( editor.model ) ).to.equal( + 'foo <$text bold="true">bar baz.foo <$text bold="true">bar baz.[]' + ); + } ); - editor.data.processor.toView = toViewStub; - setData( editor.model, '[]' ); - pasteText( editor, 'foo **bar** [baz](https://ckeditor.com).' ); + it( 'should remove "br" tags in a HTML list', () => { + setData( editor.model, '[]' ); + pasteHtml( editor, 'foo **bar**
foo **bar**' ); - expect( toViewStub.callCount ).to.equal( 1 ); - } ); + expect( getData( editor.model ) ).to.equal( + 'foo <$text bold="true">barfoo <$text bold="true">bar[]' + ); + } ); - // TODO add Chrome, Firefox, Safari, Edge clipboard examples. - it( 'should parse correctly Mac type clipboard', () => { - setData( editor.model, '[]' ); - pasteHtml( editor, 'foo **bar** [baz](https://ckeditor.com).' ); + // TODO add Chrome, Firefox, Safari, Edge clipboard examples. + describe( 'Mac', () => { + it( 'should parse correctly Mac type clipboard', () => { + setData( editor.model, '[]' ); + pasteHtml( editor, 'foo **bar** [baz](https://ckeditor.com).' ); - expect( getData( editor.model ) ).to.equal( 'foo <$text bold="true">bar baz.[]' ); - } ); + expect( getData( editor.model ) ).to.equal( 'foo <$text bold="true">bar baz.[]' ); + } ); + } ); - it( 'should parse correctly Windows type clipboard', () => { - setData( editor.model, '[]' ); - pasteHtml( editor, - ` + describe( 'Linux', () => { + it( 'should parse correctly Windows type clipboard', () => { + setData( editor.model, '[]' ); + pasteHtml( editor, + ` @@ -114,16 +111,20 @@ describe( 'PasteFromMarkdownExperimental', () => { ` - ); + ); - expect( getData( editor.model ) ).to.equal( 'foo <$text bold="true">bar baz.[]' ); - } ); + expect( getData( editor.model ) ).to.equal( 'foo <$text bold="true">bar baz.[]' ); + } ); + } ); - it( 'should parse correctly Linux type clipboard', () => { - setData( editor.model, '[]' ); - pasteHtml( editor, 'foo **bar** [baz](https://ckeditor.com).' ); + describe( 'Windows', () => { + it( 'should parse correctly Linux type clipboard', () => { + setData( editor.model, '[]' ); + pasteHtml( editor, 'foo **bar** [baz](https://ckeditor.com).' ); - expect( getData( editor.model ) ).to.equal( 'foo <$text bold="true">bar baz.[]' ); + expect( getData( editor.model ) ).to.equal( 'foo <$text bold="true">bar baz.[]' ); + } ); + } ); } ); function pressShiftKey( editor ) { From 393ed71a93622bfd8677b0df132848061dff8c2c Mon Sep 17 00:00:00 2001 From: Marcin Panek Date: Thu, 23 Nov 2023 12:37:57 +0100 Subject: [PATCH 23/33] Removed not needed `@private` tags in JSDoc. --- .../src/pastefrommarkdownexperimental.ts | 9 --------- 1 file changed, 9 deletions(-) diff --git a/packages/ckeditor5-markdown-gfm/src/pastefrommarkdownexperimental.ts b/packages/ckeditor5-markdown-gfm/src/pastefrommarkdownexperimental.ts index 53802b85bc6..6474a8cc995 100644 --- a/packages/ckeditor5-markdown-gfm/src/pastefrommarkdownexperimental.ts +++ b/packages/ckeditor5-markdown-gfm/src/pastefrommarkdownexperimental.ts @@ -90,7 +90,6 @@ export default class PasteFromMarkdownExperimental extends Plugin { * Then removes a single wrapper HTML tag or wrappers for sibling tags, and if there are no more tags left, * returns the remaining text. Returns null, if there are any remaining HTML tags detected. * - * @private * @param htmlString Clipboard content in `text/html` type format. */ private _parseMarkdownFromHtml( htmlString: string ): string | null { @@ -110,7 +109,6 @@ export default class PasteFromMarkdownExperimental extends Plugin { /** * Removes OS specific tags. * - * @private * @param htmlString Clipboard content in `text/html` type format. */ private _removeOsSpecificTags( htmlString: string ): string { @@ -128,7 +126,6 @@ export default class PasteFromMarkdownExperimental extends Plugin { /** * Removes a single HTML wrapper tag from string. * - * @private * @param htmlString Clipboard content without any OS specific tags. */ private _removeWrapperTag( htmlString: string ): string { @@ -138,7 +135,6 @@ export default class PasteFromMarkdownExperimental extends Plugin { /** * Removes multiple HTML wrapper tags from a list of sibling HTML tags. * - * @private * @param htmlString Clipboard content without any OS specific tags. */ private _removeListWrapperTagsAndBrs( htmlString: string ): string { @@ -163,7 +159,6 @@ export default class PasteFromMarkdownExperimental extends Plugin { /** * Determines if sting contains sibling HTML tags at root level. * - * @private * @param htmlString Clipboard content without any OS specific tags. */ private _isHtmlList( htmlString: string ): boolean { @@ -175,9 +170,6 @@ export default class PasteFromMarkdownExperimental extends Plugin { /** * Determines if string contains any HTML tags. - * - * @private - * @param str */ private _containsAnyRemainingHtmlTags( str: string ): boolean { return /<[^>]+>[\s\S]*<[^>]+>/.test( str ); @@ -186,7 +178,6 @@ export default class PasteFromMarkdownExperimental extends Plugin { /** * Replaces the reserved HTML entities with the actual characters. * - * @private * @param htmlString Clipboard content without any tags. */ private _replaceHtmlReservedEntitiesWithCharacters( htmlString: string ) { From 82770e9ac88e7db4378606dec2dc492e1d2fa313 Mon Sep 17 00:00:00 2001 From: Kamil Piechaczek Date: Fri, 24 Nov 2023 09:27:55 +0100 Subject: [PATCH 24/33] Addressed docs issues. --- .../docs/_snippets/features/paste-from-markdown.html | 4 ++-- packages/ckeditor5-markdown-gfm/docs/assets/markdown.txt | 2 +- .../ckeditor5-markdown-gfm/docs/features/paste-markdown.md | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/ckeditor5-markdown-gfm/docs/_snippets/features/paste-from-markdown.html b/packages/ckeditor5-markdown-gfm/docs/_snippets/features/paste-from-markdown.html index 525502ac034..9cd2585d71f 100644 --- a/packages/ckeditor5-markdown-gfm/docs/_snippets/features/paste-from-markdown.html +++ b/packages/ckeditor5-markdown-gfm/docs/_snippets/features/paste-from-markdown.html @@ -1,7 +1,7 @@ - diff --git a/packages/ckeditor5-markdown-gfm/docs/assets/markdown.txt b/packages/ckeditor5-markdown-gfm/docs/assets/markdown.txt index 8a1e874fe57..a1056590b32 100644 --- a/packages/ckeditor5-markdown-gfm/docs/assets/markdown.txt +++ b/packages/ckeditor5-markdown-gfm/docs/assets/markdown.txt @@ -69,4 +69,4 @@ Or a code block. ## Images -![CKEditor 5](https://ckeditor.com/assets/images/webp/illustration/home-banner-d49ece364f.webp) \ No newline at end of file +![CKEditor 5](https://user-images.githubusercontent.com/1099479/179190754-f4aaf2b3-21cc-49c4-a454-8de4a00cc70e.jpg) diff --git a/packages/ckeditor5-markdown-gfm/docs/features/paste-markdown.md b/packages/ckeditor5-markdown-gfm/docs/features/paste-markdown.md index e61d011582d..ad35c4144c7 100644 --- a/packages/ckeditor5-markdown-gfm/docs/features/paste-markdown.md +++ b/packages/ckeditor5-markdown-gfm/docs/features/paste-markdown.md @@ -36,7 +36,7 @@ To enable this data processor in your editor, install the [`@ckeditor/ckeditor5- npm install --save @ckeditor/ckeditor5-markdown-gfm ``` -Then add the {@link module:markdown-gfm/markdown~Markdown} plugin to the editor configuration, which will change the default {@link module:engine/dataprocessor/dataprocessor~DataProcessor data processor} to the {@link module:markdown-gfm/gfmdataprocessor~GFMDataProcessor}: +Then add the {@link module:markdown-gfm/pastefrommarkdownexperimental~PasteFromMarkdownExperimental} plugin to the editor configuration: ```js import { ClassicEditor } from '@ckeditor/ckeditor5-editor-classic'; From 0fb3cdb59643d93cd028c645fc9426b465ff6ecd Mon Sep 17 00:00:00 2001 From: godai78 Date: Fri, 24 Nov 2023 14:53:21 +0100 Subject: [PATCH 25/33] Docs: updating demos; meta data. [short flow] --- .../docs/_snippets/features/markdown.js | 6 ++--- .../_snippets/features/paste-from-markdown.js | 23 ++++++++++++++----- .../docs/features/markdown.md | 1 + .../docs/features/paste-markdown.md | 3 ++- 4 files changed, 23 insertions(+), 10 deletions(-) diff --git a/packages/ckeditor5-markdown-gfm/docs/_snippets/features/markdown.js b/packages/ckeditor5-markdown-gfm/docs/_snippets/features/markdown.js index 015e793edb7..ad2d80dd5eb 100644 --- a/packages/ckeditor5-markdown-gfm/docs/_snippets/features/markdown.js +++ b/packages/ckeditor5-markdown-gfm/docs/_snippets/features/markdown.js @@ -14,7 +14,7 @@ import { CodeBlock } from '@ckeditor/ckeditor5-code-block'; import ArticlePluginSet from '@ckeditor/ckeditor5-core/tests/_utils/articlepluginset'; import { CKBox, CKBoxImageEdit } from '@ckeditor/ckeditor5-ckbox'; import { HorizontalLine } from '@ckeditor/ckeditor5-horizontal-line'; -import { ImageUpload, ImageInsert, PictureEditing } from '@ckeditor/ckeditor5-image'; +import { ImageUpload, ImageInsert, PictureEditing, AutoImage } from '@ckeditor/ckeditor5-image'; import { TodoList } from '@ckeditor/ckeditor5-list'; import { SourceEditing } from '@ckeditor/ckeditor5-source-editing'; @@ -23,8 +23,8 @@ import { Markdown } from '@ckeditor/ckeditor5-markdown-gfm'; ClassicEditor .create( document.querySelector( '#snippet-markdown' ), { plugins: [ - ArticlePluginSet, SourceEditing, CKBox, CKBoxImageEdit, ImageInsert, ImageUpload, PictureEditing, CloudServices, Markdown, - Code, CodeBlock, TodoList, Strikethrough, HorizontalLine + ArticlePluginSet, SourceEditing, CKBox, CKBoxImageEdit, ImageInsert, ImageUpload, PictureEditing, AutoImage, + CloudServices, Markdown, Code, CodeBlock, TodoList, Strikethrough, HorizontalLine ], toolbar: { items: [ diff --git a/packages/ckeditor5-markdown-gfm/docs/_snippets/features/paste-from-markdown.js b/packages/ckeditor5-markdown-gfm/docs/_snippets/features/paste-from-markdown.js index 74aa76f55a2..6cff08356f4 100644 --- a/packages/ckeditor5-markdown-gfm/docs/_snippets/features/paste-from-markdown.js +++ b/packages/ckeditor5-markdown-gfm/docs/_snippets/features/paste-from-markdown.js @@ -12,8 +12,8 @@ import { HorizontalLine } from '@ckeditor/ckeditor5-horizontal-line'; import { SourceEditing } from '@ckeditor/ckeditor5-source-editing'; import { DocumentList, TodoDocumentList, AdjacentListsSupport } from '@ckeditor/ckeditor5-list'; import { Markdown, PasteFromMarkdownExperimental } from '@ckeditor/ckeditor5-markdown-gfm'; -import { CKBox } from '@ckeditor/ckeditor5-ckbox'; -import { PictureEditing, ImageResize, AutoImage } from '@ckeditor/ckeditor5-image'; +import { CKBox, CKBoxImageEdit } from '@ckeditor/ckeditor5-ckbox'; +import { PictureEditing, ImageInsert, ImageResize, AutoImage } from '@ckeditor/ckeditor5-image'; import { LinkImage } from '@ckeditor/ckeditor5-link'; import { Font } from '@ckeditor/ckeditor5-font'; @@ -32,7 +32,8 @@ const plugins = ClassicEditor.builtinPlugins // Then, add Markdown-specific features. .concat( [ SourceEditing, Code, Strikethrough, Underline, Markdown, CodeBlock, HorizontalLine, DocumentList, TodoDocumentList, - AdjacentListsSupport, PasteFromMarkdownExperimental, CKBox, PictureEditing, ImageResize, AutoImage, LinkImage, Font + AdjacentListsSupport, PasteFromMarkdownExperimental, CKBox, CKBoxImageEdit, + PictureEditing, ImageInsert, ImageResize, AutoImage, LinkImage, Font ] ); ClassicEditor @@ -42,13 +43,23 @@ ClassicEditor items: [ 'undo', 'redo', '|', 'sourceEditing', '|', 'heading', '|', 'bold', 'italic', 'underline', 'strikethrough', 'code', - '|', 'link', 'uploadImage', 'insertTable', 'mediaEmbed', 'blockQuote', 'codeBlock', 'horizontalLine', + '-', 'link', 'insertImage', 'insertTable', 'mediaEmbed', 'blockQuote', 'codeBlock', 'horizontalLine', '|', 'bulletedList', 'numberedList', 'todoList', 'outdent', 'indent' - ] + ], + shouldNotGroupWhenFull: true }, cloudServices: CS_CONFIG, image: { - toolbar: [ 'imageStyle:inline', 'imageStyle:block', 'imageStyle:side', '|', 'toggleImageCaption', 'imageTextAlternative' ] + toolbar: [ + 'imageStyle:inline', + 'imageStyle:block', + 'imageStyle:side', + '|', + 'toggleImageCaption', + 'imageTextAlternative', + '|', + 'ckboxImageEdit' + ] }, codeBlock: { languages: [ diff --git a/packages/ckeditor5-markdown-gfm/docs/features/markdown.md b/packages/ckeditor5-markdown-gfm/docs/features/markdown.md index 59074ed3024..b5cc064ac1e 100644 --- a/packages/ckeditor5-markdown-gfm/docs/features/markdown.md +++ b/packages/ckeditor5-markdown-gfm/docs/features/markdown.md @@ -1,6 +1,7 @@ --- title: Markdown output meta-title: Markdown output | CKEditor 5 Documentation +meta-description: The Markdown plugin lets you switch the default CKEditor 5 output from HTML to Markdown. category: features --- diff --git a/packages/ckeditor5-markdown-gfm/docs/features/paste-markdown.md b/packages/ckeditor5-markdown-gfm/docs/features/paste-markdown.md index ad35c4144c7..8352a2034f7 100644 --- a/packages/ckeditor5-markdown-gfm/docs/features/paste-markdown.md +++ b/packages/ckeditor5-markdown-gfm/docs/features/paste-markdown.md @@ -1,9 +1,10 @@ --- menu-title: Paste Markdown meta-title: Paste Markdown | CKEditor 5 Documentation +meta-description: The paste Markdown feature lets users paste Markdown-formatted content straight into CKEditor 5. category: features-pasting order: 40 -modified_at: 2023-11-10 +modified_at: 2023-11-24 --- # Paste Markdown From 003c5ee4def66ab173e50df4fd33df333b3a9f81 Mon Sep 17 00:00:00 2001 From: Marcin Panek Date: Fri, 24 Nov 2023 16:11:25 +0100 Subject: [PATCH 26/33] Paste markdown logic improvements after review. --- .../src/pastefrommarkdownexperimental.ts | 42 +++++-------------- 1 file changed, 11 insertions(+), 31 deletions(-) diff --git a/packages/ckeditor5-markdown-gfm/src/pastefrommarkdownexperimental.ts b/packages/ckeditor5-markdown-gfm/src/pastefrommarkdownexperimental.ts index 6474a8cc995..b95d7fd37ad 100644 --- a/packages/ckeditor5-markdown-gfm/src/pastefrommarkdownexperimental.ts +++ b/packages/ckeditor5-markdown-gfm/src/pastefrommarkdownexperimental.ts @@ -8,14 +8,14 @@ */ import { type Editor, Plugin } from 'ckeditor5/src/core'; -import { Clipboard, type ClipboardInputTransformationEvent, type ClipboardPipeline } from 'ckeditor5/src/clipboard'; +import { type ClipboardInputTransformationEvent, ClipboardPipeline } from 'ckeditor5/src/clipboard'; import GFMDataProcessor from './gfmdataprocessor'; import type { ViewDocumentKeyDownEvent } from 'ckeditor5/src/engine'; /** * The GitHub Flavored Markdown (GFM) paste plugin. * - * For a detailed overview, check the {@glink features/pasting/paste-markdown Markdown feature} guide. + * For a detailed overview, check the {@glink features/pasting/paste-markdown Paste Markdown feature} guide. */ export default class PasteFromMarkdownExperimental extends Plugin { /** @@ -43,7 +43,7 @@ export default class PasteFromMarkdownExperimental extends Plugin { * @inheritDoc */ public static get requires() { - return [ Clipboard ] as const; + return [ ClipboardPipeline ] as const; } /** @@ -71,6 +71,7 @@ export default class PasteFromMarkdownExperimental extends Plugin { if ( !dataAsTextHtml ) { const dataAsTextPlain = data.dataTransfer.getData( 'text/plain' ); + data.content = this._gfmDataProcessor.toView( dataAsTextPlain ); return; @@ -94,10 +95,7 @@ export default class PasteFromMarkdownExperimental extends Plugin { */ private _parseMarkdownFromHtml( htmlString: string ): string | null { const withoutOsSpecificTags = this._removeOsSpecificTags( htmlString ); - - const withoutWrapperTag = this._isHtmlList( withoutOsSpecificTags ) ? - this._removeListWrapperTagsAndBrs( withoutOsSpecificTags ) : - this._removeWrapperTag( withoutOsSpecificTags ); + const withoutWrapperTag = this._removeFirstLevelWrapperTagsAndBrs( withoutOsSpecificTags ); if ( this._containsAnyRemainingHtmlTags( withoutWrapperTag ) ) { return null; @@ -123,21 +121,12 @@ export default class PasteFromMarkdownExperimental extends Plugin { return withoutBodyTag.replace( /^/, '' ).replace( /$/, '' ).trim(); } - /** - * Removes a single HTML wrapper tag from string. - * - * @param htmlString Clipboard content without any OS specific tags. - */ - private _removeWrapperTag( htmlString: string ): string { - return htmlString.replace( /^<(\w+)\b[^>]*>\s*([\s\S]*?)\s*<\/\1>/, '$2' ).trim(); - } - /** * Removes multiple HTML wrapper tags from a list of sibling HTML tags. * * @param htmlString Clipboard content without any OS specific tags. */ - private _removeListWrapperTagsAndBrs( htmlString: string ): string { + private _removeFirstLevelWrapperTagsAndBrs( htmlString: string ): string { const tempDiv = document.createElement( 'div' ); tempDiv.innerHTML = htmlString; @@ -156,23 +145,11 @@ export default class PasteFromMarkdownExperimental extends Plugin { return tempDiv.innerHTML; } - /** - * Determines if sting contains sibling HTML tags at root level. - * - * @param htmlString Clipboard content without any OS specific tags. - */ - private _isHtmlList( htmlString: string ): boolean { - const tempDiv = document.createElement( 'div' ); - tempDiv.innerHTML = htmlString; - - return tempDiv.children.length > 1; - } - /** * Determines if string contains any HTML tags. */ private _containsAnyRemainingHtmlTags( str: string ): boolean { - return /<[^>]+>[\s\S]*<[^>]+>/.test( str ); + return str.includes( '<' ); } /** @@ -181,6 +158,9 @@ export default class PasteFromMarkdownExperimental extends Plugin { * @param htmlString Clipboard content without any tags. */ private _replaceHtmlReservedEntitiesWithCharacters( htmlString: string ) { - return htmlString.replace( '>', '>' ).replace( '<', '<' ); + return htmlString + .replace( new RegExp( '>', 'g' ), '>' ) + .replace( new RegExp( '<', 'g' ), '<' ) + .replace( new RegExp( ' ', 'g' ), ' ' ); } } From 288837c4bf843a03d7c546c546f07bd3a4cf90fa Mon Sep 17 00:00:00 2001 From: Marcin Panek Date: Fri, 24 Nov 2023 16:18:25 +0100 Subject: [PATCH 27/33] Fix test. --- .../tests/pastefrommarkdownexperimental.js | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/packages/ckeditor5-markdown-gfm/tests/pastefrommarkdownexperimental.js b/packages/ckeditor5-markdown-gfm/tests/pastefrommarkdownexperimental.js index a736a316bc8..33a942599dd 100644 --- a/packages/ckeditor5-markdown-gfm/tests/pastefrommarkdownexperimental.js +++ b/packages/ckeditor5-markdown-gfm/tests/pastefrommarkdownexperimental.js @@ -98,26 +98,26 @@ describe( 'PasteFromMarkdownExperimental', () => { } ); } ); - describe( 'Linux', () => { + describe( 'Windows', () => { it( 'should parse correctly Windows type clipboard', () => { setData( editor.model, '[]' ); pasteHtml( editor, - ` - - - - foo **bar** [baz](https://ckeditor.com). - - - - ` + '' + + '' + + '' + + '' + + 'foo **bar** [baz](https://ckeditor.com).' + + '' + + '' + + '' + + '' ); - expect( getData( editor.model ) ).to.equal( 'foo <$text bold="true">bar baz.[]' ); + expect( getData( editor.model ).trim() ).to.equal( 'foo <$text bold="true">bar baz.[]' ); } ); } ); - describe( 'Windows', () => { + describe( 'Linux', () => { it( 'should parse correctly Linux type clipboard', () => { setData( editor.model, '[]' ); pasteHtml( editor, 'foo **bar** [baz](https://ckeditor.com).' ); From fec4a16647462c2f6de6118d880a7ca11bf6857f Mon Sep 17 00:00:00 2001 From: Marcin Panek Date: Wed, 29 Nov 2023 13:30:43 +0100 Subject: [PATCH 28/33] Changed logic to not parse markdown for first level formatting tags. --- .../src/pastefrommarkdownexperimental.ts | 51 ++++++++++++++----- .../tests/pastefrommarkdownexperimental.js | 9 ++++ 2 files changed, 46 insertions(+), 14 deletions(-) diff --git a/packages/ckeditor5-markdown-gfm/src/pastefrommarkdownexperimental.ts b/packages/ckeditor5-markdown-gfm/src/pastefrommarkdownexperimental.ts index b95d7fd37ad..321da62a829 100644 --- a/packages/ckeditor5-markdown-gfm/src/pastefrommarkdownexperimental.ts +++ b/packages/ckeditor5-markdown-gfm/src/pastefrommarkdownexperimental.ts @@ -7,11 +7,13 @@ * @module markdown-gfm/pastefrommarkdownexperimental */ -import { type Editor, Plugin } from 'ckeditor5/src/core'; -import { type ClipboardInputTransformationEvent, ClipboardPipeline } from 'ckeditor5/src/clipboard'; +import { Plugin, type Editor } from 'ckeditor5/src/core'; +import { ClipboardPipeline, type ClipboardInputTransformationEvent } from 'ckeditor5/src/clipboard'; import GFMDataProcessor from './gfmdataprocessor'; import type { ViewDocumentKeyDownEvent } from 'ckeditor5/src/engine'; +const FORMATTING_TAGS = [ 'B', 'STRONG', 'I', 'EM', 'MARK', 'SMALL', 'DEL', 'INS', 'SUB', 'SUP' ]; + /** * The GitHub Flavored Markdown (GFM) paste plugin. * @@ -77,6 +79,10 @@ export default class PasteFromMarkdownExperimental extends Plugin { return; } + if ( this._isNotMarkdownBecauseContainsFormattingTags( dataAsTextHtml ) ) { + return; + } + const markdownFromHtml = this._parseMarkdownFromHtml( dataAsTextHtml ); if ( markdownFromHtml ) { @@ -95,6 +101,7 @@ export default class PasteFromMarkdownExperimental extends Plugin { */ private _parseMarkdownFromHtml( htmlString: string ): string | null { const withoutOsSpecificTags = this._removeOsSpecificTags( htmlString ); + const withoutWrapperTag = this._removeFirstLevelWrapperTagsAndBrs( withoutOsSpecificTags ); if ( this._containsAnyRemainingHtmlTags( withoutWrapperTag ) ) { @@ -121,28 +128,44 @@ export default class PasteFromMarkdownExperimental extends Plugin { return withoutBodyTag.replace( /^/, '' ).replace( /$/, '' ).trim(); } + /** + * If the input HTML string contains any first level formatting tags + * like , or , then we should not treat it as markdown. + * + * @param htmlString Clipboard content. + */ + private _isNotMarkdownBecauseContainsFormattingTags( htmlString: string ): boolean { + const parser = new DOMParser(); + const { body: tempElement } = parser.parseFromString( htmlString, 'text/html' ); + + const tagNames = Array.from( tempElement.children ).map( el => el.tagName ); + + return tagNames.some( el => FORMATTING_TAGS.includes( el ) ); + } + /** * Removes multiple HTML wrapper tags from a list of sibling HTML tags. * * @param htmlString Clipboard content without any OS specific tags. */ private _removeFirstLevelWrapperTagsAndBrs( htmlString: string ): string { - const tempDiv = document.createElement( 'div' ); - tempDiv.innerHTML = htmlString; + const parser = new DOMParser(); + const { body: tempElement } = parser.parseFromString( htmlString, 'text/html' ); + + const brElements = tempElement.querySelectorAll( 'br' ); + + for ( const br of brElements ) { + br.replaceWith( '\n' ); + } - const outerElements = tempDiv.querySelectorAll( ':not(:empty)' ); - const brElements = tempDiv.querySelectorAll( 'br' ); + const outerElements = tempElement.querySelectorAll( ':scope > *' ); for ( const element of outerElements ) { const elementClone = element.cloneNode( true ); element.replaceWith( ...elementClone.childNodes ); } - for ( const br of brElements ) { - br.replaceWith( '\n' ); - } - - return tempDiv.innerHTML; + return tempElement.innerHTML; } /** @@ -159,8 +182,8 @@ export default class PasteFromMarkdownExperimental extends Plugin { */ private _replaceHtmlReservedEntitiesWithCharacters( htmlString: string ) { return htmlString - .replace( new RegExp( '>', 'g' ), '>' ) - .replace( new RegExp( '<', 'g' ), '<' ) - .replace( new RegExp( ' ', 'g' ), ' ' ); + .replace( />/g, '>' ) + .replace( /</g, '<' ) + .replace( / /g, ' ' ); } } diff --git a/packages/ckeditor5-markdown-gfm/tests/pastefrommarkdownexperimental.js b/packages/ckeditor5-markdown-gfm/tests/pastefrommarkdownexperimental.js index 33a942599dd..c65de61905c 100644 --- a/packages/ckeditor5-markdown-gfm/tests/pastefrommarkdownexperimental.js +++ b/packages/ckeditor5-markdown-gfm/tests/pastefrommarkdownexperimental.js @@ -88,6 +88,15 @@ describe( 'PasteFromMarkdownExperimental', () => { ); } ); + it( 'should not parse as markdown if first level formatting tags detected', () => { + setData( editor.model, '[]' ); + pasteHtml( editor, 'foo **bar**
foo **bar**' ); + + expect( getData( editor.model ) ).to.equal( + '<$text bold="true">foo **bar**foo **bar**[]' + ); + } ); + // TODO add Chrome, Firefox, Safari, Edge clipboard examples. describe( 'Mac', () => { it( 'should parse correctly Mac type clipboard', () => { From f68a762d5ac31ab01ee867af4983b0c36e99dfe2 Mon Sep 17 00:00:00 2001 From: Marcin Panek Date: Fri, 1 Dec 2023 13:52:57 +0100 Subject: [PATCH 29/33] Changed list of exclusions to list of accepted first level markdown tag wrappers. --- .../src/pastefrommarkdownexperimental.ts | 8 ++++---- .../tests/pastefrommarkdownexperimental.js | 4 +--- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/packages/ckeditor5-markdown-gfm/src/pastefrommarkdownexperimental.ts b/packages/ckeditor5-markdown-gfm/src/pastefrommarkdownexperimental.ts index 321da62a829..2082d88bf1f 100644 --- a/packages/ckeditor5-markdown-gfm/src/pastefrommarkdownexperimental.ts +++ b/packages/ckeditor5-markdown-gfm/src/pastefrommarkdownexperimental.ts @@ -12,7 +12,7 @@ import { ClipboardPipeline, type ClipboardInputTransformationEvent } from 'ckedi import GFMDataProcessor from './gfmdataprocessor'; import type { ViewDocumentKeyDownEvent } from 'ckeditor5/src/engine'; -const FORMATTING_TAGS = [ 'B', 'STRONG', 'I', 'EM', 'MARK', 'SMALL', 'DEL', 'INS', 'SUB', 'SUP' ]; +const ALLOWED_MARKDOWN_FIRST_LEVEL_TAGS = [ 'SPAN', 'BR', 'PRE' ]; /** * The GitHub Flavored Markdown (GFM) paste plugin. @@ -79,7 +79,7 @@ export default class PasteFromMarkdownExperimental extends Plugin { return; } - if ( this._isNotMarkdownBecauseContainsFormattingTags( dataAsTextHtml ) ) { + if ( !this._containsOnlyAllowedFirstLevelTags( dataAsTextHtml ) ) { return; } @@ -134,13 +134,13 @@ export default class PasteFromMarkdownExperimental extends Plugin { * * @param htmlString Clipboard content. */ - private _isNotMarkdownBecauseContainsFormattingTags( htmlString: string ): boolean { + private _containsOnlyAllowedFirstLevelTags( htmlString: string ): boolean { const parser = new DOMParser(); const { body: tempElement } = parser.parseFromString( htmlString, 'text/html' ); const tagNames = Array.from( tempElement.children ).map( el => el.tagName ); - return tagNames.some( el => FORMATTING_TAGS.includes( el ) ); + return tagNames.every( el => ALLOWED_MARKDOWN_FIRST_LEVEL_TAGS.includes( el ) ); } /** diff --git a/packages/ckeditor5-markdown-gfm/tests/pastefrommarkdownexperimental.js b/packages/ckeditor5-markdown-gfm/tests/pastefrommarkdownexperimental.js index c65de61905c..b82933038fa 100644 --- a/packages/ckeditor5-markdown-gfm/tests/pastefrommarkdownexperimental.js +++ b/packages/ckeditor5-markdown-gfm/tests/pastefrommarkdownexperimental.js @@ -114,9 +114,7 @@ describe( 'PasteFromMarkdownExperimental', () => { '' + '' + '' + - '' + - 'foo **bar** [baz](https://ckeditor.com).' + - '' + + 'foo **bar** [baz](https://ckeditor.com).' + '' + '' + '' From 2f320d2934565f531db4110f658131f5b0c28358 Mon Sep 17 00:00:00 2001 From: Marcin Panek Date: Fri, 1 Dec 2023 14:17:28 +0100 Subject: [PATCH 30/33] Added `CODE` to `ALLOWED_MARKDOWN_FIRST_LEVEL_TAGS` for Windows compatibility. --- .../src/pastefrommarkdownexperimental.ts | 2 +- .../tests/pastefrommarkdownexperimental.js | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/ckeditor5-markdown-gfm/src/pastefrommarkdownexperimental.ts b/packages/ckeditor5-markdown-gfm/src/pastefrommarkdownexperimental.ts index 2082d88bf1f..e32948f2057 100644 --- a/packages/ckeditor5-markdown-gfm/src/pastefrommarkdownexperimental.ts +++ b/packages/ckeditor5-markdown-gfm/src/pastefrommarkdownexperimental.ts @@ -12,7 +12,7 @@ import { ClipboardPipeline, type ClipboardInputTransformationEvent } from 'ckedi import GFMDataProcessor from './gfmdataprocessor'; import type { ViewDocumentKeyDownEvent } from 'ckeditor5/src/engine'; -const ALLOWED_MARKDOWN_FIRST_LEVEL_TAGS = [ 'SPAN', 'BR', 'PRE' ]; +const ALLOWED_MARKDOWN_FIRST_LEVEL_TAGS = [ 'SPAN', 'BR', 'PRE', 'CODE' ]; /** * The GitHub Flavored Markdown (GFM) paste plugin. diff --git a/packages/ckeditor5-markdown-gfm/tests/pastefrommarkdownexperimental.js b/packages/ckeditor5-markdown-gfm/tests/pastefrommarkdownexperimental.js index b82933038fa..c65de61905c 100644 --- a/packages/ckeditor5-markdown-gfm/tests/pastefrommarkdownexperimental.js +++ b/packages/ckeditor5-markdown-gfm/tests/pastefrommarkdownexperimental.js @@ -114,7 +114,9 @@ describe( 'PasteFromMarkdownExperimental', () => { '' + '' + '' + - 'foo **bar** [baz](https://ckeditor.com).' + + '' + + 'foo **bar** [baz](https://ckeditor.com).' + + '' + '' + '' + '' From 6c380b36f81f1a5e32c816c7397aea20f9324b42 Mon Sep 17 00:00:00 2001 From: Marcin Panek Date: Fri, 1 Dec 2023 14:21:26 +0100 Subject: [PATCH 31/33] Changed logic in `_containsOnlyAllowedTags()` to use regex and not `DOMParser()`. --- .../src/pastefrommarkdownexperimental.ts | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/packages/ckeditor5-markdown-gfm/src/pastefrommarkdownexperimental.ts b/packages/ckeditor5-markdown-gfm/src/pastefrommarkdownexperimental.ts index e32948f2057..565f751303f 100644 --- a/packages/ckeditor5-markdown-gfm/src/pastefrommarkdownexperimental.ts +++ b/packages/ckeditor5-markdown-gfm/src/pastefrommarkdownexperimental.ts @@ -12,7 +12,7 @@ import { ClipboardPipeline, type ClipboardInputTransformationEvent } from 'ckedi import GFMDataProcessor from './gfmdataprocessor'; import type { ViewDocumentKeyDownEvent } from 'ckeditor5/src/engine'; -const ALLOWED_MARKDOWN_FIRST_LEVEL_TAGS = [ 'SPAN', 'BR', 'PRE', 'CODE' ]; +const ALLOWED_MARKDOWN_TAGS = [ 'SPAN', 'BR', 'PRE', 'CODE' ]; /** * The GitHub Flavored Markdown (GFM) paste plugin. @@ -79,7 +79,7 @@ export default class PasteFromMarkdownExperimental extends Plugin { return; } - if ( !this._containsOnlyAllowedFirstLevelTags( dataAsTextHtml ) ) { + if ( !this._containsOnlyAllowedTags( dataAsTextHtml ) ) { return; } @@ -134,13 +134,11 @@ export default class PasteFromMarkdownExperimental extends Plugin { * * @param htmlString Clipboard content. */ - private _containsOnlyAllowedFirstLevelTags( htmlString: string ): boolean { - const parser = new DOMParser(); - const { body: tempElement } = parser.parseFromString( htmlString, 'text/html' ); - - const tagNames = Array.from( tempElement.children ).map( el => el.tagName ); + private _containsOnlyAllowedTags( htmlString: string ): boolean { + const tagsMatches = htmlString.match( /<([a-zA-Z]+)>/g ) || []; + const tagNames = tagsMatches.map( match => match.replace( /[<>]/g, '' ).toUpperCase() ); - return tagNames.every( el => ALLOWED_MARKDOWN_FIRST_LEVEL_TAGS.includes( el ) ); + return tagNames.every( el => ALLOWED_MARKDOWN_TAGS.includes( el ) ); } /** From 56606fe406d0f68f17f73be46eacbf83eacbebc8 Mon Sep 17 00:00:00 2001 From: Marcin Panek Date: Fri, 1 Dec 2023 15:20:38 +0100 Subject: [PATCH 32/33] Revert "Changed logic in `_containsOnlyAllowedTags()` to use regex and not `DOMParser()`." This reverts commit 6c380b36f81f1a5e32c816c7397aea20f9324b42. --- .../src/pastefrommarkdownexperimental.ts | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/packages/ckeditor5-markdown-gfm/src/pastefrommarkdownexperimental.ts b/packages/ckeditor5-markdown-gfm/src/pastefrommarkdownexperimental.ts index 565f751303f..e32948f2057 100644 --- a/packages/ckeditor5-markdown-gfm/src/pastefrommarkdownexperimental.ts +++ b/packages/ckeditor5-markdown-gfm/src/pastefrommarkdownexperimental.ts @@ -12,7 +12,7 @@ import { ClipboardPipeline, type ClipboardInputTransformationEvent } from 'ckedi import GFMDataProcessor from './gfmdataprocessor'; import type { ViewDocumentKeyDownEvent } from 'ckeditor5/src/engine'; -const ALLOWED_MARKDOWN_TAGS = [ 'SPAN', 'BR', 'PRE', 'CODE' ]; +const ALLOWED_MARKDOWN_FIRST_LEVEL_TAGS = [ 'SPAN', 'BR', 'PRE', 'CODE' ]; /** * The GitHub Flavored Markdown (GFM) paste plugin. @@ -79,7 +79,7 @@ export default class PasteFromMarkdownExperimental extends Plugin { return; } - if ( !this._containsOnlyAllowedTags( dataAsTextHtml ) ) { + if ( !this._containsOnlyAllowedFirstLevelTags( dataAsTextHtml ) ) { return; } @@ -134,11 +134,13 @@ export default class PasteFromMarkdownExperimental extends Plugin { * * @param htmlString Clipboard content. */ - private _containsOnlyAllowedTags( htmlString: string ): boolean { - const tagsMatches = htmlString.match( /<([a-zA-Z]+)>/g ) || []; - const tagNames = tagsMatches.map( match => match.replace( /[<>]/g, '' ).toUpperCase() ); + private _containsOnlyAllowedFirstLevelTags( htmlString: string ): boolean { + const parser = new DOMParser(); + const { body: tempElement } = parser.parseFromString( htmlString, 'text/html' ); + + const tagNames = Array.from( tempElement.children ).map( el => el.tagName ); - return tagNames.every( el => ALLOWED_MARKDOWN_TAGS.includes( el ) ); + return tagNames.every( el => ALLOWED_MARKDOWN_FIRST_LEVEL_TAGS.includes( el ) ); } /** From 663babba3d9b6cf6c8b78b6daae882e46377cd36 Mon Sep 17 00:00:00 2001 From: Marcin Panek Date: Fri, 1 Dec 2023 16:20:58 +0100 Subject: [PATCH 33/33] Final refactoring. --- packages/ckeditor5-markdown-gfm/package.json | 1 + .../src/pastefrommarkdownexperimental.ts | 8 ++++---- .../tests/manual/pastefrommarkdown/pastefrommarkdown.js | 5 ++++- 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/packages/ckeditor5-markdown-gfm/package.json b/packages/ckeditor5-markdown-gfm/package.json index 513dcc7e464..29e73293c39 100644 --- a/packages/ckeditor5-markdown-gfm/package.json +++ b/packages/ckeditor5-markdown-gfm/package.json @@ -28,6 +28,7 @@ "@ckeditor/ckeditor5-editor-classic": "40.1.0", "@ckeditor/ckeditor5-engine": "40.1.0", "@ckeditor/ckeditor5-essentials": "40.1.0", + "@ckeditor/ckeditor5-font": "40.1.0", "@ckeditor/ckeditor5-heading":"40.1.0", "@ckeditor/ckeditor5-horizontal-line": "40.1.0", "@ckeditor/ckeditor5-image": "40.1.0", diff --git a/packages/ckeditor5-markdown-gfm/src/pastefrommarkdownexperimental.ts b/packages/ckeditor5-markdown-gfm/src/pastefrommarkdownexperimental.ts index e32948f2057..0f96276dc9a 100644 --- a/packages/ckeditor5-markdown-gfm/src/pastefrommarkdownexperimental.ts +++ b/packages/ckeditor5-markdown-gfm/src/pastefrommarkdownexperimental.ts @@ -79,10 +79,6 @@ export default class PasteFromMarkdownExperimental extends Plugin { return; } - if ( !this._containsOnlyAllowedFirstLevelTags( dataAsTextHtml ) ) { - return; - } - const markdownFromHtml = this._parseMarkdownFromHtml( dataAsTextHtml ); if ( markdownFromHtml ) { @@ -102,6 +98,10 @@ export default class PasteFromMarkdownExperimental extends Plugin { private _parseMarkdownFromHtml( htmlString: string ): string | null { const withoutOsSpecificTags = this._removeOsSpecificTags( htmlString ); + if ( !this._containsOnlyAllowedFirstLevelTags( withoutOsSpecificTags ) ) { + return null; + } + const withoutWrapperTag = this._removeFirstLevelWrapperTagsAndBrs( withoutOsSpecificTags ); if ( this._containsAnyRemainingHtmlTags( withoutWrapperTag ) ) { diff --git a/packages/ckeditor5-markdown-gfm/tests/manual/pastefrommarkdown/pastefrommarkdown.js b/packages/ckeditor5-markdown-gfm/tests/manual/pastefrommarkdown/pastefrommarkdown.js index 81c69385ced..15df1b1499e 100644 --- a/packages/ckeditor5-markdown-gfm/tests/manual/pastefrommarkdown/pastefrommarkdown.js +++ b/packages/ckeditor5-markdown-gfm/tests/manual/pastefrommarkdown/pastefrommarkdown.js @@ -33,10 +33,12 @@ import TableToolbar from '@ckeditor/ckeditor5-table/src/tabletoolbar'; import HorizontalLine from '@ckeditor/ckeditor5-horizontal-line/src/horizontalline'; import Markdown from '../../../src/markdown'; import PasteFromMarkdownExperimental from '../../../src/pastefrommarkdownexperimental'; +import FontFamily from '@ckeditor/ckeditor5-font/src/fontfamily'; ClassicEditor .create( document.querySelector( '#editor' ), { plugins: [ + FontFamily, PasteFromMarkdownExperimental, Markdown, Essentials, @@ -86,7 +88,8 @@ ClassicEditor '|', 'undo', 'redo', - 'horizontalLine' + 'horizontalLine', + 'fontFamily' ], image: { toolbar: [ 'imageStyle:inline', 'imageStyle:block', 'imageStyle:side', '|', 'imageTextAlternative' ]