• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

ckeditor / ckeditor5 / 6f7c4ea6-49ab-4ae8-85cc-c93a702378a1

09 Jul 2024 09:52AM UTC coverage: 100.0%. Remained the same
6f7c4ea6-49ab-4ae8-85cc-c93a702378a1

push

circleci

web-flow
Merge stable into master

13840 of 13840 branches covered (100.0%)

Branch coverage included in aggregate %.

36579 of 36579 relevant lines covered (100.0%)

11225.02 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

100.0
/packages/ckeditor5-autoformat/src/autoformat.ts
1
/**
2
 * @license Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved.
3
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
4
 */
5

6
/**
7
 * @module autoformat/autoformat
8
 */
9
import type { HeadingCommand } from '@ckeditor/ckeditor5-heading';
10

11
import { Plugin, type Editor } from 'ckeditor5/src/core.js';
12
import type { Range, Writer } from 'ckeditor5/src/engine.js';
13
import { Delete } from 'ckeditor5/src/typing.js';
14

15
import blockAutoformatEditing from './blockautoformatediting.js';
16
import inlineAutoformatEditing from './inlineautoformatediting.js';
17

18
/**
19
 * Enables a set of predefined autoformatting actions.
20
 *
21
 * For a detailed overview, check the {@glink features/autoformat Autoformatting} feature guide
22
 * and the {@glink api/autoformat package page}.
23
 */
24
export default class Autoformat extends Plugin {
25
        /**
26
         * @inheritDoc
27
         */
28
        public static get requires() {
29
                return [ Delete ] as const;
1,430✔
30
        }
31

32
        /**
33
         * @inheritDoc
34
         */
35
        public static get pluginName() {
36
                return 'Autoformat' as const;
1,145✔
37
        }
38

39
        /**
40
         * @inheritDoc
41
         */
42
        public afterInit(): void {
43
                const editor = this.editor;
286✔
44
                const t = this.editor.t;
286✔
45

46
                this._addListAutoformats();
286✔
47
                this._addBasicStylesAutoformats();
286✔
48
                this._addHeadingAutoformats();
286✔
49
                this._addBlockQuoteAutoformats();
286✔
50
                this._addCodeBlockAutoformats();
286✔
51
                this._addHorizontalLineAutoformats();
286✔
52

53
                // Add the information about the keystroke to the accessibility database.
54
                editor.accessibility.addKeystrokeInfos( {
286✔
55
                        keystrokes: [
56
                                {
57
                                        label: t( 'Revert autoformatting action' ),
58
                                        keystroke: 'Backspace'
59
                                }
60
                        ]
61
                } );
62
        }
63

64
        /**
65
         * Adds autoformatting related to the {@link module:list/list~List}.
66
         *
67
         * When typed:
68
         * - `* ` or `- ` – A paragraph will be changed into a bulleted list.
69
         * - `1. ` or `1) ` – A paragraph will be changed into a numbered list ("1" can be any digit or a list of digits).
70
         * - `[] ` or `[ ] ` – A paragraph will be changed into a to-do list.
71
         * - `[x] ` or `[ x ] ` – A paragraph will be changed into a checked to-do list.
72
         */
73
        private _addListAutoformats(): void {
74
                const commands = this.editor.commands;
286✔
75

76
                if ( commands.get( 'bulletedList' ) ) {
286✔
77
                        blockAutoformatEditing( this.editor, this, /^[*-]\s$/, 'bulletedList' );
232✔
78
                }
79

80
                if ( commands.get( 'numberedList' ) ) {
286✔
81
                        blockAutoformatEditing( this.editor, this, /^1[.|)]\s$/, 'numberedList' );
232✔
82
                }
83

84
                if ( commands.get( 'todoList' ) ) {
286✔
85
                        blockAutoformatEditing( this.editor, this, /^\[\s?\]\s$/, 'todoList' );
214✔
86
                }
87

88
                if ( commands.get( 'checkTodoList' ) ) {
286✔
89
                        blockAutoformatEditing( this.editor, this, /^\[\s?x\s?\]\s$/, () => {
214✔
90
                                this.editor.execute( 'todoList' );
16✔
91
                                this.editor.execute( 'checkTodoList' );
16✔
92
                        } );
93
                }
94
        }
95

96
        /**
97
         * Adds autoformatting related to the {@link module:basic-styles/bold~Bold},
98
         * {@link module:basic-styles/italic~Italic}, {@link module:basic-styles/code~Code}
99
         * and {@link module:basic-styles/strikethrough~Strikethrough}
100
         *
101
         * When typed:
102
         * - `**foobar**` – `**` characters are removed and `foobar` is set to bold,
103
         * - `__foobar__` – `__` characters are removed and `foobar` is set to bold,
104
         * - `*foobar*` – `*` characters are removed and `foobar` is set to italic,
105
         * - `_foobar_` – `_` characters are removed and `foobar` is set to italic,
106
         * - ``` `foobar` – ``` ` ``` characters are removed and `foobar` is set to code,
107
         * - `~~foobar~~` – `~~` characters are removed and `foobar` is set to strikethrough.
108
         */
