inlineautoformatengine.js 4.6 KB

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