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

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

09 Jul 2024 09:52AM CUT 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/blockautoformatediting.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
import type { Command, Editor } from 'ckeditor5/src/core.js';
7

8
import {
9
        LiveRange,
10
        type DocumentChangeEvent,
11
        type Item,
12
        type Text
13
} from 'ckeditor5/src/engine.js';
14

15
import { first } from 'ckeditor5/src/utils.js';
16

17
import type Autoformat from './autoformat.js';
18

19
import type { Delete } from 'ckeditor5/src/typing.js';
20

21
/**
22
 * The block autoformatting engine. It allows to format various block patterns. For example,
23
 * it can be configured to turn a paragraph starting with `*` and followed by a space into a list item.
24
 *
25
 * The autoformatting operation is integrated with the undo manager,
26
 * so the autoformatting step can be undone if the user's intention was not to format the text.
27
 *
28
 * See the {@link module:autoformat/blockautoformatediting~blockAutoformatEditing `blockAutoformatEditing`} documentation
29
 * to learn how to create custom block autoformatters. You can also use
30
 * the {@link module:autoformat/autoformat~Autoformat} feature which enables a set of default autoformatters
31
 * (lists, headings, bold and italic).
32
 *
33
 * @module autoformat/blockautoformatediting
34
 */
35

36
/**
37
 * Creates a listener triggered on {@link module:engine/model/document~Document#event:change:data `change:data`} event in the document.
38
 * Calls the callback when inserted text matches the regular expression or the command name
39
 * if provided instead of the callback.
40
 *
41
 * Examples of usage:
42
 *
43
 * To convert a paragraph into heading 1 when `- ` is typed, using just the command name:
44
 *
45
 * ```ts
46
 * blockAutoformatEditing( editor, plugin, /^\- $/, 'heading1' );
47
 * ```
48
 *
49
 * To convert a paragraph into heading 1 when `- ` is typed, using just the callback:
50
 *
51
 * ```ts
52
 * blockAutoformatEditing( editor, plugin, /^\- $/, ( context ) => {
53
 *         const { match } = context;
54
 *         const headingLevel = match[ 1 ].length;
55
 *
56
 *         editor.execute( 'heading', {
57
 *                 formatId: `heading${ headingLevel }`
58
 *         } );
59
 * } );
60
 * ```
61
 *
62
 * @param editor The editor instance.
63
 * @param plugin The autoformat plugin instance.
64
 * @param pattern The regular expression to execute on just inserted text. The regular expression is tested against the text
65
 * from the beginning until the caret position.
66
 * @param callbackOrCommand The callback to execute or the command to run when the text is matched.
67
 * In case of providing the callback, it receives the following parameter:
68
 * * match RegExp.exec() result of matching the pattern to inserted text.
69
 */
70
export default function blockAutoformatEditing(
71
        editor: Editor,
72
        plugin: Autoformat,
73
        pattern: RegExp,
74
        callbackOrCommand: string | ( ( context: { match: RegExpExecArray } ) => unknown )
75
): void {
76
        let callback: ( context: { match: RegExpExecArray } ) => unknown;
77
        let command: Command | null = null;
2,248✔
78

79
        if ( typeof callbackOrCommand == 'function' ) {
2,248✔
80
                callback = callbackOrCommand;
1,126✔
81
        } else {
82
                // We assume that the actual command name was provided.
83
                command = editor.commands.get( callbackOrCommand )!;
1,122✔
84

85
                callback = () => {
1,122✔
86
                        editor.execute( callbackOrCommand );
68✔
87
                };
88
        }
89

90
        editor.model.document.on<DocumentChangeEvent>( 'change:data', ( evt, batch ) => {
2,248✔
91
                if ( command && !command.isEnabled || !plugin.isEnabled ) {
7,934✔
92
                        return;
44✔
93
                }
94

95
                const range = first( editor.model.document.selection.getRanges() )!;
7,890✔
96

97
                if ( !range.isCollapsed ) {
7,890✔
98
                        return;
2✔
99
                }
100

101
                if ( batch.isUndo || !batch.isLocal ) {
7,888✔
102
                        return;
164✔
103
                }
104

105
                const changes = Array.from( editor.model.document.differ.getChanges() );
7,724✔
106
                const entry = changes[ 0 ];
7,724✔
107

108
                // Typing is represented by only a single change.
109
                if ( changes.length != 1 || entry.type !== 'insert' || entry.name != '$text' || entry.length != 1 ) {
7,724✔
110
                        return;
5,818✔
111
                }
112

113
                const blockToFormat = entry.position.parent;
1,906✔
114

115
                // Block formatting should be disabled in codeBlocks (#5800).
116
                if ( blockToFormat.is( 'element', 'codeBlock' ) ) {
1,906✔
117
                        return;
20✔
118
                }
119

120
                // Only list commands and custom callbacks can be applied inside a list.
121
                if ( blockToFormat.is( 'element', 'listItem' ) &&
1,886✔
122
                        typeof callbackOrCommand !== 'function' &&
123
                        ![ 'numberedList', 'bulletedList', 'todoList' ].includes( callbackOrCommand )
124
                ) {
125
                        return;
36✔
126
                }
127

128
                // In case a command is bound, do not re-execute it over an existing block style which would result in a style removal.
129
                // Instead, just drop processing so that autoformat trigger text is not lost. E.g. writing "# " in a level 1 heading.
130
                if ( command && command.value === true ) {
1,850✔
131
                        return;
37✔
132
                }
133

134
                const firstNode = blockToFormat.getChild( 0 ) as Text;
1,813✔
135

136
                const firstNodeRange = editor.model.createRangeOn( firstNode );
1,813✔
137

138
                // Range is only expected to be within or at the very end of the first text node.
139
                if ( !firstNodeRange.containsRange( range ) && !range.end.isEqual( firstNodeRange.end ) ) {
1,813✔
140
                        return;
302✔
141
                }
142

143
                const match = pattern.exec( firstNode.data.substr( 0, range.end.offset ) );
1,511✔
144

145
                // ...and this text node's data match the pattern.
146
                if ( !match ) {
1,511✔
147
                        return;
1,397✔
148
                }
149

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

157
                        const wasChanged = callback( { match } );
114✔
158

159
                        // Remove matched text.
160
                        if ( wasChanged !== false ) {
114✔
161
                                writer.remove( range );
107✔
162

163
                                const selectionRange = editor.model.document.selection.getFirstRange()!;
107✔
164
                                const blockRange = writer.createRangeIn( blockToFormat );
107✔
165

166
                                // If the block is empty and the document selection has been moved when
167
                                // applying formatting (e.g. is now in newly created block).
168
                                if ( blockToFormat.isEmpty && !blockRange.isEqual( selectionRange ) && !blockRange.containsRange( selectionRange, true ) ) {
107✔
169
                                        writer.remove( blockToFormat as Item );
10✔
170
                                }
171
                        }
172
                        range.detach();
114✔
173

174
                        editor.model.enqueueChange( () => {
114✔
175
                                const deletePlugin: Delete = editor.plugins.get( 'Delete' );
114✔
176

177
                                deletePlugin.requestUndoOnBackspace();
114✔
178
                        } );
179
                } );
180
        } );
181
}
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