瀏覽代碼

Merge pull request #1 from ckeditor/t/ckeditor5/908

Feature: Introduced the Remove Format feature. Closes ckeditor/ckeditor5#908.
Aleksander Nowodzinski 6 年之前
父節點
當前提交
b20b3cabbf

+ 0 - 0
packages/ckeditor5-remove-format/docs/_snippets/features/build-remove-format-source.html


+ 24 - 0
packages/ckeditor5-remove-format/docs/_snippets/features/build-remove-format-source.js

@@ -0,0 +1,24 @@
+/**
+ * @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 RemoveFormat from '@ckeditor/ckeditor5-remove-format/src/removeformat';
+
+import Alignment from '@ckeditor/ckeditor5-alignment/src/alignment';
+import Font from '@ckeditor/ckeditor5-font/src/font';
+import Bold from '@ckeditor/ckeditor5-basic-styles/src/bold';
+import Italic from '@ckeditor/ckeditor5-basic-styles/src/italic';
+import Underline from '@ckeditor/ckeditor5-basic-styles/src/underline';
+import Strikethrough from '@ckeditor/ckeditor5-basic-styles/src/strikethrough';
+import Subscript from '@ckeditor/ckeditor5-basic-styles/src/subscript';
+import Superscript from '@ckeditor/ckeditor5-basic-styles/src/superscript';
+import Code from '@ckeditor/ckeditor5-basic-styles/src/code';
+
+ClassicEditor.builtinPlugins.push(
+	RemoveFormat, Alignment, Font, Bold, Italic, Underline, Strikethrough, Subscript, Superscript, Code );
+
+window.ClassicEditor = ClassicEditor;

+ 23 - 0
packages/ckeditor5-remove-format/docs/_snippets/features/remove-format.html

@@ -0,0 +1,23 @@
+<div id="editor">
+	<p>
+		You can use the <strong>remove format</strong> feature to easily clean up text formatting.
+	</p>
+	<p>
+		Some examples of the formatting removed by this feature:
+		<strong>bold</strong>,
+		<i>italics</i>,
+		<u>underline</u>,
+		<s>strikethrough</s>,
+		<code>code</code>,
+		<sub>subscript</sub>,
+		<sup>superscript</sup>,
+		<span class="text-tiny">font size</span> as well as
+		<span style="font-family: 'Courier New', Courier, monospace;">font family</span>.
+	</p>
+	<p style="text-align: center">
+		Remove format resets the text alignment too.
+	</p>
+	<p>
+		<b>Note</b>: The feature will not erase non-formatting content — <a href="https://ckeditor.com">your links</a> are safe!
+	</p>
+</div>

+ 37 - 0
packages/ckeditor5-remove-format/docs/_snippets/features/remove-format.js

@@ -0,0 +1,37 @@
+/**
+ * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/* globals ClassicEditor, console, window, document */
+
+ClassicEditor
+	.create( document.querySelector( '#editor' ), {
+		toolbar: {
+			items: [
+				'removeformat',
+				'|',
+				'bold',
+				'italic',
+				'underline',
+				'strikethrough',
+				'code',
+				'subscript',
+				'superscript',
+				'fontSize',
+				'fontFamily',
+				'alignment',
+				'link',
+				'|',
+				'undo',
+				'redo'
+			],
+			viewportTopOffset: window.getViewportTopOffsetConfig()
+		}
+	} )
+	.then( editor => {
+		window.editor = editor;
+	} )
+	.catch( err => {
+		console.error( err.stack );
+	} );

+ 34 - 0
packages/ckeditor5-remove-format/docs/api/remove-format.md

@@ -0,0 +1,34 @@
+---
+category: api-reference
+---
+
+# CKEditor 5 remove format feature
+
+[![npm version](https://badge.fury.io/js/%40ckeditor%2Fckeditor5-remove-format.svg)](https://www.npmjs.com/package/@ckeditor/ckeditor5-remove-format)
+
+This package implements the remove format feature for CKEditor 5.
+
+## Demo
+
+Check out the {@link features/remove-format#demo demo in the remove format feature} guide.
+
+## Documentation
+
+See the {@link features/remove-format remove format feature} guide and the {@link module:remove-format/removeformat~RemoveFormat} plugin documentation.
+
+## Installation
+
+```bash
+npm install --save @ckeditor/ckeditor5-remove-format
+```
+
+## Contribute
+
+The source code of this package is available on GitHub in https://github.com/ckeditor/ckeditor5-remove-format.
+
+## External links
+
+* [`@ckeditor/ckeditor5-remove-format` on npm](https://www.npmjs.com/package/@ckeditor/ckeditor5-remove-format)
+* [`ckeditor/ckeditor5-remove-format` on GitHub](https://github.com/ckeditor/ckeditor5-remove-format)
+* [Issue tracker](https://github.com/ckeditor/ckeditor5-remove-format/issues)
+* [Changelog](https://github.com/ckeditor/ckeditor5-remove-format/blob/master/CHANGELOG.md)

+ 68 - 0
packages/ckeditor5-remove-format/docs/features/remove-format.md

@@ -0,0 +1,68 @@
+---
+title: Removing Text Formatting
+menu-title: Remove Format
+category: features
+---
+
+{@snippet features/build-remove-format-source}
+
+The {@link module:remove-format/removeformat~RemoveFormat Remove Format} feature allows to quickly remove any text formatting applied using inline HTML elements and CSS styles, like {@link features/basic-styles basic text styles} (bold, italic, etc.), {@link features/font font family, size, and color} and similar. Note that block—level formatting ({@link features/headings headings}, {@link features/image images}) and semantic data ({@link features/link links}) will not be removed.
+
+## Demo
+
+Select the content you want to clean up and press the "Remove Format" button in the toolbar:
+
+{@snippet features/remove-format}
+
+## Configuring the remove format feature
+
+The feature has no integration–level configuration. Once enabled, it works out–of–the–box with all {@link features/index core editor features}.
+
+## Integrating with editor features
+
+To make it possible for the remove formatting feature to work with your custom content, you must first mark it in the {@link framework/guides/architecture/editing-engine#schema schema}. All you need to do is set the `isFormatting` property on your custom {@link framework/guides/architecture/editing-engine#text-attributes text attribute}. {@link module:engine/model/schema~Schema#setAttributeProperties Learn more about attribute properties.}
+
+## Installation
+
+To add this feature to your editor install the [`@ckeditor/ckeditor5-remove-format`](https://www.npmjs.com/package/@ckeditor/ckeditor5-remove-format) package:
+
+```bash
+npm install --save @ckeditor/ckeditor5-remove-format
+```
+
+And add it to your plugin list and the toolbar configuration:
+
+```js
+import RemoveFormat from '@ckeditor/ckeditor5-remove-format/src/removeformat';
+
+ClassicEditor
+	.create( document.querySelector( '#editor' ), {
+		plugins: [ RemoveFormat, ... ],
+		toolbar: [ 'removeFormat', ... ]
+	} )
+	.then( ... )
+	.catch( ... );
+```
+
+<info-box info>
+	Read more about {@link builds/guides/integration/installing-plugins installing plugins}.
+</info-box>
+
+## Common API
+
+The {@link module:remove-format/removeformat~RemoveFormat} plugin registers the `'removeFormat'` UI button component the command of the same name implemented by {@link module:remove-format/removeformatcommand~RemoveFormatCommand}.
+
+The command can be executed using the {@link module:core/editor/editor~Editor#execute `editor.execute()`} method:
+
+```js
+// Removes all the formatting in the selection.
+editor.execute( 'removeFormat' );
+```
+
+<info-box>
+	We recommend using the official {@link framework/guides/development-tools#ckeditor-5-inspector CKEditor 5 inspector} for development and debugging. It will give you tons of useful information about the state of the editor such as internal data structures, selection, commands, and many more.
+</info-box>
+
+## Contribute
+
+The source code of the feature is available on GitHub in https://github.com/ckeditor/ckeditor5-remove-format.

+ 3 - 0
packages/ckeditor5-remove-format/lang/contexts.json

@@ -0,0 +1,3 @@
+{
+	"Remove Format": "The label of the remove format toolbar button."
+}

+ 3 - 2
packages/ckeditor5-remove-format/package.json

@@ -12,7 +12,8 @@
   "dependencies": {
     "@ckeditor/ckeditor5-core": "^12.0.0",
     "@ckeditor/ckeditor5-ui": "^12.0.0",
-    "@ckeditor/ckeditor5-utils": "^12.0.0"
+    "@ckeditor/ckeditor5-utils": "^12.0.0",
+    "eslint-plugin-ckeditor5-rules": "^0.0.2"
   },
   "devDependencies": {
     "@ckeditor/ckeditor5-basic-styles": "^11.0.0",
@@ -24,7 +25,7 @@
     "@ckeditor/ckeditor5-link": "^11.0.0",
     "@ckeditor/ckeditor5-typing": "^12.0.0",
     "@ckeditor/ckeditor5-undo": "^11.0.0",
-    "eslint": "^5.5.0",
+    "eslint": "^5.15.3",
     "eslint-config-ckeditor5": "^1.0.11",
     "husky": "^1.3.1",
     "lint-staged": "^7.0.0"

+ 45 - 0
packages/ckeditor5-remove-format/src/removeformat.js

@@ -0,0 +1,45 @@
+/**
+ * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/**
+ * @module remove-format/removeformat
+ */
+
+import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
+
+import RemoveFormatUI from './removeformatui';
+import RemoveFormatCommand from './removeformatcommand';
+
+/**
+ * The remove format plugin.
+ *
+ * This is a "glue" plugin which loads the {@link module:remove-format/removeformatcommand~RemoveFormatCommand command}
+ * and the {@link module:remove-format/removeformatui~RemoveFormatUI UI}.
+ *
+ * For a detailed overview, check out the {@glink features/remove-format Remove Format} feature documentation.
+ *
+ * @extends module:core/plugin~Plugin
+ */
+export default class RemoveFormat extends Plugin {
+	/**
+	 * @inheritDoc
+	 */
+	static get requires() {
+		return [ RemoveFormatCommand, RemoveFormatUI ];
+	}
+
+	init() {
+		const editor = this.editor;
+
+		editor.commands.add( 'removeFormat', new RemoveFormatCommand( editor ) );
+	}
+
+	/**
+	 * @inheritDoc
+	 */
+	static get pluginName() {
+		return 'RemoveFormat';
+	}
+}

+ 106 - 0
packages/ckeditor5-remove-format/src/removeformatcommand.js

@@ -0,0 +1,106 @@
+/**
+ * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/**
+ * @module remove-format/removeformatcommand
+ */
+
+import Command from '@ckeditor/ckeditor5-core/src/command';
+import first from '@ckeditor/ckeditor5-utils/src/first';
+
+/**
+ * The remove format command.
+ *
+ * It is used by the {@link module:remove-format/removeformat~RemoveFormat remove format feature}
+ * to clear the formatting in the selection.
+ *
+ *		editor.execute( 'removeFormat' );
+ *
+ * @extends module:core/command~Command
+ */
+export default class RemoveFormatCommand extends Command {
+	/**
+	 * @inheritDoc
+	 */
+	refresh() {
+		const model = this.editor.model;
+
+		this.isEnabled = !!first( this._getFormattingItems( model.document.selection, model.schema ) );
+	}
+
+	/**
+	 * @inheritDoc
+	 */
+	execute() {
+		const model = this.editor.model;
+		const schema = model.schema;
+
+		model.change( writer => {
+			for ( const item of this._getFormattingItems( model.document.selection, schema ) ) {
+				if ( item.is( 'selection' ) ) {
+					for ( const attributeName of this._getFormattingAttributes( item, schema ) ) {
+						writer.removeSelectionAttribute( attributeName );
+					}
+				} else {
+					// Workaround for items with multiple removable attributes. See
+					// https://github.com/ckeditor/ckeditor5-remove-format/pull/1#pullrequestreview-220515609
+					const itemRange = writer.createRangeOn( item );
+
+					for ( const attributeName of this._getFormattingAttributes( item, schema ) ) {
+						writer.removeAttribute( attributeName, itemRange );
+					}
+				}
+			}
+		} );
+	}
+
+	/**
+	 * Returns an iterable of items in a selection (including selection itself) that have formatting model
+	 * attributes to be removed by the feature.
+	 *
+	 * @protected
+	 * @param {module:engine/model/documentselection~DocumentSelection} selection
+	 * @param {module:engine/model/schema~Schema} schema Schema describing the item.
+	 * @returns {Iterable.<module:engine/model/item~Item>|Iterable.<module:engine/model/documentselection~DocumentSelection>}
+	 */
+	* _getFormattingItems( selection, schema ) {
+		const itemHasRemovableFormatting = item => {
+			return !!first( this._getFormattingAttributes( item, schema ) );
+		};
+
+		for ( const curRange of selection.getRanges() ) {
+			for ( const item of curRange.getItems() ) {
+				if ( itemHasRemovableFormatting( item ) ) {
+					yield item;
+				}
+			}
+		}
+
+		// Finally the selection might be formatted as well, so make sure to check it.
+		if ( itemHasRemovableFormatting( selection ) ) {
+			yield selection;
+		}
+	}
+
+	/**
+	 * Returns an iterable of formatting attributes of a given model item.
+	 *
+	 * **Note:** Formatting items have the `isFormatting` property set `true`.
+	 *
+	 * @protected
+	 * @param {module:engine/model/item~Item|module:engine/model/documentselection~DocumentSelection} item
+	 * @param {module:engine/model/schema~Schema} schema Schema describing the item.
+	 * @returns {Iterable.<String>} Names of formatting attributes found in a given item.
+	 */
+	* _getFormattingAttributes( item, schema ) {
+		for ( const [ attributeName ] of item.getAttributes() ) {
+			const attributeProperties = schema.getAttributeProperties( attributeName );
+
+			if ( attributeProperties && attributeProperties.isFormatting ) {
+				yield attributeName;
+			}
+		}
+	}
+}

+ 55 - 0
packages/ckeditor5-remove-format/src/removeformatui.js

@@ -0,0 +1,55 @@
+/**
+ * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/**
+ * @module remove-format/removeformatui
+ */
+
+import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
+import ButtonView from '@ckeditor/ckeditor5-ui/src/button/buttonview';
+import removeFormatIcon from '../theme/icons/remove-format.svg';
+
+const REMOVE_FORMAT = 'removeFormat';
+
+/**
+ * The remove format UI plugin. It registers a `'removeFormat'` button which can be
+ * used in the toolbar.
+ *
+ * @extends module:core/plugin~Plugin
+ */
+export default class RemoveFormatUI extends Plugin {
+	/**
+	 * @inheritDoc
+	 */
+	static get pluginName() {
+		return 'RemoveFormatUI';
+	}
+
+	/**
+	 * @inheritDoc
+	 */
+	init() {
+		const editor = this.editor;
+		const t = editor.t;
+
+		editor.ui.componentFactory.add( REMOVE_FORMAT, locale => {
+			const command = editor.commands.get( REMOVE_FORMAT );
+			const view = new ButtonView( locale );
+
+			view.set( {
+				label: t( 'Remove Format' ),
+				icon: removeFormatIcon,
+				tooltip: true
+			} );
+
+			view.bind( 'isOn', 'isEnabled' ).to( command, 'value', 'isEnabled' );
+
+			// Execute the command.
+			this.listenTo( view, 'execute', () => editor.execute( REMOVE_FORMAT ) );
+
+			return view;
+		} );
+	}
+}

+ 94 - 0
packages/ckeditor5-remove-format/tests/integration.js

@@ -0,0 +1,94 @@
+/**
+ * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/* global document */
+
+import RemoveFormat from '../src/removeformat';
+import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';
+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 Link from '@ckeditor/ckeditor5-link/src/link';
+import Bold from '@ckeditor/ckeditor5-basic-styles/src/bold';
+import Underline from '@ckeditor/ckeditor5-basic-styles/src/underline';
+
+import ClassicTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/classictesteditor';
+import {
+	getData as getModelData,
+	setData as setModelData
+} from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
+
+describe( 'RemoveFormat', () => {
+	let editor, model, element;
+
+	beforeEach( () => {
+		element = document.createElement( 'div' );
+		document.body.appendChild( element );
+
+		return ClassicTestEditor
+			.create( element, {
+				plugins: [ Paragraph, Heading, Image, Bold, Underline, RemoveFormat, Image, ImageCaption, Link ]
+			} )
+			.then( newEditor => {
+				editor = newEditor;
+				model = editor.model;
+			} );
+	} );
+
+	afterEach( () => {
+		element.remove();
+
+		return editor.destroy();
+	} );
+
+	describe( 'works correctly with multiline ranges', () => {
+		it( 'does remove pure formatting markup', () => {
+			setModelData( model, '<paragraph>foo[<$text bold="true">foo</$text></paragraph>' +
+				'<paragraph><$text bold="true">bar</$text>]bar</paragraph>' );
+
+			editor.execute( 'removeFormat' );
+
+			expect( getModelData( model ) )
+				.to.equal( '<paragraph>foo[foo</paragraph><paragraph>bar]bar</paragraph>' );
+		} );
+
+		it( 'does not touch non-formatting markup', () => {
+			setModelData( model, '<paragraph>[<$text linkHref="url">foo</$text></paragraph><image src="assets/sample.png">' +
+				'<caption>caption</caption></image><paragraph>bar]</paragraph>' );
+
+			editor.execute( 'removeFormat' );
+
+			expect( getModelData( model ) )
+				.to.equal( '<paragraph>[<$text linkHref="url">foo</$text></paragraph>' +
+					'<image src="assets/sample.png"><caption>caption</caption></image><paragraph>bar]</paragraph>' );
+		} );
+
+		it( 'removes the content from within widget editable', () => {
+			setModelData( model, '<paragraph>[</paragraph>' +
+				'<image src="assets/sample.png"><caption><$text bold="true">foo</$text></caption></image><paragraph>bar]</paragraph>' );
+
+			editor.execute( 'removeFormat' );
+
+			expect( getModelData( model ) )
+				.to.equal( '<paragraph>' +
+					'[</paragraph><image src="assets/sample.png"><caption>foo</caption></image><paragraph>bar]</paragraph>' );
+		} );
+	} );
+
+	describe( 'Handles correctly known issues', () => {
+		it( 'doesn\'t break after removing format from attribute wrapped around another attribute', () => {
+			// Edge case reported in https://github.com/ckeditor/ckeditor5-remove-format/pull/1#pullrequestreview-220515609
+			setModelData( model, '<paragraph>' +
+					'f[<$text underline="true">o</$text>' +
+					'<$text underline="true" bold="true">ob</$text>' +
+					'<$text underline="true">a</$text>]r' +
+				'</paragraph>' );
+
+			editor.execute( 'removeFormat' );
+
+			expect( getModelData( model ) ).to.equal( '<paragraph>f[ooba]r</paragraph>' );
+		} );
+	} );
+} );

二進制
packages/ckeditor5-remove-format/tests/manual/assets/sample.png


+ 20 - 0
packages/ckeditor5-remove-format/tests/manual/removeformat.html

@@ -0,0 +1,20 @@
+<div id="editor">
+	<p>
+		Paragraph with
+		<strong>bold</strong>
+		,
+		<em>emphasize</em>
+		and some
+		<u>underline</u>
+		.
+	</p>
+
+	<figure class="image">
+		<img src="assets/sample.png" alt="Autumn fields">
+		<figcaption>Autumn <em>fields</em> by Aleksander Nowodzi&#x144;ski</figcaption>
+	</figure>
+
+	<p>
+		<a href="#">Links</a> are not removed.
+	</p>
+</div>

+ 39 - 0
packages/ckeditor5-remove-format/tests/manual/removeformat.js

@@ -0,0 +1,39 @@
+/**
+ * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/* global console, window */
+
+import global from '@ckeditor/ckeditor5-utils/src/dom/global';
+
+import ClassicEditor from '@ckeditor/ckeditor5-editor-classic/src/classiceditor';
+import Bold from '@ckeditor/ckeditor5-basic-styles/src/bold';
+import Enter from '@ckeditor/ckeditor5-enter/src/enter';
+import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';
+import Typing from '@ckeditor/ckeditor5-typing/src/typing';
+import Undo from '@ckeditor/ckeditor5-undo/src/undo';
+import Clipboard from '@ckeditor/ckeditor5-clipboard/src/clipboard';
+import ShiftEnter from '@ckeditor/ckeditor5-enter/src/shiftenter';
+import Image from '@ckeditor/ckeditor5-image/src/image';
+import ImageCaption from '@ckeditor/ckeditor5-image/src/imagecaption';
+import ImageToolbar from '@ckeditor/ckeditor5-image/src/imagetoolbar';
+import RemoveFormat from '../../src/removeformat';
+import Link from '@ckeditor/ckeditor5-link/src/link';
+import Italic from '@ckeditor/ckeditor5-basic-styles/src/italic';
+import Underline from '@ckeditor/ckeditor5-basic-styles/src/underline';
+
+ClassicEditor
+	.create( global.document.querySelector( '#editor' ), {
+		plugins: [
+			Bold, Clipboard, Enter, Italic, Link, Paragraph, RemoveFormat, ShiftEnter, Typing,
+			Underline, Undo, Image, Image, ImageCaption, ImageToolbar
+		],
+		toolbar: [ 'removeFormat', '|', 'italic', 'bold', 'link', 'underline', '|', 'undo', 'redo' ]
+	} )
+	.then( editor => {
+		window.editor = editor;
+	} )
+	.catch( err => {
+		console.error( err.stack );
+	} );

+ 10 - 0
packages/ckeditor5-remove-format/tests/manual/removeformat.md

@@ -0,0 +1,10 @@
+# Remove Format
+
+Select the content and press the "Remove Format" button in the toolbar.
+
+## Things to consider
+
+* Bold, emphasize and underline should be removed.
+* Links **must not** be removed.
+* Try to remove some formatting from an image caption by creating a selection containing the entire widget.
+* The button should be disabled if your selection doesn't contain any formatting that might be removed.

+ 164 - 0
packages/ckeditor5-remove-format/tests/removeformatcommand.js

@@ -0,0 +1,164 @@
+/**
+ * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+import RemoveFormatCommand from '../src/removeformatcommand';
+import Command from '@ckeditor/ckeditor5-core/src/command';
+import ModelTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/modeltesteditor';
+import {
+	getData,
+	setData
+} from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
+
+describe( 'RemoveFormatCommand', () => {
+	let editor, model, command;
+
+	beforeEach( () => {
+		return ModelTestEditor.create()
+			.then( newEditor => {
+				editor = newEditor;
+				model = editor.model;
+
+				command = new RemoveFormatCommand( newEditor );
+				editor.commands.add( 'removeFormat', command );
+
+				model.schema.register( 'p', {
+					inheritAllFrom: '$block'
+				} );
+
+				model.schema.addAttributeCheck( ( ctx, attributeName ) => {
+					// Bold will be used as an example formatting attribute.
+					if ( ctx.endsWith( 'p $text' ) && attributeName == 'bold' ) {
+						return true;
+					}
+				} );
+
+				model.schema.addAttributeCheck( ( ctx, attributeName ) => {
+					// Text attribtue "irrelevant" will be used to make sure that non-formatting
+					// is note being removed.
+					if ( ctx.endsWith( 'p $text' ) && attributeName == 'irrelevant' ) {
+						return true;
+					}
+				} );
+
+				model.schema.setAttributeProperties( 'bold', {
+					isFormatting: true
+				} );
+			} );
+	} );
+
+	afterEach( () => {
+		editor.destroy();
+	} );
+
+	it( 'is a command', () => {
+		expect( RemoveFormatCommand.prototype ).to.be.instanceOf( Command );
+		expect( command ).to.be.instanceOf( Command );
+	} );
+
+	describe( 'isEnabled', () => {
+		const expectEnabledPropertyToBe = expectedValue => expect( command ).to.have.property( 'isEnabled', expectedValue );
+		const cases = {
+			'state when in non-formatting markup': {
+				input: '<p>fo[]o</p>',
+				assert: () => expectEnabledPropertyToBe( false )
+			},
+
+			'state with collapsed selection in formatting markup': {
+				input: '<p>f<$text bold="true">o[]o</$text></p>',
+				assert: () => expectEnabledPropertyToBe( true )
+			},
+
+			'state with selection containing formatting in the middle': {
+				input: '<p>f[oo <$text bold="true">bar</$text> ba]z</p>',
+				assert: () => expectEnabledPropertyToBe( true )
+			},
+
+			'state with partially selected formatting at the start': {
+				input: '<p><$text bold="true">b[ar</$text> ba]z</p>',
+				assert: () => expectEnabledPropertyToBe( true )
+			},
+
+			'state with partially selected formatting at the end': {
+				input: '<p>f[oo <$text bold="true">ba]z</$text></p>',
+				assert: () => expectEnabledPropertyToBe( true )
+			},
+
+			'state with formatted selection alone': {
+				input: '<p>fo[]o</p>',
+				setDataOptions: {
+					selectionAttributes: {
+						bold: true,
+						irrelevant: true
+					}
+				},
+				assert: () => expectEnabledPropertyToBe( true )
+			}
+		};
+
+		generateTypicalUseCases( cases );
+	} );
+
+	describe( 'execute()', () => {
+		const expectModelToBeEqual = expectedValue => expect( getData( model ) ).to.equal( expectedValue );
+		const cases = {
+			'state when in non-formatting markup': {
+				input: '<p>fo[]o</p>',
+				assert: () => expectModelToBeEqual( '<p>fo[]o</p>' )
+			},
+
+			'state with collapsed selection in formatting markup': {
+				input: '<p>f<$text bold="true">o[]o</$text></p>',
+				assert: () => expectModelToBeEqual( '<p>f<$text bold="true">o</$text>[]<$text bold="true">o</$text></p>' )
+			},
+
+			'state with selection containing formatting in the middle': {
+				input: '<p>f[oo <$text bold="true">bar</$text> ba]z</p>',
+				assert: () => expectModelToBeEqual( '<p>f[oo bar ba]z</p>' )
+			},
+
+			'state with partially selected formatting at the start': {
+				input: '<p><$text bold="true">b[ar</$text> ba]z</p>',
+				assert: () => expectModelToBeEqual( '<p><$text bold="true">b</$text>[ar ba]z</p>' )
+			},
+
+			'state with partially selected formatting at the end': {
+				input: '<p>f[oo <$text bold="true">ba]z</$text></p>',
+				assert: () => expectModelToBeEqual( '<p>f[oo ba]<$text bold="true">z</$text></p>' )
+			},
+
+			'state with formatted selection alone': {
+				input: '<p>fo[]o</p>',
+				setDataOptions: {
+					selectionAttributes: {
+						bold: true,
+						irrelevant: true
+					}
+				},
+				assert: () => {
+					expect( model.document.selection.hasAttribute( 'bold' ) ).to.equal( false );
+					expect( model.document.selection.hasAttribute( 'irrelevant' ) ).to.equal( true );
+				}
+			}
+		};
+
+		generateTypicalUseCases( cases, {
+			beforeAssert: () => command.execute()
+		} );
+	} );
+
+	function generateTypicalUseCases( useCases, options ) {
+		for ( const [ key, testConfig ] of Object.entries( useCases ) ) {
+			it( key, () => {
+				setData( model, testConfig.input, testConfig.setDataOptions );
+
+				if ( options && options.beforeAssert ) {
+					options.beforeAssert();
+				}
+
+				testConfig.assert();
+			} );
+		}
+	}
+} );

+ 108 - 0
packages/ckeditor5-remove-format/tests/removeformatui.js

@@ -0,0 +1,108 @@
+/**
+ * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/* global document */
+
+import RemoveFormat from '../src/removeformat';
+import RemoveFormatUI from '../src/removeformatui';
+import ClassicTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/classictesteditor';
+import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils';
+import {
+	_clear as clearTranslations,
+	add as addTranslations
+} from '@ckeditor/ckeditor5-utils/src/translation-service';
+
+describe( 'RemoveFormatUI', () => {
+	let editor, command, element, button;
+
+	testUtils.createSinonSandbox();
+
+	before( () => {
+		addTranslations( 'en', {
+			'Remove Format': 'Remove Format'
+		} );
+
+		addTranslations( 'pl', {
+			'Remove Format': 'Usuń formatowanie'
+		} );
+	} );
+
+	after( () => {
+		clearTranslations();
+	} );
+
+	beforeEach( () => {
+		element = document.createElement( 'div' );
+		document.body.appendChild( element );
+
+		return ClassicTestEditor
+			.create( element, {
+				plugins: [ RemoveFormat, RemoveFormatUI ]
+			} )
+			.then( newEditor => {
+				editor = newEditor;
+				command = editor.commands.get( 'removeFormat' );
+				button = editor.ui.componentFactory.create( 'removeFormat' );
+			} );
+	} );
+
+	afterEach( () => {
+		element.remove();
+
+		return editor.destroy();
+	} );
+
+	describe( 'removeformat button', () => {
+		describe( 'is bound to the command state', () => {
+			it( 'isEnabled', () => {
+				command.isEnabled = false;
+
+				expect( button.isEnabled ).to.be.false;
+
+				command.isEnabled = true;
+				expect( button.isEnabled ).to.be.true;
+			} );
+		} );
+
+		it( 'should change relay execute to the command', () => {
+			const commandSpy = testUtils.sinon.spy( command, 'execute' );
+
+			button.fire( 'execute' );
+
+			sinon.assert.calledOnce( commandSpy );
+		} );
+
+		describe( 'localization', () => {
+			beforeEach( () => {
+				return localizedEditor();
+			} );
+
+			it( 'label localized correctly', () => {
+				expect( button.label ).to.equal( 'Usuń formatowanie' );
+			} );
+
+			function localizedEditor() {
+				const editorElement = document.createElement( 'div' );
+				document.body.appendChild( editorElement );
+
+				return ClassicTestEditor
+					.create( editorElement, {
+						plugins: [ RemoveFormat, RemoveFormatUI ],
+						toolbar: [ 'removeFormat' ],
+						language: 'pl'
+					} )
+					.then( newEditor => {
+						editor = newEditor;
+						button = editor.ui.componentFactory.create( 'removeFormat' );
+						command = editor.commands.get( 'removeFormat' );
+
+						editorElement.remove();
+
+						return editor.destroy();
+					} );
+			}
+		} );
+	} );
+} );

File diff suppressed because it is too large
+ 1 - 0
packages/ckeditor5-remove-format/theme/icons/remove-format.svg