8
0

enableenginedebug.js 15 KB

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