enableenginedebug.js 20 KB

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