diff --git a/packages/ckeditor5-clipboard/docs/features/paste-plain-text.md b/packages/ckeditor5-clipboard/docs/features/paste-plain-text.md index 0597463e1bb..be8782e5f0c 100644 --- a/packages/ckeditor5-clipboard/docs/features/paste-plain-text.md +++ b/packages/ckeditor5-clipboard/docs/features/paste-plain-text.md @@ -71,6 +71,7 @@ Feel free to open a [new feature request](https://github.com/ckeditor/ckeditor5/ ## Related features CKEditor 5 supports a wider range of paste features, including: +* {@link features/paste-markdown Paste Markdown} – Paste Markdown formatted content straight into the editor. * {@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/import-word Import from Word} – Convert Word files directly into HTML content. 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.html b/packages/ckeditor5-markdown-gfm/docs/_snippets/features/paste-from-markdown.html new file mode 100644 index 00000000000..9cd2585d71f --- /dev/null +++ b/packages/ckeditor5-markdown-gfm/docs/_snippets/features/paste-from-markdown.html @@ -0,0 +1,10 @@ + + + +

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..6cff08356f4 --- /dev/null +++ b/packages/ckeditor5-markdown-gfm/docs/_snippets/features/paste-from-markdown.js @@ -0,0 +1,94 @@ +/** + * @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 { Code, Strikethrough, Underline } 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 { DocumentList, TodoDocumentList, AdjacentListsSupport } from '@ckeditor/ckeditor5-list'; +import { Markdown, PasteFromMarkdownExperimental } from '@ckeditor/ckeditor5-markdown-gfm'; +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'; + +// Umberto combines all `packages/*/docs` into the `docs/` directory. The import path must be valid after merging all directories. +import ClassicEditor from '../build-classic'; + +const plugins = 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, Underline, Markdown, CodeBlock, HorizontalLine, DocumentList, TodoDocumentList, + AdjacentListsSupport, PasteFromMarkdownExperimental, CKBox, CKBoxImageEdit, + PictureEditing, ImageInsert, ImageResize, AutoImage, LinkImage, Font + ] ); + +ClassicEditor + .create( document.querySelector( '#snippet-paste-from-markdown' ), { + plugins, + toolbar: { + items: [ + 'undo', 'redo', '|', 'sourceEditing', '|', 'heading', + '|', 'bold', 'italic', 'underline', 'strikethrough', 'code', + '-', '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', + '|', + 'ckboxImageEdit' + ] + }, + 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/assets/markdown.txt b/packages/ckeditor5-markdown-gfm/docs/assets/markdown.txt new file mode 100644 index 00000000000..a1056590b32 --- /dev/null +++ b/packages/ckeditor5-markdown-gfm/docs/assets/markdown.txt @@ -0,0 +1,72 @@ +# Headings of various kinds +## h2 Heading +### h3 Heading +#### h4 Heading +##### h5 Heading + + +## Various ways to create horizontal rules + +___ + +--- + +*** + +## Basic formatting + +**This is bold text** + +__This is bold text__ + +*This is italic text* + +_This is italic text_ + +~~Strikethrough~~ + + +## Block quotes + +> Basic block quotes + + +## Lists + +Unordered lists + ++ A list is created by starting a line with `+`, `-`, or `*`. ++ Indenting an item by two spaces will turn it into a sub-list. + + That can be nested further: + + Like this! + +Ordered lists + +1. You can create ordered lists... +2. ...with sequential numbers... +3. ...incrementing by 1. + + +1. Or you can just use 1... +1. ...for all the items. + +## Code formatting + +This covers either inline `code`... + +``` +Or a code block. +``` + +## Tables + +| Header | Header | +| ------ | ----------- | +| Tables are tricky | And need some special care | + +## Links +[CKEditor 5 main site](https://ckeditor.com/) + +## Images + +![CKEditor 5](https://user-images.githubusercontent.com/1099479/179190754-f4aaf2b3-21cc-49c4-a454-8de4a00cc70e.jpg) diff --git a/packages/ckeditor5-markdown-gfm/docs/features/markdown.md b/packages/ckeditor5-markdown-gfm/docs/features/markdown.md index b9f721b3d96..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 --- @@ -41,7 +42,7 @@ editor.setData( 'This is **bold**.' ); The data processor outputs the GFM Markdown syntax. "GFM" stands for "GitHub Flavored Markdown" – a Markdown dialect used by [GitHub](https://github.com). Markdown lacks any formal specification (although the [CommonMark](https://commonmark.org/) initiative aims to close this gap) and has many dialects, often incompatible with one another. -When converting the output produced by this data processor, make sure to use a compatible Markdown-to-HTML converter (for example the [marked](https://www.npmjs.com/package/marked) library). +When converting the output produced by this data processor, make sure to use a compatible Markdown-to-HTML converter (for example, the [marked](https://www.npmjs.com/package/marked) library). While the CKEditor 5 architecture supports changing the data format, in most scenarios we do recommend sticking to the default format which is HTML (supported by the {@link module:engine/dataprocessor/htmldataprocessor~HtmlDataProcessor}). HTML remains [the best standard for rich-text data](https://medium.com/content-uneditable/a-standard-for-rich-text-data-4b3a507af552). @@ -116,6 +117,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-markdown Paste Markdown} – Paste Markdown formatted content straight into the editor. ## Contribute diff --git a/packages/ckeditor5-markdown-gfm/docs/features/paste-markdown.md b/packages/ckeditor5-markdown-gfm/docs/features/paste-markdown.md new file mode 100644 index 00000000000..8352a2034f7 --- /dev/null +++ b/packages/ckeditor5-markdown-gfm/docs/features/paste-markdown.md @@ -0,0 +1,88 @@ +--- +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-24 +--- + +# Paste Markdown + +The paste Markdown feature lets users paste Markdown-formatted content straight into the editor. It will be then converted into rich text on the fly. + + + This feature is still in the experimental phase. See the [known issues](#known-issues) section to learn more. + + +## Demo + +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](%BASE_PATH%/assets/markdown.txt) for convenience. + +{@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/pastefrommarkdownexperimental~PasteFromMarkdownExperimental} plugin to the editor configuration: + +```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 { PasteFromMarkdownExperimental } from '@ckeditor/ckeditor5-markdown-gfm'; + +ClassicEditor + .create( document.querySelector( '#snippet-markdown' ), { + plugins: [ + PasteFromMarkdownExperimental, + Essentials, + Bold, + Italic, + // More plugins. + // ... + ], + // More of editor's config. + // ... + } ) + .then( /* ... */ ) + .catch( /* ... */ ); + +``` + + + Read more about {@link installation/plugins/installing-plugins installing plugins}. + + +## Known issues + +While the Paste Markdown feature is already stable enough to use it, please remember it still needs some more testing. We are now mostly concentrating on testing it in connection with other tools and plugins. If you have any observations, suggestions or other piece of information you want to share with us, feel free to put them in [this GitHub issue](https://github.com/ckeditor/ckeditor5/issues/2321). + +## 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/autoformat Autoformatting} – Format your content on the go with Markdown-like shortcodes. + +## Contribute + +The source code of the feature is available on GitHub at [https://github.com/ckeditor/ckeditor5/tree/master/packages/ckeditor5-markdown-gfm](https://github.com/ckeditor/ckeditor5/tree/master/packages/ckeditor5-markdown-gfm) diff --git a/packages/ckeditor5-markdown-gfm/package.json b/packages/ckeditor5-markdown-gfm/package.json index 0c2c68fbe5e..29e73293c39 100644 --- a/packages/ckeditor5-markdown-gfm/package.json +++ b/packages/ckeditor5-markdown-gfm/package.json @@ -18,15 +18,29 @@ "turndown-plugin-gfm": "1.0.2" }, "devDependencies": { + "@ckeditor/ckeditor5-autoformat":"40.1.0", "@ckeditor/ckeditor5-basic-styles": "40.1.0", + "@ckeditor/ckeditor5-block-quote": "40.1.0", + "@ckeditor/ckeditor5-clipboard": "40.1.0", "@ckeditor/ckeditor5-code-block": "40.1.0", "@ckeditor/ckeditor5-core": "40.1.0", "@ckeditor/ckeditor5-dev-utils": "^39.0.0", "@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", + "@ckeditor/ckeditor5-indent":"40.1.0", + "@ckeditor/ckeditor5-link": "40.1.0", "@ckeditor/ckeditor5-list": "40.1.0", + "@ckeditor/ckeditor5-media-embed":"40.1.0", + "@ckeditor/ckeditor5-paragraph": "40.1.0", "@ckeditor/ckeditor5-table": "40.1.0", "@ckeditor/ckeditor5-theme-lark": "40.1.0", + "@ckeditor/ckeditor5-undo": "40.1.0", + "@ckeditor/ckeditor5-utils": "40.1.0", "@types/marked": "^4.0.8", "typescript": "^4.8.4", "webpack": "^5.58.1", 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; } } 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/pastefrommarkdownexperimental.ts b/packages/ckeditor5-markdown-gfm/src/pastefrommarkdownexperimental.ts new file mode 100644 index 00000000000..0f96276dc9a --- /dev/null +++ b/packages/ckeditor5-markdown-gfm/src/pastefrommarkdownexperimental.ts @@ -0,0 +1,189 @@ +/** + * @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/pastefrommarkdownexperimental + */ + +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 ALLOWED_MARKDOWN_FIRST_LEVEL_TAGS = [ 'SPAN', 'BR', 'PRE', 'CODE' ]; + +/** + * The GitHub Flavored Markdown (GFM) paste plugin. + * + * For a detailed overview, check the {@glink features/pasting/paste-markdown Paste Markdown feature} 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 ) => { + 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 ); + + return; + } + + const markdownFromHtml = this._parseMarkdownFromHtml( dataAsTextHtml ); + + 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 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. + * + * @param htmlString Clipboard content in `text/html` type format. + */ + 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 ) ) { + return null; + } + + return this._replaceHtmlReservedEntitiesWithCharacters( withoutWrapperTag ); + } + + /** + * Removes OS specific tags. + * + * @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. + 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 _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_FIRST_LEVEL_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 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 = tempElement.querySelectorAll( ':scope > *' ); + + for ( const element of outerElements ) { + const elementClone = element.cloneNode( true ); + element.replaceWith( ...elementClone.childNodes ); + } + + return tempElement.innerHTML; + } + + /** + * Determines if string contains any HTML tags. + */ + private _containsAnyRemainingHtmlTags( str: string ): boolean { + return str.includes( '<' ); + } + + /** + * Replaces the reserved HTML entities with the actual characters. + * + * @param htmlString Clipboard content without any tags. + */ + private _replaceHtmlReservedEntitiesWithCharacters( htmlString: string ) { + return htmlString + .replace( />/g, '>' ) + .replace( /</g, '<' ) + .replace( / /g, ' ' ); + } +} 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..1bb87c650df --- /dev/null +++ b/packages/ckeditor5-markdown-gfm/tests/manual/pastefrommarkdown/pastefrommarkdown.html @@ -0,0 +1,16 @@ + + +

Markdown output 🛫

+ +

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..15df1b1499e --- /dev/null +++ b/packages/ckeditor5-markdown-gfm/tests/manual/pastefrommarkdown/pastefrommarkdown.js @@ -0,0 +1,128 @@ +/** + * @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'; +import FontFamily from '@ckeditor/ckeditor5-font/src/fontfamily'; + +ClassicEditor + .create( document.querySelector( '#editor' ), { + plugins: [ + FontFamily, + 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', + 'fontFamily' + ], + 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/) diff --git a/packages/ckeditor5-markdown-gfm/tests/pastefrommarkdownexperimental.js b/packages/ckeditor5-markdown-gfm/tests/pastefrommarkdownexperimental.js new file mode 100644 index 00000000000..c65de61905c --- /dev/null +++ b/packages/ckeditor5-markdown-gfm/tests/pastefrommarkdownexperimental.js @@ -0,0 +1,171 @@ +/** + * @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 { 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 PasteFromMarkdownExperimental from '../src/pastefrommarkdownexperimental'; + +describe( 'PasteFromMarkdownExperimental', () => { + let editorElement, editor; + + beforeEach( () => { + editorElement = global.document.createElement( 'div' ); + global.document.body.appendChild( editorElement ); + + return ClassicTestEditor + .create( editorElement, { + plugins: [ ClipboardPipeline, PasteFromMarkdownExperimental, Paragraph, Bold, Italic, Undo ] + } ) + .then( newEditor => { + editor = newEditor; + } ); + } ); + + afterEach( () => { + editorElement.remove(); + + return editor.destroy(); + } ); + + 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 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 **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).' ); + + 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).[]' ); + } ); + + 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).' + ); + + expect( getData( editor.model ) ).to.equal( + 'foo <$text bold="true">bar baz.foo <$text bold="true">bar baz.[]' + ); + } ); + + 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">barfoo <$text bold="true">bar[]' + ); + } ); + + 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', () => { + setData( editor.model, '[]' ); + pasteHtml( editor, 'foo **bar** [baz](https://ckeditor.com).' ); + + expect( getData( editor.model ) ).to.equal( 'foo <$text bold="true">bar baz.[]' ); + } ); + } ); + + describe( 'Windows', () => { + it( 'should parse correctly Windows type clipboard', () => { + setData( editor.model, '[]' ); + pasteHtml( editor, + '' + + '' + + '' + + '' + + 'foo **bar** [baz](https://ckeditor.com).' + + '' + + '' + + '' + + '' + ); + + expect( getData( editor.model ).trim() ).to.equal( 'foo <$text bold="true">bar baz.[]' ); + } ); + } ); + + describe( 'Linux', () => { + 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 ]; + } + }; + } +} ); diff --git a/packages/ckeditor5-paste-from-office/docs/features/paste-from-google-docs.md b/packages/ckeditor5-paste-from-office/docs/features/paste-from-google-docs.md index 2ccc1cea691..072256283e9 100644 --- a/packages/ckeditor5-paste-from-office/docs/features/paste-from-google-docs.md +++ b/packages/ckeditor5-paste-from-office/docs/features/paste-from-google-docs.md @@ -91,6 +91,7 @@ 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-office-enhanced paste from Office enhanced} – Paste from Office enhanced is a premium version of the plugin that offers far greater capabilities. * {@link features/import-word Import from Word} – Convert Word files directly into HTML content. +* {@link features/paste-markdown Paste Markdown} – Paste Markdown formatted content straight into the editor. ## Contribute diff --git a/packages/ckeditor5-paste-from-office/docs/features/paste-from-office.md b/packages/ckeditor5-paste-from-office/docs/features/paste-from-office.md index a05bc789b8e..0b3fffbb645 100644 --- a/packages/ckeditor5-paste-from-office/docs/features/paste-from-office.md +++ b/packages/ckeditor5-paste-from-office/docs/features/paste-from-office.md @@ -128,6 +128,7 @@ CKEditor 5 supports a wider range of paste features, including: * {@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. You can read more about the differences between the paste from Office and import from Word features in the {@link features/features-comparison dedicated comparison guide}. +* {@link features/paste-markdown Paste Markdown} – Paste Markdown formatted content straight into the editor. ## Contribute