Skip to content

Commit

Permalink
Merge pull request #7346 from ckeditor/i/1239-code-autoformat
Browse files Browse the repository at this point in the history
Fix (autoformat): Autoformatting should not occur inside an existing text with a model `code` attribute. Closes #1239.
  • Loading branch information
oleq committed Jun 8, 2020
2 parents 6036d4a + 6eb7be2 commit ad3562a
Show file tree
Hide file tree
Showing 8 changed files with 472 additions and 337 deletions.
2 changes: 1 addition & 1 deletion packages/ckeditor5-autoformat/docs/features/autoformat.md
Expand Up @@ -81,7 +81,7 @@ ClassicEditor

## Creating custom autoformatters

The {@link module:autoformat/autoformat~Autoformat} feature bases on {@link module:autoformat/blockautoformatediting~BlockAutoformatEditing} and {@link module:autoformat/inlineautoformatediting~InlineAutoformatEditing} tools to create the autoformatters mentioned above.
The {@link module:autoformat/autoformat~Autoformat} feature bases on {@link module:autoformat/blockautoformatediting~blockAutoformatEditing} and {@link module:autoformat/inlineautoformatediting~inlineAutoformatEditing} tools to create the autoformatters mentioned above.

You can use these tools to create your own autoformatters. Check the [`Autoformat` feature's code](https://github.com/ckeditor/ckeditor5/blob/master/packages/ckeditor5-autoformat/src/autoformat.js) as an example.

Expand Down
41 changes: 14 additions & 27 deletions packages/ckeditor5-autoformat/src/autoformat.js
Expand Up @@ -7,8 +7,8 @@
* @module autoformat/autoformat
*/

import BlockAutoformatEditing from './blockautoformatediting';
import InlineAutoformatEditing from './inlineautoformatediting';
import blockAutoformatEditing from './blockautoformatediting';
import inlineAutoformatEditing from './inlineautoformatediting';
import Plugin from '@ckeditor/ckeditor5-core/src/plugin';

/**
Expand Down Expand Up @@ -51,13 +51,11 @@ export default class Autoformat extends Plugin {
const commands = this.editor.commands;

if ( commands.get( 'bulletedList' ) ) {
// eslint-disable-next-line no-new
new BlockAutoformatEditing( this.editor, /^[*-]\s$/, 'bulletedList' );
blockAutoformatEditing( this.editor, this, /^[*-]\s$/, 'bulletedList' );
}

if ( commands.get( 'numberedList' ) ) {
// eslint-disable-next-line no-new
new BlockAutoformatEditing( this.editor, /^1[.|)]\s$/, 'numberedList' );
blockAutoformatEditing( this.editor, this, /^1[.|)]\s$/, 'numberedList' );
}
}

Expand All @@ -80,39 +78,31 @@ export default class Autoformat extends Plugin {
const commands = this.editor.commands;

if ( commands.get( 'bold' ) ) {
/* eslint-disable no-new */
const boldCallback = getCallbackFunctionForInlineAutoformat( this.editor, 'bold' );

new InlineAutoformatEditing( this.editor, /(\*\*)([^*]+)(\*\*)$/g, boldCallback );
new InlineAutoformatEditing( this.editor, /(__)([^_]+)(__)$/g, boldCallback );
/* eslint-enable no-new */
inlineAutoformatEditing( this.editor, this, /(\*\*)([^*]+)(\*\*)$/g, boldCallback );
inlineAutoformatEditing( this.editor, this, /(__)([^_]+)(__)$/g, boldCallback );
}

if ( commands.get( 'italic' ) ) {
/* eslint-disable no-new */
const italicCallback = getCallbackFunctionForInlineAutoformat( this.editor, 'italic' );

// The italic autoformatter cannot be triggered by the bold markers, so we need to check the
// text before the pattern (e.g. `(?:^|[^\*])`).
new InlineAutoformatEditing( this.editor, /(?:^|[^*])(\*)([^*_]+)(\*)$/g, italicCallback );
new InlineAutoformatEditing( this.editor, /(?:^|[^_])(_)([^_]+)(_)$/g, italicCallback );
/* eslint-enable no-new */
inlineAutoformatEditing( this.editor, this, /(?:^|[^*])(\*)([^*_]+)(\*)$/g, italicCallback );
inlineAutoformatEditing( this.editor, this, /(?:^|[^_])(_)([^_]+)(_)$/g, italicCallback );
}

