@ckeditor/ckeditor5-typing 47.6.1 → 48.0.0-alpha.1

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.
Files changed (41) hide show
  1. package/LICENSE.md +1 -1
  2. package/ckeditor5-metadata.json +1 -1
  3. package/dist/index.css +3 -0
  4. package/dist/index.css.map +1 -0
  5. package/dist/index.js +2 -2
  6. package/dist/index.js.map +1 -1
  7. package/{src → dist}/texttransformation.d.ts +3 -1
  8. package/package.json +22 -41
  9. package/src/augmentation.js +0 -5
  10. package/src/delete.js +0 -122
  11. package/src/deletecommand.js +0 -212
  12. package/src/deleteobserver.js +0 -262
  13. package/src/index.js +0 -21
  14. package/src/input.js +0 -486
  15. package/src/inserttextcommand.js +0 -87
  16. package/src/inserttextobserver.js +0 -112
  17. package/src/texttransformation.js +0 -237
  18. package/src/textwatcher.js +0 -123
  19. package/src/twostepcaretmovement.js +0 -661
  20. package/src/typing.js +0 -33
  21. package/src/typingconfig.js +0 -5
  22. package/src/utils/changebuffer.js +0 -148
  23. package/src/utils/findattributerange.js +0 -41
  24. package/src/utils/getlasttextline.js +0 -43
  25. package/src/utils/inlinehighlight.js +0 -74
  26. /package/{src → dist}/augmentation.d.ts +0 -0
  27. /package/{src → dist}/delete.d.ts +0 -0
  28. /package/{src → dist}/deletecommand.d.ts +0 -0
  29. /package/{src → dist}/deleteobserver.d.ts +0 -0
  30. /package/{src → dist}/index.d.ts +0 -0
  31. /package/{src → dist}/input.d.ts +0 -0
  32. /package/{src → dist}/inserttextcommand.d.ts +0 -0
  33. /package/{src → dist}/inserttextobserver.d.ts +0 -0
  34. /package/{src → dist}/textwatcher.d.ts +0 -0
  35. /package/{src → dist}/twostepcaretmovement.d.ts +0 -0
  36. /package/{src → dist}/typing.d.ts +0 -0
  37. /package/{src → dist}/typingconfig.d.ts +0 -0
  38. /package/{src → dist}/utils/changebuffer.d.ts +0 -0
  39. /package/{src → dist}/utils/findattributerange.d.ts +0 -0
  40. /package/{src → dist}/utils/getlasttextline.d.ts +0 -0
  41. /package/{src → dist}/utils/inlinehighlight.d.ts +0 -0
