enableenginedebug.js 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588
  1. /**
  2. * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
  3. * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
  4. */
  5. /**
  6. * @module engine/dev-utils/enableenginedebug
  7. */
  8. /* global console */
  9. import OperationReplayer from './operationreplayer';
  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 SplitOperation from '../model/operation/splitoperation';
  25. import MergeOperation from '../model/operation/mergeoperation';
  26. import Model from '../model/model';
  27. import ModelDocument from '../model/document';
  28. import ModelDocumentFragment from '../model/documentfragment';
  29. import ModelRootElement from '../model/rootelement';
  30. import ViewDocument from '../view/document';
  31. import ViewElement from '../view/element';
  32. import ViewText from '../view/text';
  33. import ViewTextProxy from '../view/textproxy';
  34. import ViewDocumentFragment from '../view/documentfragment';
  35. import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
  36. import Editor from '@ckeditor/ckeditor5-core/src/editor/editor';
  37. // Sandbox class allows creating mocks of the functions and restoring these mocks to the original values.
  38. class Sandbox {
  39. constructor() {
  40. // An array that contains functions which restore the original values of mocked objects.
  41. // @private
  42. // @type {Array.<Function>}
  43. this._restores = [];
  44. }
  45. // Creates a new mock.
  46. //
  47. // @param {Object} object Object to mock.
  48. // @param {String} methodName Function to mock.
  49. // @param {Function} fakeMethod Function that will be executed.
  50. mock( object, methodName, fakeMethod ) {
  51. const originalMethod = object[ methodName ];
  52. object[ methodName ] = fakeMethod;
  53. this._restores.unshift( () => {
  54. if ( originalMethod ) {
  55. object[ methodName ] = originalMethod;
  56. } else {
  57. delete object[ methodName ];
  58. }
  59. } );
  60. }
  61. // Restores all mocked functions.
  62. restore() {
  63. for ( const restore of this._restores ) {
  64. restore();
  65. }
  66. this._restores = [];
  67. }
  68. }
  69. const sandbox = new Sandbox();
  70. const treeDump = Symbol( '_treeDump' );
  71. // Maximum number of stored states of model and view document.
  72. const maxTreeDumpLength = 20;
  73. // Separator used to separate stringified operations.
  74. const LOG_SEPARATOR = '-------';
  75. // Specified whether debug tools were already enabled.
  76. let enabled = false;
  77. // Logging function used to log debug messages.
  78. let logger = console;
  79. /**
  80. * Enhances model classes with logging methods. Returns a plugin that should be loaded in the editor to
  81. * enable debugging features.
  82. *
  83. * Every operation applied on {@link module:engine/model/document~Document model.Document} is logged.
  84. *
  85. * The following classes are expanded with `log` and meaningful `toString` methods:
  86. * * {@link module:engine/model/position~Position model.Position},
  87. * * {@link module:engine/model/range~Range model.Range},
  88. * * {@link module:engine/model/text~Text model.Text},
  89. * * {@link module:engine/model/element~Element model.Element},
  90. * * {@link module:engine/model/rootelement~RootElement model.RootElement},
  91. * * {@link module:engine/model/documentfragment~DocumentFragment model.DocumentFragment},
  92. * * {@link module:engine/model/document~Document model.Document},
  93. * * all {@link module:engine/model/operation/operation~Operation operations}
  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, the 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. *
  103. * Additionally, the following classes are expanded with `logTree` and `printTree` methods:
  104. * * {@link module:engine/model/element~Element model.Element},
  105. * * {@link module:engine/model/documentfragment~DocumentFragment model.DocumentFragment},
  106. * * {@link module:engine/view/element~Element view.Element},
  107. * * {@link module:engine/view/documentfragment~DocumentFragment view.DocumentFragment}.
  108. *
  109. * Finally, the following methods are added to {@link module:core/editor/editor~Editor}: `logModel`, `logView`, `logDocuments`.
  110. * All those methods take one parameter, which is the version of the {@link module:engine/model/document~Document model document}
  111. * for which the model or view document state should be logged.
  112. *
  113. * @param {Object} [_logger] An object with functions used to log messages and errors. By default messages are logged to the console.
  114. * If specified, it is expected to have `log()` and `error()` methods.
  115. * @returns {module:engine/dev-utils/enableenginedebug~DebugPlugin} The plugin to be loaded into the editor.
  116. */
  117. export default function enableEngineDebug( _logger = console ) {
  118. logger = _logger;
  119. if ( !enabled ) {
  120. enabled = true;
  121. enableLoggingTools();
  122. enableDocumentTools();
  123. enableReplayerTools();
  124. }
  125. return DebugPlugin;
  126. }
  127. /**
  128. * Restores all methods that have been overwritten.
  129. */
  130. export function disableEngineDebug() {
  131. sandbox.restore();
  132. enabled = false;
  133. }
  134. function enableLoggingTools() {
  135. sandbox.mock( ModelPosition.prototype, 'toString', function() {
  136. return `${ this.root } [ ${ this.path.join( ', ' ) } ]`;
  137. } );
  138. sandbox.mock( ModelPosition.prototype, 'log', function() {
  139. logger.log( 'ModelPosition: ' + this );
  140. } );
  141. sandbox.mock( ModelRange.prototype, 'toString', function() {
  142. return `${ this.root } [ ${ this.start.path.join( ', ' ) } ] - [ ${ this.end.path.join( ', ' ) } ]`;
  143. } );
  144. sandbox.mock( ModelRange.prototype, 'log', function() {
  145. logger.log( 'ModelRange: ' + this );
  146. } );
  147. sandbox.mock( ModelText.prototype, 'toString', function() {
  148. return `#${ this.data }`;
  149. } );
  150. sandbox.mock( ModelText.prototype, 'logExtended', function() {
  151. logger.log( `ModelText: ${ this }, attrs: ${ mapString( this.getAttributes() ) }` );
  152. } );
  153. sandbox.mock( ModelText.prototype, 'log', function() {
  154. logger.log( 'ModelText: ' + this );
  155. } );
  156. sandbox.mock( ModelTextProxy.prototype, 'toString', function() {
  157. return `#${ this.data }`;
  158. } );
  159. sandbox.mock( ModelTextProxy.prototype, 'logExtended', function() {
  160. logger.log( `ModelTextProxy: ${ this }, attrs: ${ mapString( this.getAttributes() ) }` );
  161. } );
  162. sandbox.mock( ModelTextProxy.prototype, 'log', function() {
  163. logger.log( 'ModelTextProxy: ' + this );
  164. } );
  165. sandbox.mock( ModelElement.prototype, 'toString', function() {
  166. return `<${ this.rootName || this.name }>`;
  167. } );
  168. sandbox.mock( ModelElement.prototype, 'log', function() {
  169. logger.log( 'ModelElement: ' + this );
  170. } );
  171. sandbox.mock( ModelElement.prototype, 'logExtended', function() {
  172. logger.log( `ModelElement: ${ this }, ${ this.childCount } children, attrs: ${ mapString( this.getAttributes() ) }` );
  173. } );
  174. sandbox.mock( ModelElement.prototype, 'logAll', function() {
  175. logger.log( '--------------------' );
  176. this.logExtended();
  177. logger.log( 'List of children:' );
  178. for ( const child of this.getChildren() ) {
  179. child.log();
  180. }
  181. } );
  182. sandbox.mock( ModelElement.prototype, 'printTree', function( level = 0 ) {
  183. let string = '';
  184. string += '\t'.repeat( level ) + `<${ this.rootName || this.name }${ mapToTags( this.getAttributes() ) }>`;
  185. for ( const child of this.getChildren() ) {
  186. string += '\n';
  187. if ( child.is( 'text' ) ) {
  188. const textAttrs = mapToTags( child._attrs );
  189. string += '\t'.repeat( level + 1 );
  190. if ( textAttrs !== '' ) {
  191. string += `<$text${ textAttrs }>` + child.data + '</$text>';
  192. } else {
  193. string += child.data;
  194. }
  195. } else {
  196. string += child.printTree( level + 1 );
  197. }
  198. }
  199. if ( this.childCount ) {
  200. string += '\n' + '\t'.repeat( level );
  201. }
  202. string += `</${ this.rootName || this.name }>`;
  203. return string;
  204. } );
  205. sandbox.mock( ModelElement.prototype, 'logTree', function() {
  206. logger.log( this.printTree() );
  207. } );
  208. sandbox.mock( ModelRootElement.prototype, 'toString', function() {
  209. return this.rootName;
  210. } );
  211. sandbox.mock( ModelRootElement.prototype, 'log', function() {
  212. logger.log( 'ModelRootElement: ' + this );
  213. } );
  214. sandbox.mock( ModelDocumentFragment.prototype, 'toString', function() {
  215. return 'documentFragment';
  216. } );
  217. sandbox.mock( ModelDocumentFragment.prototype, 'log', function() {
  218. logger.log( 'ModelDocumentFragment: ' + this );
  219. } );
  220. sandbox.mock( ModelDocumentFragment.prototype, 'printTree', function() {
  221. let string = 'ModelDocumentFragment: [';
  222. for ( const child of this.getChildren() ) {
  223. string += '\n';
  224. if ( child.is( 'text' ) ) {
  225. const textAttrs = mapToTags( child._attrs );
  226. string += '\t'.repeat( 1 );
  227. if ( textAttrs !== '' ) {
  228. string += `<$text${ textAttrs }>` + child.data + '</$text>';
  229. } else {
  230. string += child.data;
  231. }
  232. } else {
  233. string += child.printTree( 1 );
  234. }
  235. }
  236. string += '\n]';
  237. return string;
  238. } );
  239. sandbox.mock( ModelDocumentFragment.prototype, 'logTree', function() {
  240. logger.log( this.printTree() );
  241. } );
  242. sandbox.mock( Operation.prototype, 'log', function() {
  243. logger.log( this.toString() );
  244. } );
  245. sandbox.mock( AttributeOperation.prototype, 'toString', function() {
  246. return getClassName( this ) + `( ${ this.baseVersion } ): ` +
  247. `"${ this.key }": ${ JSON.stringify( this.oldValue ) } -> ${ JSON.stringify( this.newValue ) }, ${ this.range }`;
  248. } );
  249. sandbox.mock( DetachOperation.prototype, 'toString', function() {
  250. const range = ModelRange._createFromPositionAndShift( this.sourcePosition, this.howMany );
  251. const nodes = Array.from( range.getItems() );
  252. const nodeString = nodes.length > 1 ? `[ ${ nodes.length } ]` : nodes[ 0 ];
  253. return getClassName( this ) + `( ${ this.baseVersion } ): ${ nodeString } -> ${ range }`;
  254. } );
  255. sandbox.mock( InsertOperation.prototype, 'toString', function() {
  256. const nodeString = this.nodes.length > 1 ? `[ ${ this.nodes.length } ]` : this.nodes.getNode( 0 );
  257. return getClassName( this ) + `( ${ this.baseVersion } ): ${ nodeString } -> ${ this.position }`;
  258. } );
  259. sandbox.mock( MarkerOperation.prototype, 'toString', function() {
  260. return getClassName( this ) + `( ${ this.baseVersion } ): ` +
  261. `"${ this.name }": ${ this.oldRange } -> ${ this.newRange }`;
  262. } );
  263. sandbox.mock( MoveOperation.prototype, 'toString', function() {
  264. const range = ModelRange._createFromPositionAndShift( this.sourcePosition, this.howMany );
  265. return getClassName( this ) + `( ${ this.baseVersion } ): ${ range } -> ${ this.targetPosition }`;
  266. } );
  267. sandbox.mock( NoOperation.prototype, 'toString', function() {
  268. return `NoOperation( ${ this.baseVersion } )`;
  269. } );
  270. sandbox.mock( RenameOperation.prototype, 'toString', function() {
  271. return getClassName( this ) + `( ${ this.baseVersion } ): ` +
  272. `${ this.position }: "${ this.oldName }" -> "${ this.newName }"`;
  273. } );
  274. sandbox.mock( RootAttributeOperation.prototype, 'toString', function() {
  275. return getClassName( this ) + `( ${ this.baseVersion } ): ` +
  276. `"${ this.key }": ${ JSON.stringify( this.oldValue ) } -> ${ JSON.stringify( this.newValue ) }, ${ this.root.rootName }`;
  277. } );
  278. sandbox.mock( MergeOperation.prototype, 'toString', function() {
  279. return getClassName( this ) + `( ${ this.baseVersion } ): ` +
  280. `${ this.sourcePosition } -> ${ this.targetPosition } ( ${ this.howMany } ), ${ this.graveyardPosition }`;
  281. } );
  282. sandbox.mock( SplitOperation.prototype, 'toString', function() {
  283. return getClassName( this ) + `( ${ this.baseVersion } ): ${ this.splitPosition } ` +
  284. `( ${ this.howMany } ) -> ${ this.insertionPosition }${ this.graveyardPosition ? ' with ' + this.graveyardPosition : '' }`;
  285. } );
  286. sandbox.mock( ViewText.prototype, 'toString', function() {
  287. return `#${ this.data }`;
  288. } );
  289. sandbox.mock( ViewText.prototype, 'logExtended', function() {
  290. logger.log( 'ViewText: ' + this );
  291. } );
  292. sandbox.mock( ViewText.prototype, 'log', function() {
  293. logger.log( 'ViewText: ' + this );
  294. } );
  295. sandbox.mock( ViewTextProxy.prototype, 'toString', function() {
  296. return `#${ this.data }`;
  297. } );
  298. sandbox.mock( ViewTextProxy.prototype, 'logExtended', function() {
  299. logger.log( 'ViewTextProxy: ' + this );
  300. } );
  301. sandbox.mock( ViewTextProxy.prototype, 'log', function() {
  302. logger.log( 'ViewTextProxy: ' + this );
  303. } );
  304. sandbox.mock( ViewElement.prototype, 'printTree', function( level = 0 ) {
  305. let string = '';
  306. string += '\t'.repeat( level ) + `<${ this.name }${ mapToTags( this.getAttributes() ) }>`;
  307. for ( const child of this.getChildren() ) {
  308. if ( child.is( 'text' ) ) {
  309. string += '\n' + '\t'.repeat( level + 1 ) + child.data;
  310. } else {
  311. string += '\n' + child.printTree( level + 1 );
  312. }
  313. }
  314. if ( this.childCount ) {
  315. string += '\n' + '\t'.repeat( level );
  316. }
  317. string += `</${ this.name }>`;
  318. return string;
  319. } );
  320. sandbox.mock( ViewElement.prototype, 'logTree', function() {
  321. logger.log( this.printTree() );
  322. } );
  323. sandbox.mock( ViewDocumentFragment.prototype, 'printTree', function() {
  324. let string = 'ViewDocumentFragment: [';
  325. for ( const child of this.getChildren() ) {
  326. if ( child.is( 'text' ) ) {
  327. string += '\n' + '\t'.repeat( 1 ) + child.data;
  328. } else {
  329. string += '\n' + child.printTree( 1 );
  330. }
  331. }
  332. string += '\n]';
  333. return string;
  334. } );
  335. sandbox.mock( ViewDocumentFragment.prototype, 'logTree', function() {
  336. logger.log( this.printTree() );
  337. } );
  338. }
  339. function enableReplayerTools() {
  340. const _modelApplyOperation = Model.prototype.applyOperation;
  341. sandbox.mock( Model.prototype, 'applyOperation', function( operation ) {
  342. if ( !this._appliedOperations ) {
  343. this._appliedOperations = [];
  344. }
  345. this._appliedOperations.push( operation );
  346. return _modelApplyOperation.call( this, operation );
  347. } );
  348. sandbox.mock( Model.prototype, 'getAppliedOperations', function() {
  349. if ( !this._appliedOperations ) {
  350. return '';
  351. }
  352. return this._appliedOperations.map( JSON.stringify ).join( LOG_SEPARATOR );
  353. } );
  354. sandbox.mock( Model.prototype, 'createReplayer', function( stringifiedOperations ) {
  355. return new OperationReplayer( this, LOG_SEPARATOR, stringifiedOperations );
  356. } );
  357. }
  358. function enableDocumentTools() {
  359. const _modelApplyOperation = Model.prototype.applyOperation;
  360. sandbox.mock( Model.prototype, 'applyOperation', function( operation ) {
  361. logger.log( 'Applying ' + operation );
  362. if ( !this._operationLogs ) {
  363. this._operationLogs = [];
  364. }
  365. this._operationLogs.push( JSON.stringify( operation ) );
  366. return _modelApplyOperation.call( this, operation );
  367. } );
  368. sandbox.mock( ModelDocument.prototype, 'log', function( version = null ) {
  369. version = version === null ? this.version : version;
  370. logDocument( this, version );
  371. } );
  372. sandbox.mock( ViewDocument.prototype, 'log', function( version ) {
  373. logDocument( this, version );
  374. } );
  375. sandbox.mock( Editor.prototype, 'logModel', function( version = null ) {
  376. version = version === null ? this.model.document.version : version;
  377. this.model.document.log( version );
  378. } );
  379. sandbox.mock( Editor.prototype, 'logView', function( version ) {
  380. this.editing.view.document.log( version );
  381. } );
  382. sandbox.mock( Editor.prototype, 'logDocuments', function( version = null ) {
  383. version = version === null ? this.model.document.version : version;
  384. this.logModel( version );
  385. this.logView( version );
  386. } );
  387. function logDocument( document, version ) {
  388. logger.log( '--------------------' );
  389. if ( document[ treeDump ][ version ] ) {
  390. logger.log( document[ treeDump ][ version ] );
  391. } else {
  392. logger.log( 'Tree log unavailable for given version: ' + version );
  393. }
  394. }
  395. }
  396. /**
  397. * Plugin that enables debugging features on the editor's model and view documents.
  398. */
  399. class DebugPlugin extends Plugin {
  400. constructor( editor ) {
  401. super( editor );
  402. const model = this.editor.model;
  403. const modelDocument = model.document;
  404. const view = this.editor.editing.view;
  405. const viewDocument = view.document;
  406. modelDocument[ treeDump ] = [];
  407. viewDocument[ treeDump ] = [];
  408. dumpTrees( modelDocument, modelDocument.version );
  409. dumpTrees( viewDocument, modelDocument.version );
  410. model.on( 'applyOperation', () => {
  411. dumpTrees( modelDocument, modelDocument.version );
  412. }, { priority: 'lowest' } );
  413. model.document.on( 'change', () => {
  414. dumpTrees( viewDocument, modelDocument.version );
  415. }, { priority: 'lowest' } );
  416. }
  417. }
  418. // Helper function, stores the `document` state for a given `version` as a string in a private property.
  419. function dumpTrees( document, version ) {
  420. let string = '';
  421. for ( const root of document.roots ) {
  422. string += root.printTree() + '\n';
  423. }
  424. document[ treeDump ][ version ] = string.substr( 0, string.length - 1 ); // Remove the last "\n".
  425. const overflow = document[ treeDump ].length - maxTreeDumpLength;
  426. if ( overflow > 0 ) {
  427. document[ treeDump ][ overflow - 1 ] = null;
  428. }
  429. }
  430. // Helper function, returns the class name of a given `Operation`.
  431. // @param {module:engine/model/operation/operation~Operation}
  432. // @returns {String} Class name.
  433. function getClassName( obj ) {
  434. return obj.constructor.className;
  435. }
  436. // Helper function, converts a map to the {"key1":"value1","key2":"value2"} format.
  437. // @param {Map} map Map to convert.
  438. // @returns {String} Converted map.
  439. function mapString( map ) {
  440. const obj = {};
  441. for ( const entry of map ) {
  442. obj[ entry[ 0 ] ] = entry[ 1 ];
  443. }
  444. return JSON.stringify( obj );
  445. }
  446. // Helper function, converts a map to the key1="value1" key2="value1" format.
  447. // @param {Map} map Map to convert.
  448. // @returns {String} Converted map.
  449. function mapToTags( map ) {
  450. let string = '';
  451. for ( const entry of map ) {
  452. string += ` ${ entry[ 0 ] }=${ JSON.stringify( entry[ 1 ] ) }`;
  453. }
  454. return string;
  455. }