8
0

conversion.js 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591
  1. /**
  2. * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
  3. * For licensing, see LICENSE.md.
  4. */
  5. /**
  6. * @module engine/conversion/conversion
  7. */
  8. import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
  9. import {
  10. downcastElementToElement,
  11. downcastAttributeToElement,
  12. downcastAttributeToAttribute
  13. } from './downcast-converters';
  14. import {
  15. upcastElementToElement,
  16. upcastElementToAttribute,
  17. upcastAttributeToAttribute
  18. } from './upcast-converters';
  19. /**
  20. * An utility class that helps organizing dispatchers and adding converters to them.
  21. */
  22. export default class Conversion {
  23. /**
  24. * Creates new Conversion instance.
  25. */
  26. constructor() {
  27. /**
  28. * @private
  29. * @member {Map}
  30. */
  31. this._dispatchersGroups = new Map();
  32. }
  33. /**
  34. * Registers one or more converters under given group name. Then, group name can be used to assign a converter
  35. * to multiple dispatchers at once.
  36. *
  37. * If given group name is used for a second time,
  38. * {@link module:utils/ckeditorerror~CKEditorError conversion-register-group-exists} error is thrown.
  39. *
  40. * @param {String} groupName A name for dispatchers group.
  41. * @param {Array.<module:engine/conversion/downcastdispatcher~DowncastDispatcher|
  42. * module:engine/conversion/upcastdispatcher~UpcastDispatcher>} dispatchers Dispatchers to register
  43. * under given name.
  44. */
  45. register( groupName, dispatchers ) {
  46. if ( this._dispatchersGroups.has( groupName ) ) {
  47. /**
  48. * Trying to register a group name that was already registered.
  49. *
  50. * @error conversion-register-group-exists
  51. */
  52. throw new CKEditorError( 'conversion-register-group-exists: Trying to register a group name that was already registered.' );
  53. }
  54. this._dispatchersGroups.set( groupName, dispatchers );
  55. }
  56. /**
  57. * Provides chainable API to assign converters to dispatchers registered under given group name. Converters are added
  58. * by calling `.add()` method of an object returned by this function.
  59. *
  60. * conversion.for( 'downcast' )
  61. * .add( conversionHelperA )
  62. * .add( conversionHelperB );
  63. *
  64. * In above example, `conversionHelperA` and `conversionHelperB` will be called for all dispatchers from `'model'` group.
  65. *
  66. * `.add()` takes exactly one parameter, which is a function. That function should accept one parameter, which
  67. * is a dispatcher instance. The function should add an actual converter to passed dispatcher instance.
  68. *
  69. * Conversion helpers for most common cases are already provided. They are flexible enough to cover most use cases.
  70. * See documentation to learn how they can be configured.
  71. *
  72. * For downcast (model to view conversion), these are:
  73. *
  74. * * {@link module:engine/conversion/downcast-converters~downcastElementToElement downcast element to element converter},
  75. * * {@link module:engine/conversion/downcast-converters~downcastAttributeToElement downcast attribute to element converter},
  76. * * {@link module:engine/conversion/downcast-converters~downcastAttributeToAttribute downcast attribute to attribute converter}.
  77. *
  78. * For upcast (view to model conversion), these are:
  79. *
  80. * * {@link module:engine/conversion/upcast-converters~upcastElementToElement upcast element to element converter},
  81. * * {@link module:engine/conversion/upcast-converters~upcastElementToAttribute upcast attribute to element converter},
  82. * * {@link module:engine/conversion/upcast-converters~upcastAttributeToAttribute upcast attribute to attribute converter}.
  83. *
  84. * An example of using conversion helpers to convert `paragraph` model element to `p` view element (and back):
  85. *
  86. * // Define conversion configuration - model element 'paragraph' should be converted to view element 'p'.
  87. * const config = { model: 'paragraph', view: 'p' };
  88. *
  89. * // Add converters to proper dispatchers using conversion helpers.
  90. * conversion.for( 'downcast' ).add( downcastElementToElement( config ) );
  91. * conversion.for( 'upcast' ).add( upcastElementToElement( config ) );
  92. *
  93. * An example of providing custom conversion helper that uses custom converter function:
  94. *
  95. * // Adding custom `myConverter` converter for 'paragraph' element insertion, with default priority ('normal').
  96. * conversion.for( 'downcast' ).add( conversion.customConverter( 'insert:paragraph', myConverter ) );
  97. *
  98. * @param {String} groupName Name of dispatchers group to add converters to.
  99. * @returns {Object} Object with `.add()` method, providing a way to add converters.
  100. */
  101. for( groupName ) {
  102. const dispatchers = this._getDispatchers( groupName );
  103. return {
  104. add( conversionHelper ) {
  105. _addToDispatchers( dispatchers, conversionHelper );
  106. return this;
  107. }
  108. };
  109. }
  110. /**
  111. * Sets up converters between the model and the view which convert a model element to a view element (and vice versa).
  112. * For example, model `<paragraph>Foo</paragraph>` is `<p>Foo</p>` in the view.
  113. *
  114. * // Simple conversion from `paragraph` model element to `<p>` view element (and vice versa).
  115. * conversion.elementToElement( { model: 'paragraph', view: 'p' } );
  116. *
  117. * // Override other converters by specifying converter definition with higher priority.
  118. * conversion.elementToElement( { model: 'paragraph', view: 'div', priority: 'high' } );
  119. *
  120. * // View specified as an object instead of a string.
  121. * conversion.elementToElement( {
  122. * model: 'fancyParagraph',
  123. * view: {
  124. * name: 'p',
  125. * classes: 'fancy'
  126. * }
  127. * } );
  128. *
  129. * // Use `upcastAlso` to define other view elements that should be also converted to `paragraph` element.
  130. * conversion.elementToElement( {
  131. * model: 'paragraph',
  132. * view: 'p',
  133. * upcastAlso: [
  134. * 'div',
  135. * {
  136. * // Any element with `display: block` style.
  137. * styles: {
  138. * display: 'block'
  139. * }
  140. * }
  141. * ]
  142. * } );
  143. *
  144. * // `upcastAlso` set as callback enables a conversion of a wide range of different view elements.
  145. * conversion.elementToElement( {
  146. * model: 'heading',
  147. * view: 'h2',
  148. * // Convert "headling-like" paragraphs to headings.
  149. * upcastAlso: viewElement => {
  150. * const fontSize = viewElement.getStyle( 'font-size' );
  151. *
  152. * if ( !fontSize ) {
  153. * return null;
  154. * }
  155. *
  156. * const match = fontSize.match( /(\d+)\s*px/ );
  157. *
  158. * if ( !match ) {
  159. * return null;
  160. * }
  161. *
  162. * const size = Number( match[ 1 ] );
  163. *
  164. * if ( size > 26 ) {
  165. * // Returned value be an object with the matched properties.
  166. * // Those properties will be "consumed" during conversion.
  167. * // See `engine.view.Matcher~MatcherPattern` and `engine.view.Matcher#match` for more.
  168. *
  169. * return { name: true, styles: [ 'font-size' ] };
  170. * }
  171. *
  172. * return null;
  173. * }
  174. * } );
  175. *
  176. * `definition.model` is a `String` with a model element name to converter from/to.
  177. * See {@link module:engine/conversion/conversion~ConverterDefinition} to learn about other parameters.
  178. *
  179. * @param {module:engine/conversion/conversion~ConverterDefinition} definition Converter definition.
  180. */
  181. elementToElement( definition ) {
  182. // Set up downcast converter.
  183. this.for( 'downcast' ).add( downcastElementToElement( definition ) );
  184. // Set up upcast converter.
  185. for ( const { model, view } of _getAllUpcastDefinitions( definition ) ) {
  186. this.for( 'upcast' ).add(
  187. upcastElementToElement( {
  188. model,
  189. view,
  190. priority: definition.priority
  191. } )
  192. );
  193. }
  194. }
  195. /**
  196. * Sets up converters between the model and the view which convert a model attribute to a view element (and vice versa).
  197. * For example, model text node with data `"Foo"` and `bold` attribute is `<strong>Foo</strong>` in the view.
  198. *
  199. * // Simple conversion from `bold=true` attribute to `<strong>` view element (and vice versa).
  200. * conversion.attributeToElement( { model: 'bold', view: 'strong' } );
  201. *
  202. * // Override other converters by specifying converter definition with higher priority.
  203. * conversion.attributeToElement( { model: 'bold', view: 'b', priority: 'high' } );
  204. *
  205. * // View specified as an object instead of a string.
  206. * conversion.attributeToElement( {
  207. * model: 'bold',
  208. * view: {
  209. * name: 'span',
  210. * classes: 'bold'
  211. * }
  212. * } );
  213. *
  214. * // Use `config.model.name` to define conversion only from given node type, `$text` in this case.
  215. * // The same attribute on different elements may be then handled by a different converter.
  216. * conversion.attributeToElement( {
  217. * model: {
  218. * key: 'textDecoration',
  219. * values: [ 'underline', 'lineThrough' ],
  220. * name: '$text'
  221. * },
  222. * view: {
  223. * underline: {
  224. * name: 'span',
  225. * styles: {
  226. * 'text-decoration': 'underline'
  227. * }
  228. * },
  229. * lineThrough: {
  230. * name: 'span',
  231. * styles: {
  232. * 'text-decoration': 'line-through'
  233. * }
  234. * }
  235. * }
  236. * } );
  237. *
  238. * // Use `upcastAlso` to define other view elements that should be also converted to `bold` attribute.
  239. * conversion.attributeToElement( {
  240. * model: 'bold',
  241. * view: 'strong',
  242. * upcastAlso: [
  243. * 'b',
  244. * {
  245. * name: 'span',
  246. * classes: 'bold'
  247. * },
  248. * {
  249. * name: 'span',
  250. * styles: {
  251. * 'font-weight': 'bold'
  252. * }
  253. * },
  254. * viewElement => {
  255. * const fontWeight = viewElement.getStyle( 'font-weight' );
  256. *
  257. * if ( viewElement.is( 'span' ) && fontWeight && /\d+/.test() && Number( fontWeight ) > 500 ) {
  258. * // Returned value be an object with the matched properties.
  259. * // Those properties will be "consumed" during conversion.
  260. * // See `engine.view.Matcher~MatcherPattern` and `engine.view.Matcher#match` for more.
  261. *
  262. * return {
  263. * name: true,
  264. * styles: [ 'font-weight' ]
  265. * };
  266. * }
  267. * }
  268. * ]
  269. * } );
  270. *
  271. * // Conversion from/to a model attribute key which value is an enum (`fontSize=big|small`).
  272. * // `upcastAlso` set as callback enables a conversion of a wide range of different view elements.
  273. * conversion.attributeToElement( {
  274. * model: {
  275. * key: 'fontSize',
  276. * values: [ 'big', 'small' ]
  277. * },
  278. * view: {
  279. * big: {
  280. * name: 'span',
  281. * styles: {
  282. * 'font-size': '1.2em'
  283. * }
  284. * },
  285. * small: {
  286. * name: 'span',
  287. * styles: {
  288. * 'font-size': '0.8em'
  289. * }
  290. * }
  291. * },
  292. * upcastAlso: {
  293. * big: viewElement => {
  294. * const fontSize = viewElement.getStyle( 'font-size' );
  295. *
  296. * if ( !fontSize ) {
  297. * return null;
  298. * }
  299. *
  300. * const match = fontSize.match( /(\d+)\s*px/ );
  301. *
  302. * if ( !match ) {
  303. * return null;
  304. * }
  305. *
  306. * const size = Number( match[ 1 ] );
  307. *
  308. * if ( viewElement.is( 'span' ) && size > 10 ) {
  309. * // Returned value be an object with the matched properties.
  310. * // Those properties will be "consumed" during conversion.
  311. * // See `engine.view.Matcher~MatcherPattern` and `engine.view.Matcher#match` for more.
  312. *
  313. * return { name: true, styles: [ 'font-size' ] };
  314. * }
  315. *
  316. * return null;
  317. * },
  318. * small: viewElement => {
  319. * const fontSize = viewElement.getStyle( 'font-size' );
  320. *
  321. * if ( !fontSize ) {
  322. * return null;
  323. * }
  324. *
  325. * const match = fontSize.match( /(\d+)\s*px/ );
  326. *
  327. * if ( !match ) {
  328. * return null;
  329. * }
  330. *
  331. * const size = Number( match[ 1 ] );
  332. *
  333. * if ( viewElement.is( 'span' ) && size < 10 ) {
  334. * // Returned value be an object with the matched properties.
  335. * // Those properties will be "consumed" during conversion.
  336. * // See `engine.view.Matcher~MatcherPattern` and `engine.view.Matcher#match` for more.
  337. *
  338. * return { name: true, styles: [ 'font-size' ] };
  339. * }
  340. *
  341. * return null;
  342. * }
  343. * }
  344. * } );
  345. *
  346. * `definition.model` parameter specifies what model attribute should be converted from/to. It can be a `{ key, value }` object
  347. * describing attribute key and value to convert or a `String` specifying just attribute key (then `value` is set to `true`).
  348. * See {@link module:engine/conversion/conversion~ConverterDefinition} to learn about other parameters.
  349. *
  350. * @param {module:engine/conversion/conversion~ConverterDefinition} definition Converter definition.
  351. */
  352. attributeToElement( definition ) {
  353. // Set up downcast converter.
  354. this.for( 'downcast' ).add( downcastAttributeToElement( definition ) );
  355. // Set up upcast converter.
  356. for ( const { model, view } of _getAllUpcastDefinitions( definition ) ) {
  357. this.for( 'upcast' ).add(
  358. upcastElementToAttribute( {
  359. view,
  360. model,
  361. priority: definition.priority
  362. } )
  363. );
  364. }
  365. }
  366. /**
  367. * Sets up converters between the model and the view which convert a model attribute to a view attribute (and vice versa).
  368. * For example, `<image src='foo.jpg'></image>` is converted to `<img src='foo.jpg'></img>` (same attribute key and value).
  369. *
  370. * // Simple conversion from `source` model attribute to `src` view attribute (and vice versa).
  371. * conversion.attributeToAttribute( { model: 'source', view: 'src' } );
  372. *
  373. * // Attributes values are strictly specified.
  374. * conversion.attributeToAttribute( {
  375. * model: {
  376. * name: 'image',
  377. * key: 'aside',
  378. * values: [ 'aside' ]
  379. * },
  380. * view: {
  381. * aside: {
  382. * name: 'img',
  383. * key: 'class',
  384. * value: [ 'aside', 'half-size' ]
  385. * }
  386. * }
  387. * } );
  388. *
  389. * // Set style attribute.
  390. * conversion.attributeToAttribute( {
  391. * model: {
  392. * name: 'image',
  393. * key: 'aside',
  394. * values: [ 'aside' ]
  395. * },
  396. * view: {
  397. * aside: {
  398. * name: 'img',
  399. * key: 'style',
  400. * value: {
  401. * float: 'right',
  402. * width: '50%',
  403. * margin: '5px'
  404. * }
  405. * }
  406. * }
  407. * } );
  408. *
  409. * // Conversion from/to a model attribute key which value is an enum (`align=right|center`).
  410. * // Use `upcastAlso` to define other view elements that should be also converted to `align=right` attribute.
  411. * conversion.attributeToAttribute( {
  412. * model: {
  413. * key: 'align',
  414. * values: [ 'right', 'center' ]
  415. * },
  416. * view: {
  417. * right: {
  418. * key: 'class',
  419. * value: 'align-right'
  420. * },
  421. * center: {
  422. * key: 'class',
  423. * value: 'align-center'
  424. * }
  425. * },
  426. * upcastAlso: {
  427. * right: {
  428. * styles: {
  429. * 'text-align': 'right'
  430. * }
  431. * },
  432. * center: {
  433. * styles: {
  434. * 'text-align': 'center'
  435. * }
  436. * }
  437. * }
  438. * } );
  439. *
  440. * `definition.model` parameter specifies what model attribute should be converted from/to.
  441. * It can be a `{ key, [ values ], [ name ] }` object or a `String`, which will be treated like `{ key: definition.model }`.
  442. * `key` property is the model attribute key to convert from/to.
  443. * `values` are the possible model attribute values. If `values` is not set, model attribute value will be the same as the
  444. * view attribute value.
  445. * If `name` is set, conversion will be set up only for model elements with the given name.
  446. *
  447. * `definition.view` parameter specifies what view attribute should be converted from/to.
  448. * It can be a `{ key, value, [ name ] }` object or a `String`, which will be treated like `{ key: definition.view }`.
  449. * `key` property is the view attribute key to convert from/to.
  450. * `value` is the view attribute value to convert from/to. If `definition.value` is not set, view attribute value will be
  451. * the same as the model attribute value.
  452. * If `key` is `'class'`, `value` can be a `String` or an array of `String`s.
  453. * If `key` is `'style'`, `value` is an object with key-value pairs.
  454. * In other cases, `value` is a `String`.
  455. * If `name` is set, conversion will be set up only for model elements with the given name.
  456. * If `definition.model.values` is set, `definition.view` is an object which assigns values from `definition.model.values`
  457. * to `{ key, value, [ name ] }` objects.
  458. *
  459. * `definition.upcastAlso` specifies which other matching view elements should be also upcast to given model configuration.
  460. * If `definition.model.values` is set, `definition.upcastAlso` should be an object assigning values from `definition.model.values`
  461. * to {@link module:engine/view/matcher~MatcherPattern}s or arrays of {@link module:engine/view/matcher~MatcherPattern}s.
  462. *
  463. * **Note:** `definition.model` and `definition.view` form should be mirrored, that is the same type of parameters should
  464. * be given in both parameters.
  465. *
  466. * @param {Object} definition Converter definition.
  467. * @param {String|Object} definition.model Model attribute to convert from/to.
  468. * @param {String|Object} definition.view View attribute to convert from/to.
  469. * @param {module:engine/view/matcher~MatcherPattern|Array.<module:engine/view/matcher~MatcherPattern>} [definition.upcastAlso]
  470. * Any view element matching `definition.upcastAlso` will also be converted to the given model attribute. `definition.upcastAlso`
  471. * is used only if `config.model.values` is specified.
  472. */
  473. attributeToAttribute( definition ) {
  474. // Set up downcast converter.
  475. this.for( 'downcast' ).add( downcastAttributeToAttribute( definition ) );
  476. // Set up upcast converter.
  477. for ( const { model, view } of _getAllUpcastDefinitions( definition ) ) {
  478. this.for( 'upcast' ).add(
  479. upcastAttributeToAttribute( {
  480. view,
  481. model
  482. } )
  483. );
  484. }
  485. }
  486. /**
  487. * Returns dispatchers registered under given group name.
  488. *
  489. * If given group name has not been registered,
  490. * {@link module:utils/ckeditorerror~CKEditorError conversion-for-unknown-group} error is thrown.
  491. *
  492. * @private
  493. * @param {String} groupName
  494. * @returns {Array.<module:engine/conversion/downcastdispatcher~DowncastDispatcher|
  495. * module:engine/conversion/upcastdispatcher~UpcastDispatcher>}
  496. */
  497. _getDispatchers( groupName ) {
  498. const dispatchers = this._dispatchersGroups.get( groupName );
  499. if ( !dispatchers ) {
  500. /**
  501. * Trying to add a converter to an unknown dispatchers group.
  502. *
  503. * @error conversion-for-unknown-group
  504. */
  505. throw new CKEditorError( 'conversion-for-unknown-group: Trying to add a converter to an unknown dispatchers group.' );
  506. }
  507. return dispatchers;
  508. }
  509. }
  510. /**
  511. * Defines how the model should be converted from/to the view.
  512. *
  513. * @typedef {Object} module:engine/conversion/conversion~ConverterDefinition
  514. *
  515. * @property {*} [model] Model conversion definition. Describes model element or model attribute to convert. This parameter differs
  516. * for different functions that accepts `ConverterDefinition`. See the description of a function to learn how to set it.
  517. * @property {module:engine/view/elementdefinition~ElementDefinition|Object} view Definition of a view element to convert from/to.
  518. * If `model` describes multiple values, `view` is an object that assigns those values (`view` object keys) to view element definitions
  519. * (`view` object values).
  520. * @property {module:engine/view/matcher~MatcherPattern|Array.<module:engine/view/matcher~MatcherPattern>} [upcastAlso]
  521. * Any view element matching `upcastAlso` will also be converted to model. If `model` describes multiple values, `upcastAlso`
  522. * is an object that assigns those values (`upcastAlso` object keys) to {@link module:engine/view/matcher~MatcherPattern}s
  523. * (`upcastAlso` object values).
  524. * @property {module:utils/priorities~PriorityString} [priority] Conversion priority.
  525. */
  526. // Helper function for `Conversion` `.add()` method.
  527. //
  528. // Calls `conversionHelper` on each dispatcher from the group specified earlier in `.for()` call, effectively
  529. // adding converters to all specified dispatchers.
  530. //
  531. // @private
  532. // @param {Array.<module:engine/conversion/downcastdispatcher~DowncastDispatcher|
  533. // module:engine/conversion/upcastdispatcher~UpcastDispatcher>} dispatchers
  534. // @param {Function} conversionHelper
  535. function _addToDispatchers( dispatchers, conversionHelper ) {
  536. for ( const dispatcher of dispatchers ) {
  537. conversionHelper( dispatcher );
  538. }
  539. }
  540. // Helper function that creates a joint array out of an item passed in `definition.view` and items passed in
  541. // `definition.upcastAlso`.
  542. //
  543. // @param {module:engine/conversion/conversion~ConverterDefinition} definition
  544. // @returns {Array} Array containing view definitions.
  545. function* _getAllUpcastDefinitions( definition ) {
  546. if ( definition.model.values ) {
  547. for ( const value of definition.model.values ) {
  548. const model = { key: definition.model.key, value };
  549. const view = definition.view[ value ];
  550. const upcastAlso = definition.upcastAlso ? definition.upcastAlso[ value ] : undefined;
  551. yield* _getUpcastDefinition( model, view, upcastAlso );
  552. }
  553. } else {
  554. yield* _getUpcastDefinition( definition.model, definition.view, definition.upcastAlso );
  555. }
  556. }
  557. function* _getUpcastDefinition( model, view, upcastAlso ) {
  558. yield { model, view };
  559. if ( upcastAlso ) {
  560. upcastAlso = Array.isArray( upcastAlso ) ? upcastAlso : [ upcastAlso ];
  561. for ( const upcastAlsoItem of upcastAlso ) {
  562. yield { model, view: upcastAlsoItem };
  563. }
  564. }
  565. }