conversion.js 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646
  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/conversion/conversion
  7. */
  8. import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
  9. import UpcastHelpers from './upcasthelpers';
  10. import DowncastHelpers from './downcasthelpers';
  11. /**
  12. * A utility class that helps add converters to upcast and downcast dispatchers.
  13. *
  14. * We recommend reading the {@glink framework/guides/architecture/editing-engine Editing engine architecture} guide first to
  15. * understand the core concepts of the conversion mechanisms.
  16. *
  17. * An instance of the conversion manager is available in the
  18. * {@link module:core/editor/editor~Editor#conversion `editor.conversion`} property
  19. * and by default has the following groups of dispatchers (i.e. directions of conversion):
  20. *
  21. * * `downcast` (editing and data downcasts)
  22. * * `editingDowncast`
  23. * * `dataDowncast`
  24. * * `upcast`
  25. *
  26. * # One-way converters
  27. *
  28. * To add a converter to a specific group, use the {@link module:engine/conversion/conversion~Conversion#for `for()`}
  29. * method:
  30. *
  31. * // Add a converter to editing downcast and data downcast.
  32. * editor.conversion.for( 'downcast' ).elementToElement( config ) );
  33. *
  34. * // Add a converter to the data pipepline only:
  35. * editor.conversion.for( 'dataDowncast' ).elementToElement( dataConversionConfig ) );
  36. *
  37. * // And a slightly different one for the editing pipeline:
  38. * editor.conversion.for( 'editingDowncast' ).elementToElement( editingConversionConfig ) );
  39. *
  40. * See {@link module:engine/conversion/conversion~Conversion#for `for()`} method documentation to learn more about
  41. * available conversion helpers and how to use your custom ones.
  42. *
  43. * # Two-way converters
  44. *
  45. * Besides using one-way converters via the `for()` method, you can also use other methods available in this
  46. * class to add two-way converters (upcast and downcast):
  47. *
  48. * * {@link module:engine/conversion/conversion~Conversion#elementToElement `elementToElement()`} –
  49. * Model element to view element and vice versa.
  50. * * {@link module:engine/conversion/conversion~Conversion#attributeToElement `attributeToElement()`} –
  51. * Model attribute to view element and vice versa.
  52. * * {@link module:engine/conversion/conversion~Conversion#attributeToAttribute `attributeToAttribute()`} –
  53. * Model attribute to view element and vice versa.
  54. */
  55. export default class Conversion {
  56. /**
  57. * Creates a new conversion instance.
  58. *
  59. * @param {module:engine/conversion/downcastdispatcher~DowncastDispatcher|
  60. * Array.<module:engine/conversion/downcastdispatcher~DowncastDispatcher>} downcastDispatchers
  61. * @param {module:engine/conversion/upcastdispatcher~UpcastDispatcher|
  62. * Array.<module:engine/conversion/upcastdispatcher~UpcastDispatcher>} upcastDispatchers
  63. */
  64. constructor( downcastDispatchers, upcastDispatchers ) {
  65. /**
  66. * Maps dispatchers group name to ConversionHelpers instances.
  67. *
  68. * @private
  69. * @member {Map.<String,module:engine/conversion/conversionhelpers~ConversionHelpers>}
  70. */
  71. this._helpers = new Map();
  72. // Define default 'downcast' & 'upcast' dispatchers groups. Those groups are always available as two-way converters needs them.
  73. this._downcast = Array.isArray( downcastDispatchers ) ? downcastDispatchers : [ downcastDispatchers ];
  74. this._createConversionHelpers( { name: 'downcast', dispatchers: this._downcast, isDowncast: true } );
  75. this._upcast = Array.isArray( upcastDispatchers ) ? upcastDispatchers : [ upcastDispatchers ];
  76. this._createConversionHelpers( { name: 'upcast', dispatchers: this._upcast, isDowncast: false } );
  77. }
  78. /**
  79. * Define an alias for registered dispatcher.
  80. *
  81. * const conversion = new Conversion(
  82. * [ dataDowncastDispatcher, editingDowncastDispatcher ],
  83. * upcastDispatcher
  84. * );
  85. *
  86. * conversion.addAlias( 'dataDowncast', dataDowncastDispatcher );
  87. *
  88. * @param {String} alias An alias of a dispatcher.
  89. * @param {module:engine/conversion/downcastdispatcher~DowncastDispatcher|
  90. * module:engine/conversion/upcastdispatcher~UpcastDispatcher} dispatcher Dispatcher which should have an alias.
  91. */
  92. addAlias( alias, dispatcher ) {
  93. const isDowncast = this._downcast.includes( dispatcher );
  94. const isUpcast = this._upcast.includes( dispatcher );
  95. if ( !isUpcast && !isDowncast ) {
  96. /**
  97. * Trying to register and alias for a dispatcher that nas not been registered.
  98. *
  99. * @error conversion-add-alias-dispatcher-not-registered
  100. */
  101. throw new CKEditorError(
  102. 'conversion-add-alias-dispatcher-not-registered: ' +
  103. 'Trying to register and alias for a dispatcher that nas not been registered.',
  104. this
  105. );
  106. }
  107. this._createConversionHelpers( { name: alias, dispatchers: [ dispatcher ], isDowncast } );
  108. }
  109. /**
  110. * Provides a chainable API to assign converters to conversion dispatchers group.
  111. *
  112. * If the given group name has not been registered, the
  113. * {@link module:utils/ckeditorerror~CKEditorError `conversion-for-unknown-group` error} is thrown.
  114. *
  115. * You can use conversion helpers available directly in the `for()` chain or your custom ones via
  116. * the {@link module:engine/conversion/conversionhelpers~ConversionHelpers#add `add()`} method.
  117. *
  118. * # Using bulit-in conversion helpers
  119. *
  120. * The `for()` chain comes with a set of conversion helpers which you can use like this:
  121. *
  122. * editor.conversion.for( 'downcast' )
  123. * .elementToElement( config1 ) // Adds an element-to-element downcast converter.
  124. * .attributeToElement( config2 ); // Adds an attribute-to-element downcast converter.
  125. *
  126. * editor.conversion.for( 'upcast' )
  127. * .elementToAttribute( config3 ); // Adds an element-to-attribute upcast converter.
  128. *
  129. * Refer to the documentation of built-in conversion helpers to learn about their configuration options.
  130. *
  131. * * downcast (model-to-view) conversion helpers:
  132. *
  133. * * {@link module:engine/conversion/downcasthelpers~DowncastHelpers#elementToElement `elementToElement()`},
  134. * * {@link module:engine/conversion/downcasthelpers~DowncastHelpers#attributeToElement `attributeToElement()`},
  135. * * {@link module:engine/conversion/downcasthelpers~DowncastHelpers#attributeToAttribute `attributeToAttribute()`}.
  136. * * {@link module:engine/conversion/downcasthelpers~DowncastHelpers#markerToElement `markerToElement()`}.
  137. * * {@link module:engine/conversion/downcasthelpers~DowncastHelpers#markerToHighlight `markerToHighlight()`}.
  138. *
  139. * * upcast (view-to-model) conversion helpers:
  140. *
  141. * * {@link module:engine/conversion/upcasthelpers~UpcastHelpers#elementToElement `elementToElement()`},
  142. * * {@link module:engine/conversion/upcasthelpers~UpcastHelpers#elementToAttribute `elementToAttribute()`},
  143. * * {@link module:engine/conversion/upcasthelpers~UpcastHelpers#attributeToAttribute `attributeToAttribute()`}.
  144. * * {@link module:engine/conversion/upcasthelpers~UpcastHelpers#elementToMarker `elementToMarker()`}.
  145. *
  146. * # Using custom conversion helpers
  147. *
  148. * If you need to implement a nontypical converter, you can do so by calling:
  149. *
  150. * editor.conversion.for( direction ).add( customHelper );
  151. *
  152. * The `.add()` method takes exactly one parameter, which is a function. This function should accept one parameter that
  153. * is a dispatcher instance. The function should add an actual converter to the passed dispatcher instance.
  154. *
  155. * Example:
  156. *
  157. * editor.conversion.for( 'upcast' ).add( dispatcher => {
  158. * dispatcher.on( 'element:a', ( evt, data, conversionApi ) => {
  159. * // Do something with a view <a> element.
  160. * } );
  161. * } );
  162. *
  163. * Refer to the documentation of {@link module:engine/conversion/upcastdispatcher~UpcastDispatcher}
  164. * and {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher} to learn how to write
  165. * custom converters.
  166. *
  167. * @param {String} groupName The name of dispatchers group to add the converters to.
  168. * @returns {module:engine/conversion/downcasthelpers~DowncastHelpers|module:engine/conversion/upcasthelpers~UpcastHelpers}
  169. */
  170. for( groupName ) {
  171. if ( !this._helpers.has( groupName ) ) {
  172. /**
  173. * Trying to add a converter to an unknown dispatchers group.
  174. *
  175. * @error conversion-for-unknown-group
  176. */
  177. throw new CKEditorError( 'conversion-for-unknown-group: Trying to add a converter to an unknown dispatchers group.', this );
  178. }
  179. return this._helpers.get( groupName );
  180. }
  181. /**
  182. * Sets up converters between the model and the view that convert a model element to a view element (and vice versa).
  183. * For example, the model `<paragraph>Foo</paragraph>` is `<p>Foo</p>` in the view.
  184. *
  185. * // A simple conversion from the `paragraph` model element to the `<p>` view element (and vice versa).
  186. * editor.conversion.elementToElement( { model: 'paragraph', view: 'p' } );
  187. *
  188. * // Override other converters by specifying a converter definition with a higher priority.
  189. * editor.conversion.elementToElement( { model: 'paragraph', view: 'div', converterPriority: 'high' } );
  190. *
  191. * // View specified as an object instead of a string.
  192. * editor.conversion.elementToElement( {
  193. * model: 'fancyParagraph',
  194. * view: {
  195. * name: 'p',
  196. * classes: 'fancy'
  197. * }
  198. * } );
  199. *
  200. * // Use `upcastAlso` to define other view elements that should also be converted to a `paragraph` element.
  201. * editor.conversion.elementToElement( {
  202. * model: 'paragraph',
  203. * view: 'p',
  204. * upcastAlso: [
  205. * 'div',
  206. * {
  207. * // Any element with the `display: block` style.
  208. * styles: {
  209. * display: 'block'
  210. * }
  211. * }
  212. * ]
  213. * } );
  214. *
  215. * // `upcastAlso` set as callback enables a conversion of a wide range of different view elements.
  216. * editor.conversion.elementToElement( {
  217. * model: 'heading',
  218. * view: 'h2',
  219. * // Convert "headling-like" paragraphs to headings.
  220. * upcastAlso: viewElement => {
  221. * const fontSize = viewElement.getStyle( 'font-size' );
  222. *
  223. * if ( !fontSize ) {
  224. * return null;
  225. * }
  226. *
  227. * const match = fontSize.match( /(\d+)\s*px/ );
  228. *
  229. * if ( !match ) {
  230. * return null;
  231. * }
  232. *
  233. * const size = Number( match[ 1 ] );
  234. *
  235. * if ( size > 26 ) {
  236. * // Returned value can be an object with the matched properties.
  237. * // These properties will be "consumed" during the conversion.
  238. * // See `engine.view.Matcher~MatcherPattern` and `engine.view.Matcher#match` for more details.
  239. *
  240. * return { name: true, styles: [ 'font-size' ] };
  241. * }
  242. *
  243. * return null;
  244. * }
  245. * } );
  246. *
  247. * `definition.model` is a `String` with a model element name to convert from or to.
  248. * See {@link module:engine/conversion/conversion~ConverterDefinition} to learn about other parameters.
  249. *
  250. * @param {module:engine/conversion/conversion~ConverterDefinition} definition The converter definition.
  251. */
  252. elementToElement( definition ) {
  253. // Set up downcast converter.
  254. this.for( 'downcast' ).elementToElement( definition );
  255. // Set up upcast converter.
  256. for ( const { model, view } of _getAllUpcastDefinitions( definition ) ) {
  257. this.for( 'upcast' )
  258. .elementToElement( {
  259. model,
  260. view,
  261. converterPriority: definition.converterPriority
  262. } );
  263. }
  264. }
  265. /**
  266. * Sets up converters between the model and the view that convert a model attribute to a view element (and vice versa).
  267. * For example, a model text node with `"Foo"` as data and the `bold` attribute is `<strong>Foo</strong>` in the view.
  268. *
  269. * // A simple conversion from the `bold=true` attribute to the `<strong>` view element (and vice versa).
  270. * editor.conversion.attributeToElement( { model: 'bold', view: 'strong' } );
  271. *
  272. * // Override other converters by specifying a converter definition with a higher priority.
  273. * editor.conversion.attributeToElement( { model: 'bold', view: 'b', converterPriority: 'high' } );
  274. *
  275. * // View specified as an object instead of a string.
  276. * editor.conversion.attributeToElement( {
  277. * model: 'bold',
  278. * view: {
  279. * name: 'span',
  280. * classes: 'bold'
  281. * }
  282. * } );
  283. *
  284. * // Use `config.model.name` to define the conversion only from a given node type, `$text` in this case.
  285. * // The same attribute on different elements may then be handled by a different converter.
  286. * editor.conversion.attributeToElement( {
  287. * model: {
  288. * key: 'textDecoration',
  289. * values: [ 'underline', 'lineThrough' ],
  290. * name: '$text'
  291. * },
  292. * view: {
  293. * underline: {
  294. * name: 'span',
  295. * styles: {
  296. * 'text-decoration': 'underline'
  297. * }
  298. * },
  299. * lineThrough: {
  300. * name: 'span',
  301. * styles: {
  302. * 'text-decoration': 'line-through'
  303. * }
  304. * }
  305. * }
  306. * } );
  307. *
  308. * // Use `upcastAlso` to define other view elements that should also be converted to the `bold` attribute.
  309. * editor.conversion.attributeToElement( {
  310. * model: 'bold',
  311. * view: 'strong',
  312. * upcastAlso: [
  313. * 'b',
  314. * {
  315. * name: 'span',
  316. * classes: 'bold'
  317. * },
  318. * {
  319. * name: 'span',
  320. * styles: {
  321. * 'font-weight': 'bold'
  322. * }
  323. * },
  324. * viewElement => {
  325. * const fontWeight = viewElement.getStyle( 'font-weight' );
  326. *
  327. * if ( viewElement.is( 'span' ) && fontWeight && /\d+/.test() && Number( fontWeight ) > 500 ) {
  328. * // Returned value can be an object with the matched properties.
  329. * // These properties will be "consumed" during the conversion.
  330. * // See `engine.view.Matcher~MatcherPattern` and `engine.view.Matcher#match` for more details.
  331. *
  332. * return {
  333. * name: true,
  334. * styles: [ 'font-weight' ]
  335. * };
  336. * }
  337. * }
  338. * ]
  339. * } );
  340. *
  341. * // Conversion from and to a model attribute key whose value is an enum (`fontSize=big|small`).
  342. * // `upcastAlso` set as callback enables a conversion of a wide range of different view elements.
  343. * editor.conversion.attributeToElement( {
  344. * model: {
  345. * key: 'fontSize',
  346. * values: [ 'big', 'small' ]
  347. * },
  348. * view: {
  349. * big: {
  350. * name: 'span',
  351. * styles: {
  352. * 'font-size': '1.2em'
  353. * }
  354. * },
  355. * small: {
  356. * name: 'span',
  357. * styles: {
  358. * 'font-size': '0.8em'
  359. * }
  360. * }
  361. * },
  362. * upcastAlso: {
  363. * big: viewElement => {
  364. * const fontSize = viewElement.getStyle( 'font-size' );
  365. *
  366. * if ( !fontSize ) {
  367. * return null;
  368. * }
  369. *
  370. * const match = fontSize.match( /(\d+)\s*px/ );
  371. *
  372. * if ( !match ) {
  373. * return null;
  374. * }
  375. *
  376. * const size = Number( match[ 1 ] );
  377. *
  378. * if ( viewElement.is( 'span' ) && size > 10 ) {
  379. * // Returned value can be an object with the matched properties.
  380. * // These properties will be "consumed" during the conversion.
  381. * // See `engine.view.Matcher~MatcherPattern` and `engine.view.Matcher#match` for more details.
  382. *
  383. * return { name: true, styles: [ 'font-size' ] };
  384. * }
  385. *
  386. * return null;
  387. * },
  388. * small: viewElement => {
  389. * const fontSize = viewElement.getStyle( 'font-size' );
  390. *
  391. * if ( !fontSize ) {
  392. * return null;
  393. * }
  394. *
  395. * const match = fontSize.match( /(\d+)\s*px/ );
  396. *
  397. * if ( !match ) {
  398. * return null;
  399. * }
  400. *
  401. * const size = Number( match[ 1 ] );
  402. *
  403. * if ( viewElement.is( 'span' ) && size < 10 ) {
  404. * // Returned value can be an object with the matched properties.
  405. * // These properties will be "consumed" during the conversion.
  406. * // See `engine.view.Matcher~MatcherPattern` and `engine.view.Matcher#match` for more details.
  407. *
  408. * return { name: true, styles: [ 'font-size' ] };
  409. * }
  410. *
  411. * return null;
  412. * }
  413. * }
  414. * } );
  415. *
  416. * The `definition.model` parameter specifies which model attribute should be converted from or to. It can be a `{ key, value }` object
  417. * describing the attribute key and value to convert or a `String` specifying just the attribute key (then `value` is set to `true`).
  418. * See {@link module:engine/conversion/conversion~ConverterDefinition} to learn about other parameters.
  419. *
  420. * @param {module:engine/conversion/conversion~ConverterDefinition} definition The converter definition.
  421. */
  422. attributeToElement( definition ) {
  423. // Set up downcast converter.
  424. this.for( 'downcast' ).attributeToElement( definition );
  425. // Set up upcast converter.
  426. for ( const { model, view } of _getAllUpcastDefinitions( definition ) ) {
  427. this.for( 'upcast' )
  428. .elementToAttribute( {
  429. view,
  430. model,
  431. converterPriority: definition.converterPriority
  432. } );
  433. }
  434. }
  435. /**
  436. * Sets up converters between the model and the view that convert a model attribute to a view attribute (and vice versa).
  437. * For example, `<image src='foo.jpg'></image>` is converted to `<img src='foo.jpg'></img>` (the same attribute key and value).
  438. * This type of converters is intended to be used with {@link module:engine/model/element~Element model element} nodes.
  439. * To convert text attributes {@link module:engine/conversion/conversion~Conversion#attributeToElement `attributeToElement converter`}
  440. * should be set up.
  441. *
  442. * // A simple conversion from the `source` model attribute to the `src` view attribute (and vice versa).
  443. * editor.conversion.attributeToAttribute( { model: 'source', view: 'src' } );
  444. *
  445. * // Attribute values are strictly specified.
  446. * editor.conversion.attributeToAttribute( {
  447. * model: {
  448. * name: 'image',
  449. * key: 'aside',
  450. * values: [ 'aside' ]
  451. * },
  452. * view: {
  453. * aside: {
  454. * name: 'img',
  455. * key: 'class',
  456. * value: [ 'aside', 'half-size' ]
  457. * }
  458. * }
  459. * } );
  460. *
  461. * // Set the style attribute.
  462. * editor.conversion.attributeToAttribute( {
  463. * model: {
  464. * name: 'image',
  465. * key: 'aside',
  466. * values: [ 'aside' ]
  467. * },
  468. * view: {
  469. * aside: {
  470. * name: 'img',
  471. * key: 'style',
  472. * value: {
  473. * float: 'right',
  474. * width: '50%',
  475. * margin: '5px'
  476. * }
  477. * }
  478. * }
  479. * } );
  480. *
  481. * // Conversion from and to a model attribute key whose value is an enum (`align=right|center`).
  482. * // Use `upcastAlso` to define other view elements that should also be converted to the `align=right` attribute.
  483. * editor.conversion.attributeToAttribute( {
  484. * model: {
  485. * key: 'align',
  486. * values: [ 'right', 'center' ]
  487. * },
  488. * view: {
  489. * right: {
  490. * key: 'class',
  491. * value: 'align-right'
  492. * },
  493. * center: {
  494. * key: 'class',
  495. * value: 'align-center'
  496. * }
  497. * },
  498. * upcastAlso: {
  499. * right: {
  500. * styles: {
  501. * 'text-align': 'right'
  502. * }
  503. * },
  504. * center: {
  505. * styles: {
  506. * 'text-align': 'center'
  507. * }
  508. * }
  509. * }
  510. * } );
  511. *
  512. * The `definition.model` parameter specifies which model attribute should be converted from and to.
  513. * It can be a `{ key, [ values ], [ name ] }` object or a `String`, which will be treated like `{ key: definition.model }`.
  514. * The `key` property is the model attribute key to convert from and to.
  515. * The `values` are the possible model attribute values. If `values` is not set, the model attribute value will be the same as the
  516. * view attribute value.
  517. * If `name` is set, the conversion will be set up only for model elements with the given name.
  518. *
  519. * The `definition.view` parameter specifies which view attribute should be converted from and to.
  520. * It can be a `{ key, value, [ name ] }` object or a `String`, which will be treated like `{ key: definition.view }`.
  521. * The `key` property is the view attribute key to convert from and to.
  522. * The `value` is the view attribute value to convert from and to. If `definition.value` is not set, the view attribute value will be
  523. * the same as the model attribute value.
  524. * If `key` is `'class'`, `value` can be a `String` or an array of `String`s.
  525. * If `key` is `'style'`, `value` is an object with key-value pairs.
  526. * In other cases, `value` is a `String`.
  527. * If `name` is set, the conversion will be set up only for model elements with the given name.
  528. * If `definition.model.values` is set, `definition.view` is an object that assigns values from `definition.model.values`
  529. * to `{ key, value, [ name ] }` objects.
  530. *
  531. * `definition.upcastAlso` specifies which other matching view elements should also be upcast to the given model configuration.
  532. * If `definition.model.values` is set, `definition.upcastAlso` should be an object assigning values from `definition.model.values`
  533. * to {@link module:engine/view/matcher~MatcherPattern}s or arrays of {@link module:engine/view/matcher~MatcherPattern}s.
  534. *
  535. * **Note:** `definition.model` and `definition.view` form should be mirrored, so the same types of parameters should
  536. * be given in both parameters.
  537. *
  538. * @param {Object} definition The converter definition.
  539. * @param {String|Object} definition.model The model attribute to convert from and to.
  540. * @param {String|Object} definition.view The view attribute to convert from and to.
  541. * @param {module:engine/view/matcher~MatcherPattern|Array.<module:engine/view/matcher~MatcherPattern>} [definition.upcastAlso]
  542. * Any view element matching `definition.upcastAlso` will also be converted to the given model attribute. `definition.upcastAlso`
  543. * is used only if `config.model.values` is specified.
  544. */
  545. attributeToAttribute( definition ) {
  546. // Set up downcast converter.
  547. this.for( 'downcast' ).attributeToAttribute( definition );
  548. // Set up upcast converter.
  549. for ( const { model, view } of _getAllUpcastDefinitions( definition ) ) {
  550. this.for( 'upcast' )
  551. .attributeToAttribute( {
  552. view,
  553. model
  554. } );
  555. }
  556. }
  557. /**
  558. * Creates and caches conversion helpers for given dispatchers group.
  559. *
  560. * @private
  561. * @param {Object} options
  562. * @param {String} options.name Group name.
  563. * @param {Array.<module:engine/conversion/downcastdispatcher~DowncastDispatcher|
  564. * module:engine/conversion/upcastdispatcher~UpcastDispatcher>} options.dispatchers
  565. * @param {Boolean} options.isDowncast
  566. */
  567. _createConversionHelpers( { name, dispatchers, isDowncast } ) {
  568. if ( this._helpers.has( name ) ) {
  569. /**
  570. * Trying to register a group name that has already been registered.
  571. *
  572. * @error conversion-group-exists
  573. */
  574. throw new CKEditorError( 'conversion-group-exists: Trying to register a group name that has already been registered.', this );
  575. }
  576. const helpers = isDowncast ? new DowncastHelpers( dispatchers ) : new UpcastHelpers( dispatchers );
  577. this._helpers.set( name, helpers );
  578. }
  579. }
  580. /**
  581. * Defines how the model should be converted from and to the view.
  582. *
  583. * @typedef {Object} module:engine/conversion/conversion~ConverterDefinition
  584. *
  585. * @property {*} [model] The model conversion definition. Describes the model element or model attribute to convert. This parameter differs
  586. * for different functions that accept `ConverterDefinition`. See the description of the function to learn how to set it.
  587. * @property {module:engine/view/elementdefinition~ElementDefinition|Object} view The definition of the view element to convert from and
  588. * to. If `model` describes multiple values, `view` is an object that assigns these values (`view` object keys) to view element definitions
  589. * (`view` object values).
  590. * @property {module:engine/view/matcher~MatcherPattern|Array.<module:engine/view/matcher~MatcherPattern>} [upcastAlso]
  591. * Any view element matching `upcastAlso` will also be converted to the model. If `model` describes multiple values, `upcastAlso`
  592. * is an object that assigns these values (`upcastAlso` object keys) to {@link module:engine/view/matcher~MatcherPattern}s
  593. * (`upcastAlso` object values).
  594. * @property {module:utils/priorities~PriorityString} [converterPriority] The converter priority.
  595. */
  596. // Helper function that creates a joint array out of an item passed in `definition.view` and items passed in
  597. // `definition.upcastAlso`.
  598. //
  599. // @param {module:engine/conversion/conversion~ConverterDefinition} definition
  600. // @returns {Array} Array containing view definitions.
  601. function* _getAllUpcastDefinitions( definition ) {
  602. if ( definition.model.values ) {
  603. for ( const value of definition.model.values ) {
  604. const model = { key: definition.model.key, value };
  605. const view = definition.view[ value ];
  606. const upcastAlso = definition.upcastAlso ? definition.upcastAlso[ value ] : undefined;
  607. yield* _getUpcastDefinition( model, view, upcastAlso );
  608. }
  609. } else {
  610. yield* _getUpcastDefinition( definition.model, definition.view, definition.upcastAlso );
  611. }
  612. }
  613. function* _getUpcastDefinition( model, view, upcastAlso ) {
  614. yield { model, view };
  615. if ( upcastAlso ) {
  616. upcastAlso = Array.isArray( upcastAlso ) ? upcastAlso : [ upcastAlso ];
  617. for ( const upcastAlsoItem of upcastAlso ) {
  618. yield { model, view: upcastAlsoItem };
  619. }
  620. }
  621. }