@@ -1,112 +0,0 @@
1
- /**
2
- * @license Copyright (c) 2003-2026, 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
- * @module typing/inserttextobserver
7
- */
8
- import { env, EventInfo } from '@ckeditor/ckeditor5-utils';
9
- import { ViewDocumentDomEventData, Observer, FocusObserver } from '@ckeditor/ckeditor5-engine';
10
- // @if CK_DEBUG_TYPING // const { _buildLogMessage } = require( '@ckeditor/ckeditor5-engine/src/dev-utils/utils.js' );
11
- const TYPING_INPUT_TYPES = [
12
- // For collapsed range:
13
- // - This one is a regular typing (all browsers, all systems).
14
- // - This one is used by Chrome when typing accented letter – 2nd step when the user selects the accent (Mac).
15
- // For non-collapsed range:
16
- // - This one is used by Chrome when typing accented letter – when the selection box first appears (Mac).
17
- // - This one is used by Safari when accepting spell check suggestions from the context menu (Mac).
18
- 'insertText',
19
- // This one is used by Safari when typing accented letter (Mac).
20
- // This one is used by Safari when accepting spell check suggestions from the autocorrection pop-up (Mac).
21
- 'insertReplacementText'
22
- ];
23
- const TYPING_INPUT_TYPES_ANDROID = [
24
- ...TYPING_INPUT_TYPES,
25
- 'insertCompositionText'
26
- ];
27
- /**
28
- * Text insertion observer introduces the {@link module:engine/view/document~ViewDocument#event:insertText} event.
29
- */
30
- export class InsertTextObserver extends Observer {
31
- /**
32
- * Instance of the focus observer. Insert text observer calls
33
- * {@link module:engine/view/observer/focusobserver~FocusObserver#flush} to mark the latest focus change as complete.
34
- */
35
- focusObserver;
36
- /**
37
- * @inheritDoc
38
- */
39
- constructor(view) {
40
- super(view);
41
- this.focusObserver = view.getObserver(FocusObserver);
42
- // On Android composition events should immediately be applied to the model. Rendering is not disabled.
43
- // On non-Android the model is updated only on composition end.
44
- // On Android we can't rely on composition start/end to update model.
45
- const typingInputTypes = env.isAndroid ? TYPING_INPUT_TYPES_ANDROID : TYPING_INPUT_TYPES;
46
- const viewDocument = view.document;
47
- viewDocument.on('beforeinput', (evt, data) => {
48
- if (!this.isEnabled) {
49
- return;
50
- }
51
- const { data: text, targetRanges, inputType, domEvent, isComposing } = data;
52
- if (!typingInputTypes.includes(inputType)) {
53
- return;
54
- }
55
- // Mark the latest focus change as complete (we are typing in editable after the focus
56
- // so the selection is in the focused element).
57
- this.focusObserver.flush();
58
- const eventInfo = new EventInfo(viewDocument, 'insertText');
59
- viewDocument.fire(eventInfo, new ViewDocumentDomEventData(view, domEvent, {
60
- text,
61
- selection: view.createSelection(targetRanges),
62
- isComposing
63
- }));
64
- // Stop the beforeinput event if `delete` event was stopped.
65
- // https://github.com/ckeditor/ckeditor5/issues/753
66
- if (eventInfo.stop.called) {
67
- evt.stop();
68
- }
69
- });
70
- // On Android composition events are immediately applied to the model.
71
- // On non-Android the model is updated only on composition end.
72
- // On Android we can't rely on composition start/end to update model.
73
- if (!env.isAndroid) {
74
- // Note: The priority must be lower than the CompositionObserver handler to call it after the renderer is unblocked.
75
- // This is important for view to DOM position mapping.
76
- // This causes the effect of first remove composed DOM and then reapply it after model modification.
77
- viewDocument.on('compositionend', (evt, { data, domEvent }) => {
78
- if (!this.isEnabled) {
79
- return;
80
- }
81
- // In case of aborted composition.
82
- if (!data) {
83
- return;
84
- }
85
- // @if CK_DEBUG_TYPING // if ( ( window as any ).logCKETyping ) {
86
- // @if CK_DEBUG_TYPING // console.log( ..._buildLogMessage( this, 'InsertTextObserver',
87
- // @if CK_DEBUG_TYPING // `%cFire insertText event, %c${ JSON.stringify( data ) }`,
88
- // @if CK_DEBUG_TYPING // 'font-weight: bold',
89
- // @if CK_DEBUG_TYPING // 'color: blue'
90
- // @if CK_DEBUG_TYPING // ) );
91
- // @if CK_DEBUG_TYPING // }
92
- // How do we know where to insert the composed text?
93
- // 1. The SelectionObserver is blocked and the view is not updated with the composition changes.
94
- // 2. The last moment before it's locked is the `compositionstart` event.
95
- // 3. The `SelectionObserver` is listening for `compositionstart` event and immediately converts
96
- // the selection. Handle this at the low priority so after the rendering is blocked.
97
- viewDocument.fire('insertText', new ViewDocumentDomEventData(view, domEvent, {
98
- text: data,
99
- isComposing: true
100
- }));
101
- }, { priority: 'low' });
102
- }
103
- }
104
- /**
105
- * @inheritDoc
106
- */
107
- observe() { }
108
- /**
109
- * @inheritDoc
110
- */
111
- stopObserving() { }
112
- }
@@ -1,237 +0,0 @@
1
- /**
2
- * @license Copyright (c) 2003-2026, 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
- * @module typing/texttransformation
7
- */
8
- import { Plugin } from '@ckeditor/ckeditor5-core';
9
- import { TextWatcher } from './textwatcher.js';
10
- import { escapeRegExp } from 'es-toolkit/compat';
11
- // All named transformations.
12
- const TRANSFORMATIONS = {
13
- // Common symbols:
14
- copyright: { from: '(c)', to: '©' },
15
- registeredTrademark: { from: '(r)', to: '®' },
16
- trademark: { from: '(tm)', to: '™' },
17
- // Mathematical:
18
- oneHalf: { from: /(^|[^/a-z0-9])(1\/2)([^/a-z0-9])$/i, to: [null, '½', null] },
19
- oneThird: { from: /(^|[^/a-z0-9])(1\/3)([^/a-z0-9])$/i, to: [null, '⅓', null] },
20
- twoThirds: { from: /(^|[^/a-z0-9])(2\/3)([^/a-z0-9])$/i, to: [null, '⅔', null] },
21
- oneForth: { from: /(^|[^/a-z0-9])(1\/4)([^/a-z0-9])$/i, to: [null, '¼', null] },
22
- threeQuarters: { from: /(^|[^/a-z0-9])(3\/4)([^/a-z0-9])$/i, to: [null, '¾', null] },
23
- lessThanOrEqual: { from: '<=', to: '≤' },
24
- greaterThanOrEqual: { from: '>=', to: '≥' },
25
- notEqual: { from: '!=', to: '≠' },
26
- arrowLeft: { from: '<-', to: '←' },
27
- arrowRight: { from: '->', to: '→' },
28
- // Typography:
29
- horizontalEllipsis: { from: '...', to: '…' },
30
- enDash: { from: /(^| )(--)( )$/, to: [null, '–', null] },
31
- emDash: { from: /(^| )(---)( )$/, to: [null, '—', null] },
32
- // Quotations:
33
- // English, US
34
- quotesPrimary: { from: buildQuotesRegExp('"'), to: [null, '“', null, '”'] },
35
- quotesSecondary: { from: buildQuotesRegExp('\''), to: [null, '‘', null, '’'] },
36
- // English, UK
37
- quotesPrimaryEnGb: { from: buildQuotesRegExp('\''), to: [null, '‘', null, '’'] },
38
- quotesSecondaryEnGb: { from: buildQuotesRegExp('"'), to: [null, '“', null, '”'] },
39
- // Polish
40
- quotesPrimaryPl: { from: buildQuotesRegExp('"'), to: [null, '„', null, '”'] },
41
- quotesSecondaryPl: { from: buildQuotesRegExp('\''), to: [null, '‚', null, '’'] }
42
- };
43
- // Transformation groups.
44
- const TRANSFORMATION_GROUPS = {
45
- symbols: ['copyright', 'registeredTrademark', 'trademark'],
46
- mathematical: [
47
- 'oneHalf', 'oneThird', 'twoThirds', 'oneForth', 'threeQuarters',
48
- 'lessThanOrEqual', 'greaterThanOrEqual', 'notEqual',
49
- 'arrowLeft', 'arrowRight'
50
- ],
51
- typography: ['horizontalEllipsis', 'enDash', 'emDash'],
52
- quotes: ['quotesPrimary', 'quotesSecondary']
53
- };
54
- // A set of default transformations provided by the feature.
55
- const DEFAULT_TRANSFORMATIONS = [
56
- 'symbols',
57
- 'mathematical',
58
- 'typography',
59
- 'quotes'
60
- ];
61
- /**
62
- * The text transformation plugin.
63
- */
64
- export class TextTransformation extends Plugin {
65
- /**
66
- * @inheritDoc
67
- */
68
- static get requires() {
69
- return ['Delete', 'Input'];
70
- }
71
- /**
72
- * @inheritDoc
73
- */
74
- static get pluginName() {
75
- return 'TextTransformation';
76
- }
77
- /**
78
- * @inheritDoc
79
- */
80
- static get isOfficialPlugin() {
81
- return true;
82
- }
83
- /**
84
- * @inheritDoc
85
- */
86
- constructor(editor) {
87
- super(editor);
88
- editor.config.define('typing', {
89
- transformations: {
90
- include: DEFAULT_TRANSFORMATIONS
91
- }
92
- });
93
- }
94
- /**
95
- * @inheritDoc
96
- */
97
- init() {
98
- const model = this.editor.model;
99
- const modelSelection = model.document.selection;
100
- modelSelection.on('change:range', () => {
101
- // Disable plugin when selection is inside a code block or inline code.
102
- const anchor = modelSelection.anchor;
103
- const isInCodeBlock = !!anchor && anchor.parent.is('element', 'codeBlock');
104
- const isInInlineCode = modelSelection.hasAttribute('code');
105
- this.isEnabled = !(isInCodeBlock || isInInlineCode);
106
- });
107
- this._enableTransformationWatchers();
108
- }
109
- /**
110
- * Create new TextWatcher listening to the editor for typing and selection events.
111
- */
112
- _enableTransformationWatchers() {
113
- const editor = this.editor;
114
- const model = editor.model;
115
- const deletePlugin = editor.plugins.get('Delete');
116
- const normalizedTransformations = normalizeTransformations(editor.config.get('typing.transformations'));
117
- const testCallback = (text) => {
118
- for (const normalizedTransformation of normalizedTransformations) {
119
- const from = normalizedTransformation.from;
120
- const match = from.test(text);
121
- if (match) {
122
- return { normalizedTransformation };
123
- }
124
- }
125
- };
126
- const watcher = new TextWatcher(editor.model, testCallback);
127
- watcher.on('matched:data', (evt, data) => {
128
- if (!data.batch.isTyping) {
129
- return;
130
- }
131
- const { from, to } = data.normalizedTransformation;
132
- const matches = from.exec(data.text);
133
- const replaces = to(matches.slice(1));
134
- const matchedRange = data.range;
135
- let changeIndex = matches.index;
136
- model.enqueueChange(writer => {
137
- for (let i = 1; i < matches.length; i++) {
138
- const match = matches[i];
139
- const replaceWith = replaces[i - 1];
140
- if (replaceWith == null) {
141
- changeIndex += match.length;
142
- continue;
143
- }
144
- const replacePosition = matchedRange.start.getShiftedBy(changeIndex);
145
- const replaceRange = model.createRange(replacePosition, replacePosition.getShiftedBy(match.length));
146
- const attributes = getTextAttributesAfterPosition(replacePosition);
147
- model.insertContent(writer.createText(replaceWith, attributes), replaceRange);
148
- changeIndex += replaceWith.length;
149
- }
150
- model.enqueueChange(() => {
151
- deletePlugin.requestUndoOnBackspace();
152
- });
153
- });
154
- });
155
- watcher.bind('isEnabled').to(this);
156
- }
157
- }
158
- /**
159
- * Normalizes the configuration `from` parameter value.
160
- * The normalized value for the `from` parameter is a RegExp instance. If the passed `from` is already a RegExp instance,
161
- * it is returned unchanged.
162
- */
163
- function normalizeFrom(from) {
164
- if (typeof from == 'string') {
165
- return new RegExp(`(${escapeRegExp(from)})$`);
166
- }
167
- // `from` is already a regular expression.
168
- return from;
169
- }
170
- /**
171
- * Normalizes the configuration `to` parameter value.
172
- * The normalized value for the `to` parameter is a function that takes an array and returns an array. See more in the
173
- * configuration description. If the passed `to` is already a function, it is returned unchanged.
174
- */
175
- function normalizeTo(to) {
176
- if (typeof to == 'string') {
177
- return () => [to];
178
- }
179
- else if (to instanceof Array) {
180
- return () => to;
181
- }
182
- // `to` is already a function.
183
- return to;
184
- }
185
- /**
186
- * For given `position` returns attributes for the text that is after that position.
187
- * The text can be in the same text node as the position (`foo[]bar`) or in the next text node (`foo[]<$text bold="true">bar</$text>`).
188
- */
189
- function getTextAttributesAfterPosition(position) {
190
- const textNode = position.textNode ? position.textNode : position.nodeAfter;
191
- return textNode.getAttributes();
192
- }
193
- /**
194
- * Returns a RegExp pattern string that detects a sentence inside a quote.
195
- *
196
- * @param quoteCharacter The character to create a pattern for.
197
- */
198
- function buildQuotesRegExp(quoteCharacter) {
199
- return new RegExp(`(^|\\s)(${quoteCharacter})([^${quoteCharacter}]*)(${quoteCharacter})$`);
200
- }
201
- /**
202
- * Reads text transformation config and returns normalized array of transformations objects.
203
- */
204
- function normalizeTransformations(config) {
205
- const extra = config.extra || [];
206
- const remove = config.remove || [];
207
- const isNotRemoved = (transformation) => !remove.includes(transformation);
208
- const configured = config.include.concat(extra).filter(isNotRemoved);
209
- return expandGroupsAndRemoveDuplicates(configured)
210
- .filter(isNotRemoved) // Filter out 'remove' transformations as they might be set in group.
211
- .map(transformation => (typeof transformation == 'string' && TRANSFORMATIONS[transformation] ? TRANSFORMATIONS[transformation] : transformation))
212
- // Filter out transformations set as string that has not been found.
213
- .filter((transformation) => typeof transformation === 'object')
214
- .map(transformation => ({
215
- from: normalizeFrom(transformation.from),
216
- to: normalizeTo(transformation.to)
217
- }));
218
- }
219
- /**
220
- * Reads definitions and expands named groups if needed to transformation names.
221
- * This method also removes duplicated named transformations if any.
222
- */
223
- function expandGroupsAndRemoveDuplicates(definitions) {
224
- // Set is using to make sure that transformation names are not duplicated.
225
- const definedTransformations = new Set();
226
- for (const transformationOrGroup of definitions) {
227
- if (typeof transformationOrGroup == 'string' && TRANSFORMATION_GROUPS[transformationOrGroup]) {
228
- for (const transformation of TRANSFORMATION_GROUPS[transformationOrGroup]) {
229
- definedTransformations.add(transformation);
230
- }
231
- }
232
- else {
233
- definedTransformations.add(transformationOrGroup);
234
- }
235
- }
236
- return Array.from(definedTransformations);
237
- }
@@ -1,123 +0,0 @@
1
- /**
2
- * @license Copyright (c) 2003-2026, 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
- * @module typing/textwatcher
7
- */
8
- import { ObservableMixin } from '@ckeditor/ckeditor5-utils';
9
- import { getLastTextLine } from './utils/getlasttextline.js';
10
- /**
11
- * The text watcher feature.
12
- *
13
- * Fires the {@link module:typing/textwatcher~TextWatcher#event:matched:data `matched:data`},
14
- * {@link module:typing/textwatcher~TextWatcher#event:matched:selection `matched:selection`} and
15
- * {@link module:typing/textwatcher~TextWatcher#event:unmatched `unmatched`} events on typing or selection changes.
16
- */
17
- export class TextWatcher extends /* #__PURE__ */ ObservableMixin() {
18
- /**
19
- * The editor's model.
20
- */
21
- model;
22
- /**
23
- * The function used to match the text.
24
- *
25
- * The test callback can return 3 values:
26
- *
27
- * * `false` if there is no match,
28
- * * `true` if there is a match,
29
- * * an object if there is a match and we want to pass some additional information to the {@link #event:matched:data} event.
30
- */
31
- testCallback;
32
- /**
33
- * Whether there is a match currently.
34
- */
35
- _hasMatch;
36
- /**
37
- * Creates a text watcher instance.
38
- *
39
- * @param testCallback See {@link module:typing/textwatcher~TextWatcher#testCallback}.
40
- */
41
- constructor(model, testCallback) {
42
- super();
43
- this.model = model;
44
- this.testCallback = testCallback;
45
- this._hasMatch = false;
46
- this.set('isEnabled', true);
47
- // Toggle text watching on isEnabled state change.
48
- this.on('change:isEnabled', () => {
49
- if (this.isEnabled) {
50
- this._startListening();
51
- }
52
- else {
53
- this.stopListening(model.document.selection);
54
- this.stopListening(model.document);
55
- }
56
- });
57
- this._startListening();
58
- }
59
- /**
60
- * Flag indicating whether there is a match currently.
61
- */
62
- get hasMatch() {
63
- return this._hasMatch;
64
- }
65
- /**
66
- * Starts listening to the editor for typing and selection events.
67
- */
68
- _startListening() {
69
- const model = this.model;
70
- const document = model.document;
71
- this.listenTo(document.selection, 'change:range', (evt, { directChange }) => {
72
- // Indirect changes (i.e. when the user types or external changes are applied) are handled in the document's change event.
73
- if (!directChange) {
74
- return;
75
- }
76
- // Act only on collapsed selection.
77
- if (!document.selection.isCollapsed) {
78
- if (this.hasMatch) {
79
- this.fire('unmatched');
80
- this._hasMatch = false;
81
- }
82
- return;
83
- }
84
- this._evaluateTextBeforeSelection('selection');
85
- });
86
- this.listenTo(document, 'change:data', (evt, batch) => {
87
- if (batch.isUndo || !batch.isLocal) {
88
- return;
89
- }
90
- this._evaluateTextBeforeSelection('data', { batch });
91
- });
92
- }
93
- /**
94
- * Checks the editor content for matched text.
95
- *
96
- * @fires matched:data
97
- * @fires matched:selection
98
- * @fires unmatched
99
- *
100
- * @param suffix A suffix used for generating the event name.
101
- * @param data Data object for event.
102
- */
103
- _evaluateTextBeforeSelection(suffix, data = {}) {
104
- const model = this.model;
105
- const document = model.document;
106
- const selection = document.selection;
107
- const rangeBeforeSelection = model.createRange(model.createPositionAt(selection.focus.parent, 0), selection.focus);
108
- const { text, range } = getLastTextLine(rangeBeforeSelection, model);
109
- const testResult = this.testCallback(text);
110
- if (!testResult && this.hasMatch) {
111
- this.fire('unmatched');
112
- }
113
- this._hasMatch = !!testResult;
114
- if (testResult) {
115
- const eventData = Object.assign(data, { text, range });
116
- // If the test callback returns an object with additional data, assign the data as well.
117
- if (typeof testResult == 'object') {
118
- Object.assign(eventData, testResult);
119
- }
120
- this.fire(`matched:${suffix}`, eventData);
121
- }
122
- }
123
- }