8
0

codeblockediting.js 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307
  1. /**
  2. * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
  3. * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
  4. */
  5. /* global document */
  6. import CodeBlockEditing from '../src/codeblockediting';
  7. import CodeBlockCommand from '../src/codeblockcommand';
  8. import AlignmentEditing from '@ckeditor/ckeditor5-alignment/src/alignmentediting';
  9. import BoldEditing from '@ckeditor/ckeditor5-basic-styles/src/bold/boldediting';
  10. import Enter from '@ckeditor/ckeditor5-enter/src/enter';
  11. import ShiftEnter from '@ckeditor/ckeditor5-enter/src/shiftenter';
  12. import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';
  13. import ClassicTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/classictesteditor';
  14. import { getData as getModelData, setData as setModelData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
  15. import { getData as getViewData } from '@ckeditor/ckeditor5-engine/src/dev-utils/view';
  16. describe( 'CodeBlockEditing', () => {
  17. let editor, element, model, view;
  18. beforeEach( () => {
  19. element = document.createElement( 'div' );
  20. document.body.appendChild( element );
  21. return ClassicTestEditor
  22. .create( element, {
  23. plugins: [ CodeBlockEditing, AlignmentEditing, BoldEditing, Enter, Paragraph ]
  24. } )
  25. .then( newEditor => {
  26. editor = newEditor;
  27. model = editor.model;
  28. view = editor.editing.view;
  29. } );
  30. } );
  31. afterEach( () => {
  32. return editor.destroy().then( () => element.remove() );
  33. } );
  34. it( 'defines plugin name', () => {
  35. expect( CodeBlockEditing.pluginName ).to.equal( 'CodeBlockEditing' );
  36. } );
  37. it( 'defines plugin dependencies', () => {
  38. expect( CodeBlockEditing.requires ).to.have.members( [ ShiftEnter ] );
  39. } );
  40. it( 'adds a codeBlock command', () => {
  41. expect( editor.commands.get( 'codeBlock' ) ).to.be.instanceOf( CodeBlockCommand );
  42. } );
  43. it( 'allows for codeBlock in the $root', () => {
  44. expect( model.schema.checkChild( [ '$root' ], 'codeBlock' ) ).to.be.true;
  45. } );
  46. it( 'allows only for $text in codeBlock', () => {
  47. expect( model.schema.checkChild( [ '$root', 'codeBlock' ], '$text' ) ).to.equal( true );
  48. expect( model.schema.checkChild( [ '$root', 'codeBlock' ], '$block' ) ).to.equal( false );
  49. expect( model.schema.checkChild( [ '$root', 'codeBlock' ], 'codeBlock' ) ).to.equal( false );
  50. } );
  51. it( 'disallows all attributes for codeBlock', () => {
  52. setModelData( model, '<codeBlock>f[o]o</codeBlock>' );
  53. editor.execute( 'alignment', { value: 'right' } );
  54. editor.execute( 'bold' );
  55. expect( getModelData( model ) ).to.equal( '<codeBlock>f[o]o</codeBlock>' );
  56. } );
  57. it( 'should force shiftEnter command when pressing enter inside a codeBlock', () => {
  58. const enterCommand = editor.commands.get( 'enter' );
  59. const shiftEnterCommand = editor.commands.get( 'shiftEnter' );
  60. sinon.spy( enterCommand, 'execute' );
  61. sinon.spy( shiftEnterCommand, 'execute' );
  62. setModelData( model, '<codeBlock>foo[]bar</codeBlock>' );
  63. editor.editing.view.document.fire( 'enter', {
  64. preventDefault: () => {}
  65. } );
  66. expect( getModelData( model ) ).to.equal( '<codeBlock>foo<softBreak></softBreak>[]bar</codeBlock>' );
  67. sinon.assert.calledOnce( shiftEnterCommand.execute );
  68. sinon.assert.notCalled( enterCommand.execute );
  69. } );
  70. it( 'should execute enter command when pressing enter out of codeBlock', () => {
  71. const enterCommand = editor.commands.get( 'enter' );
  72. const shiftEnterCommand = editor.commands.get( 'shiftEnter' );
  73. sinon.spy( enterCommand, 'execute' );
  74. sinon.spy( shiftEnterCommand, 'execute' );
  75. setModelData( model, '<paragraph>foo[]bar</paragraph>' );
  76. editor.editing.view.document.fire( 'enter', {
  77. preventDefault: () => {}
  78. } );
  79. expect( getModelData( model ) ).to.equal( '<paragraph>foo</paragraph><paragraph>[]bar</paragraph>' );
  80. sinon.assert.calledOnce( enterCommand.execute );
  81. sinon.assert.notCalled( shiftEnterCommand.execute );
  82. } );
  83. describe( 'editing pipeline m -> v', () => {
  84. it( 'should convert empty codeBlock to empty pre tag', () => {
  85. setModelData( model, '<codeBlock></codeBlock>' );
  86. expect( getViewData( view ) ).to.equal( '<pre><code>[]</code></pre>' );
  87. } );
  88. it( 'should convert non-empty codeBlock to pre tag', () => {
  89. setModelData( model, '<codeBlock>Foo</codeBlock>' );
  90. expect( getViewData( view ) ).to.equal( '<pre><code>{}Foo</code></pre>' );
  91. } );
  92. it( 'should convert codeBlock with softBreaks to pre tag #1', () => {
  93. setModelData( model,
  94. '<codeBlock>' +
  95. 'Foo<softBreak></softBreak>' +
  96. 'Bar<softBreak></softBreak>' +
  97. 'Biz' +
  98. '</codeBlock>'
  99. );
  100. expect( getViewData( view ) ).to.equal( '<pre><code>{}Foo<br></br>Bar<br></br>Biz</code></pre>' );
  101. } );
  102. it( 'should convert codeBlock with softBreaks to pre tag #2', () => {
  103. setModelData( model,
  104. '<codeBlock>' +
  105. '<softBreak></softBreak>' +
  106. '<softBreak></softBreak>' +
  107. 'Foo' +
  108. '<softBreak></softBreak>' +
  109. '<softBreak></softBreak>' +
  110. '</codeBlock>'
  111. );
  112. expect( getViewData( view ) ).to.equal( '<pre><code>[]<br></br><br></br>Foo<br></br><br></br></code></pre>' );
  113. } );
  114. } );
  115. describe( 'data pipeline m -> v conversion ', () => {
  116. it( 'should convert empty codeBlock to empty pre tag', () => {
  117. setModelData( model, '<codeBlock></codeBlock>' );
  118. expect( editor.getData( { trim: 'none' } ) ).to.equal( '<pre><code>&nbsp;</code></pre>' );
  119. } );
  120. it( 'should convert non-empty codeBlock to pre tag', () => {
  121. setModelData( model, '<codeBlock>Foo</codeBlock>' );
  122. expect( editor.getData() ).to.equal( '<pre><code>Foo</code></pre>' );
  123. } );
  124. it( 'should convert codeBlock with softBreaks to pre tag #1', () => {
  125. setModelData( model,
  126. '<codeBlock>' +
  127. 'Foo<softBreak></softBreak>' +
  128. 'Bar<softBreak></softBreak>' +
  129. 'Biz' +
  130. '</codeBlock>' +
  131. '<paragraph>A<softBreak></softBreak>B</paragraph>'
  132. );
  133. expect( editor.getData() ).to.equal( '<pre><code>Foo\nBar\nBiz</code></pre><p>A<br>B</p>' );
  134. } );
  135. it( 'should convert codeBlock with softBreaks to pre tag #2', () => {
  136. setModelData( model,
  137. '<codeBlock>' +
  138. '<softBreak></softBreak>' +
  139. '<softBreak></softBreak>' +
  140. 'Foo' +
  141. '<softBreak></softBreak>' +
  142. '<softBreak></softBreak>' +
  143. '</codeBlock>'
  144. );
  145. expect( editor.getData() ).to.equal( '<pre><code>\n\nFoo\n\n</code></pre>' );
  146. } );
  147. it( 'should convert codeBlock with html content', () => {
  148. setModelData( model, '<codeBlock>[]</codeBlock>' );
  149. model.change( writer => writer.insertText( '<div><p>Foo</p></div>', model.document.selection.getFirstPosition() ) );
  150. expect( editor.getData() ).to.equal( '<pre><code>&lt;div&gt;&lt;p&gt;Foo&lt;/p&gt;&lt;/div&gt;</code></pre>' );
  151. } );
  152. it( 'should be overridable', () => {
  153. editor.data.downcastDispatcher.on( 'insert:codeBlock', ( evt, data, api ) => {
  154. const targetViewPosition = api.mapper.toViewPosition( model.createPositionBefore( data.item ) );
  155. const code = api.writer.createContainerElement( 'code' );
  156. api.consumable.consume( data.item, 'insert' );
  157. api.writer.insert( targetViewPosition, code );
  158. api.mapper.bindElements( data.item, code );
  159. }, { priority: 'high' } );
  160. editor.data.downcastDispatcher.on( 'insert:softBreak', ( evt, data, api ) => {
  161. const position = api.mapper.toViewPosition( model.createPositionBefore( data.item ) );
  162. api.consumable.consume( data.item, 'insert' );
  163. api.writer.insert( position, api.writer.createText( '\n' ) );
  164. }, { priority: 'highest' } );
  165. setModelData( model, '<codeBlock>Foo<softBreak></softBreak>Bar</codeBlock>' );
  166. expect( editor.getData() ).to.equal( '<code>Foo\nBar</code>' );
  167. } );
  168. } );
  169. describe( 'data pipeline v -> m conversion ', () => {
  170. it( 'should not convert empty pre tag to code block', () => {
  171. editor.setData( '<pre></pre>' );
  172. expect( getModelData( model ) ).to.equal( '<paragraph>[]</paragraph>' );
  173. } );
  174. it( 'should not convert pre with no code child to code block', () => {
  175. editor.setData( '<pre><samp></samp></pre>' );
  176. expect( getModelData( model ) ).to.equal( '<paragraph>[]</paragraph>' );
  177. } );
  178. it( 'should convert pre > code to code block', () => {
  179. editor.setData( '<pre><code></code></pre>' );
  180. expect( getModelData( model ) ).to.equal( '<codeBlock>[]</codeBlock>' );
  181. } );
  182. it( 'should convert pre > code with multi-line text to code block #1', () => {
  183. editor.setData( '<pre><code>foo\nbar</code></pre>' );
  184. expect( getModelData( model ) ).to.equal(
  185. '<codeBlock>[]' +
  186. 'foo' +
  187. '<softBreak></softBreak>' +
  188. 'bar' +
  189. '</codeBlock>'
  190. );
  191. } );
  192. it( 'should convert pre > code with multi-line text to code block #2', () => {
  193. editor.setData( '<pre><code>\n\nfoo\n\n</code></pre>' );
  194. expect( getModelData( model ) ).to.equal(
  195. '<codeBlock>[]' +
  196. '<softBreak></softBreak>' +
  197. '<softBreak></softBreak>' +
  198. 'foo' +
  199. '<softBreak></softBreak>' +
  200. '<softBreak></softBreak>' +
  201. '</codeBlock>'
  202. );
  203. } );
  204. it( 'should convert pre > code with HTML inside', () => {
  205. editor.setData( '<pre><code><p>Foo</p>\n<p>Bar</p></code></pre>' );
  206. expect( getModelData( model ) ).to.equal(
  207. '<codeBlock>[]' +
  208. '<p>Foo</p>' +
  209. '<softBreak></softBreak>' +
  210. '<p>Bar</p>' +
  211. '</codeBlock>'
  212. );
  213. } );
  214. it( 'should convert pre > code tag with HTML and nested pre > code tag', () => {
  215. editor.setData( '<pre><code><p>Foo</p><pre>Bar</pre><p>Biz</p></code></pre>' );
  216. expect( getModelData( model ) ).to.equal( '<codeBlock>[]<p>Foo</p><pre>Bar</pre><p>Biz</p></codeBlock>' );
  217. } );
  218. it( 'should convert pre > code tag with escaped html content', () => {
  219. editor.setData( '<pre><code>&lt;div&gt;&lt;p&gt;Foo&lt;/p&gt;&lt;/div&gt;</code></pre>' );
  220. expect( getModelData( model ) ).to.equal( '<codeBlock>[]<div><p>Foo</p></div></codeBlock>' );
  221. } );
  222. it( 'should be overridable', () => {
  223. editor.data.upcastDispatcher.on( 'element:pre', ( evt, data, api ) => {
  224. const modelItem = api.writer.createElement( 'codeBlock' );
  225. api.writer.appendText( 'Hello World!', modelItem );
  226. api.writer.insert( modelItem, data.modelCursor );
  227. api.consumable.consume( data.viewItem, { name: true } );
  228. data.modelCursor = api.writer.createPositionAfter( modelItem );
  229. data.modelRange = api.writer.createRangeOn( modelItem );
  230. }, { priority: 'high' } );
  231. editor.setData( '<pre><code>Foo Bar</code></pre>' );
  232. expect( getModelData( model ) ).to.equal( '<codeBlock>[]Hello World!</codeBlock>' );
  233. } );
  234. } );
  235. } );