@ckeditor/ckeditor5-autoformat 39.0.2 → 40.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build/autoformat.js.map +1 -0
- package/package.json +2 -2
- package/src/augmentation.d.ts +10 -10
- package/src/augmentation.js +5 -5
- package/src/autoformat.d.ts +81 -81
- package/src/autoformat.js +189 -189
- package/src/blockautoformatediting.d.ts +57 -57
- package/src/blockautoformatediting.js +137 -137
- package/src/index.d.ts +9 -9
- package/src/index.js +9 -9
- package/src/inlineautoformatediting.d.ts +83 -83
- package/src/inlineautoformatediting.js +174 -174
|
@@ -1,174 +1,174 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @license Copyright (c) 2003-2023, 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
|
-
* Enables autoformatting mechanism for a given {@link module:core/editor/editor~Editor}.
|
|
7
|
-
*
|
|
8
|
-
* It formats the matched text by applying the given model attribute or by running the provided formatting callback.
|
|
9
|
-
* On every {@link module:engine/model/document~Document#event:change:data data change} in the model document
|
|
10
|
-
* the autoformatting engine checks the text on the left of the selection
|
|
11
|
-
* and executes the provided action if the text matches given criteria (regular expression or callback).
|
|
12
|
-
*
|
|
13
|
-
* @param editor The editor instance.
|
|
14
|
-
* @param plugin The autoformat plugin instance.
|
|
15
|
-
* @param testRegexpOrCallback The regular expression or callback to execute on text.
|
|
16
|
-
* Provided regular expression *must* have three capture groups. The first and the third capture group
|
|
17
|
-
* should match opening and closing delimiters. The second capture group should match the text to format.
|
|
18
|
-
*
|
|
19
|
-
* ```ts
|
|
20
|
-
* // Matches the `**bold text**` pattern.
|
|
21
|
-
* // There are three capturing groups:
|
|
22
|
-
* // - The first to match the starting `**` delimiter.
|
|
23
|
-
* // - The second to match the text to format.
|
|
24
|
-
* // - The third to match the ending `**` delimiter.
|
|
25
|
-
* inlineAutoformatEditing( editor, plugin, /(\*\*)([^\*]+?)(\*\*)$/g, formatCallback );
|
|
26
|
-
* ```
|
|
27
|
-
*
|
|
28
|
-
* When a function is provided instead of the regular expression, it will be executed with the text to match as a parameter.
|
|
29
|
-
* The function should return proper "ranges" to delete and format.
|
|
30
|
-
*
|
|
31
|
-
* ```ts
|
|
32
|
-
* {
|
|
33
|
-
* remove: [
|
|
34
|
-
* [ 0, 1 ], // Remove the first letter from the given text.
|
|
35
|
-
* [ 5, 6 ] // Remove the 6th letter from the given text.
|
|
36
|
-
* ],
|
|
37
|
-
* format: [
|
|
38
|
-
* [ 1, 5 ] // Format all letters from 2nd to 5th.
|
|
39
|
-
* ]
|
|
40
|
-
* }
|
|
41
|
-
* ```
|
|
42
|
-
*
|
|
43
|
-
* @param formatCallback A callback to apply actual formatting.
|
|
44
|
-
* It should return `false` if changes should not be applied (e.g. if a command is disabled).
|
|
45
|
-
*
|
|
46
|
-
* ```ts
|
|
47
|
-
* inlineAutoformatEditing( editor, plugin, /(\*\*)([^\*]+?)(\*\*)$/g, ( writer, rangesToFormat ) => {
|
|
48
|
-
* const command = editor.commands.get( 'bold' );
|
|
49
|
-
*
|
|
50
|
-
* if ( !command.isEnabled ) {
|
|
51
|
-
* return false;
|
|
52
|
-
* }
|
|
53
|
-
*
|
|
54
|
-
* const validRanges = editor.model.schema.getValidRanges( rangesToFormat, 'bold' );
|
|
55
|
-
*
|
|
56
|
-
* for ( let range of validRanges ) {
|
|
57
|
-
* writer.setAttribute( 'bold', true, range );
|
|
58
|
-
* }
|
|
59
|
-
* } );
|
|
60
|
-
* ```
|
|
61
|
-
*/
|
|
62
|
-
export default function inlineAutoformatEditing(editor, plugin, testRegexpOrCallback, formatCallback) {
|
|
63
|
-
let regExp;
|
|
64
|
-
let testCallback;
|
|
65
|
-
if (testRegexpOrCallback instanceof RegExp) {
|
|
66
|
-
regExp = testRegexpOrCallback;
|
|
67
|
-
}
|
|
68
|
-
else {
|
|
69
|
-
testCallback = testRegexpOrCallback;
|
|
70
|
-
}
|
|
71
|
-
// A test callback run on changed text.
|
|
72
|
-
testCallback = testCallback || (text => {
|
|
73
|
-
let result;
|
|
74
|
-
const remove = [];
|
|
75
|
-
const format = [];
|
|
76
|
-
while ((result = regExp.exec(text)) !== null) {
|
|
77
|
-
// There should be full match and 3 capture groups.
|
|
78
|
-
if (result && result.length < 4) {
|
|
79
|
-
break;
|
|
80
|
-
}
|
|
81
|
-
let { index, '1': leftDel, '2': content, '3': rightDel } = result;
|
|
82
|
-
// Real matched string - there might be some non-capturing groups so we need to recalculate starting index.
|
|
83
|
-
const found = leftDel + content + rightDel;
|
|
84
|
-
index += result[0].length - found.length;
|
|
85
|
-
// Start and End offsets of delimiters to remove.
|
|
86
|
-
const delStart = [
|
|
87
|
-
index,
|
|
88
|
-
index + leftDel.length
|
|
89
|
-
];
|
|
90
|
-
const delEnd = [
|
|
91
|
-
index + leftDel.length + content.length,
|
|
92
|
-
index + leftDel.length + content.length + rightDel.length
|
|
93
|
-
];
|
|
94
|
-
remove.push(delStart);
|
|
95
|
-
remove.push(delEnd);
|
|
96
|
-
format.push([index + leftDel.length, index + leftDel.length + content.length]);
|
|
97
|
-
}
|
|
98
|
-
return {
|
|
99
|
-
remove,
|
|
100
|
-
format
|
|
101
|
-
};
|
|
102
|
-
});
|
|
103
|
-
editor.model.document.on('change:data', (evt, batch) => {
|
|
104
|
-
if (batch.isUndo || !batch.isLocal || !plugin.isEnabled) {
|
|
105
|
-
return;
|
|
106
|
-
}
|
|
107
|
-
const model = editor.model;
|
|
108
|
-
const selection = model.document.selection;
|
|
109
|
-
// Do nothing if selection is not collapsed.
|
|
110
|
-
if (!selection.isCollapsed) {
|
|
111
|
-
return;
|
|
112
|
-
}
|
|
113
|
-
const changes = Array.from(model.document.differ.getChanges());
|
|
114
|
-
const entry = changes[0];
|
|
115
|
-
// Typing is represented by only a single change.
|
|
116
|
-
if (changes.length != 1 || entry.type !== 'insert' || entry.name != '$text' || entry.length != 1) {
|
|
117
|
-
return;
|
|
118
|
-
}
|
|
119
|
-
const focus = selection.focus;
|
|
120
|
-
const block = focus.parent;
|
|
121
|
-
const { text, range } = getTextAfterCode(model.createRange(model.createPositionAt(block, 0), focus), model);
|
|
122
|
-
const testOutput = testCallback(text);
|
|
123
|
-
const rangesToFormat = testOutputToRanges(range.start, testOutput.format, model);
|
|
124
|
-
const rangesToRemove = testOutputToRanges(range.start, testOutput.remove, model);
|
|
125
|
-
if (!(rangesToFormat.length && rangesToRemove.length)) {
|
|
126
|
-
return;
|
|
127
|
-
}
|
|
128
|
-
// Use enqueueChange to create new batch to separate typing batch from the auto-format changes.
|
|
129
|
-
model.enqueueChange(writer => {
|
|
130
|
-
// Apply format.
|
|
131
|
-
const hasChanged = formatCallback(writer, rangesToFormat);
|
|
132
|
-
// Strict check on `false` to have backward compatibility (when callbacks were returning `undefined`).
|
|
133
|
-
if (hasChanged === false) {
|
|
134
|
-
return;
|
|
135
|
-
}
|
|
136
|
-
// Remove delimiters - use reversed order to not mix the offsets while removing.
|
|
137
|
-
for (const range of rangesToRemove.reverse()) {
|
|
138
|
-
writer.remove(range);
|
|
139
|
-
}
|
|
140
|
-
model.enqueueChange(() => {
|
|
141
|
-
const deletePlugin = editor.plugins.get('Delete');
|
|
142
|
-
deletePlugin.requestUndoOnBackspace();
|
|
143
|
-
});
|
|
144
|
-
});
|
|
145
|
-
});
|
|
146
|
-
}
|
|
147
|
-
/**
|
|
148
|
-
* Converts output of the test function provided to the inlineAutoformatEditing and converts it to the model ranges
|
|
149
|
-
* inside provided block.
|
|
150
|
-
*/
|
|
151
|
-
function testOutputToRanges(start, arrays, model) {
|
|
152
|
-
return arrays
|
|
153
|
-
.filter(array => (array[0] !== undefined && array[1] !== undefined))
|
|
154
|
-
.map(array => {
|
|
155
|
-
return model.createRange(start.getShiftedBy(array[0]), start.getShiftedBy(array[1]));
|
|
156
|
-
});
|
|
157
|
-
}
|
|
158
|
-
/**
|
|
159
|
-
* Returns the last text line after the last code element from the given range.
|
|
160
|
-
* It is similar to {@link module:typing/utils/getlasttextline.getLastTextLine `getLastTextLine()`},
|
|
161
|
-
* but it ignores any text before the last `code`.
|
|
162
|
-
*/
|
|
163
|
-
function getTextAfterCode(range, model) {
|
|
164
|
-
let start = range.start;
|
|
165
|
-
const text = Array.from(range.getItems()).reduce((rangeText, node) => {
|
|
166
|
-
// Trim text to a last occurrence of an inline element and update range start.
|
|
167
|
-
if (!(node.is('$text') || node.is('$textProxy')) || node.getAttribute('code')) {
|
|
168
|
-
start = model.createPositionAfter(node);
|
|
169
|
-
return '';
|
|
170
|
-
}
|
|
171
|
-
return rangeText + node.data;
|
|
172
|
-
}, '');
|
|
173
|
-
return { text, range: model.createRange(start, range.end) };
|
|
174
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* @license Copyright (c) 2003-2023, 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
|
+
* Enables autoformatting mechanism for a given {@link module:core/editor/editor~Editor}.
|
|
7
|
+
*
|
|
8
|
+
* It formats the matched text by applying the given model attribute or by running the provided formatting callback.
|
|
9
|
+
* On every {@link module:engine/model/document~Document#event:change:data data change} in the model document
|
|
10
|
+
* the autoformatting engine checks the text on the left of the selection
|
|
11
|
+
* and executes the provided action if the text matches given criteria (regular expression or callback).
|
|
12
|
+
*
|
|
13
|
+
* @param editor The editor instance.
|
|
14
|
+
* @param plugin The autoformat plugin instance.
|
|
15
|
+
* @param testRegexpOrCallback The regular expression or callback to execute on text.
|
|
16
|
+
* Provided regular expression *must* have three capture groups. The first and the third capture group
|
|
17
|
+
* should match opening and closing delimiters. The second capture group should match the text to format.
|
|
18
|
+
*
|
|
19
|
+
* ```ts
|
|
20
|
+
* // Matches the `**bold text**` pattern.
|
|
21
|
+
* // There are three capturing groups:
|
|
22
|
+
* // - The first to match the starting `**` delimiter.
|
|
23
|
+
* // - The second to match the text to format.
|
|
24
|
+
* // - The third to match the ending `**` delimiter.
|
|
25
|
+
* inlineAutoformatEditing( editor, plugin, /(\*\*)([^\*]+?)(\*\*)$/g, formatCallback );
|
|
26
|
+
* ```
|
|
27
|
+
*
|
|
28
|
+
* When a function is provided instead of the regular expression, it will be executed with the text to match as a parameter.
|
|
29
|
+
* The function should return proper "ranges" to delete and format.
|
|
30
|
+
*
|
|
31
|
+
* ```ts
|
|
32
|
+
* {
|
|
33
|
+
* remove: [
|
|
34
|
+
* [ 0, 1 ], // Remove the first letter from the given text.
|
|
35
|
+
* [ 5, 6 ] // Remove the 6th letter from the given text.
|
|
36
|
+
* ],
|
|
37
|
+
* format: [
|
|
38
|
+
* [ 1, 5 ] // Format all letters from 2nd to 5th.
|
|
39
|
+
* ]
|
|
40
|
+
* }
|
|
41
|
+
* ```
|
|
42
|
+
*
|
|
43
|
+
* @param formatCallback A callback to apply actual formatting.
|
|
44
|
+
* It should return `false` if changes should not be applied (e.g. if a command is disabled).
|
|
45
|
+
*
|
|
46
|
+
* ```ts
|
|
47
|
+
* inlineAutoformatEditing( editor, plugin, /(\*\*)([^\*]+?)(\*\*)$/g, ( writer, rangesToFormat ) => {
|
|
48
|
+
* const command = editor.commands.get( 'bold' );
|
|
49
|
+
*
|
|
50
|
+
* if ( !command.isEnabled ) {
|
|
51
|
+
* return false;
|
|
52
|
+
* }
|
|
53
|
+
*
|
|
54
|
+
* const validRanges = editor.model.schema.getValidRanges( rangesToFormat, 'bold' );
|
|
55
|
+
*
|
|
56
|
+
* for ( let range of validRanges ) {
|
|
57
|
+
* writer.setAttribute( 'bold', true, range );
|
|
58
|
+
* }
|
|
59
|
+
* } );
|
|
60
|
+
* ```
|
|
61
|
+
*/
|
|
62
|
+
export default function inlineAutoformatEditing(editor, plugin, testRegexpOrCallback, formatCallback) {
|
|
63
|
+
let regExp;
|
|
64
|
+
let testCallback;
|
|
65
|
+
if (testRegexpOrCallback instanceof RegExp) {
|
|
66
|
+
regExp = testRegexpOrCallback;
|
|
67
|
+
}
|
|
68
|
+
else {
|
|
69
|
+
testCallback = testRegexpOrCallback;
|
|
70
|
+
}
|
|
71
|
+
// A test callback run on changed text.
|
|
72
|
+
testCallback = testCallback || (text => {
|
|
73
|
+
let result;
|
|
74
|
+
const remove = [];
|
|
75
|
+
const format = [];
|
|
76
|
+
while ((result = regExp.exec(text)) !== null) {
|
|
77
|
+
// There should be full match and 3 capture groups.
|
|
78
|
+
if (result && result.length < 4) {
|
|
79
|
+
break;
|
|
80
|
+
}
|
|
81
|
+
let { index, '1': leftDel, '2': content, '3': rightDel } = result;
|
|
82
|
+
// Real matched string - there might be some non-capturing groups so we need to recalculate starting index.
|
|
83
|
+
const found = leftDel + content + rightDel;
|
|
84
|
+
index += result[0].length - found.length;
|
|
85
|
+
// Start and End offsets of delimiters to remove.
|
|
86
|
+
const delStart = [
|
|
87
|
+
index,
|
|
88
|
+
index + leftDel.length
|
|
89
|
+
];
|
|
90
|
+
const delEnd = [
|
|
91
|
+
index + leftDel.length + content.length,
|
|
92
|
+
index + leftDel.length + content.length + rightDel.length
|
|
93
|
+
];
|
|
94
|
+
remove.push(delStart);
|
|
95
|
+
remove.push(delEnd);
|
|
96
|
+
format.push([index + leftDel.length, index + leftDel.length + content.length]);
|
|
97
|
+
}
|
|
98
|
+
return {
|
|
99
|
+
remove,
|
|
100
|
+
format
|
|
101
|
+
};
|
|
102
|
+
});
|
|
103
|
+
editor.model.document.on('change:data', (evt, batch) => {
|
|
104
|
+
if (batch.isUndo || !batch.isLocal || !plugin.isEnabled) {
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
const model = editor.model;
|
|
108
|
+
const selection = model.document.selection;
|
|
109
|
+
// Do nothing if selection is not collapsed.
|
|
110
|
+
if (!selection.isCollapsed) {
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
const changes = Array.from(model.document.differ.getChanges());
|
|
114
|
+
const entry = changes[0];
|
|
115
|
+
// Typing is represented by only a single change.
|
|
116
|
+
if (changes.length != 1 || entry.type !== 'insert' || entry.name != '$text' || entry.length != 1) {
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
const focus = selection.focus;
|
|
120
|
+
const block = focus.parent;
|
|
121
|
+
const { text, range } = getTextAfterCode(model.createRange(model.createPositionAt(block, 0), focus), model);
|
|
122
|
+
const testOutput = testCallback(text);
|
|
123
|
+
const rangesToFormat = testOutputToRanges(range.start, testOutput.format, model);
|
|
124
|
+
const rangesToRemove = testOutputToRanges(range.start, testOutput.remove, model);
|
|
125
|
+
if (!(rangesToFormat.length && rangesToRemove.length)) {
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
// Use enqueueChange to create new batch to separate typing batch from the auto-format changes.
|
|
129
|
+
model.enqueueChange(writer => {
|
|
130
|
+
// Apply format.
|
|
131
|
+
const hasChanged = formatCallback(writer, rangesToFormat);
|
|
132
|
+
// Strict check on `false` to have backward compatibility (when callbacks were returning `undefined`).
|
|
133
|
+
if (hasChanged === false) {
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
// Remove delimiters - use reversed order to not mix the offsets while removing.
|
|
137
|
+
for (const range of rangesToRemove.reverse()) {
|
|
138
|
+
writer.remove(range);
|
|
139
|
+
}
|
|
140
|
+
model.enqueueChange(() => {
|
|
141
|
+
const deletePlugin = editor.plugins.get('Delete');
|
|
142
|
+
deletePlugin.requestUndoOnBackspace();
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Converts output of the test function provided to the inlineAutoformatEditing and converts it to the model ranges
|
|
149
|
+
* inside provided block.
|
|
150
|
+
*/
|
|
151
|
+
function testOutputToRanges(start, arrays, model) {
|
|
152
|
+
return arrays
|
|
153
|
+
.filter(array => (array[0] !== undefined && array[1] !== undefined))
|
|
154
|
+
.map(array => {
|
|
155
|
+
return model.createRange(start.getShiftedBy(array[0]), start.getShiftedBy(array[1]));
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Returns the last text line after the last code element from the given range.
|
|
160
|
+
* It is similar to {@link module:typing/utils/getlasttextline.getLastTextLine `getLastTextLine()`},
|
|
161
|
+
* but it ignores any text before the last `code`.
|
|
162
|
+
*/
|
|
163
|
+
function getTextAfterCode(range, model) {
|
|
164
|
+
let start = range.start;
|
|
165
|
+
const text = Array.from(range.getItems()).reduce((rangeText, node) => {
|
|
166
|
+
// Trim text to a last occurrence of an inline element and update range start.
|
|
167
|
+
if (!(node.is('$text') || node.is('$textProxy')) || node.getAttribute('code')) {
|
|
168
|
+
start = model.createPositionAfter(node);
|
|
169
|
+
return '';
|
|
170
|
+
}
|
|
171
|
+
return rangeText + node.data;
|
|
172
|
+
}, '');
|
|
173
|
+
return { text, range: model.createRange(start, range.end) };
|
|
174
|
+
}
|