@ckeditor/ckeditor5-code-block 35.4.0 → 36.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/src/converters.js CHANGED
@@ -1,311 +1,277 @@
1
1
  /**
2
- * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
2
+ * @license Copyright (c) 2003-2023, CKSource Holding sp. z o.o. All rights reserved.
3
3
  * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
4
4
  */
5
-
6
- /**
7
- * @module code-block/converters
8
- */
9
-
10
5
  import { getPropertyAssociation } from './utils';
11
-
12
6
  /**
13
7
  * A model-to-view (both editing and data) converter for the `codeBlock` element.
14
8
  *
15
9
  * Sample input:
16
10
  *
17
- * <codeBlock language="javascript">foo();<softBreak></softBreak>bar();</codeBlock>
11
+ * ```html
12
+ * <codeBlock language="javascript">foo();<softBreak></softBreak>bar();</codeBlock>
13
+ * ```
18
14
  *
19
15
  * Sample output (editing):
20
16
  *
21
- * <pre data-language="JavaScript"><code class="language-javascript">foo();<br />bar();</code></pre>
17
+ * ```html
18
+ * <pre data-language="JavaScript"><code class="language-javascript">foo();<br />bar();</code></pre>
19
+ * ```
22
20
  *
23
21
  * Sample output (data, see {@link module:code-block/converters~modelToDataViewSoftBreakInsertion}):
24
22
  *
25
- * <pre><code class="language-javascript">foo();\nbar();</code></pre>
23
+ * ```html
24
+ * <pre><code class="language-javascript">foo();\nbar();</code></pre>
25
+ * ```
26
26
  *
27
- * @param {module:engine/model/model~Model} model
28
- * @param {Array.<module:code-block/codeblock~CodeBlockLanguageDefinition>} languageDefs The normalized language
29
- * configuration passed to the feature.
30
- * @param {Boolean} [useLabels=false] When `true`, the `<pre>` element will get a `data-language` attribute with a
27
+ * @param languageDefs The normalized language configuration passed to the feature.
28
+ * @param useLabels When `true`, the `<pre>` element will get a `data-language` attribute with a
31
29
  * human–readable label of the language. Used only in the editing.
32
- * @returns {Function} Returns a conversion callback.
30
+ * @returns Returns a conversion callback.
33
31
  */
