utils.js 9.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341
  1. import ModelTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/modeltesteditor';
  2. import ListEditing from '@ckeditor/ckeditor5-list/src/listediting';
  3. import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';
  4. import Typing from '@ckeditor/ckeditor5-typing/src/typing';
  5. import UndoEditing from '@ckeditor/ckeditor5-undo/src/undoediting';
  6. import BlockQuoteEditing from '@ckeditor/ckeditor5-block-quote/src/blockquoteediting';
  7. import { getData, parse } from '../../../../src/dev-utils/model';
  8. import { transformSets } from '../../../../src/model/operation/transform';
  9. import Position from '../../../../src/model/position';
  10. import Range from '../../../../src/model/range';
  11. import OperationFactory from '../../../../src/model/operation/operationfactory';
  12. const clients = new Set();
  13. const bufferedOperations = new Set();
  14. export class Client {
  15. constructor( name ) {
  16. this.editor = null;
  17. this.document = null;
  18. this.syncedVersion = 0;
  19. this.orderNumber = null;
  20. this.name = name;
  21. }
  22. init() {
  23. return ModelTestEditor.create( {
  24. // Typing is needed for delete command.
  25. // UndoEditing is needed for undo command.
  26. // Block plugins are needed for proper data serializing.
  27. plugins: [ Typing, Paragraph, ListEditing, UndoEditing, BlockQuoteEditing ]
  28. } ).then( editor => {
  29. this.editor = editor;
  30. this.document = editor.model.document;
  31. return this;
  32. } );
  33. }
  34. setData( initModelString ) {
  35. const model = this.editor.model;
  36. const modelRoot = model.document.getRoot();
  37. // Parse data string to model.
  38. const parsedResult = parse( initModelString, model.schema, { context: [ modelRoot.name ] } );
  39. // Retrieve DocumentFragment and Selection from parsed model.
  40. const modelDocumentFragment = parsedResult.model;
  41. const selection = parsedResult.selection;
  42. model.change( writer => {
  43. // Replace existing model in document by new one.
  44. writer.remove( Range.createIn( modelRoot ) );
  45. writer.insert( modelDocumentFragment, modelRoot );
  46. } );
  47. const ranges = [];
  48. for ( const range of selection.getRanges() ) {
  49. const start = new Position( modelRoot, range.start.path );
  50. const end = new Position( modelRoot, range.end.path );
  51. ranges.push( new Range( start, end ) );
  52. }
  53. model.document.selection._setTo( ranges );
  54. // Purify graveyard so there are no artifact nodes there remaining after setting new data.
  55. // Because of those old nodes some tests may pass even though they fail in real scenarios (those tests
  56. // involve bringing back elements from graveyard, like wrap or split).
  57. model.document.graveyard._removeChildren( 0, model.document.graveyard.childCount );
  58. this.syncedVersion = this.document.version;
  59. }
  60. setSelection( start, end ) {
  61. if ( !end ) {
  62. end = start.slice();
  63. }
  64. const startPos = this._getPosition( start );
  65. const endPos = this._getPosition( end );
  66. this.editor.model.document.selection._setTo( new Range( startPos, endPos ) );
  67. }
  68. insert( itemString, path ) {
  69. const item = parse( itemString, this.editor.model.schema );
  70. const position = this._getPosition( path, 'start' );
  71. this._processAction( 'insert', item, position );
  72. }
  73. type( text, attributes, path ) {
  74. const position = this._getPosition( path, 'start' );
  75. this._processAction( 'insertText', text, attributes, position );
  76. }
  77. remove( start, end ) {
  78. const startPos = this._getPosition( start, 'start' );
  79. const endPos = this._getPosition( end, 'end' );
  80. this._processAction( 'remove', new Range( startPos, endPos ) );
  81. }
  82. delete() {
  83. this._processExecute( 'delete' );
  84. }
  85. move( target, start, end ) {
  86. const targetPos = this._getPosition( target );
  87. const startPos = this._getPosition( start, 'start' );
  88. const endPos = this._getPosition( end, 'end' );
  89. this._processAction( 'move', new Range( startPos, endPos ), targetPos );
  90. }
  91. rename( newName, path ) {
  92. const pos = this._getPosition( path, 'beforeParent' );
  93. const element = pos.nodeAfter;
  94. this._processAction( 'rename', element, newName );
  95. }
  96. setAttribute( key, value, start, end ) {
  97. const startPos = this._getPosition( start, 'start' );
  98. const endPos = this._getPosition( end, 'end' );
  99. this._processAction( 'setAttribute', key, value, new Range( startPos, endPos ) );
  100. }
  101. removeAttribute( key, start, end ) {
  102. const startPos = this._getPosition( start, 'start' );
  103. const endPos = this._getPosition( end, 'end' );
  104. this._processAction( 'removeAttribute', key, new Range( startPos, endPos ) );
  105. }
  106. setMarker( name, start, end ) {
  107. let actionName;
  108. const startPos = this._getPosition( start, 'start' );
  109. const endPos = this._getPosition( end, 'end' );
  110. if ( this.editor.model.markers.has( name ) ) {
  111. actionName = 'updateMarker';
  112. } else {
  113. actionName = 'addMarker';
  114. }
  115. const range = new Range( startPos, endPos );
  116. this._processAction( actionName, name, { range, usingOperation: true } );
  117. }
  118. removeMarker( name ) {
  119. this._processAction( 'removeMarker', name );
  120. }
  121. wrap( elementName, start, end ) {
  122. const startPos = this._getPosition( start, 'start' );
  123. const endPos = this._getPosition( end, 'end' );
  124. this._processAction( 'wrap', new Range( startPos, endPos ), elementName );
  125. }
  126. unwrap( path ) {
  127. const pos = this._getPosition( path, 'beforeParent' );
  128. const element = pos.nodeAfter;
  129. this._processAction( 'unwrap', element );
  130. }
  131. merge( path ) {
  132. const pos = this._getPosition( path, 'start' );
  133. this._processAction( 'merge', pos );
  134. }
  135. split( path ) {
  136. const pos = this._getPosition( path, 'start' );
  137. this._processAction( 'split', pos );
  138. }
  139. undo() {
  140. this._processExecute( 'undo' );
  141. }
  142. redo() {
  143. this._processExecute( 'redo' );
  144. }
  145. _processExecute( commandName, commandArgs ) {
  146. const oldVersion = this.document.version;
  147. this.editor.execute( commandName, commandArgs );
  148. const operations = this.document.history.getOperations( oldVersion );
  149. bufferOperations( Array.from( operations ), this );
  150. }
  151. _getPosition( path, type ) {
  152. if ( !path ) {
  153. return this._getPositionFromSelection( type );
  154. }
  155. return new Position( this.document.getRoot(), path );
  156. }
  157. _getPositionFromSelection( type ) {
  158. const selRange = this.editor.model.document.selection.getFirstRange();
  159. switch ( type ) {
  160. default:
  161. case 'start':
  162. return Position.createFromPosition( selRange.start );
  163. case 'end':
  164. return Position.createFromPosition( selRange.end );
  165. case 'beforeParent':
  166. return Position.createBefore( selRange.start.parent );
  167. }
  168. }
  169. getModelString() {
  170. return getData( this.editor.model, { withoutSelection: true, convertMarkers: true } );
  171. }
  172. destroy() {
  173. clients.delete( this );
  174. return this.editor.destroy();
  175. }
  176. _processAction( name, ...args ) {
  177. const oldVersion = this.document.version;
  178. this.editor.model.change( writer => {
  179. writer[ name ]( ...args );
  180. } );
  181. const operations = Array.from( this.document.history.getOperations( oldVersion ) );
  182. bufferOperations( operations, this );
  183. }
  184. static get( clientName ) {
  185. const client = new Client( clientName );
  186. client.orderNumber = clients.size;
  187. clients.add( client );
  188. return client.init();
  189. }
  190. }
  191. function bufferOperations( operations, client ) {
  192. bufferedOperations.add( { operations: operations.map( operation => JSON.stringify( operation ) ), client } );
  193. }
  194. export function syncClients() {
  195. const clientsOperations = {};
  196. // For each client, flatten all buffered operations into one set.
  197. for ( const item of bufferedOperations ) {
  198. const name = item.client.name;
  199. if ( !clientsOperations[ name ] ) {
  200. clientsOperations[ name ] = [];
  201. }
  202. clientsOperations[ name ].push( ...item.operations );
  203. }
  204. for ( const localClient of clients ) {
  205. for ( const remoteClient of clients ) {
  206. if ( remoteClient == localClient ) {
  207. continue;
  208. }
  209. const remoteOperationsJson = clientsOperations[ remoteClient.name ];
  210. if ( !remoteOperationsJson ) {
  211. continue;
  212. }
  213. const remoteOperations = remoteOperationsJson.map( op => OperationFactory.fromJSON( JSON.parse( op ), localClient.document ) );
  214. const localOperations = Array.from( localClient.document.history.getOperations( localClient.syncedVersion ) );
  215. let remoteOperationsTransformed = null;
  216. const options = {
  217. document: localClient.document,
  218. useContext: false,
  219. padWithNoOps: true
  220. };
  221. if ( localClient.orderNumber < remoteClient.orderNumber ) {
  222. remoteOperationsTransformed = transformSets( localOperations, remoteOperations, options ).operationsB;
  223. } else {
  224. remoteOperationsTransformed = transformSets( remoteOperations, localOperations, options ).operationsA;
  225. }
  226. localClient.editor.model.enqueueChange( 'transparent', writer => {
  227. for ( const operation of remoteOperationsTransformed ) {
  228. writer.batch.addOperation( operation );
  229. localClient.editor.model.applyOperation( operation );
  230. }
  231. } );
  232. }
  233. localClient.syncedVersion = localClient.document.version;
  234. }
  235. bufferedOperations.clear();
  236. }
  237. export function expectClients( expectedModelString ) {
  238. for ( const client of clients ) {
  239. expect( client.getModelString(), client.name + ' content' ).to.equal( expectedModelString );
  240. }
  241. let syncedVersion = null;
  242. for ( const client of clients ) {
  243. if ( syncedVersion === null ) {
  244. syncedVersion = client.syncedVersion;
  245. continue;
  246. }
  247. expect( client.syncedVersion, client.name + ' version' ).to.equal( syncedVersion );
  248. }
  249. }
  250. export function clearBuffer() {
  251. bufferedOperations.clear();
  252. }