inlineautoformatengine.js 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188
  1. /**
  2. * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
  3. * For licensing, see LICENSE.md.
  4. */
  5. import Range from '../engine/model/range.js';
  6. import TreeWalker from '../engine/model/treewalker.js';
  7. /**
  8. * A paragraph feature for editor.
  9. * Introduces `<paragraph>` element in the model which renders as `<p>` in the DOM and data.
  10. *
  11. * @memberOf paragraph
  12. * @extends ckeditor5.Feature
  13. */
  14. export default class InlineAutoformatEngine {
  15. /**
  16. * Assigns to `editor` to watch for pattern (either by executing that pattern or passing the text to `testCallbackOrPattern` callback).
  17. * It formats found text by executing command `formatCallbackOrCommand` or by running `formatCallbackOrCommand` format callback.
  18. *
  19. * @param {core.editor.Editor} editor Editor instance.
  20. * @param {Function|RegExp} testCallbackOrPattern RegExp literal to execute on text or test callback returning Object with offsets to
  21. * remove and offsets to format.
  22. * * Format is applied before deletion,
  23. * * RegExp literal *must* have 3 capture groups.
  24. *
  25. * Example of object that should be returned from test callback.
  26. *
  27. * {
  28. * remove: [
  29. * [ 0, 1 ],
  30. * [ 5, 6 ]
  31. * ],
  32. * format: [
  33. * [ 1, 5 ]
  34. * ],
  35. * }
  36. *
  37. * @param {Function|String} formatCallbackOrCommand Name of command to execute on matched text or format callback.
  38. * Format callback gets following parameters:
  39. * 1. {core.editor.Editor} Editor instance,
  40. * 2. {engine.model.Range} Range of matched text to format,
  41. * 3. {engine.model.Batch} Batch to group format operations.
  42. */
  43. constructor( editor, testCallbackOrPattern, formatCallbackOrCommand ) {
  44. this.editor = editor;
  45. let pattern;
  46. let command;
  47. let testCallback;
  48. let formatCallback;
  49. if ( typeof testCallbackOrPattern == 'string' ) {
  50. pattern = new RegExp( testCallbackOrPattern, 'g' );
  51. } else if ( testCallbackOrPattern instanceof RegExp ) {
  52. pattern = testCallbackOrPattern;
  53. } else {
  54. testCallback = testCallbackOrPattern;
  55. }
  56. if ( typeof formatCallbackOrCommand == 'string' ) {
  57. command = formatCallbackOrCommand;
  58. } else {
  59. formatCallback = formatCallbackOrCommand;
  60. }
  61. // A test callback run on changed text.
  62. testCallback = testCallback || ( ( text ) => {
  63. let result;
  64. let remove = [];
  65. let format = [];
  66. while ( ( result = pattern.exec( text ) ) !== null ) {
  67. // There should be full match and 3 capture groups.
  68. if ( result && result.length < 4 ) {
  69. break;
  70. }
  71. const {
  72. index,
  73. '1': leftDel,
  74. '2': content,
  75. '3': rightDel
  76. } = result;
  77. // Start and End offsets of delimiters to remove.
  78. const delStart = [
  79. index,
  80. index + leftDel.length
  81. ];
  82. const delEnd = [
  83. index + leftDel.length + content.length,
  84. index + leftDel.length + content.length + rightDel.length
  85. ];
  86. remove.push( delStart );
  87. remove.push( delEnd );
  88. format.push( [ index + leftDel.length, index + leftDel.length + content.length ] );
  89. }
  90. return {
  91. remove,
  92. format
  93. };
  94. } );
  95. // A format callback run on matched text.
  96. formatCallback = formatCallback || ( ( editor, range, batch ) => {
  97. editor.execute( command, { batch: batch } );
  98. } );
  99. editor.document.on( 'change', ( evt, type ) => {
  100. if ( type !== 'insert' ) {
  101. return;
  102. }
  103. const batch = editor.document.batch();
  104. const selection = this.editor.document.selection;
  105. const block = selection.focus.parent;
  106. const text = getText( block );
  107. if ( block.name !== 'paragraph' || !text ) {
  108. return;
  109. }
  110. const ranges = testCallback( text );
  111. // Apply format before deleting text.
  112. ranges.format.forEach( ( range ) => {
  113. if ( range[ 0 ] === undefined || range[ 1 ] === undefined ) {
  114. return;
  115. }
  116. const rangeToFormat = Range.createFromParentsAndOffsets(
  117. block, range[ 0 ],
  118. block, range[ 1 ]
  119. );
  120. editor.document.enqueueChanges( () => {
  121. selection.setRanges( [ rangeToFormat ] );
  122. } );
  123. // No `enqueueChanges()` here. The formatCallback executes command that has its own enqueueChanges block.
  124. formatCallback( this.editor, rangeToFormat, batch );
  125. editor.document.enqueueChanges( () => {
  126. selection.collapseToEnd();
  127. } );
  128. } );
  129. // Reverse order to not mix the offsets while removing.
  130. ranges.remove.slice().reverse().forEach( ( range ) => {
  131. if ( range[ 0 ] === undefined || range[ 1 ] === undefined ) {
  132. return;
  133. }
  134. const rangeToDelete = Range.createFromParentsAndOffsets(
  135. block, range[ 0 ],
  136. block, range[ 1 ]
  137. );
  138. editor.document.enqueueChanges( () => {
  139. batch.remove( rangeToDelete );
  140. } );
  141. } );
  142. } );
  143. }
  144. }
  145. // Gets whole text from provided element.
  146. //
  147. // @private
  148. // @param {engine.model.Element} element
  149. // @returns {String}
  150. function getText( element ) {
  151. let text = '';
  152. const walker = new TreeWalker( {
  153. boundaries: Range.createIn( element )
  154. } );
  155. for ( let value of walker ) {
  156. text += value.type == 'text' ? value.item.data : '';
  157. }
  158. return text;
  159. }