8
0

codeblockcommand.js 4.7 KB

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