enableenginedebug.js 22 KB

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