109
        private _addBasicStylesAutoformats(): void {
110
                const commands = this.editor.commands;
286✔
111

112
                if ( commands.get( 'bold' ) ) {
286✔
113
                        const boldCallback = getCallbackFunctionForInlineAutoformat( this.editor, 'bold' );
230✔
114

115
                        inlineAutoformatEditing( this.editor, this, /(?:^|\s)(\*\*)([^*]+)(\*\*)$/g, boldCallback );
230✔
116
                        inlineAutoformatEditing( this.editor, this, /(?:^|\s)(__)([^_]+)(__)$/g, boldCallback );
230✔
117
                }
118

119
                if ( commands.get( 'italic' ) ) {
286✔
120
                        const italicCallback = getCallbackFunctionForInlineAutoformat( this.editor, 'italic' );
227✔
121

122
                        // The italic autoformatter cannot be triggered by the bold markers, so we need to check the
123
                        // text before the pattern (e.g. `(?:^|[^\*])`).
124
                        inlineAutoformatEditing( this.editor, this, /(?:^|\s)(\*)([^*_]+)(\*)$/g, italicCallback );
227✔
125
                        inlineAutoformatEditing( this.editor, this, /(?:^|\s)(_)([^_]+)(_)$/g, italicCallback );
227✔
126
                }
127

128
                if ( commands.get( 'code' ) ) {
286✔
129
                        const codeCallback = getCallbackFunctionForInlineAutoformat( this.editor, 'code' );
227✔
130

131
                        inlineAutoformatEditing( this.editor, this, /(`)([^`]+)(`)$/g, codeCallback );
227✔
132
                }
133

134
                if ( commands.get( 'strikethrough' ) ) {
286✔
135
                        const strikethroughCallback = getCallbackFunctionForInlineAutoformat( this.editor, 'strikethrough' );
227✔
136

137
                        inlineAutoformatEditing( this.editor, this, /(~~)([^~]+)(~~)$/g, strikethroughCallback );
227✔
138
                }
139
        }
140

141
        /**
142
         * Adds autoformatting related to {@link module:heading/heading~Heading}.
143
         *
144
         * It is using a number at the end of the command name to associate it with the proper trigger:
145
         *
146
         * * `heading` with a `heading1` value will be executed when typing `#`,
147
         * * `heading` with a `heading2` value will be executed when typing `##`,
148
         * * ... up to `heading6` for `######`.
149
         */
150
        private _addHeadingAutoformats(): void {
151
                const command: HeadingCommand | undefined = this.editor.commands.get( 'heading' );
286✔
152

153
                if ( command ) {
286✔
154
                        command.modelElements
231✔
155
                                .filter( name => name.match( /^heading[1-6]$/ ) )
687✔
156
                                .forEach( modelName => {
157
                                        const level = modelName[ 7 ];
687✔
158
                                        const pattern = new RegExp( `^(#{${ level }})\\s$` );
687✔
159

160
                                        blockAutoformatEditing( this.editor, this, pattern, () => {
687✔
161
                                                // Should only be active if command is enabled and heading style associated with pattern is inactive.
162
                                                if ( !command.isEnabled || command.value === modelName ) {
16✔
163
                                                        return false;
4✔
164
                                                }
165

166
                                                this.editor.execute( 'heading', { value: modelName } );
12✔
167
                                        } );
168
                                } );
169
                }
170
        }
171

172
        /**
173
         * Adds autoformatting related to {@link module:block-quote/blockquote~BlockQuote}.
174
         *
175
         * When typed:
176
         * * `> ` – A paragraph will be changed to a block quote.
177
         */
178
        private _addBlockQuoteAutoformats(): void {
179
                if ( this.editor.commands.get( 'blockQuote' ) ) {
286✔
180
                        blockAutoformatEditing( this.editor, this, /^>\s$/, 'blockQuote' );
227✔
181
                }
182
        }
183

184
        /**
185
         * Adds autoformatting related to {@link module:code-block/codeblock~CodeBlock}.
186
         *
187
         * When typed:
188
         * - `` ``` `` – A paragraph will be changed to a code block.
189
         */
190
        private _addCodeBlockAutoformats(): void {
191
                const editor = this.editor;
286✔
192
                const selection = editor.model.document.selection;
286✔
193

194
                if ( editor.commands.get( 'codeBlock' ) ) {
286✔
195
                        blockAutoformatEditing( editor, this, /^```$/, () => {
214✔
196
                                if ( selection.getFirstPosition()!.parent.is( 'element', 'listItem' ) ) {
12✔
197
                                        return false;
2✔
198
                                }
199
                                this.editor.execute( 'codeBlock', {
10✔
200
                                        usePreviousLanguageChoice: true
201
                                } );
202
                        } );
203
                }
204
        }
205

206
        /**
207
         * Adds autoformatting related to {@link module:horizontal-line/horizontalline~HorizontalLine}.
208
         *
209
         * When typed:
210
         * - `` --- `` – Will be replaced with a horizontal line.
211
         */
212
        private _addHorizontalLineAutoformats(): void {
213
                if ( this.editor.commands.get( 'horizontalLine' ) ) {
286✔
214
                        blockAutoformatEditing( this.editor, this, /^---$/, 'horizontalLine' );
214✔
215
                }
216
        }
217
}
218

219
/**
220
 * Helper function for getting `inlineAutoformatEditing` callbacks that checks if command is enabled.
221
 */
222
function getCallbackFunctionForInlineAutoformat( editor: Editor, attributeKey: string ) {
223
        return ( writer: Writer, rangesToFormat: Array<Range> ): boolean | undefined => {
911✔
224
                const command = editor.commands.get( attributeKey )!;
30✔
225

226
                if ( !command.isEnabled ) {
30✔
227
                        return false;
2✔
228
                }
229

230
                const validRanges = editor.model.schema.getValidRanges( rangesToFormat, attributeKey );
28✔
231

232
                for ( const range of validRanges ) {
28✔
233
                        writer.setAttribute( attributeKey, true, range );
28✔
234
                }
235

236
                // After applying attribute to the text, remove given attribute from the selection.
237
                // This way user is able to type a text without attribute used by auto formatter.
238
                writer.removeSelectionAttribute( attributeKey );
28✔
239
        };
240
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc