8
0
فهرست منبع

Merge pull request #7576 from ckeditor/i/6170

Feature (autoformat): Block autoformat can also be triggered in blocks other than paragraph. Closes #6170.

Feature (autoformat): Enabled autoformat feature also for blocks that are not empty.
Marek Lewandowski 5 سال پیش
والد
کامیت
5866d4199d

+ 2 - 1
packages/ckeditor5-autoformat/package.json

@@ -23,7 +23,8 @@
     "@ckeditor/ckeditor5-heading": "^20.0.0",
     "@ckeditor/ckeditor5-heading": "^20.0.0",
     "@ckeditor/ckeditor5-list": "^20.0.0",
     "@ckeditor/ckeditor5-list": "^20.0.0",
     "@ckeditor/ckeditor5-paragraph": "^20.0.0",
     "@ckeditor/ckeditor5-paragraph": "^20.0.0",
-    "@ckeditor/ckeditor5-undo": "^20.0.0"
+    "@ckeditor/ckeditor5-undo": "^20.0.0",
+    "@ckeditor/ckeditor5-utils": "^20.0.0"
   },
   },
   "engines": {
   "engines": {
     "node": ">=12.0.0",
     "node": ">=12.0.0",

+ 5 - 4
packages/ckeditor5-autoformat/src/autoformat.js

@@ -123,16 +123,17 @@ export default class Autoformat extends Plugin {
 		if ( command ) {
 		if ( command ) {
 			command.modelElements
 			command.modelElements
 				.filter( name => name.match( /^heading[1-6]$/ ) )
 				.filter( name => name.match( /^heading[1-6]$/ ) )
-				.forEach( commandValue => {
-					const level = commandValue[ 7 ];
+				.forEach( modelName => {
+					const level = modelName[ 7 ];
 					const pattern = new RegExp( `^(#{${ level }})\\s$` );
 					const pattern = new RegExp( `^(#{${ level }})\\s$` );
 
 
 					blockAutoformatEditing( this.editor, this, pattern, () => {
 					blockAutoformatEditing( this.editor, this, pattern, () => {
-						if ( !command.isEnabled ) {
+						// Should only be active if command is enabled and heading style associated with pattern is inactive.
+						if ( !command.isEnabled || command.value === modelName ) {
 							return false;
 							return false;
 						}
 						}
 
 
-						this.editor.execute( 'heading', { value: commandValue } );
+						this.editor.execute( 'heading', { value: modelName } );
 					} );
 					} );
 				} );
 				} );
 		}
 		}

+ 27 - 4
packages/ckeditor5-autoformat/src/blockautoformatediting.js

@@ -2,7 +2,9 @@
  * @license Copyright (c) 2003-2020, CKSource - Frederico Knabben. All rights reserved.
  * @license Copyright (c) 2003-2020, CKSource - Frederico Knabben. All rights reserved.
  * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
  * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
  */
  */
+
 import LiveRange from '@ckeditor/ckeditor5-engine/src/model/liverange';
 import LiveRange from '@ckeditor/ckeditor5-engine/src/model/liverange';
+import first from '@ckeditor/ckeditor5-utils/src/first';
 
 
 /**
 /**
  * The block autoformatting engine. It allows to format various block patterns. For example,
  * The block autoformatting engine. It allows to format various block patterns. For example,
@@ -43,7 +45,8 @@ import LiveRange from '@ckeditor/ckeditor5-engine/src/model/liverange';
  *
  *
  * @param {module:core/editor/editor~Editor} editor The editor instance.
  * @param {module:core/editor/editor~Editor} editor The editor instance.
  * @param {module:autoformat/autoformat~Autoformat} plugin The autoformat plugin instance.
  * @param {module:autoformat/autoformat~Autoformat} plugin The autoformat plugin instance.
- * @param {RegExp} pattern The regular expression to execute on just inserted text.
+ * @param {RegExp} pattern The regular expression to execute on just inserted text. The regular expression is tested against the text
+ * from the beginning until the caret position.
  * @param {Function|String} callbackOrCommand The callback to execute or the command to run when the text is matched.
  * @param {Function|String} callbackOrCommand The callback to execute or the command to run when the text is matched.
  * In case of providing the callback, it receives the following parameter:
  * In case of providing the callback, it receives the following parameter:
  * * {Object} match RegExp.exec() result of matching the pattern to inserted text.
  * * {Object} match RegExp.exec() result of matching the pattern to inserted text.
@@ -68,6 +71,12 @@ export default function blockAutoformatEditing( editor, plugin, pattern, callbac
 			return;
 			return;
 		}
 		}
 
 
+		const range = first( editor.model.document.selection.getRanges() );
+
+		if ( !range.isCollapsed ) {
+			return;
+		}
+
 		if ( batch.type == 'transparent' ) {
 		if ( batch.type == 'transparent' ) {
 			return;
 			return;
 		}
 		}
@@ -82,12 +91,26 @@ export default function blockAutoformatEditing( editor, plugin, pattern, callbac
 
 
 		const blockToFormat = entry.position.parent;
 		const blockToFormat = entry.position.parent;
 
 
-		// Block formatting should trigger only if the entire content of a paragraph is a single text node... (see ckeditor5#5671).
-		if ( !blockToFormat.is( 'element', 'paragraph' ) || blockToFormat.childCount !== 1 ) {
+		// Block formatting should be disabled in codeBlocks (#5800).
+		if ( blockToFormat.is( 'element', 'codeBlock' ) ) {
+			return;
+		}
+
+		// In case a command is bound, do not re-execute it over an existing block style which would result with a style removal.
+		// Instead just drop processing so that autoformat trigger text is not lost. E.g. writing "# " in a level 1 heading.
+		if ( command && command.value === true ) {
+			return;
+		}
+
+		const firstNode = blockToFormat.getChild( 0 );
+		const firstNodeRange = editor.model.createRangeOn( firstNode );
+
+		// Range is only expected to be within or at the very end of the first text node.
+		if ( !firstNodeRange.containsRange( range ) && !range.end.isEqual( firstNodeRange.end ) ) {
 			return;
 			return;
 		}
 		}
 
 
-		const match = pattern.exec( blockToFormat.getChild( 0 ).data );
+		const match = pattern.exec( firstNode.data.substr( 0, range.end.offset ) );
 
 
 		// ...and this text node's data match the pattern.
 		// ...and this text node's data match the pattern.
 		if ( !match ) {
 		if ( !match ) {

+ 71 - 8
packages/ckeditor5-autoformat/tests/autoformat.js

@@ -77,6 +77,15 @@ describe( 'Autoformat', () => {
 			expect( getData( model ) ).to.equal( '<listItem listIndent="0" listType="bulleted">[]</listItem>' );
 			expect( getData( model ) ).to.equal( '<listItem listIndent="0" listType="bulleted">[]</listItem>' );
 		} );
 		} );
 
 
+		it( 'should replace a non-empty paragraph using the asterisk', () => {
+			setData( model, '<paragraph>*[]sample text</paragraph>' );
+			model.change( writer => {
+				writer.insertText( ' ', doc.selection.getFirstPosition() );
+			} );
+
+			expect( getData( model ) ).to.equal( '<listItem listIndent="0" listType="bulleted">[]sample text</listItem>' );
+		} );
+
 		it( 'should not replace minus character when inside bulleted list item', () => {
 		it( 'should not replace minus character when inside bulleted list item', () => {
 			setData( model, '<listItem listIndent="0" listType="bulleted">-[]</listItem>' );
 			setData( model, '<listItem listIndent="0" listType="bulleted">-[]</listItem>' );
 			model.change( writer => {
 			model.change( writer => {
@@ -115,6 +124,15 @@ describe( 'Autoformat', () => {
 			expect( getData( model ) ).to.equal( '<listItem listIndent="0" listType="numbered">[]</listItem>' );
 			expect( getData( model ) ).to.equal( '<listItem listIndent="0" listType="numbered">[]</listItem>' );
 		} );
 		} );
 
 
+		it( 'should replace a non-empty paragraph using the parenthesis format', () => {
+			setData( model, '<paragraph>1)[]sample text</paragraph>' );
+			model.change( writer => {
+				writer.insertText( ' ', doc.selection.getFirstPosition() );
+			} );
+
+			expect( getData( model ) ).to.equal( '<listItem listIndent="0" listType="numbered">[]sample text</listItem>' );
+		} );
+
 		it( 'should not replace digit character when there is no . or ) in the format', () => {
 		it( 'should not replace digit character when there is no . or ) in the format', () => {
 			setData( model, '<paragraph>1[]</paragraph>' );
 			setData( model, '<paragraph>1[]</paragraph>' );
 			model.change( writer => {
 			model.change( writer => {
@@ -150,6 +168,24 @@ describe( 'Autoformat', () => {
 
 
 			expect( getData( model ) ).to.equal( '<paragraph>Foo<softBreak></softBreak>1. []</paragraph>' );
 			expect( getData( model ) ).to.equal( '<paragraph>Foo<softBreak></softBreak>1. []</paragraph>' );
 		} );
 		} );
+
+		it( 'should be converted from a header', () => {
+			setData( model, '<heading1>1.[]</heading1>' );
+			model.change( writer => {
+				writer.insertText( ' ', doc.selection.getFirstPosition() );
+			} );
+
+			expect( getData( model ) ).to.equal( '<listItem listIndent="0" listType="numbered">[]</listItem>' );
+		} );
+
+		it( 'should be converted from a bulleted list', () => {
+			setData( model, '<listItem listIndent="0" listType="bulleted">1.[]</listItem>' );
+			model.change( writer => {
+				writer.insertText( ' ', doc.selection.getFirstPosition() );
+			} );
+
+			expect( getData( model ) ).to.equal( '<listItem listIndent="0" listType="numbered">[]</listItem>' );
+		} );
 	} );
 	} );
 
 
 	describe( 'Heading', () => {
 	describe( 'Heading', () => {
@@ -243,6 +279,15 @@ describe( 'Autoformat', () => {
 
 
 			expect( getData( model ) ).to.equal( '<paragraph>Foo<softBreak></softBreak># []</paragraph>' );
 			expect( getData( model ) ).to.equal( '<paragraph>Foo<softBreak></softBreak># []</paragraph>' );
 		} );
 		} );
+
+		it( 'should convert a header that already contains a text', () => {
+			setData( model, '<heading1>###[]foo</heading1>' );
+			model.change( writer => {
+				writer.insertText( ' ', doc.selection.getFirstPosition() );
+			} );
+
+			expect( getData( model ) ).to.equal( '<heading3>[]foo</heading3>' );
+		} );
 	} );
 	} );
 
 
 	describe( 'Block quote', () => {
 	describe( 'Block quote', () => {
@@ -255,13 +300,22 @@ describe( 'Autoformat', () => {
 			expect( getData( model ) ).to.equal( '<blockQuote><paragraph>[]</paragraph></blockQuote>' );
 			expect( getData( model ) ).to.equal( '<blockQuote><paragraph>[]</paragraph></blockQuote>' );
 		} );
 		} );
 
 
-		it( 'should not replace greater-than character when inside heading', () => {
+		it( 'should replace greater-than character in a non-empty paragraph', () => {
+			setData( model, '<paragraph>>[]foo</paragraph>' );
+			model.change( writer => {
+				writer.insertText( ' ', doc.selection.getFirstPosition() );
+			} );
+
+			expect( getData( model ) ).to.equal( '<blockQuote><paragraph>[]foo</paragraph></blockQuote>' );
+		} );
+
+		it( 'should wrap the heading if greater-than character was used', () => {
 			setData( model, '<heading1>>[]</heading1>' );
 			setData( model, '<heading1>>[]</heading1>' );
 			model.change( writer => {
 			model.change( writer => {
 				writer.insertText( ' ', doc.selection.getFirstPosition() );
 				writer.insertText( ' ', doc.selection.getFirstPosition() );
 			} );
 			} );
 
 
-			expect( getData( model ) ).to.equal( '<heading1>> []</heading1>' );
+			expect( getData( model ) ).to.equal( '<blockQuote><heading1>[]</heading1></blockQuote>' );
 		} );
 		} );
 
 
 		it( 'should not replace greater-than character when inside numbered list', () => {
 		it( 'should not replace greater-than character when inside numbered list', () => {
@@ -302,22 +356,31 @@ describe( 'Autoformat', () => {
 			expect( getData( model ) ).to.equal( '<codeBlock language="plaintext">[]</codeBlock>' );
 			expect( getData( model ) ).to.equal( '<codeBlock language="plaintext">[]</codeBlock>' );
 		} );
 		} );
 
 
-		it( 'should not replace triple grave accents when already in a code block', () => {
-			setData( model, '<codeBlock language="plaintext">``[]</codeBlock>' );
+		it( 'should replace triple grave accents in a heading', () => {
+			setData( model, '<heading1>``[]</heading1>' );
 			model.change( writer => {
 			model.change( writer => {
 				writer.insertText( '`', doc.selection.getFirstPosition() );
 				writer.insertText( '`', doc.selection.getFirstPosition() );
 			} );
 			} );
 
 
-			expect( getData( model ) ).to.equal( '<codeBlock language="plaintext">```[]</codeBlock>' );
+			expect( getData( model ) ).to.equal( '<codeBlock language="plaintext">[]</codeBlock>' );
 		} );
 		} );
 
 
-		it( 'should not replace triple grave accents when inside heading', () => {
-			setData( model, '<heading1>``[]</heading1>' );
+		it( 'should replace triple grave accents in a non-empty paragraph', () => {
+			setData( model, '<paragraph>``[]let foo = 1;</paragraph>' );
 			model.change( writer => {
 			model.change( writer => {
 				writer.insertText( '`', doc.selection.getFirstPosition() );
 				writer.insertText( '`', doc.selection.getFirstPosition() );
 			} );
 			} );
 
 
-			expect( getData( model ) ).to.equal( '<heading1>```[]</heading1>' );
+			expect( getData( model ) ).to.equal( '<codeBlock language="plaintext">[]let foo = 1;</codeBlock>' );
+		} );
+
+		it( 'should not replace triple grave accents when already in a code block', () => {
+			setData( model, '<codeBlock language="plaintext">``[]</codeBlock>' );
+			model.change( writer => {
+				writer.insertText( '`', doc.selection.getFirstPosition() );
+			} );
+
+			expect( getData( model ) ).to.equal( '<codeBlock language="plaintext">```[]</codeBlock>' );
 		} );
 		} );
 
 
 		it( 'should not replace triple grave accents when inside numbered list', () => {
 		it( 'should not replace triple grave accents when inside numbered list', () => {

+ 30 - 4
packages/ckeditor5-autoformat/tests/blockautoformatediting.js

@@ -117,7 +117,7 @@ describe( 'blockAutoformatEditing', () => {
 
 
 		it( 'should ignore other delta operations', () => {
 		it( 'should ignore other delta operations', () => {
 			const spy = testUtils.sinon.spy();
 			const spy = testUtils.sinon.spy();
-			blockAutoformatEditing( editor, plugin, /^[*]\s/, spy );
+			blockAutoformatEditing( editor, plugin, /^[*]\s$/, spy );
 
 
 			setData( model, '<paragraph>*[]</paragraph>' );
 			setData( model, '<paragraph>*[]</paragraph>' );
 			model.change( writer => {
 			model.change( writer => {
@@ -127,9 +127,23 @@ describe( 'blockAutoformatEditing', () => {
 			sinon.assert.notCalled( spy );
 			sinon.assert.notCalled( spy );
 		} );
 		} );
 
 
+		it( 'should ignore a ranged selection', () => {
+			model.schema.extend( '$text', { allowAttributes: 'foo' } );
+
+			const spy = testUtils.sinon.spy();
+			blockAutoformatEditing( editor, plugin, /^[*]\s$/, spy );
+
+			setData( model, '<paragraph>[* ]foo</paragraph>' );
+			model.change( writer => {
+				writer.setAttribute( 'foo', true, model.document.selection.getFirstRange() );
+			} );
+
+			sinon.assert.notCalled( spy );
+		} );
+
 		it( 'should stop if there is no text to run matching on', () => {
 		it( 'should stop if there is no text to run matching on', () => {
 			const spy = testUtils.sinon.spy();
 			const spy = testUtils.sinon.spy();
-			blockAutoformatEditing( editor, plugin, /^[*]\s/, spy );
+			blockAutoformatEditing( editor, plugin, /^[*]\s$/, spy );
 
 
 			setData( model, '<paragraph>[]</paragraph>' );
 			setData( model, '<paragraph>[]</paragraph>' );
 			model.change( writer => {
 			model.change( writer => {
@@ -157,7 +171,7 @@ describe( 'blockAutoformatEditing', () => {
 				} );
 				} );
 
 
 			const spy = testUtils.sinon.spy();
 			const spy = testUtils.sinon.spy();
-			blockAutoformatEditing( editor, plugin, /^[*]\s/, spy );
+			blockAutoformatEditing( editor, plugin, /^[*]\s$/, spy );
 
 
 			setData( model, '<paragraph>*<softBreak></softBreak>[]</paragraph>' );
 			setData( model, '<paragraph>*<softBreak></softBreak>[]</paragraph>' );
 			model.change( writer => {
 			model.change( writer => {
@@ -167,6 +181,18 @@ describe( 'blockAutoformatEditing', () => {
 			sinon.assert.notCalled( spy );
 			sinon.assert.notCalled( spy );
 		} );
 		} );
 
 
+		it( 'should not call callback when typing in the middle of block text', () => {
+			const spy = testUtils.sinon.spy();
+			blockAutoformatEditing( editor, plugin, /^[*]\s$/, spy );
+
+			setData( model, '<paragraph>* foo[]bar</paragraph>' );
+			model.change( writer => {
+				writer.insertText( ' ', doc.selection.getFirstPosition() );
+			} );
+
+			sinon.assert.notCalled( spy );
+		} );
+
 		it( 'should not call callback when after inline element (typing after softBreak in a "matching" paragraph)', () => {
 		it( 'should not call callback when after inline element (typing after softBreak in a "matching" paragraph)', () => {
 			// Configure the schema.
 			// Configure the schema.
 			model.schema.register( 'softBreak', {
 			model.schema.register( 'softBreak', {
@@ -185,7 +211,7 @@ describe( 'blockAutoformatEditing', () => {
 				} );
 				} );
 
 
 			const spy = testUtils.sinon.spy();
 			const spy = testUtils.sinon.spy();
-			blockAutoformatEditing( editor, plugin, /^[*]\s/, spy );
+			blockAutoformatEditing( editor, plugin, /^[*]\s$/, spy );
 
 
 			setData( model, '<paragraph>* <softBreak></softBreak>[]</paragraph>' );
 			setData( model, '<paragraph>* <softBreak></softBreak>[]</paragraph>' );
 
 

+ 5 - 7
packages/ckeditor5-autoformat/tests/manual/autoformat.md

@@ -1,14 +1,14 @@
 ## Autoformat
 ## Autoformat
 
 
-1. Type `#` and press the space in an empty paragraph to replace it with a heading.
+Note: autoformat should not work in a code blocks.
 
 
-1. Type `*` or `-` and the press space in an empty paragraph to replace it with a list item.
+1. Type `#` and press the space at the beginning of a block to replace it with a heading.
 
 
-1. Type `>` and press the space in an empty paragraph to replace it with a block quote.
+1. Type `*` or `-` and the press space at the beginning of a block to replace it with a list item.
 
 
-1. Type a number from the range **1-3** followed by a `.` and press space to replace an empty paragraph with a numbered list item.
+1. Type `>` and press the space at the beginning of a block to replace it with a block quote.
 
 
-1. Type a number from the range **1-3** followed by a `)` and press space to replace an empty paragraph with a numbered list item.
+1. Type `1` followed by a `.` or `)` and press space (at the beginning of a block) to replace it with a numbered list item.
 
 
 1. Type `*foobar*`/`_foobar_` to italicize `foobar`. `*`/`_` should be removed.
 1. Type `*foobar*`/`_foobar_` to italicize `foobar`. `*`/`_` should be removed.
 
 
@@ -22,6 +22,4 @@
 
 
 1. For every autoformat pattern: Undo until you'll see just the pattern (e.g. `- `). Typing should be then possible without triggering the autoformatting again.
 1. For every autoformat pattern: Undo until you'll see just the pattern (e.g. `- `). Typing should be then possible without triggering the autoformatting again.
 
 
-1. Typing a different pattern in an already converted block **must not** trigger the autoformatting. For example, typing `- ` in a heading should not convert a heading to a list.
-
 1. Type inline formatting (bold, italic, code, strikethrough) after a soft break (<kbd>Shift</kbd>+<kbd>Enter</kbd>).
 1. Type inline formatting (bold, italic, code, strikethrough) after a soft break (<kbd>Shift</kbd>+<kbd>Enter</kbd>).