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

ckeditor / ckeditor5 / 25927

pending completion
25927

Pull #14763

CKEditor5 code coverage

web-flow
Merge pull request #14753 from ckeditor/ck/14743-enablePlaceholder-API-should-remain-backward-compatible-for-some-time

Fix (engine): Made the `enablePlaceholder()` API to remain backward compatible for the deprecation period. It will be removed in the future. Closes #14743.
Pull Request #14763: Support for image height attribute

12436 of 12436 branches covered (100.0%)

Branch coverage included in aggregate %.

147 of 147 new or added lines in 10 files covered. (100.0%)

32669 of 32669 relevant lines covered (100.0%)

12002.96 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-2025, CKSource Holding sp. z o.o. All rights reserved.
3
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
4
 */
5

6
import type { Command, Editor } from 'ckeditor5/src/core.js';
7

8
import {
9
        ModelLiveRange,
10
        ModelSchemaContext,
11
        type ModelDocumentChangeEvent,
12
        type ModelItem,
13
        type ModelText,
14
        type ModelWriter,
15
        type ModelDocumentSelection
16
} from 'ckeditor5/src/engine.js';
17

18
import { first } from 'ckeditor5/src/utils.js';
19

20
import { type Autoformat } from './autoformat.js';
21

22
import type { Delete } from 'ckeditor5/src/typing.js';
23

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

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

82
        if ( typeof callbackOrCommand == 'function' ) {
83
                callback = callbackOrCommand;
550✔
84
        } else {
85
                // We assume that the actual command name was provided.
550✔
86
                command = editor.commands.get( callbackOrCommand )!;
31✔
87

88
                callback = () => {
89
                        editor.execute( callbackOrCommand );
90
                };
1,103✔
91
        }
3,851✔
92

24✔
93
        editor.model.document.on<ModelDocumentChangeEvent>( 'change:data', ( evt, batch ) => {
94
                if ( command && !command.isEnabled || !plugin.isEnabled ) {
95
                        return;
3,827✔
96
                }
97

3,827✔
98
                const range = first( editor.model.document.selection.getRanges() )!;
2✔
99

100
                if ( !range.isCollapsed ) {
101
                        return;
3,825✔
102
                }
84✔
103

104
                if ( batch.isUndo || !batch.isLocal ) {
105
                        return;
3,741✔
106
                }
3,741✔
107

108
                const changes = Array.from( editor.model.document.differ.getChanges() );
109
                const entry = changes[ 0 ];
3,741✔
110

2,802✔
111
                // Typing is represented by only a single change.
112
                if ( changes.length != 1 || entry.type !== 'insert' || entry.name != '$text' || entry.length != 1 ) {
113
                        return;
939✔
114
                }
115

116
                const blockToFormat = entry.position.parent;
939✔
117

10✔
118
                // Block formatting should be disabled in codeBlocks (#5800).
119
                if ( blockToFormat.is( 'element', 'codeBlock' ) ) {
120
                        return;
121
                }
929✔
122

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

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

878✔
137
                const firstNode = blockToFormat.getChild( 0 ) as ModelText;
138

139
                const firstNodeRange = editor.model.createRangeOn( firstNode );
878✔
140

152✔
141
                // Range is only expected to be within or at the very end of the first text node.
142
                if ( !firstNodeRange.containsRange( range ) && !range.end.isEqual( firstNodeRange.end ) ) {
143
                        return;
726✔
144
                }
145

146
                const match = pattern.exec( firstNode.data.substr( 0, range.end.offset ) );
726✔
147

669✔
148
                // ...and this text node's data match the pattern.
149
                if ( !match ) {
150
                        return;
151
                }
57✔
152

153
                // Use enqueueChange to create new batch to separate typing batch from the auto-format changes.
57✔
154
                editor.model.enqueueChange( writer => {
57✔
155
                        const selection = editor.model.document.selection;
57✔
156

157
                        // Matched range.
57✔
158
                        const start = writer.createPositionAt( blockToFormat, 0 );
159
                        const end = writer.createPositionAt( blockToFormat, match[ 0 ].length );
160
                        const range = new ModelLiveRange( start, end );
57✔
161

50✔
162
                        const wasChanged = callback( { match } );
163

50✔
164
                        // Remove matched text.
50✔
165
                        if ( wasChanged !== false ) {
166
                                // Store selection attributes to restore them after matched text removed.
167
                                const selectionAttributes = Array.from( selection.getAttributes() );
168

50✔
169
                                writer.remove( range );
3✔
170

171
                                const selectionRange = selection.getFirstRange()!;
172
                                const blockRange = writer.createRangeIn( blockToFormat );
57✔
173

174
                                // If the block is empty and the document selection has been moved when
57✔
175
                                // applying formatting (e.g. is now in newly created block).
57✔
176
                                if ( blockToFormat.isEmpty && !blockRange.isEqual( selectionRange ) && !blockRange.containsRange( selectionRange, true ) ) {
177
                                        writer.remove( blockToFormat as ModelItem );
57✔
178
                                }
179

180
                                // Restore selection attributes.
181
                                restoreSelectionAttributes( writer, selection, selectionAttributes );
182
                        }
183
                        range.detach();
184

185
                        editor.model.enqueueChange( () => {
186
                                const deletePlugin: Delete = editor.plugins.get( 'Delete' );
187

188
                                deletePlugin.requestUndoOnBackspace();
189
                        } );
190
                } );
191
        } );
192
}
193

194
/**
195
 * Restore allowed selection attributes.
196
 */
197
function restoreSelectionAttributes(
198
        writer: ModelWriter,
199
        selection: ModelDocumentSelection,
200
        selectionAttributes: Array<[ string, unknown ]>
201
): void {
202
        const schema = writer.model.schema;
203
        const selectionPosition = selection.getFirstPosition()!;
204
        let selectionSchemaContext = new ModelSchemaContext( selectionPosition );
205

206
        if ( schema.checkChild( selectionSchemaContext, '$text' ) ) {
207
                selectionSchemaContext = selectionSchemaContext.push( '$text' );
208
        }
209

210
        for ( const [ attributeName, attributeValue ] of selectionAttributes ) {
211
                if ( schema.checkAttribute( selectionSchemaContext, attributeName ) ) {
212
                        writer.setSelectionAttribute( attributeName, attributeValue );
213
                }
214
        }
215
}
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