Преглед на файлове

Merge branch 'master' into t/1791

Piotrek Koszuliński преди 6 години
родител
ревизия
7324d90551
променени са 28 файла, в които са добавени 1674 реда и са изтрити 6 реда
  1. 3 3
      packages/ckeditor5-engine/.travis.yml
  2. 0 1
      packages/ckeditor5-engine/README.md
  3. 0 0
      packages/ckeditor5-engine/docs/_snippets/framework/build-extending-content-source.html
  4. 15 0
      packages/ckeditor5-engine/docs/_snippets/framework/build-extending-content-source.js
  5. 11 0
      packages/ckeditor5-engine/docs/_snippets/framework/extending-content-add-external-link-target.html
  6. 47 0
      packages/ckeditor5-engine/docs/_snippets/framework/extending-content-add-external-link-target.js
  7. 14 0
      packages/ckeditor5-engine/docs/_snippets/framework/extending-content-add-heading-class.html
  8. 33 0
      packages/ckeditor5-engine/docs/_snippets/framework/extending-content-add-heading-class.js
  9. 14 0
      packages/ckeditor5-engine/docs/_snippets/framework/extending-content-add-link-class.html
  10. 43 0
      packages/ckeditor5-engine/docs/_snippets/framework/extending-content-add-link-class.js
  11. 12 0
      packages/ckeditor5-engine/docs/_snippets/framework/extending-content-add-unsafe-link-class.html
  12. 43 0
      packages/ckeditor5-engine/docs/_snippets/framework/extending-content-add-unsafe-link-class.js
  13. 12 0
      packages/ckeditor5-engine/docs/_snippets/framework/extending-content-allow-div-attributes.html
  14. 74 0
      packages/ckeditor5-engine/docs/_snippets/framework/extending-content-allow-div-attributes.js
  15. 33 0
      packages/ckeditor5-engine/docs/_snippets/framework/extending-content-allow-link-target.html
  16. 51 0
      packages/ckeditor5-engine/docs/_snippets/framework/extending-content-allow-link-target.js
  17. 7 0
      packages/ckeditor5-engine/docs/_snippets/framework/extending-content-arbitrary-attribute-values.html
  18. 68 0
      packages/ckeditor5-engine/docs/_snippets/framework/extending-content-arbitrary-attribute-values.js
  19. 23 0
      packages/ckeditor5-engine/docs/_snippets/framework/extending-content-custom-figure-attributes.html
  20. 170 0
      packages/ckeditor5-engine/docs/_snippets/framework/extending-content-custom-figure-attributes.js
  21. 1 1
      packages/ckeditor5-engine/docs/api/engine.md
  22. 359 0
      packages/ckeditor5-engine/docs/framework/guides/deep-dive/conversion-extending-output.md
  23. 128 0
      packages/ckeditor5-engine/docs/framework/guides/deep-dive/conversion-introduction.md
  24. 449 0
      packages/ckeditor5-engine/docs/framework/guides/deep-dive/conversion-preserving-custom-content.md
  25. 12 0
      packages/ckeditor5-engine/src/view/renderer.js
  26. 1 0
      packages/ckeditor5-engine/src/view/view.js
  27. 42 0
      packages/ckeditor5-engine/tests/view/renderer.js
  28. 9 1
      packages/ckeditor5-engine/theme/placeholder.css

+ 3 - 3
packages/ckeditor5-engine/.travis.yml

@@ -1,9 +1,11 @@
 sudo: required
-dist: trusty
+dist: xenial
 addons:
   chrome: stable
   firefox: latest
 language: node_js
+services:
+- xvfb
 node_js:
 - '8'
 cache:
@@ -13,8 +15,6 @@ branches:
   - stable
 before_install:
 - export START_TIME=$( date +%s )
-- export DISPLAY=:99.0
-- sh -e /etc/init.d/xvfb start
 - npm i -g yarn
 install:
 - yarn add @ckeditor/ckeditor5-dev-tests

+ 0 - 1
packages/ckeditor5-engine/README.md

@@ -1,7 +1,6 @@
 CKEditor 5 editing engine
 ========================================
 