if ( commands.get( 'code' ) ) {
/* eslint-disable no-new */
const codeCallback = getCallbackFunctionForInlineAutoformat( this.editor, 'code' );

new InlineAutoformatEditing( this.editor, /(`)([^`]+)(`)$/g, codeCallback );
/* eslint-enable no-new */
inlineAutoformatEditing( this.editor, this, /(`)([^`]+)(`)$/g, codeCallback );
}

if ( commands.get( 'strikethrough' ) ) {
/* eslint-disable no-new */
const strikethroughCallback = getCallbackFunctionForInlineAutoformat( this.editor, 'strikethrough' );

new InlineAutoformatEditing( this.editor, /(~~)([^~]+)(~~)$/g, strikethroughCallback );
/* eslint-enable no-new */
inlineAutoformatEditing( this.editor, this, /(~~)([^~]+)(~~)$/g, strikethroughCallback );
}
}

Expand All @@ -137,8 +127,7 @@ export default class Autoformat extends Plugin {
const level = commandValue[ 7 ];
const pattern = new RegExp( `^(#{${ level }})\\s$` );

// eslint-disable-next-line no-new
new BlockAutoformatEditing( this.editor, pattern, () => {
blockAutoformatEditing( this.editor, this, pattern, () => {
if ( !command.isEnabled ) {
return false;
}
Expand All @@ -159,8 +148,7 @@ export default class Autoformat extends Plugin {
*/
_addBlockQuoteAutoformats() {
if ( this.editor.commands.get( 'blockQuote' ) ) {
// eslint-disable-next-line no-new
new BlockAutoformatEditing( this.editor, /^>\s$/, 'blockQuote' );
blockAutoformatEditing( this.editor, this, /^>\s$/, 'blockQuote' );
}
}

Expand All @@ -174,13 +162,12 @@ export default class Autoformat extends Plugin {
*/
_addCodeBlockAutoformats() {
if ( this.editor.commands.get( 'codeBlock' ) ) {
// eslint-disable-next-line no-new
new BlockAutoformatEditing( this.editor, /^```$/, 'codeBlock' );
blockAutoformatEditing( this.editor, this, /^```$/, 'codeBlock' );
}
}
}

// Helper function for getting `InlineAutoformatEditing` callbacks that checks if command is enabled.
// Helper function for getting `inlineAutoformatEditing` callbacks that checks if command is enabled.
//
// @param {module:core/editor/editor~Editor} editor
// @param {String} attributeKey
Expand Down
173 changes: 82 additions & 91 deletions packages/ckeditor5-autoformat/src/blockautoformatediting.js
Expand Up @@ -2,11 +2,6 @@
* @license Copyright (c) 2003-2020, CKSource - Frederico Knabben. All rights reserved.
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
*/

/**
* @module autoformat/blockautoformatediting
*/

import LiveRange from '@ckeditor/ckeditor5-engine/src/model/liverange';

/**
Expand All @@ -16,108 +11,104 @@ import LiveRange from '@ckeditor/ckeditor5-engine/src/model/liverange';
* The autoformatting operation is integrated with the undo manager,
* so the autoformatting step can be undone if the user's intention was not to format the text.
*
* See the constructors documentation to learn how to create custom inline autoformatters. You can also use
* See the {@link module:autoformat/blockautoformatediting~blockAutoformatEditing `blockAutoformatEditing`} documentation
* to learn how to create custom block autoformatters. You can also use
* the {@link module:autoformat/autoformat~Autoformat} feature which enables a set of default autoformatters
* (lists, headings, bold and italic).
*
* @module autoformat/blockautoformatediting
*/
export default class BlockAutoformatEditing {
/**
* @inheritDoc
*/
static get pluginName() {
return 'BlockAutoformatEditing';

/**
* Creates a listener triggered on {@link module:engine/model/document~Document#event:change:data `change:data`} event in the document.
* Calls the callback when inserted text matches the regular expression or the command name
* if provided instead of the callback.
*
* Examples of usage:
*
* To convert a paragraph to heading 1 when `- ` is typed, using just the command name:
*
* blockAutoformatEditing( editor, plugin, /^\- $/, 'heading1' );
*
* To convert a paragraph to heading 1 when `- ` is typed, using just the callback:
*
* blockAutoformatEditing( editor, plugin, /^\- $/, ( context ) => {
* const { match } = context;
* const headingLevel = match[ 1 ].length;
*
* editor.execute( 'heading', {
* formatId: `heading${ headingLevel }`
* } );
* } );
*
* @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 {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.
*/
export default function blockAutoformatEditing( editor, plugin, pattern, callbackOrCommand ) {
let callback;
let command = null;

if ( typeof callbackOrCommand == 'function' ) {
callback = callbackOrCommand;
} else {
// We assume that the actual command name was provided.
command = editor.commands.get( callbackOrCommand );

callback = () => {
editor.execute( callbackOrCommand );
};
}

/**
* Creates a listener triggered on `change` event in the document.
* Calls the callback when inserted text matches the regular expression or the command name
* if provided instead of the callback.
*
* Examples of usage:
*
* To convert a paragraph to heading 1 when `- ` is typed, using just the command name:
*
* new BlockAutoformatEditing( editor, /^\- $/, 'heading1' );
*
* To convert a paragraph to heading 1 when `- ` is typed, using just the callback:
*
* new BlockAutoformatEditing( editor, /^\- $/, ( context ) => {
* const { match } = context;
* const headingLevel = match[ 1 ].length;
*
* editor.execute( 'heading', {
* formatId: `heading${ headingLevel }`
* } );
* } );
*
* @param {module:core/editor/editor~Editor} editor The editor instance.
* @param {RegExp} pattern The regular expression to execute on just inserted text.
* @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.
*/
constructor( editor, pattern, callbackOrCommand ) {
let callback;
let command = null;

if ( typeof callbackOrCommand == 'function' ) {
callback = callbackOrCommand;
} else {
// We assume that the actual command name was provided.
command = editor.commands.get( callbackOrCommand );

callback = () => {
editor.execute( callbackOrCommand );
};
editor.model.document.on( 'change:data', ( evt, batch ) => {
if ( command && !command.isEnabled || !plugin.isEnabled ) {
return;
}

editor.model.document.on( 'change', ( evt, batch ) => {
if ( command && !command.isEnabled ) {
return;
}

if ( batch.type == 'transparent' ) {
return;
}
if ( batch.type == 'transparent' ) {
return;
}

const changes = Array.from( editor.model.document.differ.getChanges() );
const entry = changes[ 0 ];
const changes = Array.from( editor.model.document.differ.getChanges() );
const entry = changes[ 0 ];

// Typing is represented by only a single change.
if ( changes.length != 1 || entry.type !== 'insert' || entry.name != '$text' || entry.length != 1 ) {
return;
}
// Typing is represented by only a single change.
if ( changes.length != 1 || entry.type !== 'insert' || entry.name != '$text' || entry.length != 1 ) {
return;
}

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( 'paragraph' ) || blockToFormat.childCount !== 1 ) {
return;
}
// Block formatting should trigger only if the entire content of a paragraph is a single text node... (see ckeditor5#5671).
if ( !blockToFormat.is( 'paragraph' ) || blockToFormat.childCount !== 1 ) {
return;
}

const match = pattern.exec( blockToFormat.getChild( 0 ).data );
const match = pattern.exec( blockToFormat.getChild( 0 ).data );

// ...and this text node's data match the pattern.
if ( !match ) {
return;
}
// ...and this text node's data match the pattern.
if ( !match ) {
return;
}

// Use enqueueChange to create new batch to separate typing batch from the auto-format changes.
editor.model.enqueueChange( writer => {
// Matched range.
const start = writer.createPositionAt( blockToFormat, 0 );
const end = writer.createPositionAt( blockToFormat, match[ 0 ].length );
const range = new LiveRange( start, end );
// Use enqueueChange to create new batch to separate typing batch from the auto-format changes.
editor.model.enqueueChange( writer => {
// Matched range.
const start = writer.createPositionAt( blockToFormat, 0 );
const end = writer.createPositionAt( blockToFormat, match[ 0 ].length );
const range = new LiveRange( start, end );

const wasChanged = callback( { match } );
const wasChanged = callback( { match } );

// Remove matched text.
if ( wasChanged !== false ) {
writer.remove( range );
}
// Remove matched text.
if ( wasChanged !== false ) {
writer.remove( range );
}

range.detach();
} );
range.detach();
} );
}
} );
}

0 comments on commit ad3562a

Please sign in to comment.