8
0

enableenginedebug.js 18 KB

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