enableenginedebug.js 19 KB

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