34
- export function modelToViewCodeBlockInsertion( model, languageDefs, useLabels = false ) {
35
- // Language CSS classes:
36
- //
37
- // {
38
- // php: 'language-php',
39
- // python: 'language-python',
40
- // javascript: 'js',
41
- // ...
42
- // }
43
- const languagesToClasses = getPropertyAssociation( languageDefs, 'language', 'class' );
44
-
45
- // Language labels:
46
- //
47
- // {
48
- // php: 'PHP',
49
- // python: 'Python',
50
- // javascript: 'JavaScript',
51
- // ...
52
- // }
53
- const languagesToLabels = getPropertyAssociation( languageDefs, 'language', 'label' );
54
-
55
- return ( evt, data, conversionApi ) => {
56
- const { writer, mapper, consumable } = conversionApi;
57
-
58
- if ( !consumable.consume( data.item, 'insert' ) ) {
59
- return;
60
- }
61
-
62
- const codeBlockLanguage = data.item.getAttribute( 'language' );
63
- const targetViewPosition = mapper.toViewPosition( model.createPositionBefore( data.item ) );
64
- const preAttributes = {};
65
-
66
- // Attributes added only in the editing view.
67
- if ( useLabels ) {
68
- preAttributes[ 'data-language' ] = languagesToLabels[ codeBlockLanguage ];
69
- preAttributes.spellcheck = 'false';
70
- }
71
-
72
- const code = writer.createContainerElement( 'code', {
73
- class: languagesToClasses[ codeBlockLanguage ] || null
74
- } );
75
-
76
- const pre = writer.createContainerElement( 'pre', preAttributes, code );
77
-
78
- writer.insert( targetViewPosition, pre );
79
- mapper.bindElements( data.item, code );
80
- };
32
+ export function modelToViewCodeBlockInsertion(model, languageDefs, useLabels = false) {
33
+ // Language CSS classes:
34
+ //
35
+ // {
36
+ // php: 'language-php',
37
+ // python: 'language-python',
38
+ // javascript: 'js',
39
+ // ...
40
+ // }
41
+ const languagesToClasses = getPropertyAssociation(languageDefs, 'language', 'class');
42
+ // Language labels:
43
+ //
44
+ // {
45
+ // php: 'PHP',
46
+ // python: 'Python',
47
+ // javascript: 'JavaScript',
48
+ // ...
49
+ // }
50
+ const languagesToLabels = getPropertyAssociation(languageDefs, 'language', 'label');
51
+ return (evt, data, conversionApi) => {
52
+ const { writer, mapper, consumable } = conversionApi;
53
+ if (!consumable.consume(data.item, 'insert')) {
54
+ return;
55
+ }
56
+ const codeBlockLanguage = data.item.getAttribute('language');
57
+ const targetViewPosition = mapper.toViewPosition(model.createPositionBefore(data.item));
58
+ const preAttributes = {};
59
+ // Attributes added only in the editing view.
60
+ if (useLabels) {
61
+ preAttributes['data-language'] = languagesToLabels[codeBlockLanguage];
62
+ preAttributes.spellcheck = 'false';
63
+ }
64
+ const codeAttributes = languagesToClasses[codeBlockLanguage] ? {
65
+ class: languagesToClasses[codeBlockLanguage]
66
+ } : undefined;
67
+ const code = writer.createContainerElement('code', codeAttributes);
68
+ const pre = writer.createContainerElement('pre', preAttributes, code);
69
+ writer.insert(targetViewPosition, pre);
70
+ mapper.bindElements(data.item, code);
71
+ };
81
72
  }
82
-
83
73
  /**
84
74
  * A model-to-data view converter for the new line (`softBreak`) separator.
85
75
  *
86
76
  * Sample input:
87
77
  *
88
- * <codeBlock ...>foo();<softBreak></softBreak>bar();</codeBlock>
78
+ * ```html
79
+ * <codeBlock ...>foo();<softBreak></softBreak>bar();</codeBlock>
80
+ * ```
89
81
  *
90
82
  * Sample output:
91
83
  *
92
- * <pre><code ...>foo();\nbar();</code></pre>
84
+ * ```html
85
+ * <pre><code ...>foo();\nbar();</code></pre>
86
+ * ```
93
87
  *
94
- * @param {module:engine/model/model~Model} model
95
- * @returns {Function} Returns a conversion callback.
88
+ * @returns Returns a conversion callback.
96
89
  */
97
- export function modelToDataViewSoftBreakInsertion( model ) {
98
- return ( evt, data, conversionApi ) => {
99
- if ( data.item.parent.name !== 'codeBlock' ) {
100
- return;
101
- }
102
-
103
- const { writer, mapper, consumable } = conversionApi;
104
-
105
- if ( !consumable.consume( data.item, 'insert' ) ) {
106
- return;
107
- }
108
-
109
- const position = mapper.toViewPosition( model.createPositionBefore( data.item ) );
110
-
111
- writer.insert( position, writer.createText( '\n' ) );
112
- };
90
+ export function modelToDataViewSoftBreakInsertion(model) {
91
+ return (evt, data, conversionApi) => {
92
+ if (data.item.parent.name !== 'codeBlock') {
93
+ return;
94
+ }
95
+ const { writer, mapper, consumable } = conversionApi;
96
+ if (!consumable.consume(data.item, 'insert')) {
97
+ return;
98
+ }
99
+ const position = mapper.toViewPosition(model.createPositionBefore(data.item));
100
+ writer.insert(position, writer.createText('\n'));
101
+ };
113
102
  }
114
-
115
103
  /**
116
104
  * A view-to-model converter for `<pre>` with the `<code>` HTML.
117
105
  *
118
106
  * Sample input:
119
107
  *
120
- * <pre><code class="language-javascript">foo();bar();</code></pre>
108
+ * ```html
109
+ * <pre><code class="language-javascript">foo();bar();</code></pre>
110
+ * ```
121
111
  *
122
112
  * Sample output:
123
113
  *
124
- * <codeBlock language="javascript">foo();bar();</codeBlock>
114
+ * ```html
115
+ * <codeBlock language="javascript">foo();bar();</codeBlock>
116
+ * ```
125
117
  *
126
- * @param {module:engine/view/view~View} editingView
127
- * @param {Array.<module:code-block/codeblock~CodeBlockLanguageDefinition>} languageDefs The normalized language
128
- * configuration passed to the feature.
129
- * @returns {Function} Returns a conversion callback.
118
+ * @param languageDefs The normalized language configuration passed to the feature.
119
+ * @returns Returns a conversion callback.
130
120
  */
131
- export function dataViewToModelCodeBlockInsertion( editingView, languageDefs ) {
132
- // Language names associated with CSS classes:
133
- //
134
- // {
135
- // 'language-php': 'php',
136
- // 'language-python': 'python',
137
- // js: 'javascript',
138
- // ...
139
- // }
140
- const classesToLanguages = getPropertyAssociation( languageDefs, 'class', 'language' );
141
- const defaultLanguageName = languageDefs[ 0 ].language;
142
-
143
- return ( evt, data, conversionApi ) => {
144
- const viewCodeElement = data.viewItem;
145
- const viewPreElement = viewCodeElement.parent;
146
-
147
- if ( !viewPreElement || !viewPreElement.is( 'element', 'pre' ) ) {
148
- return;
149
- }
150
-
151
- // In case of nested code blocks we don't want to convert to another code block.
152
- if ( data.modelCursor.findAncestor( 'codeBlock' ) ) {
153
- return;
154
- }
155
-
156
- const { consumable, writer } = conversionApi;
157
-
158
- if ( !consumable.test( viewCodeElement, { name: true } ) ) {
159
- return;
160
- }
161
-
162
- const codeBlock = writer.createElement( 'codeBlock' );
163
- const viewChildClasses = [ ...viewCodeElement.getClassNames() ];
164
-
165
- // As we're to associate each class with a model language, a lack of class (empty class) can be
166
- // also associated with a language if the language definition was configured so. Pushing an empty
167
- // string to make sure the association will work.
168
- if ( !viewChildClasses.length ) {
169
- viewChildClasses.push( '' );
170
- }
171
-
172
- // Figure out if any of the <code> element's class names is a valid programming
173
- // language class. If so, use it on the model element (becomes the language of the entire block).
174
- for ( const className of viewChildClasses ) {
175
- const language = classesToLanguages[ className ];
176
-
177
- if ( language ) {
178
- writer.setAttribute( 'language', language, codeBlock );
179
- break;
180
- }
181
- }
182
-
183
- // If no language value was set, use the default language from the config.
184
- if ( !codeBlock.hasAttribute( 'language' ) ) {
185
- writer.setAttribute( 'language', defaultLanguageName, codeBlock );
186
- }
187
-
188
- conversionApi.convertChildren( viewCodeElement, codeBlock );
189
-
190
- // Let's try to insert code block.
191
- if ( !conversionApi.safeInsert( codeBlock, data.modelCursor ) ) {
192
- return;
193
- }
194
-
195
- consumable.consume( viewCodeElement, { name: true } );
196
-
197
- conversionApi.updateConversionResult( codeBlock, data );
198
- };
121
+ export function dataViewToModelCodeBlockInsertion(editingView, languageDefs) {
122
+ // Language names associated with CSS classes:
123
+ //
124
+ // {
125
+ // 'language-php': 'php',
126
+ // 'language-python': 'python',
127
+ // js: 'javascript',
128
+ // ...
129
+ // }
130
+ const classesToLanguages = getPropertyAssociation(languageDefs, 'class', 'language');
131
+ const defaultLanguageName = languageDefs[0].language;
132
+ return (evt, data, conversionApi) => {
133
+ const viewCodeElement = data.viewItem;
134
+ const viewPreElement = viewCodeElement.parent;
135
+ if (!viewPreElement || !viewPreElement.is('element', 'pre')) {
136
+ return;
137
+ }
138
+ // In case of nested code blocks we don't want to convert to another code block.
139
+ if (data.modelCursor.findAncestor('codeBlock')) {
140
+ return;
141
+ }
142
+ const { consumable, writer } = conversionApi;
143
+ if (!consumable.test(viewCodeElement, { name: true })) {
144
+ return;
145
+ }
146
+ const codeBlock = writer.createElement('codeBlock');
147
+ const viewChildClasses = [...viewCodeElement.getClassNames()];
148
+ // As we're to associate each class with a model language, a lack of class (empty class) can be
149
+ // also associated with a language if the language definition was configured so. Pushing an empty
150
+ // string to make sure the association will work.
151
+ if (!viewChildClasses.length) {
152
+ viewChildClasses.push('');
153
+ }
154
+ // Figure out if any of the <code> element's class names is a valid programming
155
+ // language class. If so, use it on the model element (becomes the language of the entire block).
156
+ for (const className of viewChildClasses) {
157
+ const language = classesToLanguages[className];
158
+ if (language) {
159
+ writer.setAttribute('language', language, codeBlock);
160
+ break;
161
+ }
162
+ }
163
+ // If no language value was set, use the default language from the config.
164
+ if (!codeBlock.hasAttribute('language')) {
165
+ writer.setAttribute('language', defaultLanguageName, codeBlock);
166
+ }
167
+ conversionApi.convertChildren(viewCodeElement, codeBlock);
168
+ // Let's try to insert code block.
169
+ if (!conversionApi.safeInsert(codeBlock, data.modelCursor)) {
170
+ return;
171
+ }
172
+ consumable.consume(viewCodeElement, { name: true });
173
+ conversionApi.updateConversionResult(codeBlock, data);
174
+ };
199
175
  }
200
-
201
176
  /**
202
177
  * A view-to-model converter for new line characters in `<pre>`.
203
178
  *
204
179
  * Sample input:
205
180
  *
206
- * <pre><code class="language-javascript">foo();\nbar();</code></pre>
181
+ * ```html
182
+ * <pre><code class="language-javascript">foo();\nbar();</code></pre>
183
+ * ```
207
184
  *
208
185
  * Sample output:
209
186
  *
210
- * <codeBlock language="javascript">foo();<softBreak></softBreak>bar();</codeBlock>
187
+ * ```html
188
+ * <codeBlock language="javascript">foo();<softBreak></softBreak>bar();</codeBlock>
189
+ * ```
211
190
  *
212
191
  * @returns {Function} Returns a conversion callback.
213
192
  */
214
193
  export function dataViewToModelTextNewlinesInsertion() {
215
- return ( evt, data, { consumable, writer } ) => {
216
- let position = data.modelCursor;
217
-
218
- // When node is already converted then do nothing.
219
- if ( !consumable.test( data.viewItem ) ) {
220
- return;
221
- }
222
-
223
- // When not inside `codeBlock` then do nothing.
224
- if ( !position.findAncestor( 'codeBlock' ) ) {
225
- return;
226
- }
227
-
228
- consumable.consume( data.viewItem );
229
-
230
- const text = data.viewItem.data;
231
- const textLines = text.split( '\n' ).map( data => writer.createText( data ) );
232
- const lastLine = textLines[ textLines.length - 1 ];
233
-
234
- for ( const node of textLines ) {
235
- writer.insert( node, position );
236
- position = position.getShiftedBy( node.offsetSize );
237
-
238
- if ( node !== lastLine ) {
239
- const softBreak = writer.createElement( 'softBreak' );
240
-
241
- writer.insert( softBreak, position );
242
- position = writer.createPositionAfter( softBreak );
243
- }
244
- }
245
-
246
- data.modelRange = writer.createRange(
247
- data.modelCursor,
248
- position
249
- );
250
- data.modelCursor = position;
251
- };
194
+ return (evt, data, { consumable, writer }) => {
195
+ let position = data.modelCursor;
196
+ // When node is already converted then do nothing.
197
+ if (!consumable.test(data.viewItem)) {
198
+ return;
199
+ }
200
+ // When not inside `codeBlock` then do nothing.
201
+ if (!position.findAncestor('codeBlock')) {
202
+ return;
203
+ }
204
+ consumable.consume(data.viewItem);
205
+ const text = data.viewItem.data;
206
+ const textLines = text.split('\n').map(data => writer.createText(data));
207
+ const lastLine = textLines[textLines.length - 1];
208
+ for (const node of textLines) {
209
+ writer.insert(node, position);
210
+ position = position.getShiftedBy(node.offsetSize);
211
+ if (node !== lastLine) {
212
+ const softBreak = writer.createElement('softBreak');
213
+ writer.insert(softBreak, position);
214
+ position = writer.createPositionAfter(softBreak);
215
+ }
216
+ }
217
+ data.modelRange = writer.createRange(data.modelCursor, position);
218
+ data.modelCursor = position;
219
+ };
252
220
  }
253
-
254
221
  /**
255
222
  * A view-to-model converter that handles orphan text nodes (white spaces, new lines, etc.)
256
223
  * that surround `<code>` inside `<pre>`.
257
224
  *
258
225
  * Sample input:
259
226
  *
260
- * // White spaces
261
- * <pre> <code>foo()</code> </pre>
227
+ * ```html
228
+ * // White spaces
229
+ * <pre> <code>foo()</code> </pre>
262
230
  *
263
- * // White spaces
264
- * <pre> <code>foo()</code> </pre>
231
+ * // White spaces
232
+ * <pre> <code>foo()</code> </pre>
265
233
  *
266
- * // White spaces
267
- * <pre> <code>foo()</code> </pre>
234
+ * // White spaces
235
+ * <pre> <code>foo()</code> </pre>
268
236
  *
269
- * // New lines
270
- * <pre>
271
- * <code>foo()</code>
272
- * </pre>
237
+ * // New lines
238
+ * <pre>
239
+ * <code>foo()</code>
240
+ * </pre>
273
241
  *
274
- * // Redundant text
275
- * <pre>ABC<code>foo()</code>DEF</pre>
242
+ * // Redundant text
243
+ * <pre>ABC<code>foo()</code>DEF</pre>
244
+ * ```
276
245
  *
277
246
  * Unified output for each case:
278
247
  *
279
- * <codeBlock language="plaintext">foo()</codeBlock>
248
+ * ```html
249
+ * <codeBlock language="plaintext">foo()</codeBlock>
250
+ * ```
280
251
  *
281
- * @returns {Function} Returns a conversion callback.
252
+ * @returns Returns a conversion callback.
282
253
  */
283
254
  export function dataViewToModelOrphanNodeConsumer() {
284
- return ( evt, data, { consumable } ) => {
285
- const preElement = data.viewItem;
286
-
287
- // Don't clean up nested pre elements. Their content should stay as it is, they are not upcasted
288
- // to code blocks.
289
- if ( preElement.findAncestor( 'pre' ) ) {
290
- return;
291
- }
292
-
293
- const preChildren = Array.from( preElement.getChildren() );
294
- const childCodeElement = preChildren.find( node => node.is( 'element', 'code' ) );
295
-
296
- // <code>-less <pre>. It will not upcast to code block in the model, skipping.
297
- if ( !childCodeElement ) {
298
- return;
299
- }
300
-
301
- for ( const child of preChildren ) {
302
- if ( child === childCodeElement || !child.is( '$text' ) ) {
303
- continue;
304
- }
305
-
306
- // Consuming the orphan to remove it from the input data.
307
- // Second argument in `consumable.consume` is discarded for text nodes.
308
- consumable.consume( child, { name: true } );
309
- }
310
- };
255
+ return (evt, data, { consumable }) => {
256
+ const preElement = data.viewItem;
257
+ // Don't clean up nested pre elements. Their content should stay as it is, they are not upcasted
258
+ // to code blocks.
259
+ if (preElement.findAncestor('pre')) {
260
+ return;
261
+ }
262
+ const preChildren = Array.from(preElement.getChildren());
263
+ const childCodeElement = preChildren.find(node => node.is('element', 'code'));
264
+ // <code>-less <pre>. It will not upcast to code block in the model, skipping.
265
+ if (!childCodeElement) {
266
+ return;
267
+ }
268
+ for (const child of preChildren) {
269
+ if (child === childCodeElement || !child.is('$text')) {
270
+ continue;
271
+ }
272
+ // Consuming the orphan to remove it from the input data.
273
+ // Second argument in `consumable.consume` is discarded for text nodes.
274
+ consumable.consume(child, { name: true });
275
+ }
276
+ };
311
277
  }