/** * @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 code-block/outdentcodeblockcommand */ import Command from '@ckeditor/ckeditor5-core/src/command'; import { getLeadingWhiteSpaces, getIndentOutdentPositions, isModelSelectionInCodeBlock } from './utils'; /** * The code block indentation decrease command plugin. * * @extends module:core/command~Command */ export default class OutdentCodeBlockCommand extends Command { constructor( editor ) { super( editor ); /** * A sequence of characters removed from the line when the command is executed. * * @readonly * @private * @member {String} */ this._indentSequence = editor.config.get( 'codeBlock.indentSequence' ); } /** * @inheritDoc */ refresh() { this.isEnabled = this._checkEnabled(); } /** * Executes the command. When the command {@link #isEnabled is enabled}, the indentation of the * code lines in the selection will be decreased. * * @fires execute */ execute() { const editor = this.editor; const model = editor.model; model.change( writer => { const positions = getIndentOutdentPositions( model ); // Outdent all positions, for instance assuming the indent sequence is 4x space (" "): // // ^foo -> foo // // ^bar -> bar // // Also, when there is more than one position: // // // ^foobar // // ^bazqux // // // -> // // // foobar // // bazqux // for ( const position of positions ) { const range = getLastOutdentableSequenceRange( this.editor.model, position, this._indentSequence ); if ( range ) { writer.remove( range ); } } } ); } /** * Checks whether the command can be enabled in the current context. * * @private * @returns {Boolean} Whether the command should be enabled. */ _checkEnabled() { if ( !this._indentSequence ) { return false; } const model = this.editor.model; if ( !isModelSelectionInCodeBlock( model.document.selection ) ) { return false; } // Outdent command can execute only when there is an indent character sequence // in some of the lines. return getIndentOutdentPositions( model ).some( position => { return getLastOutdentableSequenceRange( model, position, this._indentSequence ); } ); } } // For a position coming from `getIndentOutdentPositions()`, it returns the range representing // the last occurrence of the indent sequence among the leading whitespaces of the code line the // position represents. // // For instance, assuming the indent sequence is 4x space (" "): // // foo^ -> null // foo^bar -> null // ^foo -> null // ^foo -> [ ]foo // ^foo bar -> [ ]foo bar // // @param {} model // @param {} position // @param {String} sequence // @returns {|null} function getLastOutdentableSequenceRange( model, position, sequence ) { // Positions start before each text node (code line). Get the node corresponding to the position. const nodeAtPosition = getCodeLineTextNodeAtPosition( position ); if ( !nodeAtPosition ) { return null; } const leadingWhiteSpaces = getLeadingWhiteSpaces( nodeAtPosition ); const lastIndexOfSequence = leadingWhiteSpaces.lastIndexOf( sequence ); // For instance, assuming the indent sequence is 4x space (" "): // // ^foo -> null // if ( lastIndexOfSequence + sequence.length !== leadingWhiteSpaces.length ) { return null; } // For instance, assuming the indent sequence is 4x space (" "): // // ^foo -> null // if ( lastIndexOfSequence === -1 ) { return null; } const { parent, startOffset } = nodeAtPosition; // Create a range that contains the **last** indent sequence among the leading whitespaces // of the line. // // For instance, assuming the indent sequence is 4x space (" "): // // ^foo -> [ ]foo // return model.createRange( model.createPositionAt( parent, startOffset + lastIndexOfSequence ), model.createPositionAt( parent, startOffset + lastIndexOfSequence + sequence.length ) ); } function getCodeLineTextNodeAtPosition( position ) { // Positions start before each text node (code line). Get the node corresponding to the position. let nodeAtPosition = position.parent.getChild( position.index ); // foo^ // foo^bar if ( !nodeAtPosition || nodeAtPosition.is( 'element', 'softBreak' ) ) { nodeAtPosition = position.nodeBefore; } // ^ // foo^bar if ( !nodeAtPosition || nodeAtPosition.is( 'element', 'softBreak' ) ) { return null; } return nodeAtPosition; }