ckeditor/ckeditor5-autoformat

View on GitHub
src/autoformat.js

Summary

Maintainability
A
1 hr
Test Coverage
/**
 * @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/autoformat
 */

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

/**
 * Enables a set of predefined autoformatting actions.
 *
 * For a detailed overview, check the {@glink features/autoformat Autoformatting feature documentation}
 * and the {@glink api/autoformat package page}.
 *
 * @extends module:core/plugin~Plugin
 */
export default class Autoformat extends Plugin {
    /**
     * @inheritDoc
     */
    static get pluginName() {
        return 'Autoformat';
    }

    /**
     * @inheritDoc
     */
    afterInit() {
        this._addListAutoformats();
        this._addBasicStylesAutoformats();
        this._addHeadingAutoformats();
        this._addBlockQuoteAutoformats();
        this._addCodeBlockAutoformats();
    }

    /**
     * Adds autoformatting related to the {@link module:list/list~List}.
     *
     * When typed:
     * - `* ` or `- ` – A paragraph will be changed to a bulleted list.
     * - `1. ` or `1) ` – A paragraph will be changed to a numbered list ("1" can be any digit or a list of digits).
     *
     * @private
     */
    _addListAutoformats() {
        const commands = this.editor.commands;

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

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

    /**
     * Adds autoformatting related to the {@link module:basic-styles/bold~Bold},
     * {@link module:basic-styles/italic~Italic}, {@link module:basic-styles/code~Code}
     * and {@link module:basic-styles/strikethrough~Strikethrough}
     *
     * When typed:
     * - `**foobar**` – `**` characters are removed and `foobar` is set to bold,
     * - `__foobar__` – `__` characters are removed and `foobar` is set to bold,
     * - `*foobar*` – `*` characters are removed and `foobar` is set to italic,
     * - `_foobar_` – `_` characters are removed and `foobar` is set to italic,
     * - ``` `foobar` – ``` ` ``` characters are removed and `foobar` is set to code,
     * - `~~foobar~~` – `~~` characters are removed and `foobar` is set to strikethrough.
     *
     * @private
     */
    _addBasicStylesAutoformats() {
        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 */
        }

        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 */
        }

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

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

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

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

    /**
     * Adds autoformatting related to {@link module:heading/heading~Heading}.
     *
     * It is using a number at the end of the command name to associate it with the proper trigger:
     *
     * * `heading` with value `heading1` will be executed when typing `#`,
     * * `heading` with value `heading2` will be executed when typing `##`,
     * * ... up to `heading6` and `######`.
     *
     * @private
     */
    _addHeadingAutoformats() {
        const command = this.editor.commands.get( 'heading' );

        if ( command ) {
            command.modelElements
                .filter( name => name.match( /^heading[1-6]$/ ) )
                .forEach( commandValue => {
                    const level = commandValue[ 7 ];
                    const pattern = new RegExp( `^(#{${ level }})\\s$` );

                    // eslint-disable-next-line no-new
                    new BlockAutoformatEditing( this.editor, pattern, () => {
                        if ( !command.isEnabled ) {
                            return false;
                        }

                        this.editor.execute( 'heading', { value: commandValue } );
                    } );
                } );
        }
    }

    /**
     * Adds autoformatting related to {@link module:block-quote/blockquote~BlockQuote}.
     *
     * When typed:
     * * `> ` – A paragraph will be changed to a block quote.
     *
     * @private
     */
    _addBlockQuoteAutoformats() {
        if ( this.editor.commands.get( 'blockQuote' ) ) {
            // eslint-disable-next-line no-new
            new BlockAutoformatEditing( this.editor, /^>\s$/, 'blockQuote' );
        }
    }

    /**
     * Adds autoformatting related to {@link module:code-block/codeblock~CodeBlock}.
     *
     * When typed:
     * - `` ``` `` – A paragraph will be changed to a code block.
     *
     * @private
     */
    _addCodeBlockAutoformats() {
        if ( this.editor.commands.get( 'codeBlock' ) ) {
            // eslint-disable-next-line no-new
            new BlockAutoformatEditing( this.editor, /^```$/, 'codeBlock' );
        }
    }
}

// Helper function for getting `InlineAutoformatEditing` callbacks that checks if command is enabled.
//
// @param {module:core/editor/editor~Editor} editor
// @param {String} attributeKey
// @returns {Function}
function getCallbackFunctionForInlineAutoformat( editor, attributeKey ) {
    return ( writer, rangesToFormat ) => {
        const command = editor.commands.get( attributeKey );

        if ( !command.isEnabled ) {
            return false;
        }

        const validRanges = editor.model.schema.getValidRanges( rangesToFormat, attributeKey );

        for ( const range of validRanges ) {
            writer.setAttribute( attributeKey, true, range );
        }

        // After applying attribute to the text, remove given attribute from the selection.
        // This way user is able to type a text without attribute used by auto formatter.
        writer.removeSelectionAttribute( attributeKey );
    };
}