utils.js 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217
  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/utils
  7. */
  8. import first from '@ckeditor/ckeditor5-utils/src/first';
  9. /**
  10. * Returns code block languages as defined in `config.codeBlock.languages` but processed:
  11. *
  12. * * To consider the editor localization, i.e. to display {@link module:code-block/codeblock~CodeBlockLanguageDefinition}
  13. * in the correct language. There is no way to use {@link module:utils/locale~Locale#t} when the user
  14. * configuration is defined because the editor does not exist yet.
  15. * * To make sure each definition has a CSS class associated with it even if not specified
  16. * in the original configuration.
  17. *
  18. * @param {module:core/editor/editor~Editor} editor
  19. * @returns {Array.<module:code-block/codeblock~CodeBlockLanguageDefinition>}.
  20. */
  21. export function getNormalizedAndLocalizedLanguageDefinitions( editor ) {
  22. const t = editor.t;
  23. const languageDefs = editor.config.get( 'codeBlock.languages' );
  24. for ( const def of languageDefs ) {
  25. if ( def.label === 'Plain text' ) {
  26. def.label = t( 'Plain text' );
  27. }
  28. if ( def.class === undefined ) {
  29. def.class = `language-${ def.language }`;
  30. }
  31. }
  32. return languageDefs;
  33. }
  34. /**
  35. * Returns an object associating certain language definition properties with others. For instance:
  36. *
  37. * For:
  38. *
  39. * const definitions = {
  40. * { language: 'php', class: 'language-php', label: 'PHP' },
  41. * { language: 'javascript', class: 'js', label: 'JavaScript' },
  42. * };
  43. *
  44. * getPropertyAssociation( definitions, 'class', 'language' );
  45. *
  46. * returns:
  47. *
  48. * {
  49. * 'language-php': 'php'
  50. * 'js': 'javascript'
  51. * }
  52. *
  53. * and
  54. *
  55. * getPropertyAssociation( definitions, 'language', 'label' );
  56. *
  57. * returns:
  58. *
  59. * {
  60. * 'php': 'PHP'
  61. * 'javascript': 'JavaScript'
  62. * }
  63. *
  64. * @param {Array.<module:code-block/codeblock~CodeBlockLanguageDefinition>}
  65. * @param {String} key
  66. * @param {String} value
  67. * @param {Object.<String,String>}
  68. */
  69. export function getPropertyAssociation( languageDefs, key, value ) {
  70. const association = {};
  71. for ( const def of languageDefs ) {
  72. if ( key === 'class' ) {
  73. // Only the first class is considered.
  74. association[ def[ key ].split( ' ' ).shift() ] = def[ value ];
  75. } else {
  76. association[ def[ key ] ] = def[ value ];
  77. }
  78. }
  79. return association;
  80. }
  81. /**
  82. * For a given model text node, it returns white spaces that precede other characters in that node.
  83. * This corresponds to the indentation part of the code block line.
  84. *
  85. * @param {module:engine/model/text~Text} codeLineNodes
  86. * @returns {String}
  87. */
  88. export function getLeadingWhiteSpaces( textNode ) {
  89. return textNode.data.match( /^(\s*)/ )[ 0 ];
  90. }
  91. /**
  92. * For a plain text containing the code (snippet), it returns a document fragment containing
  93. * model text nodes separated by soft breaks (in place of new line characters "\n"), for instance:
  94. *
  95. * Input:
  96. *
  97. * "foo()\n
  98. * bar()"
  99. *
  100. * Output:
  101. *
  102. * <DocumentFragment>
  103. * "foo()"
  104. * <softBreak></softBreak>
  105. * "bar()"
  106. * </DocumentFragment>
  107. *
  108. * @param {module:engine/model/writer~Writer} writer
  109. * @param {String} text The raw code text to be converted.
  110. */
  111. export function rawSnippetTextToModelDocumentFragment( writer, text ) {
  112. const fragment = writer.createDocumentFragment();
  113. const textLines = text.split( '\n' ).map( data => writer.createText( data ) );
  114. const lastLine = textLines[ textLines.length - 1 ];
  115. for ( const node of textLines ) {
  116. writer.append( node, fragment );
  117. if ( node !== lastLine ) {
  118. writer.appendElement( 'softBreak', fragment );
  119. }
  120. }
  121. return fragment;
  122. }
  123. /**
  124. * Returns an array of all model positions within the selection that represent code block lines.
  125. *
  126. * If the selection is collapsed, it returns the exact selection anchor position:
  127. *
  128. * <codeBlock>[]foo</codeBlock> -> <codeBlock>^foo</codeBlock>
  129. * <codeBlock>foo[]bar</codeBlock> -> <codeBlock>foo^bar</codeBlock>
  130. *
  131. * Otherwise, it returns positions **before** each text node belonging to all code blocks contained by the selection:
  132. *
  133. * <codeBlock> <codeBlock>
  134. * foo[bar ^foobar
  135. * <softBreak></softBreak> -> <softBreak></softBreak>
  136. * baz]qux ^bazqux
  137. * </codeBlock> </codeBlock>
  138. *
  139. * It also works across other non–code blocks:
  140. *
  141. * <codeBlock> <codeBlock>
  142. * foo[bar ^foobar
  143. * </codeBlock> </codeBlock>
  144. * <paragraph>text</paragraph> -> <paragraph>text</paragraph>
  145. * <codeBlock> <codeBlock>
  146. * baz]qux ^bazqux
  147. * </codeBlock> </codeBlock>
  148. *
  149. * **Note:** The positions are in reverse order so they do not get outdated when iterating over them and
  150. * the writer inserts or removes things at the same time.
  151. *
  152. * **Note:** The position is situated after the leading white spaces in the text node.
  153. *
  154. * @param {module:engine/model/model~Model} model
  155. * @returns {Array.<module:engine/model/position~Position>}
  156. */
  157. export function getIndentOutdentPositions( model ) {
  158. const selection = model.document.selection;
  159. const positions = [];
  160. // When the selection is collapsed, there's only one position we can indent or outdent.
  161. if ( selection.isCollapsed ) {
  162. positions.push( selection.anchor );
  163. }
  164. // When the selection is NOT collapsed, collect all positions starting before text nodes
  165. // (code lines) in any <codeBlock> within the selection.
  166. else {
  167. // Walk backward so positions we're about to collect here do not get outdated when
  168. // inserting or deleting using the writer.
  169. const walker = selection.getFirstRange().getWalker( {
  170. ignoreElementEnd: true,
  171. direction: 'backward'
  172. } );
  173. for ( const { item } of walker ) {
  174. if ( item.is( '$textProxy' ) && item.parent.is( 'element', 'codeBlock' ) ) {
  175. const leadingWhiteSpaces = getLeadingWhiteSpaces( item.textNode );
  176. const { parent, startOffset } = item.textNode;
  177. // Make sure the position is after all leading whitespaces in the text node.
  178. const position = model.createPositionAt( parent, startOffset + leadingWhiteSpaces.length );
  179. positions.push( position );
  180. }
  181. }
  182. }
  183. return positions;
  184. }
  185. /**
  186. * Checks if any of the blocks within the model selection is a code block.
  187. *
  188. * @param {module:engine/model/selection~Selection} selection
  189. * @returns {Boolean}
  190. */
  191. export function isModelSelectionInCodeBlock( selection ) {
  192. const firstBlock = first( selection.getSelectedBlocks() );
  193. return firstBlock && firstBlock.is( 'element', 'codeBlock' );
  194. }