enableenginedebug.js 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728
  1. /**
  2. * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
  3. * For licensing, see LICENSE.md.
  4. */
  5. /**
  6. * @module engine/dev-utils/enableenginedebug
  7. */
  8. /* global console */
  9. import DeltaReplayer from './deltareplayer';
  10. import ModelPosition from '../model/position';
  11. import ModelRange from '../model/range';
  12. import ModelText from '../model/text';
  13. import ModelTextProxy from '../model/textproxy';
  14. import ModelElement from '../model/element';
  15. import Operation from '../model/operation/operation';
  16. import AttributeOperation from '../model/operation/attributeoperation';
  17. import DetachOperation from '../model/operation/detachoperation';
  18. import InsertOperation from '../model/operation/insertoperation';
  19. import MarkerOperation from '../model/operation/markeroperation';
  20. import MoveOperation from '../model/operation/moveoperation';
  21. import NoOperation from '../model/operation/nooperation';
  22. import RenameOperation from '../model/operation/renameoperation';
  23. import RootAttributeOperation from '../model/operation/rootattributeoperation';
  24. import Delta from '../model/delta/delta';
  25. import AttributeDelta from '../model/delta/attributedelta';
  26. import InsertDelta from '../model/delta/insertdelta';
  27. import MarkerDelta from '../model/delta/markerdelta';
  28. import MergeDelta from '../model/delta/mergedelta';
  29. import MoveDelta from '../model/delta/movedelta';
  30. import RenameDelta from '../model/delta/renamedelta';
  31. import RootAttributeDelta from '../model/delta/rootattributedelta';
  32. import SplitDelta from '../model/delta/splitdelta';
  33. import UnwrapDelta from '../model/delta/unwrapdelta';
  34. import WrapDelta from '../model/delta/wrapdelta';
  35. import deltaTransform from '../model/delta/transform';
  36. import Model from '../model/model';
  37. import ModelDocument from '../model/document';
  38. import ModelDocumentFragment from '../model/documentfragment';
  39. import ModelRootElement from '../model/rootelement';
  40. import ViewDocument from '../view/document';
  41. import ViewElement from '../view/element';
  42. import ViewText from '../view/text';
  43. import ViewTextProxy from '../view/textproxy';
  44. import ViewDocumentFragment from '../view/documentfragment';
  45. import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
  46. import Editor from '@ckeditor/ckeditor5-core/src/editor/editor';
  47. import clone from '@ckeditor/ckeditor5-utils/src/lib/lodash/clone';
  48. // Sandbox class allows creating mocks of the functions and restoring these mocks to the original values.
  49. class Sandbox {
  50. constructor() {
  51. // An array that contains functions which restore the original values of mocked objects.
  52. // @private
  53. // @type {Array.<Function>}
  54. this._restores = [];
  55. }
  56. // Creates a new mock.
  57. //
  58. // @param {Object} object Object to mock.
  59. // @param {String} methodName Function to mock.
  60. // @param {Function} fakeMethod Function that will be executed.
  61. mock( object, methodName, fakeMethod ) {
  62. const originalMethod = object[ methodName ];
  63. object[ methodName ] = fakeMethod;
  64. this._restores.unshift( () => {
  65. if ( originalMethod ) {
  66. object[ methodName ] = originalMethod;
  67. } else {
  68. delete object[ methodName ];
  69. }
  70. } );
  71. }
  72. // Restores all mocked functions.
  73. restore() {
  74. for ( const restore of this._restores ) {
  75. restore();
  76. }
  77. this._restores = [];
  78. }
  79. }
  80. const sandbox = new Sandbox();
  81. const treeDump = Symbol( '_treeDump' );
  82. // Maximum number of stored states of model and view document.
  83. const maxTreeDumpLength = 20;
  84. // Separator used to separate stringified deltas
  85. const LOG_SEPARATOR = '-------';
  86. // Specified whether debug tools were already enabled.
  87. let enabled = false;
  88. // Logging function used to log debug messages.
  89. let logger = console;
  90. /**
  91. * Enhances model classes with logging methods. Returns a plugin that should be loaded in the editor to
  92. * enable debugging features.
  93. *
  94. * Every operation applied on {@link module:engine/model/document~Document model.Document} is logged.
  95. *
  96. * Following classes are expanded with `log` and meaningful `toString` methods:
  97. * * {@link module:engine/model/position~Position model.Position},
  98. * * {@link module:engine/model/range~Range model.Range},
  99. * * {@link module:engine/model/text~Text model.Text},
  100. * * {@link module:engine/model/element~Element model.Element},
  101. * * {@link module:engine/model/rootelement~RootElement model.RootElement},
  102. * * {@link module:engine/model/documentfragment~DocumentFragment model.DocumentFragment},
  103. * * {@link module:engine/model/document~Document model.Document},
  104. * * all {@link module:engine/model/operation/operation~Operation operations}
  105. * * all {@link module:engine/model/delta/delta~Delta deltas},
  106. * * {@link module:engine/view/element~Element view.Element},
  107. * * {@link module:engine/view/documentfragment~DocumentFragment view.DocumentFragment},
  108. * * {@link module:engine/view/document~Document view.Document}.
  109. *
  110. * Additionally, following logging utility methods are added:
  111. * * {@link module:engine/model/text~Text model.Text} `logExtended`,
  112. * * {@link module:engine/model/element~Element model.Element} `logExtended`,
  113. * * {@link module:engine/model/element~Element model.Element} `logAll`,
  114. * * {@link module:engine/model/delta/delta~Delta model.Delta} `logAll`.
  115. *
  116. * Additionally, following classes are expanded with `logTree` and `printTree` methods:
  117. * * {@link module:engine/model/element~Element model.Element},
  118. * * {@link module:engine/model/documentfragment~DocumentFragment model.DocumentFragment},
  119. * * {@link module:engine/view/element~Element view.Element},
  120. * * {@link module:engine/view/documentfragment~DocumentFragment view.DocumentFragment}.
  121. *
  122. * Finally, following methods are added to {@link module:core/editor/editor~Editor}: `logModel`, `logView`, `logDocuments`.
  123. * All those methods take one parameter, which is a version of {@link module:engine/model/document~Document model document}
  124. * for which model or view document state should be logged.
  125. *
  126. * @param {Object} [_logger] Object with functions used to log messages and errors. By default messages are logged to console.
  127. * If specified, it is expected to have `log()` and `error()` methods.
  128. * @returns {module:engine/dev-utils/enableenginedebug~DebugPlugin} Plugin to be loaded in the editor.
  129. */
  130. export default function enableEngineDebug( _logger = console ) {
  131. logger = _logger;
  132. if ( !enabled ) {
  133. enabled = true;
  134. enableLoggingTools();
  135. enableDocumentTools();
  136. enableReplayerTools();
  137. }
  138. return DebugPlugin;
  139. }
  140. /**
  141. * Restores all methods that have been overwritten.
  142. */
  143. export function disableEngineDebug() {
  144. sandbox.restore();
  145. enabled = false;
  146. }
  147. function enableLoggingTools() {
  148. sandbox.mock( ModelPosition.prototype, 'toString', function() {
  149. return `${ this.root } [ ${ this.path.join( ', ' ) } ]`;
  150. } );
  151. sandbox.mock( ModelPosition.prototype, 'log', function() {
  152. logger.log( 'ModelPosition: ' + this );
  153. } );
  154. sandbox.mock( ModelRange.prototype, 'toString', function() {
  155. return `${ this.root } [ ${ this.start.path.join( ', ' ) } ] - [ ${ this.end.path.join( ', ' ) } ]`;
  156. } );
  157. sandbox.mock( ModelRange.prototype, 'log', function() {
  158. logger.log( 'ModelRange: ' + this );
  159. } );
  160. sandbox.mock( ModelText.prototype, 'toString', function() {
  161. return `#${ this.data }`;
  162. } );
  163. sandbox.mock( ModelText.prototype, 'logExtended', function() {
  164. logger.log( `ModelText: ${ this }, attrs: ${ mapString( this.getAttributes() ) }` );
  165. } );
  166. sandbox.mock( ModelText.prototype, 'log', function() {
  167. logger.log( 'ModelText: ' + this );
  168. } );
  169. sandbox.mock( ModelTextProxy.prototype, 'toString', function() {
  170. return `#${ this.data }`;
  171. } );
  172. sandbox.mock( ModelTextProxy.prototype, 'logExtended', function() {
  173. logger.log( `ModelTextProxy: ${ this }, attrs: ${ mapString( this.getAttributes() ) }` );
  174. } );
  175. sandbox.mock( ModelTextProxy.prototype, 'log', function() {
  176. logger.log( 'ModelTextProxy: ' + this );
  177. } );
  178. sandbox.mock( ModelElement.prototype, 'toString', function() {
  179. return `<${ this.rootName || this.name }>`;
  180. } );
  181. sandbox.mock( ModelElement.prototype, 'log', function() {
  182. logger.log( 'ModelElement: ' + this );
  183. } );
  184. sandbox.mock( ModelElement.prototype, 'logExtended', function() {
  185. logger.log( `ModelElement: ${ this }, ${ this.childCount } children, attrs: ${ mapString( this.getAttributes() ) }` );
  186. } );
  187. sandbox.mock( ModelElement.prototype, 'logAll', function() {
  188. logger.log( '--------------------' );
  189. this.logExtended();
  190. logger.log( 'List of children:' );
  191. for ( const child of this.getChildren() ) {
  192. child.log();
  193. }
  194. } );
  195. sandbox.mock( ModelElement.prototype, 'printTree', function( level = 0 ) {
  196. let string = '';
  197. string += '\t'.repeat( level ) + `<${ this.rootName || this.name }${ mapToTags( this.getAttributes() ) }>`;
  198. for ( const child of this.getChildren() ) {
  199. string += '\n';
  200. if ( child.is( 'text' ) ) {
  201. const textAttrs = mapToTags( child._attrs );
  202. string += '\t'.repeat( level + 1 );
  203. if ( textAttrs !== '' ) {
  204. string += `<$text${ textAttrs }>` + child.data + '</$text>';
  205. } else {
  206. string += child.data;
  207. }
  208. } else {
  209. string += child.printTree( level + 1 );
  210. }
  211. }
  212. if ( this.childCount ) {
  213. string += '\n' + '\t'.repeat( level );
  214. }
  215. string += `</${ this.rootName || this.name }>`;
  216. return string;
  217. } );
  218. sandbox.mock( ModelElement.prototype, 'logTree', function() {
  219. logger.log( this.printTree() );
  220. } );
  221. sandbox.mock( ModelRootElement.prototype, 'toString', function() {
  222. return this.rootName;
  223. } );
  224. sandbox.mock( ModelRootElement.prototype, 'log', function() {
  225. logger.log( 'ModelRootElement: ' + this );
  226. } );
  227. sandbox.mock( ModelDocumentFragment.prototype, 'toString', function() {
  228. return 'documentFragment';
  229. } );
  230. sandbox.mock( ModelDocumentFragment.prototype, 'log', function() {
  231. logger.log( 'ModelDocumentFragment: ' + this );
  232. } );
  233. sandbox.mock( ModelDocumentFragment.prototype, 'printTree', function() {
  234. let string = 'ModelDocumentFragment: [';
  235. for ( const child of this.getChildren() ) {
  236. string += '\n';
  237. if ( child.is( 'text' ) ) {
  238. const textAttrs = mapToTags( child._attrs );
  239. string += '\t'.repeat( 1 );
  240. if ( textAttrs !== '' ) {
  241. string += `<$text${ textAttrs }>` + child.data + '</$text>';
  242. } else {
  243. string += child.data;
  244. }
  245. } else {
  246. string += child.printTree( 1 );
  247. }
  248. }
  249. string += '\n]';
  250. return string;
  251. } );
  252. sandbox.mock( ModelDocumentFragment.prototype, 'logTree', function() {
  253. logger.log( this.printTree() );
  254. } );
  255. sandbox.mock( Operation.prototype, 'log', function() {
  256. logger.log( this.toString() );
  257. } );
  258. sandbox.mock( AttributeOperation.prototype, 'toString', function() {
  259. return getClassName( this ) + `( ${ this.baseVersion } ): ` +
  260. `"${ this.key }": ${ JSON.stringify( this.oldValue ) } -> ${ JSON.stringify( this.newValue ) }, ${ this.range }`;
  261. } );
  262. sandbox.mock( DetachOperation.prototype, 'toString', function() {
  263. const range = ModelRange.createFromPositionAndShift( this.sourcePosition, this.howMany );
  264. const nodes = Array.from( range.getItems() );
  265. const nodeString = nodes.length > 1 ? `[ ${ nodes.length } ]` : nodes[ 0 ];
  266. return getClassName( this ) + `( ${ this.baseVersion } ): ${ nodeString } -> ${ range }`;
  267. } );
  268. sandbox.mock( InsertOperation.prototype, 'toString', function() {
  269. const nodeString = this.nodes.length > 1 ? `[ ${ this.nodes.length } ]` : this.nodes.getNode( 0 );
  270. return getClassName( this ) + `( ${ this.baseVersion } ): ${ nodeString } -> ${ this.position }`;
  271. } );
  272. sandbox.mock( MarkerOperation.prototype, 'toString', function() {
  273. return getClassName( this ) + `( ${ this.baseVersion } ): ` +
  274. `"${ this.name }": ${ this.oldRange } -> ${ this.newRange }`;
  275. } );
  276. sandbox.mock( MoveOperation.prototype, 'toString', function() {
  277. const range = ModelRange.createFromPositionAndShift( this.sourcePosition, this.howMany );
  278. return getClassName( this ) + `( ${ this.baseVersion } ): ` +
  279. `${ range } -> ${ this.targetPosition }${ this.isSticky ? ' (sticky)' : '' }`;
  280. } );
  281. sandbox.mock( NoOperation.prototype, 'toString', function() {
  282. return `NoOperation( ${ this.baseVersion } )`;
  283. } );
  284. sandbox.mock( RenameOperation.prototype, 'toString', function() {
  285. return getClassName( this ) + `( ${ this.baseVersion } ): ` +
  286. `${ this.position }: "${ this.oldName }" -> "${ this.newName }"`;
  287. } );
  288. sandbox.mock( RootAttributeOperation.prototype, 'toString', function() {
  289. return getClassName( this ) + `( ${ this.baseVersion } ): ` +
  290. `"${ this.key }": ${ JSON.stringify( this.oldValue ) } -> ${ JSON.stringify( this.newValue ) }, ${ this.root.rootName }`;
  291. } );
  292. sandbox.mock( Delta.prototype, 'log', function() {
  293. logger.log( this.toString() );
  294. } );
  295. sandbox.mock( Delta.prototype, 'logAll', function() {
  296. logger.log( '--------------------' );
  297. this.log();
  298. for ( const op of this.operations ) {
  299. op.log();
  300. }
  301. } );
  302. sandbox.mock( Delta.prototype, '_saveHistory', function( itemToSave ) {
  303. const history = itemToSave.before.history ? itemToSave.before.history : [];
  304. itemToSave.before = clone( itemToSave.before );
  305. delete itemToSave.before.history;
  306. itemToSave.before = JSON.stringify( itemToSave.before );
  307. itemToSave.transformedBy = clone( itemToSave.transformedBy );
  308. delete itemToSave.transformedBy.history;
  309. itemToSave.transformedBy = JSON.stringify( itemToSave.transformedBy );
  310. this.history = history.concat( itemToSave );
  311. } );
  312. const _deltaTransformTransform = deltaTransform.transform;
  313. sandbox.mock( deltaTransform, 'transform', function( a, b, context ) {
  314. let results;
  315. try {
  316. results = _deltaTransformTransform( a, b, context );
  317. } catch ( e ) {
  318. logger.error( 'Error during delta transformation!' );
  319. logger.error( a.toString() + ( context.isStrong ? ' (important)' : '' ) );
  320. logger.error( b.toString() + ( context.isStrong ? '' : ' (important)' ) );
  321. throw e;
  322. }
  323. for ( let i = 0; i < results.length; i++ ) {
  324. results[ i ]._saveHistory( {
  325. before: a,
  326. transformedBy: b,
  327. wasImportant: !!context.isStrong,
  328. resultIndex: i,
  329. resultsTotal: results.length
  330. } );
  331. }
  332. return results;
  333. } );
  334. sandbox.mock( AttributeDelta.prototype, 'toString', function() {
  335. return getClassName( this ) + `( ${ this.baseVersion } ): ` +
  336. `"${ this.key }": -> ${ JSON.stringify( this.value ) }, ${ this.range }, ${ this.operations.length } ops`;
  337. } );
  338. sandbox.mock( InsertDelta.prototype, 'toString', function() {
  339. const op = this._insertOperation;
  340. const nodeString = op.nodes.length > 1 ? `[ ${ op.nodes.length } ]` : op.nodes.getNode( 0 );
  341. return getClassName( this ) + `( ${ this.baseVersion } ): ${ nodeString } -> ${ op.position }`;
  342. } );
  343. sandbox.mock( MarkerDelta.prototype, 'toString', function() {
  344. const op = this.operations[ 0 ];
  345. return getClassName( this ) + `( ${ this.baseVersion } ): ` +
  346. `"${ op.name }": ${ op.oldRange } -> ${ op.newRange }`;
  347. } );
  348. sandbox.mock( MergeDelta.prototype, 'toString', function() {
  349. return getClassName( this ) + `( ${ this.baseVersion } ): ` +
  350. ( this.position ?
  351. this.position.toString() :
  352. `(move from ${ this.operations[ 0 ].sourcePosition })`
  353. );
  354. } );
  355. sandbox.mock( MoveDelta.prototype, 'toString', function() {
  356. const opStrings = [];
  357. for ( const op of this.operations ) {
  358. const range = ModelRange.createFromPositionAndShift( op.sourcePosition, op.howMany );
  359. opStrings.push( `${ range } -> ${ op.targetPosition }` );
  360. }
  361. return getClassName( this ) + `( ${ this.baseVersion } ): ` +
  362. opStrings.join( '; ' );
  363. } );
  364. sandbox.mock( RenameDelta.prototype, 'toString', function() {
  365. const op = this.operations[ 0 ];
  366. return getClassName( this ) + `( ${ this.baseVersion } ): ` +
  367. `${ op.position }: "${ op.oldName }" -> "${ op.newName }"`;
  368. } );
  369. sandbox.mock( RootAttributeDelta.prototype, 'toString', function() {
  370. const op = this.operations[ 0 ];
  371. return getClassName( this ) + `( ${ this.baseVersion } ): ` +
  372. `"${ op.key }": ${ JSON.stringify( op.oldValue ) } -> ${ JSON.stringify( op.newValue ) }, ${ op.root.rootName }`;
  373. } );
  374. sandbox.mock( SplitDelta.prototype, 'toString', function() {
  375. return getClassName( this ) + `( ${ this.baseVersion } ): ` +
  376. ( this.position ?
  377. this.position.toString() :
  378. `(clone to ${ this._cloneOperation.position || this._cloneOperation.targetPosition })`
  379. );
  380. } );
  381. sandbox.mock( UnwrapDelta.prototype, 'toString', function() {
  382. return getClassName( this ) + `( ${ this.baseVersion } ): ` +
  383. this.position.toString();
  384. } );
  385. sandbox.mock( WrapDelta.prototype, 'toString', function() {
  386. const wrapElement = this._insertOperation.nodes.getNode( 0 );
  387. return getClassName( this ) + `( ${ this.baseVersion } ): ` +
  388. `${ this.range } -> ${ wrapElement }`;
  389. } );
  390. sandbox.mock( ViewText.prototype, 'toString', function() {
  391. return `#${ this.data }`;
  392. } );
  393. sandbox.mock( ViewText.prototype, 'logExtended', function() {
  394. logger.log( 'ViewText: ' + this );
  395. } );
  396. sandbox.mock( ViewText.prototype, 'log', function() {
  397. logger.log( 'ViewText: ' + this );
  398. } );
  399. sandbox.mock( ViewTextProxy.prototype, 'toString', function() {
  400. return `#${ this.data }`;
  401. } );
  402. sandbox.mock( ViewTextProxy.prototype, 'logExtended', function() {
  403. logger.log( 'ViewTextProxy: ' + this );
  404. } );
  405. sandbox.mock( ViewTextProxy.prototype, 'log', function() {
  406. logger.log( 'ViewTextProxy: ' + this );
  407. } );
  408. sandbox.mock( ViewElement.prototype, 'printTree', function( level = 0 ) {
  409. let string = '';
  410. string += '\t'.repeat( level ) + `<${ this.name }${ mapToTags( this.getAttributes() ) }>`;
  411. for ( const child of this.getChildren() ) {
  412. if ( child.is( 'text' ) ) {
  413. string += '\n' + '\t'.repeat( level + 1 ) + child.data;
  414. } else {
  415. string += '\n' + child.printTree( level + 1 );
  416. }
  417. }
  418. if ( this.childCount ) {
  419. string += '\n' + '\t'.repeat( level );
  420. }
  421. string += `</${ this.name }>`;
  422. return string;
  423. } );
  424. sandbox.mock( ViewElement.prototype, 'logTree', function() {
  425. logger.log( this.printTree() );
  426. } );
  427. sandbox.mock( ViewDocumentFragment.prototype, 'printTree', function() {
  428. let string = 'ViewDocumentFragment: [';
  429. for ( const child of this.getChildren() ) {
  430. if ( child.is( 'text' ) ) {
  431. string += '\n' + '\t'.repeat( 1 ) + child.data;
  432. } else {
  433. string += '\n' + child.printTree( 1 );
  434. }
  435. }
  436. string += '\n]';
  437. return string;
  438. } );
  439. sandbox.mock( ViewDocumentFragment.prototype, 'logTree', function() {
  440. logger.log( this.printTree() );
  441. } );
  442. }
  443. function enableReplayerTools() {
  444. const _modelApplyOperation = Model.prototype.applyOperation;
  445. sandbox.mock( Model.prototype, 'applyOperation', function( operation ) {
  446. if ( !this._lastDelta ) {
  447. this._appliedDeltas = [];
  448. } else if ( this._lastDelta !== operation.delta ) {
  449. this._appliedDeltas.push( this._lastDelta.toJSON() );
  450. }
  451. this._lastDelta = operation.delta;
  452. return _modelApplyOperation.call( this, operation );
  453. } );
  454. sandbox.mock( Model.prototype, 'getAppliedDeltas', function() {
  455. // No deltas has been applied yet, return empty string.
  456. if ( !this._lastDelta ) {
  457. return '';
  458. }
  459. const appliedDeltas = this._appliedDeltas.concat( this._lastDelta );
  460. return appliedDeltas.map( JSON.stringify ).join( LOG_SEPARATOR );
  461. } );
  462. sandbox.mock( Model.prototype, 'createReplayer', function( stringifiedDeltas ) {
  463. return new DeltaReplayer( this, LOG_SEPARATOR, stringifiedDeltas );
  464. } );
  465. }
  466. function enableDocumentTools() {
  467. const _modelApplyOperation = Model.prototype.applyOperation;
  468. sandbox.mock( Model.prototype, 'applyOperation', function( operation ) {
  469. logger.log( 'Applying ' + operation );
  470. if ( !this._operationLogs ) {
  471. this._operationLogs = [];
  472. }
  473. this._operationLogs.push( JSON.stringify( operation.toJSON() ) );
  474. return _modelApplyOperation.call( this, operation );
  475. } );
  476. sandbox.mock( ModelDocument.prototype, 'log', function( version = null ) {
  477. version = version === null ? this.version : version;
  478. logDocument( this, version );
  479. } );
  480. sandbox.mock( ViewDocument.prototype, 'log', function( version ) {
  481. logDocument( this, version );
  482. } );
  483. sandbox.mock( Editor.prototype, 'logModel', function( version = null ) {
  484. version = version === null ? this.model.document.version : version;
  485. this.model.document.log( version );
  486. } );
  487. sandbox.mock( Editor.prototype, 'logView', function( version ) {
  488. this.editing.view.log( version );
  489. } );
  490. sandbox.mock( Editor.prototype, 'logDocuments', function( version = null ) {
  491. version = version === null ? this.model.document.version : version;
  492. this.logModel( version );
  493. this.logView( version );
  494. } );
  495. function logDocument( document, version ) {
  496. logger.log( '--------------------' );
  497. if ( document[ treeDump ][ version ] ) {
  498. logger.log( document[ treeDump ][ version ] );
  499. } else {
  500. logger.log( 'Tree log unavailable for given version: ' + version );
  501. }
  502. }
  503. }
  504. /**
  505. * Plugin that enables debugging features on the editor's model and view documents.
  506. */
  507. class DebugPlugin extends Plugin {
  508. constructor( editor ) {
  509. super( editor );
  510. const modelDocument = this.editor.model.document;
  511. const viewDocument = this.editor.editing.view;
  512. modelDocument[ treeDump ] = [];
  513. viewDocument[ treeDump ] = [];
  514. dumpTrees( modelDocument, modelDocument.version );
  515. dumpTrees( viewDocument, modelDocument.version );
  516. modelDocument.on( 'change', () => {
  517. dumpTrees( modelDocument, modelDocument.version );
  518. }, { priority: 'lowest' } );
  519. modelDocument.on( 'changesDone', () => {
  520. dumpTrees( viewDocument, modelDocument.version );
  521. }, { priority: 'lowest' } );
  522. }
  523. }
  524. // Helper function, stores `document` state for given `version` as a string in private property.
  525. function dumpTrees( document, version ) {
  526. let string = '';
  527. for ( const root of document.roots.values() ) {
  528. string += root.printTree() + '\n';
  529. }
  530. document[ treeDump ][ version ] = string.substr( 0, string.length - 1 ); // Remove the last "\n".
  531. const overflow = document[ treeDump ].length - maxTreeDumpLength;
  532. if ( overflow > 0 ) {
  533. document[ treeDump ][ overflow - 1 ] = null;
  534. }
  535. }
  536. // Helper function, returns class name of given `Delta` or `Operation`.
  537. // @param {module:engine/model/delta/delta~Delta|module:engine/model/operation/operation~Operation}
  538. // @returns {String} Class name.
  539. function getClassName( obj ) {
  540. const path = obj.constructor.className.split( '.' );
  541. return path[ path.length - 1 ];
  542. }
  543. // Helper function, converts map to {"key1":"value1","key2":"value2"} format.
  544. // @param {Map} map Map to convert.
  545. // @returns {String} Converted map.
  546. function mapString( map ) {
  547. const obj = {};
  548. for ( const entry of map ) {
  549. obj[ entry[ 0 ] ] = entry[ 1 ];
  550. }
  551. return JSON.stringify( obj );
  552. }
  553. // Helper function, converts map to key1="value1" key2="value1" format.
  554. // @param {Map} map Map to convert.
  555. // @returns {String} Converted map.
  556. function mapToTags( map ) {
  557. let string = '';
  558. for ( const entry of map ) {
  559. string += ` ${ entry[ 0 ] }=${ JSON.stringify( entry[ 1 ] ) }`;
  560. }
  561. return string;
  562. }