8
0

inlineautoformatengine.js 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197
  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 CKEditorError from '../utils/ckeditorerror.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. * * {core.editor.Editor} Editor instance,
  40. * * {engine.model.Range} Range of matched text to format,
  41. * * {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 testClb;
  48. let formatClb;
  49. if ( typeof testCallbackOrPattern == 'string' ) {
  50. pattern = new RegExp( testCallbackOrPattern );
  51. } else if ( testCallbackOrPattern instanceof RegExp ) {
  52. pattern = testCallbackOrPattern;
  53. } else {
  54. testClb = testCallbackOrPattern;
  55. }
  56. if ( typeof formatCallbackOrCommand == 'string' ) {
  57. command = formatCallbackOrCommand;
  58. } else {
  59. formatClb = formatCallbackOrCommand;
  60. }
  61. // Callback to run on changed text.
  62. testClb = testClb || ( ( text ) => {
  63. let result;
  64. let remove = [];
  65. let format = [];
  66. if ( !text ) {
  67. return;
  68. }
  69. while ( ( result = pattern.exec( text ) ) !== null ) {
  70. // If nothing matched, stop early.
  71. if ( !result ) {
  72. return;
  73. }
  74. // There should be full match and 3 capture groups.
  75. if ( result && result.length < 4 ) {
  76. throw new CKEditorError( 'inlineautoformat-missing-capture-groups: Less than 3 capture groups in regular expression.' );
  77. }
  78. const {
  79. index,
  80. '1': leftDel,
  81. '2': content,
  82. '3': rightDel
  83. } = result;
  84. // Start and End offsets of delimiters to remove.
  85. const delStart = [
  86. index,
  87. index + leftDel.length
  88. ];
  89. const delEnd = [
  90. index + leftDel.length + content.length,
  91. index + leftDel.length + content.length + rightDel.length
  92. ];
  93. remove.push( delStart );
  94. remove.push( delEnd );
  95. format.push( [ index + leftDel.length, index + leftDel.length + content.length ] );
  96. }
  97. return {
  98. remove,
  99. format
  100. };
  101. } );
  102. // Format callback to run on matched text.
  103. formatClb = formatClb || ( ( editor, range, batch ) => {
  104. editor.execute( command, { batch: batch } );
  105. } );
  106. editor.document.on( 'change', ( evt, type ) => {
  107. if ( type !== 'insert' ) {
  108. return;
  109. }
  110. const batch = editor.document.batch();
  111. const selection = this.editor.document.selection;
  112. const block = selection.focus.parent;
  113. const text = getText( block );
  114. if ( block.name !== 'paragraph' || !text ) {
  115. return;
  116. }
  117. const ranges = testClb( text );
  118. if ( !ranges ) {
  119. return;
  120. }
  121. // Apply format before deleting text.
  122. ranges.format.forEach( ( range ) => {
  123. if ( range[ 0 ] === undefined || range[ 1 ] === undefined ) {
  124. return;
  125. }
  126. const rangeToFormat = Range.createFromParentsAndOffsets(
  127. block, range[ 0 ],
  128. block, range[ 1 ]
  129. );
  130. editor.document.enqueueChanges( () => {
  131. selection.setRanges( [ rangeToFormat ] );
  132. } );
  133. // formatClb executes command that has its own enqueueChanges block.
  134. formatClb( this.editor, rangeToFormat, batch );
  135. editor.document.enqueueChanges( () => {
  136. selection.collapseToEnd();
  137. } );
  138. } );
  139. // Reverse order to not mix the offsets while removing.
  140. ranges.remove.slice().reverse().forEach( ( range ) => {
  141. if ( range[ 0 ] === undefined || range[ 1 ] === undefined ) {
  142. return;
  143. }
  144. const rangeToDelete = Range.createFromParentsAndOffsets(
  145. block, range[ 0 ],
  146. block, range[ 1 ]
  147. );
  148. editor.document.enqueueChanges( () => {
  149. batch.remove( rangeToDelete );
  150. } );
  151. } );
  152. } );
  153. }
  154. }
  155. function getText( element ) {
  156. let text = '';
  157. for ( let child of element.getChildren() ) {
  158. if ( child.data ) {
  159. text += child.data;
  160. } else if ( child.name ) {
  161. text += getText( child );
  162. }
  163. }
  164. return text;
  165. }