Skip to content

Commit

Permalink
Merge pull request #7576 from ckeditor/i/6170
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
mlewand authored Jul 22, 2020
2 parents 2c87c04 + 9648425 commit 5866d41
Show file tree
Hide file tree
Showing 6 changed files with 140 additions and 28 deletions.
3 changes: 2 additions & 1 deletion packages/ckeditor5-autoformat/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@
"@ckeditor/ckeditor5-heading": "^20.0.0",
"@ckeditor/ckeditor5-list": "^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": {
"node": ">=12.0.0",
Expand Down
9 changes: 5 additions & 4 deletions packages/ckeditor5-autoformat/src/autoformat.js
Original file line number Diff line number Diff line change
Expand Up @@ -123,16 +123,17 @@ export default class Autoformat extends Plugin {
if ( command ) {
command.modelElements
.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$` );

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;
}

this.editor.execute( 'heading', { value: commandValue } );
this.editor.execute( 'heading', { value: modelName } );
} );
} );
}
Expand Down
31 changes: 27 additions & 4 deletions packages/ckeditor5-autoformat/src/blockautoformatediting.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@
* @license Copyright (c) 2003-2020, CKSource - Frederico Knabben. All rights reserved.
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
*/

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,
Expand Down Expand Up @@ -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: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.
* In case of providing the callback, it receives the following parameter:
* * {Object} match RegExp.exec() result of matching the pattern to inserted text.
Expand All @@ -68,6 +71,12 @@ export default function blockAutoformatEditing( editor, plugin, pattern, callbac
return;
}

const range = first( editor.model.document.selection.getRanges() );

if ( !range.isCollapsed ) {
return;
}

if ( batch.type == 'transparent' ) {
return;
}
Expand All @@ -82,12 +91,26 @@ export default function blockAutoformatEditing( editor, plugin, pattern, callbac

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;
}

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.
if ( !match ) {
Expand Down
79 changes: 71 additions & 8 deletions packages/ckeditor5-autoformat/tests/autoformat.js
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,15 @@ describe( 'Autoformat', () => {
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', () => {
setData( model, '<listItem listIndent="0" listType="bulleted">-[]</listItem>' );
model.change( writer => {
Expand Down Expand Up @@ -115,6 +124,15 @@ describe( 'Autoformat', () => {
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', () => {
setData( model, '<paragraph>1[]</paragraph>' );
model.change( writer => {
Expand Down Expand Up @@ -150,6 +168,24 @@ describe( 'Autoformat', () => {

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', () => {
Expand Down Expand Up @@ -243,6 +279,15 @@ describe( 'Autoformat', () => {

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', () => {
Expand All @@ -255,13 +300,22 @@ describe( 'Autoformat', () => {
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>' );
model.change( writer => {
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', () => {
Expand Down Expand Up @@ -302,22 +356,31 @@ describe( 'Autoformat', () => {
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 => {
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 => {
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', () => {
Expand Down
34 changes: 30 additions & 4 deletions packages/ckeditor5-autoformat/tests/blockautoformatediting.js
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ describe( 'blockAutoformatEditing', () => {

it( 'should ignore other delta operations', () => {
const spy = testUtils.sinon.spy();
blockAutoformatEditing( editor, plugin, /^[*]\s/, spy );
blockAutoformatEditing( editor, plugin, /^[*]\s$/, spy );

setData( model, '<paragraph>*[]</paragraph>' );
model.change( writer => {
Expand All @@ -127,9 +127,23 @@ describe( 'blockAutoformatEditing', () => {
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', () => {
const spy = testUtils.sinon.spy();
blockAutoformatEditing( editor, plugin, /^[*]\s/, spy );
blockAutoformatEditing( editor, plugin, /^[*]\s$/, spy );

setData( model, '<paragraph>[]</paragraph>' );
model.change( writer => {
Expand Down Expand Up @@ -157,7 +171,7 @@ describe( 'blockAutoformatEditing', () => {
} );

const spy = testUtils.sinon.spy();
blockAutoformatEditing( editor, plugin, /^[*]\s/, spy );
blockAutoformatEditing( editor, plugin, /^[*]\s$/, spy );

setData( model, '<paragraph>*<softBreak></softBreak>[]</paragraph>' );
model.change( writer => {
Expand All @@ -167,6 +181,18 @@ describe( 'blockAutoformatEditing', () => {
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)', () => {
// Configure the schema.
model.schema.register( 'softBreak', {
Expand All @@ -185,7 +211,7 @@ describe( 'blockAutoformatEditing', () => {
} );

const spy = testUtils.sinon.spy();
blockAutoformatEditing( editor, plugin, /^[*]\s/, spy );
blockAutoformatEditing( editor, plugin, /^[*]\s$/, spy );

setData( model, '<paragraph>* <softBreak></softBreak>[]</paragraph>' );

Expand Down
12 changes: 5 additions & 7 deletions packages/ckeditor5-autoformat/tests/manual/autoformat.md
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
## 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.

Expand All @@ -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. 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>).

0 comments on commit 5866d41

Please sign in to comment.