Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Enabled block autoformat in non-empty blocks #7576

Merged
merged 15 commits into from
Jul 22, 2020
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
20 changes: 16 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 @@ -129,7 +129,7 @@ describe( 'blockAutoformatEditing', () => {

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 +157,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 +167,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 +197,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>).