converters.js 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206
  1. /**
  2. * @license Copyright (c) 2003-2020, CKSource - Frederico Knabben. All rights reserved.
  3. * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
  4. */
  5. /**
  6. * @module code-block/converters
  7. */
  8. import {
  9. rawSnippetTextToModelDocumentFragment,
  10. getPropertyAssociation
  11. } from './utils';
  12. /**
  13. * A model-to-view (both editing and data) converter for the `codeBlock` element.
  14. *
  15. * Sample input:
  16. *
  17. * <codeBlock language="javascript">foo();<softBreak></softBreak>bar();</codeBlock>
  18. *
  19. * Sample output (editing):
  20. *
  21. * <pre data-language="JavaScript"><code class="language-javascript">foo();<br />bar();</code></pre>
  22. *
  23. * Sample output (data, see {@link module:code-block/converters~modelToDataViewSoftBreakInsertion}):
  24. *
  25. * <pre><code class="language-javascript">foo();\nbar();</code></pre>
  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
  31. * human–readable label of the language. Used only in the editing.
  32. * @returns {Function} Returns a conversion callback.
  33. */
  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. // Language labels:
  45. //
  46. // {
  47. // php: 'PHP',
  48. // python: 'Python',
  49. // javascript: 'JavaScript',
  50. // ...
  51. // }
  52. const languagesToLabels = getPropertyAssociation( languageDefs, 'language', 'label' );
  53. return ( evt, data, conversionApi ) => {
  54. const { writer, mapper, consumable } = conversionApi;
  55. if ( !consumable.consume( data.item, 'insert' ) ) {
  56. return;
  57. }
  58. const codeBlockLanguage = data.item.getAttribute( 'language' );
  59. const targetViewPosition = mapper.toViewPosition( model.createPositionBefore( data.item ) );
  60. const preAttributes = {};
  61. // Attributes added only in the editing view.
  62. if ( useLabels ) {
  63. preAttributes[ 'data-language' ] = languagesToLabels[ codeBlockLanguage ];
  64. preAttributes.spellcheck = 'false';
  65. }
  66. const pre = writer.createContainerElement( 'pre', preAttributes );
  67. const code = writer.createContainerElement( 'code', {
  68. class: languagesToClasses[ codeBlockLanguage ] || null
  69. } );
  70. writer.insert( writer.createPositionAt( pre, 0 ), code );
  71. writer.insert( targetViewPosition, pre );
  72. mapper.bindElements( data.item, code );
  73. };
  74. }
  75. /**
  76. * A model-to-data view converter for the new line (`softBreak`) separator.
  77. *
  78. * Sample input:
  79. *
  80. * <codeBlock ...>foo();<softBreak></softBreak>bar();</codeBlock>
  81. *
  82. * Sample output:
  83. *
  84. * <pre><code ...>foo();\nbar();</code></pre>
  85. *
  86. * @param {module:engine/model/model~Model} model
  87. * @returns {Function} Returns a conversion callback.
  88. */
  89. export function modelToDataViewSoftBreakInsertion( model ) {
  90. return ( evt, data, conversionApi ) => {
  91. if ( data.item.parent.name !== 'codeBlock' ) {
  92. return;
  93. }
  94. const { writer, mapper, consumable } = conversionApi;
  95. if ( !consumable.consume( data.item, 'insert' ) ) {
  96. return;
  97. }
  98. const position = mapper.toViewPosition( model.createPositionBefore( data.item ) );
  99. writer.insert( position, writer.createText( '\n' ) );
  100. };
  101. }
  102. /**
  103. * A view-to-model converter for `<pre>` with the `<code>` HTML.
  104. *
  105. * Sample input:
  106. *
  107. * <pre><code class="language-javascript">foo();\nbar();</code></pre>
  108. *
  109. * Sample output:
  110. *
  111. * <codeBlock language="javascript">foo();<softBreak></softBreak>bar();</codeBlock>
  112. *
  113. * @param {module:engine/view/view~View} editingView
  114. * @param {Array.<module:code-block/codeblock~CodeBlockLanguageDefinition>} languageDefs The normalized language
  115. * configuration passed to the feature.
  116. * @returns {Function} Returns a conversion callback.
  117. */
  118. export function dataViewToModelCodeBlockInsertion( editingView, languageDefs ) {
  119. // Language names associated with CSS classes:
  120. //
  121. // {
  122. // 'language-php': 'php',
  123. // 'language-python': 'python',
  124. // js: 'javascript',
  125. // ...
  126. // }
  127. const classesToLanguages = getPropertyAssociation( languageDefs, 'class', 'language' );
  128. const defaultLanguageName = languageDefs[ 0 ].language;
  129. return ( evt, data, conversionApi ) => {
  130. const viewItem = data.viewItem;
  131. const viewChild = viewItem.getChild( 0 );
  132. if ( !viewChild || !viewChild.is( 'element', 'code' ) ) {
  133. return;
  134. }
  135. const { consumable, writer } = conversionApi;
  136. if ( !consumable.test( viewItem, { name: true } ) || !consumable.test( viewChild, { name: true } ) ) {
  137. return;
  138. }
  139. const codeBlock = writer.createElement( 'codeBlock' );
  140. const viewChildClasses = [ ...viewChild.getClassNames() ];
  141. // As we're to associate each class with a model language, a lack of class (empty class) can be
  142. // also associated with a language if the language definition was configured so. Pushing an empty
  143. // string to make sure the association will work.
  144. if ( !viewChildClasses.length ) {
  145. viewChildClasses.push( '' );
  146. }
  147. // Figure out if any of the <code> element's class names is a valid programming
  148. // language class. If so, use it on the model element (becomes the language of the entire block).
  149. for ( const className of viewChildClasses ) {
  150. const language = classesToLanguages[ className ];
  151. if ( language ) {
  152. writer.setAttribute( 'language', language, codeBlock );
  153. break;
  154. }
  155. }
  156. // If no language value was set, use the default language from the config.
  157. if ( !codeBlock.hasAttribute( 'language' ) ) {
  158. writer.setAttribute( 'language', defaultLanguageName, codeBlock );
  159. }
  160. // HTML elements are invalid content for `<code>`.
  161. // Read only text nodes.
  162. const textData = [ ...editingView.createRangeIn( viewChild ) ]
  163. .filter( current => current.type === 'text' )
  164. .map( ( { item } ) => item.data )
  165. .join( '' );
  166. const fragment = rawSnippetTextToModelDocumentFragment( writer, textData );
  167. writer.append( fragment, codeBlock );
  168. // Let's try to insert code block.
  169. if ( !conversionApi.safeInsert( codeBlock, data.modelCursor ) ) {
  170. return;
  171. }
  172. consumable.consume( viewItem, { name: true } );
  173. consumable.consume( viewChild, { name: true } );
  174. conversionApi.updateConversionResult( codeBlock, data );
  175. };
  176. }