codeblockcommand.js 4.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160
  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. /**
  6. * @module code-block/codeblockcommand
  7. */
  8. import Command from '@ckeditor/ckeditor5-core/src/command';
  9. import first from '@ckeditor/ckeditor5-utils/src/first';
  10. /**
  11. * The code block command plugin.
  12. *
  13. * @extends module:core/command~Command
  14. */
  15. export default class CodeBlockCommand extends Command {
  16. /**
  17. * Whether the selection starts in a code block.
  18. *
  19. * @observable
  20. * @readonly
  21. * @member {Boolean} #value
  22. */
  23. /**
  24. * @inheritDoc
  25. */
  26. refresh() {
  27. this.value = this._getValue();
  28. this.isEnabled = this._checkEnabled();
  29. }
  30. /**
  31. * Executes the command. When the command {@link #value is on}, all top-most code blocks within
  32. * the selection will be removed. If it is off, all selected blocks will be flattened and
  33. * wrapped by a code block.
  34. *
  35. * @fires execute
  36. * @param {Object} [options] Command options.
  37. * @param {Boolean} [options.forceValue] If set, it will force the command behavior. If `true`, the command will apply a code block,
  38. * otherwise the command will remove the code block. If not set, the command will act basing on its current value.
  39. */
  40. execute( options = {} ) {
  41. const editor = this.editor;
  42. const model = editor.model;
  43. const selection = model.document.selection;
  44. const firstLanguageInConfig = editor.config.get( 'codeBlock.languages' ).shift();
  45. const blocks = Array.from( selection.getSelectedBlocks() );
  46. const value = ( options.forceValue === undefined ) ? !this.value : options.forceValue;
  47. const language = options.language || firstLanguageInConfig.class;
  48. model.change( writer => {
  49. if ( value ) {
  50. this._applyCodeBlock( writer, blocks, language );
  51. } else {
  52. this._removeCodeBlock( writer, blocks );
  53. }
  54. } );
  55. }
  56. /**
  57. * Checks the command's {@link #value}.
  58. *
  59. * @private
  60. * @returns {Boolean} The current value.
  61. */
  62. _getValue() {
  63. const selection = this.editor.model.document.selection;
  64. const firstBlock = first( selection.getSelectedBlocks() );
  65. const isCodeBlock = !!( firstBlock && firstBlock.is( 'codeBlock' ) );
  66. return isCodeBlock ? firstBlock.getAttribute( 'language' ) : false;
  67. }
  68. /**
  69. * Checks whether the command can be enabled in the current context.
  70. *
  71. * @private
  72. * @returns {Boolean} Whether the command should be enabled.
  73. */
  74. _checkEnabled() {
  75. if ( this.value ) {
  76. return true;
  77. }
  78. const selection = this.editor.model.document.selection;
  79. const schema = this.editor.model.schema;
  80. const firstBlock = first( selection.getSelectedBlocks() );
  81. if ( !firstBlock ) {
  82. return false;
  83. }
  84. return canBeCodeBlock( schema, firstBlock );
  85. }
  86. /**
  87. * @private
  88. * @param {module:engine/model/writer~Writer} writer
  89. * @param {Array.<module:engine/model/element~Element>} blocks
  90. * @param {String} [language]
  91. */
  92. _applyCodeBlock( writer, blocks, language ) {
  93. const schema = this.editor.model.schema;
  94. const allowedBlocks = blocks.filter( block => canBeCodeBlock( schema, block ) );
  95. for ( const block of allowedBlocks ) {
  96. writer.rename( block, 'codeBlock' );
  97. writer.setAttribute( 'language', language, block );
  98. schema.removeDisallowedAttributes( [ block ], writer );
  99. }
  100. allowedBlocks.reverse().forEach( ( currentBlock, i ) => {
  101. const nextBlock = allowedBlocks[ i + 1 ];
  102. if ( currentBlock.previousSibling === nextBlock ) {
  103. writer.appendElement( 'softBreak', nextBlock );
  104. writer.merge( writer.createPositionBefore( currentBlock ) );
  105. }
  106. } );
  107. }
  108. /**
  109. * @private
  110. * @param {module:engine/model/writer~Writer} writer
  111. * @param {Array.<module:engine/model/element~Element>} blocks
  112. */
  113. _removeCodeBlock( writer, blocks ) {
  114. const codeBlocks = blocks.filter( block => block.is( 'codeBlock' ) );
  115. for ( const block of codeBlocks ) {
  116. const range = writer.createRangeOn( block );
  117. for ( const item of Array.from( range.getItems() ).reverse() ) {
  118. if ( item.is( 'softBreak' ) && item.parent.is( 'codeBlock' ) ) {
  119. const { position } = writer.split( writer.createPositionBefore( item ) );
  120. writer.rename( position.nodeAfter, 'paragraph' );
  121. writer.removeAttribute( 'language', position.nodeAfter );
  122. writer.remove( item );
  123. }
  124. }
  125. writer.rename( block, 'paragraph' );
  126. writer.removeAttribute( 'language', block );
  127. }
  128. }
  129. }
  130. function canBeCodeBlock( schema, element ) {
  131. if ( element.is( 'rootElement' ) || schema.isLimit( element ) ) {
  132. return false;
  133. }
  134. return schema.checkChild( element.parent, 'codeBlock' );
  135. }