-[![Join the chat at https://gitter.im/ckeditor/ckeditor5](https://badges.gitter.im/ckeditor/ckeditor5.svg)](https://gitter.im/ckeditor/ckeditor5?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
 [![npm version](https://badge.fury.io/js/%40ckeditor%2Fckeditor5-engine.svg)](https://www.npmjs.com/package/@ckeditor/ckeditor5-engine)
 [![Build Status](https://travis-ci.org/ckeditor/ckeditor5-engine.svg?branch=master)](https://travis-ci.org/ckeditor/ckeditor5-engine)
 [![Coverage Status](https://coveralls.io/repos/github/ckeditor/ckeditor5-engine/badge.svg?branch=master)](https://coveralls.io/github/ckeditor/ckeditor5-engine?branch=master)

+ 0 - 0
packages/ckeditor5-engine/docs/_snippets/framework/build-extending-content-source.html


+ 15 - 0
packages/ckeditor5-engine/docs/_snippets/framework/build-extending-content-source.js

@@ -0,0 +1,15 @@
+/**
+ * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/* globals window */
+
+import ClassicEditor from '@ckeditor/ckeditor5-build-classic/src/ckeditor';
+import Code from '@ckeditor/ckeditor5-basic-styles/src/code';
+import Font from '@ckeditor/ckeditor5-font/src/font';
+
+ClassicEditor.builtinPlugins.push( Code );
+ClassicEditor.builtinPlugins.push( Font );
+
+window.ClassicEditor = ClassicEditor;

+ 11 - 0
packages/ckeditor5-engine/docs/_snippets/framework/extending-content-add-external-link-target.html

@@ -0,0 +1,11 @@
+<style>
+	#snippet-link-external + .ck-editor .ck-content a[target="_blank"]::after {
+		content: '\29C9';
+	}
+</style>
+
+<div id="snippet-link-external">
+	<p>External links in this <a href="https://ckeditor.com">editor</a> have a <code>target="_blank"</code>
+	<a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes">attribute</a> and will open in a new
+	<a href="https://developer.mozilla.org/en-US/docs/Web/API/Window">window</a>.</p>
+</div>

+ 47 - 0
packages/ckeditor5-engine/docs/_snippets/framework/extending-content-add-external-link-target.js

@@ -0,0 +1,47 @@
+/**
+ * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/* globals ClassicEditor, console, window, document */
+
+import { CS_CONFIG } from '@ckeditor/ckeditor5-cloud-services/tests/_utils/cloud-services-config';
+
+function AddTargetToExternalLinks( editor ) {
+	editor.conversion.for( 'downcast' ).add( dispatcher => {
+		dispatcher.on( 'attribute:linkHref', ( evt, data, conversionApi ) => {
+			const viewWriter = conversionApi.writer;
+			const viewSelection = viewWriter.document.selection;
+			const viewElement = viewWriter.createAttributeElement( 'a', {
+				target: '_blank'
+			}, {
+				priority: 5
+			} );
+
+			if ( data.attributeNewValue.match( /ckeditor\.com/ ) ) {
+				viewWriter.unwrap( conversionApi.mapper.toViewRange( data.range ), viewElement );
+			} else {
+				if ( data.item.is( 'selection' ) ) {
+					viewWriter.wrap( viewSelection.getFirstRange(), viewElement );
+				} else {
+					viewWriter.wrap( conversionApi.mapper.toViewRange( data.range ), viewElement );
+				}
+			}
+		}, { priority: 'low' } );
+	} );
+}
+
+ClassicEditor
+	.create( document.querySelector( '#snippet-link-external' ), {
+		cloudServices: CS_CONFIG,
+		extraPlugins: [ AddTargetToExternalLinks ],
+		toolbar: {
+			viewportTopOffset: window.getViewportTopOffsetConfig()
+		}
+	} )
+	.then( editor => {
+		window.editor = editor;
+	} )
+	.catch( err => {
+		console.error( err.stack );
+	} );

+ 14 - 0
packages/ckeditor5-engine/docs/_snippets/framework/extending-content-add-heading-class.html

@@ -0,0 +1,14 @@
+<style>
+	.ck-content h2.my-heading {
+		font-family: Georgia, Times, Times New Roman, serif;
+		border-left: 6px solid #fd0000;
+		padding-left: .8em;
+		padding: .1em .8em;
+	}
+</style>
+
+<div id="snippet-heading-class">
+	<h2>Heading with <code>.my-heading</code> class</h2>
+	<h3>Regular heading</h3>
+	<p>Some content.</p>
+</div>

+ 33 - 0
packages/ckeditor5-engine/docs/_snippets/framework/extending-content-add-heading-class.js

@@ -0,0 +1,33 @@
+/**
+ * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/* globals ClassicEditor, console, window, document */
+
+import { CS_CONFIG } from '@ckeditor/ckeditor5-cloud-services/tests/_utils/cloud-services-config';
+
+function AddClassToAllHeading1( editor ) {
+	editor.conversion.for( 'downcast' ).add( dispatcher => {
+		dispatcher.on( 'insert:heading1', ( evt, data, conversionApi ) => {
+			const viewWriter = conversionApi.writer;
+
+			viewWriter.addClass( 'my-heading', conversionApi.mapper.toViewElement( data.item ) );
+		}, { priority: 'low' } );
+	} );
+}
+
+ClassicEditor
+	.create( document.querySelector( '#snippet-heading-class' ), {
+		cloudServices: CS_CONFIG,
+		extraPlugins: [ AddClassToAllHeading1 ],
+		toolbar: {
+			viewportTopOffset: window.getViewportTopOffsetConfig()
+		}
+	} )
+	.then( editor => {
+		window.editor = editor;
+	} )
+	.catch( err => {
+		console.error( err.stack );
+	} );

+ 14 - 0
packages/ckeditor5-engine/docs/_snippets/framework/extending-content-add-link-class.html

@@ -0,0 +1,14 @@
+<style>
+	.my-green-link {
+		color: #209a25;
+		border: 1px solid #209a25;
+		border-radius: 2px;
+		padding: 0 3px;
+		box-shadow: 1px 1px 0 0 #209a25;
+	}
+</style>
+
+<div id="snippet-link-classes">
+	<p>All links in this <a href="https://ckeditor.com">editor</a> have a custom <code>.my-green-link</code>
+	CSS <a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/class">class</a> attribute.</p>
+</div>

+ 43 - 0
packages/ckeditor5-engine/docs/_snippets/framework/extending-content-add-link-class.js

@@ -0,0 +1,43 @@
+/**
+ * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/* globals ClassicEditor, console, window, document */
+
+import { CS_CONFIG } from '@ckeditor/ckeditor5-cloud-services/tests/_utils/cloud-services-config';
+
+function AddClassToAllLinks( editor ) {
+	editor.conversion.for( 'downcast' ).add( dispatcher => {
+		dispatcher.on( 'attribute:linkHref', ( evt, data, conversionApi ) => {
+			const viewWriter = conversionApi.writer;
+			const viewSelection = viewWriter.document.selection;
+			const viewElement = viewWriter.createAttributeElement( 'a', {
+				class: 'my-green-link'
+			}, {
+				priority: 5
+			} );
+
+			if ( data.item.is( 'selection' ) ) {
+				viewWriter.wrap( viewSelection.getFirstRange(), viewElement );
+			} else {
+				viewWriter.wrap( conversionApi.mapper.toViewRange( data.range ), viewElement );
+			}
+		}, { priority: 'low' } );
+	} );
+}
+
+ClassicEditor
+	.create( document.querySelector( '#snippet-link-classes' ), {
+		cloudServices: CS_CONFIG,
+		extraPlugins: [ AddClassToAllLinks ],
+		toolbar: {
+			viewportTopOffset: window.getViewportTopOffsetConfig()
+		}
+	} )
+	.then( editor => {
+		window.editor = editor;
+	} )
+	.catch( err => {
+		console.error( err.stack );
+	} );

+ 12 - 0
packages/ckeditor5-engine/docs/_snippets/framework/extending-content-add-unsafe-link-class.html

@@ -0,0 +1,12 @@
+<style>
+	.unsafe-link {
+		padding: 0 2px;
+		outline: 2px dashed red;
+		background: #ffff00;
+	}
+</style>
+
+<div id="snippet-link-unsafe-classes">
+	<p>All links in this <a href="https://ckeditor.com">editor</a> that do not use the <a href="http://developer.mozilla.org/en-US/docs/Glossary/https">HTTPS</a> protocol
+	have a custom <code>.unsafe-link</code> CSS <a href="http://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/class">class</a> that marks them red.</p>
+</div>

+ 43 - 0
packages/ckeditor5-engine/docs/_snippets/framework/extending-content-add-unsafe-link-class.js

@@ -0,0 +1,43 @@
+/**
+ * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/* globals ClassicEditor, console, window, document */
+
+import { CS_CONFIG } from '@ckeditor/ckeditor5-cloud-services/tests/_utils/cloud-services-config';
+
+function AddClassToUnsafeLinks( editor ) {
+	editor.conversion.for( 'downcast' ).add( dispatcher => {
+		dispatcher.on( 'attribute:linkHref', ( evt, data, conversionApi ) => {
+			const viewWriter = conversionApi.writer;
+			const viewSelection = viewWriter.document.selection;
+			const viewElement = viewWriter.createAttributeElement( 'a', { class: 'unsafe-link' }, { priority: 5 } );
+
+			if ( data.attributeNewValue.match( /http:\/\// ) ) {
+				if ( data.item.is( 'selection' ) ) {
+					viewWriter.wrap( viewSelection.getFirstRange(), viewElement );
+				} else {
+					viewWriter.wrap( conversionApi.mapper.toViewRange( data.range ), viewElement );
+				}
+			} else {
+				viewWriter.unwrap( conversionApi.mapper.toViewRange( data.range ), viewElement );
+			}
+		}, { priority: 'low' } );
+	} );
+}
+
+ClassicEditor
+	.create( document.querySelector( '#snippet-link-unsafe-classes' ), {
+		cloudServices: CS_CONFIG,
+		extraPlugins: [ AddClassToUnsafeLinks ],
+		toolbar: {
+			viewportTopOffset: window.getViewportTopOffsetConfig()
+		}
+	} )
+	.then( editor => {
+		window.editor = editor;
+	} )
+	.catch( err => {
+		console.error( err.stack );
+	} );

+ 12 - 0
packages/ckeditor5-engine/docs/_snippets/framework/extending-content-allow-div-attributes.html

@@ -0,0 +1,12 @@
+<div id="snippet-div-attributes">
+	<div id="special-section-a" style="background: #eafbd7;padding:.8em .8em 0;margin-bottom:.8em;border:1px solid #8bc34a;">
+		<p><strong>Special section A</strong>: it has set "style" and "id" attributes.</p>
+	</div>
+
+	<p>Regular content of the editor.</p>
+
+	<div id="special-section-b" style="background: #ffeed0;padding:.8em .8em 0;margin-bottom:.8em;border:1px solid #ff8f00;" spellcheck="false">
+		<p><strong>Special section B</strong>: it has set "style", "id" and spellcheck="false" attributes.</p>
+		<p>This section disables native browser spellchecker.</p>
+	</div>
+</div>

+ 74 - 0
packages/ckeditor5-engine/docs/_snippets/framework/extending-content-allow-div-attributes.js

@@ -0,0 +1,74 @@
+/**
+ * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/* globals ClassicEditor, console, window, document */
+
+import { CS_CONFIG } from '@ckeditor/ckeditor5-cloud-services/tests/_utils/cloud-services-config';
+
+function ConvertDivAttributes( editor ) {
+	// Allow divs in the model.
+	editor.model.schema.register( 'div', {
+		allowWhere: '$block',
+		allowContentOf: '$root'
+	} );
+
+	// Allow divs in the model to have all attributes.
+	editor.model.schema.addAttributeCheck( context => {
+		if ( context.endsWith( 'div' ) ) {
+			return true;
+		}
+	} );
+
+	// View-to-model converter converting a view div with all its attributes to the model.
+	editor.conversion.for( 'upcast' ).elementToElement( {
+		view: 'div',
+		model: ( viewElement, modelWriter ) => {
+			return modelWriter.createElement( 'div', viewElement.getAttributes() );
+		}
+	} );
+
+	// Model-to-view convert for the div element (attrbiutes are converted separately).
+	editor.conversion.for( 'downcast' ).elementToElement( {
+		model: 'div',
+		view: 'div'
+	} );
+
+	// Model-to-view converter for div attributes.
+	// Note that we use a lower-level, event-based API here.
+	editor.conversion.for( 'downcast' ).add( dispatcher => {
+		dispatcher.on( 'attribute', ( evt, data, conversionApi ) => {
+			// Convert div attributes only.
+			if ( data.item.name != 'div' ) {
+				return;
+			}
+
+			const viewWriter = conversionApi.writer;
+			const viewDiv = conversionApi.mapper.toViewElement( data.item );
+
+			// In the model-to-view conversion we convert changes. An attribute can be added or removed or changed.
+			// The below code handles all 3 cases.
+			if ( data.attributeNewValue ) {
+				viewWriter.setAttribute( data.attributeKey, data.attributeNewValue, viewDiv );
+			} else {
+				viewWriter.removeAttribute( data.attributeKey, viewDiv );
+			}
+		} );
+	} );
+}
+
+ClassicEditor
+	.create( document.querySelector( '#snippet-div-attributes' ), {
+		cloudServices: CS_CONFIG,
+		extraPlugins: [ ConvertDivAttributes ],
+		toolbar: {
+			viewportTopOffset: window.getViewportTopOffsetConfig()
+		}
+	} )
+	.then( editor => {
+		window.editor = editor;
+	} )
+	.catch( err => {
+		console.error( err.stack );
+	} );

Файловите разлики са ограничени, защото са твърде много
+ 33 - 0
packages/ckeditor5-engine/docs/_snippets/framework/extending-content-allow-link-target.html


+ 51 - 0
packages/ckeditor5-engine/docs/_snippets/framework/extending-content-allow-link-target.js

@@ -0,0 +1,51 @@
+/**
+ * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/* globals ClassicEditor, console, window, document */
+
+import { CS_CONFIG } from '@ckeditor/ckeditor5-cloud-services/tests/_utils/cloud-services-config';
+
+function AllowLinkTarget( editor ) {
+	editor.model.schema.extend( '$text', { allowAttributes: 'linkTarget' } );
+
+	editor.conversion.for( 'downcast' ).attributeToElement( {
+		model: 'linkTarget',
+		view: ( attributeValue, writer ) => {
+			const linkElement = writer.createAttributeElement( 'a', { target: attributeValue }, { priority: 5 } );
+			writer.setCustomProperty( 'link', true, linkElement );
+
+			return linkElement;
+		},
+		converterPriority: 'low'
+	} );
+
+	editor.conversion.for( 'upcast' ).attributeToAttribute( {
+		view: {
+			name: 'a',
+			key: 'target'
+		},
+		model: 'linkTarget',
+		converterPriority: 'low'
+	} );
+}
+
+ClassicEditor
+	.create( document.querySelector( '#snippet-link-target' ), {
+		cloudServices: CS_CONFIG,
+		extraPlugins: [ AllowLinkTarget ],
+		toolbar: {
+			viewportTopOffset: window.getViewportTopOffsetConfig()
+		}
+	} )
+	.then( editor => {
+		window.editor = editor;
+
+		document.querySelector( '#snippet-link-target-content-update' ).addEventListener( 'click', () => {
+			editor.setData( document.querySelector( '#snippet-link-target-content' ).value );
+		} );
+	} )
+	.catch( err => {
+		console.error( err.stack );
+	} );

+ 7 - 0
packages/ckeditor5-engine/docs/_snippets/framework/extending-content-arbitrary-attribute-values.html

@@ -0,0 +1,7 @@
+<div id="snippet-arbitrary-attribute-values">
+	<ul>
+		<li>test: <span style="font-size:13.5px">13.5px (gets rounded to 14px automatically)</span></li>
+		<li>test: <span style="font-size:32px">32px</span></li>
+		<li>test: <span style="font-size:8px">8px</span></li>
+	</ul>
+</div>

+ 68 - 0
packages/ckeditor5-engine/docs/_snippets/framework/extending-content-arbitrary-attribute-values.js

@@ -0,0 +1,68 @@
+/**
+ * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/* globals ClassicEditor, console, window, document */
+
+import { CS_CONFIG } from '@ckeditor/ckeditor5-cloud-services/tests/_utils/cloud-services-config';
+
+function HandleFontSizeValue( editor ) {
+	// Add special catch-all converter for font-size feature.
+	editor.conversion.for( 'upcast' ).elementToAttribute( {
+		view: {
+			name: 'span',
+			styles: {
+				'font-size': /[\s\S]+/
+			}
+		},
+		model: {
+			key: 'fontSize',
+			value: viewElement => {
+				const value = parseFloat( viewElement.getStyle( 'font-size' ) ).toFixed( 0 );
+
+				// It might be needed to further convert the value to meet business requirements.
+				// In the sample the font-size is configured to handle only the sizes:
+				// 10, 12, 14, 'default', 18, 20, 22
+				// Other sizes will be converted to the model but the UI might not be aware of them.
+
+				// The font-size feature expects numeric values to be Number not String.
+				return parseInt( value );
+			}
+		},
+		converterPriority: 'high'
+	} );
+
+	// Add special converter for font-size feature to convert all (even not configured) model attribute values.
+	editor.conversion.for( 'downcast' ).attributeToElement( {
+		model: {
+			key: 'fontSize'
+		},
+		view: ( modelValue, viewWriter ) => {
+			return viewWriter.createAttributeElement( 'span', {
+				style: `font-size:${ modelValue }px`
+			} );
+		},
+		converterPriority: 'high'
+	} );
+}
+
+ClassicEditor
+	.create( document.querySelector( '#snippet-arbitrary-attribute-values' ), {
+		cloudServices: CS_CONFIG,
+		extraPlugins: [ HandleFontSizeValue ],
+		toolbar: {
+			items: [ 'heading', '|', 'bold', 'italic', '|', 'fontSize' ],
+			viewportTopOffset: window.getViewportTopOffsetConfig()
+		},
+		fontSize: {
+			options: [ 10, 12, 14, 'default', 18, 20, 22 ]
+		}
+	} )
+	.then( editor => {
+		window.editor = editor;
+	} )
+	.catch( err => {
+		console.error( err.stack );
+	} );
+

+ 23 - 0
packages/ckeditor5-engine/docs/_snippets/framework/extending-content-custom-figure-attributes.html

@@ -0,0 +1,23 @@
+<head>
+	<style>
+		.ck-content figure.table-foo table,
+		.ck-content figure.img-foo {
+			border: 2px solid deeppink;
+		}
+	</style>
+</head>
+
+<div id="snippet-custom-figure-attributes">
+	<h2>Image:</h2>
+	<figure class="image">
+		<img alt="bar" class="img-foo" id="img-foo-1" src="%BASE_PATH%/assets/img/fields.jpg">
+		<figcaption>Caption</figcaption>
+	</figure>
+
+	<h2>Table:</h2>
+	<table class="table-foo" id="table-foo-1" >
+		<tr>
+			<td>foo</td>
+		</tr>
+	</table>
+</div>

+ 170 - 0
packages/ckeditor5-engine/docs/_snippets/framework/extending-content-custom-figure-attributes.js

@@ -0,0 +1,170 @@
+/**
+ * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/* globals ClassicEditor, console, window, document */
+
+import { CS_CONFIG } from '@ckeditor/ckeditor5-cloud-services/tests/_utils/cloud-services-config';
+
+/**
+ * Plugin that converts custom attributes for elements that are wrapped in <figure> in the view.
+ */
+function CustomFigureAttributes( editor ) {
+	// Define on which elements the css classes should be preserved:
+	setupCustomClassConversion( 'img', 'image', editor );
+	setupCustomClassConversion( 'table', 'table', editor );
+
+	editor.conversion.for( 'upcast' ).add( upcastCustomClasses( 'figure' ), { priority: 'low' } );
+
+	// Define custom attributes that should be preserved.
+	setupCustomAttributeConversion( 'img', 'image', 'id', editor );
+	setupCustomAttributeConversion( 'table', 'table', 'id', editor );
+}
+
+/**
+ * Setups conversion that preservers classes on img/table elements
+ */
+function setupCustomClassConversion( viewElementName, modelElementName, editor ) {
+	// The 'customClass' attribute will store custom classes from data in the model so schema definitions to allow this attribute.
+	editor.model.schema.extend( modelElementName, { allowAttributes: [ 'customClass' ] } );
+
+	// Define upcast converters for <img> and <table> elements with "low" priority so they are run after default converters.
+	editor.conversion.for( 'upcast' ).add( upcastCustomClasses( viewElementName ), { priority: 'low' } );
+
+	// Define downcast converters for model element with "low" priority so they are run after default converters.
+	editor.conversion.for( 'downcast' ).add( downcastCustomClasses( modelElementName ), { priority: 'low' } );
+}
+
+/**
+ * Setups conversion for custom attribute on view elements contained inside figure.
+ *
+ * This method:
+ *
+ * - adds proper schema rules
+ * - adds an upcast converter
+ * - adds a downcast converter
+ */
+function setupCustomAttributeConversion( viewElementName, modelElementName, viewAttribute, editor ) {
+	// Extend schema to store attribute in the model.
+	const modelAttribute = `custom${ viewAttribute }`;
+
+	editor.model.schema.extend( modelElementName, { allowAttributes: [ modelAttribute ] } );
+
+	editor.conversion.for( 'upcast' ).add( upcastAttribute( viewElementName, viewAttribute, modelAttribute ) );
+	editor.conversion.for( 'downcast' ).add( downcastAttribute( modelElementName, viewElementName, viewAttribute, modelAttribute ) );
+}
+
+/**
+ * Creates upcast converter that will pass all classes from view element to model element.
+ */
+function upcastCustomClasses( elementName ) {
+	return dispatcher => dispatcher.on( `element:${ elementName }`, ( evt, data, conversionApi ) => {
+		const viewItem = data.viewItem;
+		const modelRange = data.modelRange;
+
+		const modelElement = modelRange && modelRange.start.nodeAfter;
+
+		if ( !modelElement ) {
+			return;
+		}
+
+		// The upcast conversion pick up classes from base element and from figure element also so it should be extensible.
+		const currentAttributeValue = modelElement.getAttribute( 'customClass' ) || [];
+
+		currentAttributeValue.push( ...viewItem.getClassNames() );
+
+		conversionApi.writer.setAttribute( 'customClass', currentAttributeValue, modelElement );
+	} );
+}
+
+/**
+ * Creates downcast converter that add classes defined in `customClass` attribute to given view element.
+ *
+ * This converter expects that view element is nested in figure element.
+ */
+function downcastCustomClasses( modelElementName ) {
+	return dispatcher => dispatcher.on( `insert:${ modelElementName }`, ( evt, data, conversionApi ) => {
+		const modelElement = data.item;
+
+		const viewFigure = conversionApi.mapper.toViewElement( modelElement );
+
+		if ( !viewFigure ) {
+			return;
+		}
+
+		// The below code assumes that classes are set on <figure> element...
+		conversionApi.writer.addClass( modelElement.getAttribute( 'customClass' ), viewFigure );
+
+		// ... but if you preferIf the classes should be passed to the <img> find the view element inside figure:
+		//
+		// const viewElement = findViewChild( viewFigure, viewElementName, conversionApi );
+		//
+		// conversionApi.writer.addClass( modelElement.getAttribute( 'customClass' ), viewElement );
+	} );
+}
+
+/**
+ * Helper method that search for given view element in all children of model element.
+ *
+ * @param {module:engine/view/item~Item} viewElement
+ * @param {String} viewElementName
+ * @param {module:engine/conversion/downcastdispatcher~DowncastConversionApi} conversionApi
+ * @return {module:engine/view/item~Item}
+ */
+function findViewChild( viewElement, viewElementName, conversionApi ) {
+	const viewChildren = Array.from( conversionApi.writer.createRangeIn( viewElement ).getItems() );
+
+	return viewChildren.find( item => item.is( viewElementName ) );
+}
+
+/**
+ * Returns custom attribute upcast converter.
+ */
+function upcastAttribute( viewElementName, viewAttribute, modelAttribute ) {
+	return dispatcher => dispatcher.on( `element:${ viewElementName }`, ( evt, data, conversionApi ) => {
+		const viewItem = data.viewItem;
+		const modelRange = data.modelRange;
+
+		const modelElement = modelRange && modelRange.start.nodeAfter;
+
+		if ( !modelElement ) {
+			return;
+		}
+
+		conversionApi.writer.setAttribute( modelAttribute, viewItem.getAttribute( viewAttribute ), modelElement );
+	} );
+}
+
+/**
+ * Returns custom attribute downcast converter.
+ */
+function downcastAttribute( modelElementName, viewElementName, viewAttribute, modelAttribute ) {
+	return dispatcher => dispatcher.on( `insert:${ modelElementName }`, ( evt, data, conversionApi ) => {
+		const modelElement = data.item;
+
+		const viewFigure = conversionApi.mapper.toViewElement( modelElement );
+		const viewElement = findViewChild( viewFigure, viewElementName, conversionApi );
+
+		if ( !viewElement ) {
+			return;
+		}
+
+		conversionApi.writer.setAttribute( viewAttribute, modelElement.getAttribute( modelAttribute ), viewElement );
+	} );
+}
+
+ClassicEditor
+	.create( document.querySelector( '#snippet-custom-figure-attributes' ), {
+		cloudServices: CS_CONFIG,
+		extraPlugins: [ CustomFigureAttributes ],
+		toolbar: {
+			viewportTopOffset: window.getViewportTopOffsetConfig()
+		}
+	} )
+	.then( editor => {
+		window.editor = editor;
+	} )
+	.catch( err => {
+		console.error( err.stack );
+	} );

+ 1 - 1
packages/ckeditor5-engine/docs/api/engine.md

@@ -30,5 +30,5 @@ The source code of this package is available on GitHub in https://github.com/cke
 
 * [`@ckeditor/ckeditor5-engine` on npm](https://www.npmjs.com/package/@ckeditor/ckeditor5-engine)
 * [`ckeditor/ckeditor5-engine` on GitHub](https://github.com/ckeditor/ckeditor5-engine)
-* [Issue tracker](https://github.com/ckeditor/ckeditor5-engine/issues)
+* [Issue tracker](https://github.com/ckeditor/ckeditor5/issues)
 * [Changelog](https://github.com/ckeditor/ckeditor5-engine/blob/master/CHANGELOG.md)

+ 359 - 0
packages/ckeditor5-engine/docs/framework/guides/deep-dive/conversion-extending-output.md

@@ -0,0 +1,359 @@
+---
+category: framework-deep-dive-conversion
+menu-title: Extending editor output
+order: 20
+---
+
+{@snippet framework/build-extending-content-source}
+
+# Extending editor output
+
+In this guide, we will focus on customization to the one–way {@link framework/guides/architecture/editing-engine#editing-pipeline "downcast"} pipeline of the editor, which transforms data from the model to the editing view and the output data only. The following examples do not customize the model and do not process the (input) data — you can picture them as post–processors (filters) applied to the output only.
+
+If you want to learn how to load some extra content (element, attributes, classes) into the editor, check out the {@link framework/guides/deep-dive/conversion-preserving-custom-content next guide} of this guide.
+
+## Before starting
+
+### Code architecture
+
+It is recommended that the code that customizes editor data and editing pipelines is delivered as {@link framework/guides/architecture/core-editor-architecture#plugins plugins} and all examples in this chapter follow this convention.
+
+Also for the sake of simplicity all examples use the same {@link module:editor-classic/classiceditor~ClassicEditor `ClassicEditor`} but keep in mind that code snippets will work with other editors too.
+
+Finally, none of the converters covered in this guide require to import any module from CKEditor 5 Framework, hence, you can write them without rebuilding the editor. In other words, such converters can easily be added to existing CKEditor 5 builds.
+
+### Granular converters
+
+You can create separate converters for data and editing (downcast) pipelines. The former (`dataDowncast`) will customize the data in the editor output (e.g. when {@link module:core/editor/utils/dataapimixin~DataApi#getData `editor.getData()`}) and the later (`editingDowncast`) will only work for the content of the editor when editing.
+
+If you do not want to complicate your conversion, you can just add a single (`downcast`) converter which will apply both to the data and the editing view. We did that in all examples to keep them simple but keep in mind you have options:
+
+```js
+// Adds a conversion dispatcher for the editing downcast pipeline only.
+editor.conversion.for( 'editingDowncast' ).add( dispatcher => {
+	// ...
+} );
+
+// Adds a conversion dispatcher for the data downcast pipeline only.
+editor.conversion.for( 'dataDowncast' ).add( dispatcher => {
+	// ...
+} );
+
+// Adds a conversion dispatcher for both data and editing downcast pipelines.
+editor.conversion.for( 'downcast' ).add( dispatcher => {
+	// ...
+} );
+```
+
+### CKEditor 5 inspector
+
+{@link framework/guides/development-tools#ckeditor-5-inspector CKEditor 5 inspector} is an invaluable help with working with the model and view structures. It allows browsing their structure and checking selection positions like in typical browser dev tools. Make sure to enable CKEditor 5 inspector when playing with CKEditor 5.
+
+## Adding a CSS class to inline elements
+
+In this example all links (`<a href="...">...</a>`) get the `.my-green-link` CSS class. That includes all links in the editor output (`editor.getData()`) and all links in the edited content (existing and future ones).
+
+<info-box>
+	Note that the same behavior can be obtained with {@link features/link#custom-link-attributes-decorators link decorators}:
+
+	```js
+	ClassicEditor
+		.create( ..., {
+			// ...
+			link: {
+				decorators: {
+					addGreenLink: {
+						mode: 'automatic',
+						attributes: {
+							class: 'my-green-link'
+						}
+					}
+				}
+			}
+		} )
+	```
+</info-box>
+
+{@snippet framework/extending-content-add-link-class}
+
+Adding a custom CSS class to all links is made by a custom converter plugged into the downcast pipeline, following the default converters brought by the {@link features/link Link} feature:
+
+```js
+// This plugin brings customization to the downcast pipeline of the editor.
+function AddClassToAllLinks( editor ) {
+	// Both data and editing pipelines are affected by this conversion.
+	editor.conversion.for( 'downcast' ).add( dispatcher => {
+		// Links are represented in the model as a "linkHref" attribute.
+		// Use the "low" listener priority to apply the changes after the Link feature.
+		dispatcher.on( 'attribute:linkHref', ( evt, data, conversionApi ) => {
+			const viewWriter = conversionApi.writer;
+			const viewSelection = viewWriter.document.selection;
+
+			// Adding a new CSS class is done by wrapping all link ranges and selection
+			// in a new attribute element with a class.
+			const viewElement = viewWriter.createAttributeElement( 'a', {
+					class: 'my-green-link'
+				}, {
+					priority: 5
+				} );
+
+			if ( data.item.is( 'selection' ) ) {
+				viewWriter.wrap( viewSelection.getFirstRange(), viewElement );
+			} else {
+				viewWriter.wrap( conversionApi.mapper.toViewRange( data.range ), viewElement );
+			}
+		}, { priority: 'low' } );
+	} );
+}
+```
+
+Activate the plugin in the editor:
+
+```js
+ClassicEditor
+	.create( ..., {
+		extraPlugins: [ AddClassToAllLinks ],
+	} )
+	.then( editor => {
+		// ...
+	} )
+	.catch( err => {
+		console.error( err.stack );
+	} );
+```
+
+Add some CSS styles for `.my-green-link` to see the customization in action:
+
+```css
+.my-green-link {
+	color: #209a25;
+	border: 1px solid #209a25;
+	border-radius: 2px;
+	padding: 0 3px;
+	box-shadow: 1px 1px 0 0 #209a25;
+}
+```
+
+## Adding an HTML attribute to certain inline elements
+
+In this example all links (`<a href="...">...</a>`) which do not have "ckeditor.com" in their `href="..."` get the `target="_blank"` attribute. That includes all links in the editor output (`editor.getData()`) and all links in the edited content (existing and future ones).
+
+<info-box>
+	Note that similar behavior can be obtained with {@link module:link/link~LinkConfig#addTargetToExternalLinks link decorators}:
+
+	```js
+	ClassicEditor
+		.create( ..., {
+			// ...
+			link: {
+				addTargetToExternalLinks: true
+			}
+		} )
+	```
+</info-box>
+
+{@snippet framework/extending-content-add-external-link-target}
+
+**Note:** Edit the URL of the links including "ckeditor.com" and other domains to see them marked as "internal" or "external".
+
+Adding the `target` attribute to all "external" links is made by a custom converter plugged into the downcast pipeline, following the default converters brought by the {@link features/link Link} feature:
+
+```js
+// This plugin brings customization to the downcast pipeline of the editor.
+function AddTargetToExternalLinks( editor ) {
+	// Both data and editing pipelines are affected by this conversion.
+	editor.conversion.for( 'downcast' ).add( dispatcher => {
+		// Links are represented in the model as a "linkHref" attribute.
+		// Use the "low" listener priority to apply the changes after the Link feature.
+		dispatcher.on( 'attribute:linkHref', ( evt, data, conversionApi ) => {
+			const viewWriter = conversionApi.writer;
+			const viewSelection = viewWriter.document.selection;
+
+			// Adding a new CSS class is done by wrapping all link ranges and selection
+			// in a new attribute element with the "target" attribute.
+			const viewElement = viewWriter.createAttributeElement( 'a', {
+					target: '_blank'
+				}, {
+					priority: 5
+				} );
+
+			if ( data.attributeNewValue.match( /ckeditor\.com/ ) ) {
+				viewWriter.unwrap( conversionApi.mapper.toViewRange( data.range ), viewElement );
+			} else {
+				if ( data.item.is( 'selection' ) ) {
+					viewWriter.wrap( viewSelection.getFirstRange(), viewElement );
+				} else {
+					viewWriter.wrap( conversionApi.mapper.toViewRange( data.range ), viewElement );
+				}
+			}
+		}, { priority: 'low' } );
+	} );
+}
+```
+
+Activate the plugin in the editor:
+
+```js
+ClassicEditor
+	.create( ..., {
+		extraPlugins: [ AddTargetToExternalLinks ],
+	} )
+	.then( editor => {
+		// ...
+	} )
+	.catch( err => {
+		console.error( err.stack );
+	} );
+```
+
+Add some CSS styles for links with `target="_blank"` to mark them with with the "&#10697;" symbol:
+
+```css
+a[target="_blank"]::after {
+	content: '\29C9';
+}
+```
+
+## Adding a CSS class to certain inline elements
+
+In this example all links (`<a href="...">...</a>`) which do not have "https://" in their `href="..."` attribute get the `.unsafe-link` CSS class. That includes all links in the editor output (`editor.getData()`) and all links in the edited content (existing and future ones).
+
+<info-box>
+	Note that the same behavior can be obtained with {@link features/link#custom-link-attributes-decorators link decorators}:
+
+	```js
+	ClassicEditor
+		.create( ..., {
+			// ...
+			link: {
+				decorators: {
+					markUnsafeLink: {
+						mode: 'automatic',
+						callback: url => /^(http:)?\/\//.test( url ),
+						attributes: {
+							class: 'unsafe-link'
+						}
+					}
+				}
+			}
+		} )
+	```
+</info-box>
+
+{@snippet framework/extending-content-add-unsafe-link-class}
+
+**Note:** Edit the URL of the links using "http://" or "https://" to see them marked as "safe" or "unsafe".
+
+Adding the `.unsafe-link` CSS class to all "unsafe" links is made by a custom converter plugged into the downcast pipeline, following the default converters brought by the {@link features/link Link} feature:
+
+```js
+// This plugin brings customization to the downcast pipeline of the editor.
+function AddClassToUnsafeLinks( editor ) {
+	// Both data and editing pipelines are affected by this conversion.
+	editor.conversion.for( 'downcast' ).add( dispatcher => {
+		// Links are represented in the model as a "linkHref" attribute.
+		// Use the "low" listener priority to apply the changes after the Link feature.
+		dispatcher.on( 'attribute:linkHref', ( evt, data, conversionApi ) => {
+			const viewWriter = conversionApi.writer;
+			const viewSelection = viewWriter.document.selection;
+
+			// Adding a new CSS class is done by wrapping all link ranges and selection
+			// in a new attribute element with the "target" attribute.
+			const viewElement = viewWriter.createAttributeElement( 'a', {
+					class: 'unsafe-link'
+				}, {
+					priority: 5
+				} );
+
+			if ( data.attributeNewValue.match( /http:\/\// ) ) {
+				if ( data.item.is( 'selection' ) ) {
+					viewWriter.wrap( viewSelection.getFirstRange(), viewElement );
+				} else {
+					viewWriter.wrap( conversionApi.mapper.toViewRange( data.range ), viewElement );
+				}
+			} else {
+				viewWriter.unwrap( conversionApi.mapper.toViewRange( data.range ), viewElement );
+			}
+		}, { priority: 'low' } );
+	} );
+}
+```
+
+Activate the plugin in the editor:
+
+```js
+ClassicEditor
+	.create( ..., {
+		extraPlugins: [ AddClassToUnsafeLinks ],
+	} )
+	.then( editor => {
+		// ...
+	} )
+	.catch( err => {
+		console.error( err.stack );
+	} );
+```
+
+Add some CSS styles for "unsafe" to make them visible:
+
+```css
+.unsafe-link {
+	padding: 0 2px;
+	outline: 2px dashed red;
+	background: #ffff00;
+}
+```
+
+## Adding a CSS class to block elements
+
+In this example all second–level headings (`<h2>...</h2>`) get the `.my-heading` CSS class. That includes all heading elements in the editor output (`editor.getData()`) and in the edited content (existing and future ones).
+
+{@snippet framework/extending-content-add-heading-class}
+
+Adding a custom CSS class to all `<h2>...</h2>` elements is made by a custom converter plugged into the downcast pipeline, following the default converters brought by the {@link features/headings Headings} feature:
+
+<info-box>
+	The `heading1` element in the model corresponds to `<h2>...</h2>` in the output HTML because in the default {@link features/headings#configuring-heading-levels headings feature configuration} `<h1>...</h1>` is reserved for the top–most heading of a webpage.
+</info-box>
+
+```js
+// This plugin brings customization to the downcast pipeline of the editor.
+function AddClassToAllHeading1( editor ) {
+	// Both data and editing pipelines are affected by this conversion.
+	editor.conversion.for( 'downcast' ).add( dispatcher => {
+		// Headings are represented in the model as a "heading1" element.
+		// Use the "low" listener priority to apply the changes after the Headings feature.
+		dispatcher.on( 'insert:heading1', ( evt, data, conversionApi ) => {
+			const viewWriter = conversionApi.writer;
+
+			viewWriter.addClass( 'my-heading', conversionApi.mapper.toViewElement( data.item ) );
+		}, { priority: 'low' } );
+	} );
+}
+```
+
+Activate the plugin in the editor:
+
+```js
+ClassicEditor
+	.create( ..., {
+		extraPlugins: [ AddClassToAllHeading1 ],
+	} )
+	.then( editor => {
+		// ...
+	} )
+	.catch( err => {
+		console.error( err.stack );
+	} );
+```
+
+Add some CSS styles for `.my-heading` to see the customization in action:
+
+```css
+.my-heading {
+	font-family: Georgia, Times, Times New Roman, serif;
+	border-left: 6px solid #fd0000;
+	padding-left: .8em;
+	padding: .1em .8em;
+}
+```

Файловите разлики са ограничени, защото са твърде много
+ 128 - 0
packages/ckeditor5-engine/docs/framework/guides/deep-dive/conversion-introduction.md


Файловите разлики са ограничени, защото са твърде много
+ 449 - 0
packages/ckeditor5-engine/docs/framework/guides/deep-dive/conversion-preserving-custom-content.md


+ 12 - 0
packages/ckeditor5-engine/src/view/renderer.js

@@ -102,6 +102,13 @@ export default class Renderer {
 		this.isFocused = false;
 
 		/**
+		 * Indicates if the composition is in progress inside the view document view.
+		 *
+		 * @member {Boolean}
+		 */
+		this.isComposing = false;
+
+		/**
 		 * The text node in which the inline filler was rendered.
 		 *
 		 * @private
@@ -760,6 +767,11 @@ export default class Renderer {
 	 * @returns {Boolean}
 	 */
 	_domSelectionNeedsUpdate( domSelection ) {
+		// Remain DOM selection untouched while composing. See #1782.
+		if ( this.isComposing ) {
+			return false;
+		}
+
 		if ( !this.domConverter.isDomSelectionCorrect( domSelection ) ) {
 			// Current DOM selection is in incorrect position. We need to update it.
 			return true;

+ 1 - 0
packages/ckeditor5-engine/src/view/view.js

@@ -105,6 +105,7 @@ export default class View {
 		 */
 		this._renderer = new Renderer( this.domConverter, this.document.selection );
 		this._renderer.bind( 'isFocused' ).to( this.document );
+		this._renderer.bind( 'isComposing' ).to( this.document );
 
 		/**
 		 * A DOM root attributes cache. It saves the initial values of DOM root attributes before the DOM element

+ 42 - 0
packages/ckeditor5-engine/tests/view/renderer.js

@@ -3687,6 +3687,48 @@ describe( 'Renderer', () => {
 				return viewData.repeat( repeat );
 			}
 		} );
+
+		// #1782
+		it( 'should leave dom selection untouched while composing', () => {
+			const { view: viewP, selection: newSelection } = parse( '<container:p>[]</container:p>' );
+
+			viewRoot._appendChild( viewP );
+			selection._setTo( newSelection );
+
+			renderer.markToSync( 'children', viewRoot );
+			renderer.render();
+
+			// Mock IME typing in Safari: <p>[c]</p>.
+			renderer.isComposing = true;
+			const domText = document.createTextNode( 'c' );
+			domRoot.firstChild.appendChild( domText );
+			const range = document.createRange();
+			range.setStart( domText, 0 );
+			range.setEnd( domText, 1 );
+			const domSelection = document.getSelection();
+			domSelection.removeAllRanges();
+			domSelection.addRange( range );
+
+			// <container:p>c[]</container:p>
+			viewP._appendChild( new ViewText( 'c' ) );
+			selection._setTo( [
+				new ViewRange( new ViewPosition( viewP.getChild( 0 ), 1 ), new ViewPosition( viewP.getChild( 0 ), 1 ) )
+			] );
+
+			renderer.markToSync( 'children', viewP );
+			renderer.render();
+
+			expect( domRoot.childNodes.length ).to.equal( 1 );
+			expect( domRoot.firstChild.childNodes.length ).to.equal( 1 );
+			expect( domRoot.firstChild.firstChild.data ).to.equal( 'c' );
+
+			const currentRange = domSelection.getRangeAt( 0 );
+			expect( currentRange.collapsed ).to.equal( false );
+			expect( currentRange.startContainer ).to.equal( domRoot.firstChild.firstChild );
+			expect( currentRange.startOffset ).to.equal( 0 );
+			expect( currentRange.endContainer ).to.equal( domRoot.firstChild.firstChild );
+			expect( currentRange.endOffset ).to.equal( 1 );
+		} );
 	} );
 
 	describe( '#922', () => {

+ 9 - 1
packages/ckeditor5-engine/theme/placeholder.css

@@ -4,7 +4,8 @@
  */
 
 /* See ckeditor/ckeditor5#936. */
-.ck.ck-placeholder, .ck .ck-placeholder {
+.ck.ck-placeholder,
+.ck .ck-placeholder {
 	&::before {
 		content: attr(data-placeholder);
 
@@ -12,3 +13,10 @@
 		pointer-events: none;
 	}
 }
+
+/* See ckeditor/ckeditor5#1987. */
+.ck.ck-read-only .ck-placeholder {
+	&::before {
+		display: none;
+	}
+}