buildmodelconverter.js 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371
  1. /**
  2. * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
  3. * For licensing, see LICENSE.md.
  4. */
  5. /**
  6. * @module engine/conversion/buildmodelconverter
  7. */
  8. import {
  9. insertElement,
  10. insertUIElement,
  11. setAttribute,
  12. removeAttribute,
  13. removeUIElement,
  14. wrapItem,
  15. unwrapItem,
  16. wrapRange,
  17. unwrapRange
  18. } from './model-to-view-converters';
  19. import { convertSelectionAttribute, convertSelectionMarker } from './model-selection-to-view-converters';
  20. import ViewAttributeElement from '../view/attributeelement';
  21. import ViewContainerElement from '../view/containerelement';
  22. import ViewUIElement from '../view/uielement';
  23. import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
  24. /**
  25. * Provides chainable, high-level API to easily build basic model-to-view converters that are appended to given
  26. * dispatchers. In many cases, this is the API that should be used to specify how abstract model elements and
  27. * attributes should be represented in the view (and then later in DOM). Instances of this class are created by
  28. * {@link module:engine/conversion/buildmodelconverter~buildModelConverter}.
  29. *
  30. * If you need more complex converters, see {@link module:engine/conversion/modelconversiondispatcher~ModelConversionDispatcher},
  31. * {@link module:engine/conversion/model-to-view-converters}, {@link module:engine/conversion/modelconsumable~ModelConsumable},
  32. * {@link module:engine/conversion/mapper~Mapper}.
  33. *
  34. * Using this API it is possible to create four kinds of converters:
  35. *
  36. * 1. Model element to view element converter. This is a converter that takes the model element and represents it
  37. * in the view.
  38. *
  39. * buildModelConverter().for( dispatcher ).fromElement( 'paragraph' ).toElement( 'p' );
  40. * buildModelConverter().for( dispatcher ).fromElement( 'image' ).toElement( 'img' );
  41. *
  42. * 2. Model attribute to view attribute converter. This is a converter that operates on model element attributes
  43. * and converts them to view element attributes. It is suitable for elements like `image` (`src`, `title` attributes).
  44. *
  45. * buildModelConverter().for( dispatcher ).fromElement( 'image' ).toElement( 'img' );
  46. * buildModelConverter().for( dispatcher ).fromAttribute( 'src' ).toAttribute();
  47. *
  48. * 3. Model attribute to view element converter. This is a converter that takes model attributes and represents them
  49. * as view elements. Elements created by this kind of converter are wrapping other view elements. Wrapped view nodes
  50. * correspond to model nodes had converter attribute. It is suitable for attributes like `bold`, where `bold` attribute
  51. * set on model text nodes is converter to `strong` view element.
  52. *
  53. * buildModelConverter().for( dispatcher ).fromAttribute( 'bold' ).toElement( 'strong' );
  54. *
  55. * 4. Model marker to view element converter. This is a converter that converts markers from given group to view attribute element.
  56. * Markers, basically, are {@link module:engine/model/liverange~LiveRange} instances, that are named. In this conversion, model range is
  57. * converted to view range, then that view range is wrapped (or unwrapped, if range is removed) in a view attribute element.
  58. * To learn more about markers, see {@link module:engine/model/markercollection~MarkerCollection}.
  59. *
  60. * const viewSpanSearchResult = new ViewAttributeElement( 'span', { class: 'search-result' } );
  61. * buildModelConverter().for( dispatcher ).fromMarker( 'searchResult' ).toElement( viewSpanSearchResult );
  62. *
  63. * It is possible to provide various different parameters for
  64. * {@link module:engine/conversion/buildmodelconverter~ModelConverterBuilder#toElement}
  65. * and {@link module:engine/conversion/buildmodelconverter~ModelConverterBuilder#toAttribute} methods.
  66. * See their descriptions to learn more.
  67. *
  68. * It is also possible to {@link module:engine/conversion/buildmodelconverter~ModelConverterBuilder#withPriority change default priority}
  69. * of created converters to decide which converter should be fired earlier and which later. This is useful if you have
  70. * a general converter but also want to provide different special-case converters (i.e. given model element is converted
  71. * always to given view element, but if it has given attribute it is converter to other view element). For this,
  72. * use {@link module:engine/conversion/buildmodelconverter~ModelConverterBuilder#withPriority withPriority} right after `from...` method.
  73. *
  74. * Note that `to...` methods are "terminators", which means that should be the last one used in building converter.
  75. *
  76. * You can use {@link module:engine/conversion/buildviewconverter~ViewConverterBuilder}
  77. * to create "opposite" converters - from view to model.
  78. */
  79. class ModelConverterBuilder {
  80. /**
  81. * Creates `ModelConverterBuilder` with given `dispatchers` registered to it.
  82. */
  83. constructor() {
  84. /**
  85. * Dispatchers to which converters will be attached.
  86. *
  87. * @type {Array.<module:engine/conversion/modelconversiondispatcher~ModelConversionDispatcher>}
  88. * @private
  89. */
  90. this._dispatchers = [];
  91. /**
  92. * Contains data about registered "from" query.
  93. *
  94. * @type {Object}
  95. * @private
  96. */
  97. this._from = null;
  98. }
  99. /**
  100. * Set one or more dispatchers which the built converter will be attached to.
  101. *
  102. * @chainable
  103. * @param {...module:engine/conversion/modelconversiondispatcher~ModelConversionDispatcher} dispatchers One or more dispatchers.
  104. * @returns {module:engine/conversion/buildmodelconverter~ModelConverterBuilder}
  105. */
  106. for( ...dispatchers ) {
  107. this._dispatchers = dispatchers;
  108. return this;
  109. }
  110. /**
  111. * Registers what model element should be converted.
  112. *
  113. * @chainable
  114. * @param {String} elementName Name of element to convert.
  115. * @returns {module:engine/conversion/buildmodelconverter~ModelConverterBuilder}
  116. */
  117. fromElement( elementName ) {
  118. this._from = {
  119. type: 'element',
  120. name: elementName,
  121. priority: null
  122. };
  123. return this;
  124. }
  125. /**
  126. * Registers what model attribute should be converted.
  127. *
  128. * @chainable
  129. * @param {String} key Key of attribute to convert.
  130. * @returns {module:engine/conversion/buildmodelconverter~ModelConverterBuilder}
  131. */
  132. fromAttribute( key ) {
  133. this._from = {
  134. type: 'attribute',
  135. key: key,
  136. priority: null
  137. };
  138. return this;
  139. }
  140. /**
  141. * Registers what type of non-collapsed marker should be converted. For collapsed markers conversion, see
  142. * {@link ~fromCollapsedMarker}.
  143. *
  144. * @chainable
  145. * @param {String} markerName Name of marker to convert.
  146. * @returns {module:engine/conversion/modelconverterbuilder~ModelConverterBuilder}
  147. */
  148. fromMarker( markerName ) {
  149. this._from = {
  150. type: 'marker',
  151. name: markerName,
  152. priority: null,
  153. collapsed: false
  154. };
  155. return this;
  156. }
  157. /**
  158. * Registers what type of collapsed marker should be converted. For non-collapsed markers conversion, see {@link ~fromMarker}.
  159. *
  160. * @chainable
  161. * @param {String} markerName Name of marker to convert.
  162. * @returns {module:engine/conversion/modelconverterbuilder~ModelConverterBuilder}
  163. */
  164. fromCollapsedMarker( markerName ) {
  165. this._from = {
  166. type: 'marker',
  167. name: markerName,
  168. priority: null,
  169. collapsed: true
  170. };
  171. return this;
  172. }
  173. /**
  174. * Changes default priority for built converter. The lower the number, the earlier converter will be fired.
  175. * Default priority is `10`.
  176. *
  177. * **Note:** Keep in mind that event priority, that is set by this modifier, is used for attribute priority
  178. * when {@link module:engine/view/writer~writer} is used. This changes how view elements are ordered,
  179. * i.e.: `<strong><em>foo</em></strong>` vs `<em><strong>foo</strong></em>`. Using priority you can also
  180. * prevent node merging, i.e.: `<span class="bold"><span class="theme">foo</span><span>` vs `<span class="bold theme">foo</span>`.
  181. * If you want to prevent merging, just set different priority for both converters.
  182. *
  183. * buildModelConverter().for( dispatcher ).fromAttribute( 'bold' ).withPriority( 2 ).toElement( 'strong' );
  184. * buildModelConverter().for( dispatcher ).fromAttribute( 'italic' ).withPriority( 3 ).toElement( 'em' );
  185. *
  186. * @chainable
  187. * @param {Number} priority Converter priority.
  188. * @returns {module:engine/conversion/buildmodelconverter~ModelConverterBuilder}
  189. */
  190. withPriority( priority ) {
  191. this._from.priority = priority;
  192. return this;
  193. }
  194. /**
  195. * Registers what view element will be created by converter.
  196. *
  197. * Method accepts various ways of providing how the view element will be created. You can pass view element name as
  198. * `string`, view element instance which will be cloned and used, or creator function which returns view element that
  199. * will be used. Keep in mind that when you view element instance or creator function, it has to be/return a
  200. * proper type of view element: {@link module:engine/view/containerelement~ContainerElement ViewContainerElement} if you convert
  201. * from element or {@link module:engine/view/attributeelement~AttributeElement ViewAttributeElement} if you convert from attribute.
  202. *
  203. * buildModelConverter().for( dispatcher ).fromElement( 'paragraph' ).toElement( 'p' );
  204. *
  205. * buildModelConverter().for( dispatcher ).fromElement( 'image' ).toElement( new ViewContainerElement( 'img' ) );
  206. *
  207. * buildModelConverter().for( dispatcher )
  208. * .fromElement( 'header' )
  209. * .toElement( ( data ) => new ViewContainerElement( 'h' + data.item.getAttribute( 'level' ) ) );
  210. *
  211. * buildModelConverter().for( dispatcher ).fromAttribute( 'bold' ).toElement( new ViewAttributeElement( 'strong' ) );
  212. *
  213. * Creator function will be passed different values depending whether conversion is from element or from attribute:
  214. *
  215. * * from element: dispatcher's
  216. * {@link module:engine/conversion/modelconversiondispatcher~ModelConversionDispatcher#event:insert insert event} parameters
  217. * will be passed,
  218. * * from attribute: there is one parameter and it is attribute value.
  219. *
  220. * This method also registers model selection to view selection converter, if conversion is from attribute.
  221. *
  222. * This method creates the converter and adds it as a callback to a proper
  223. * {@link module:engine/conversion/modelconversiondispatcher~ModelConversionDispatcher conversion dispatcher} event.
  224. *
  225. * @param {String|module:engine/view/element~ViewElement|Function} element Element created by converter or
  226. * a function that returns view element.
  227. */
  228. toElement( element ) {
  229. const priority = this._from.priority === null ? 'normal' : this._from.priority;
  230. for ( let dispatcher of this._dispatchers ) {
  231. if ( this._from.type == 'element' ) {
  232. // From model element to view element -> insert element.
  233. element = typeof element == 'string' ? new ViewContainerElement( element ) : element;
  234. dispatcher.on( 'insert:' + this._from.name, insertElement( element ), { priority } );
  235. } else if ( this._from.type == 'attribute' ) {
  236. // From model attribute to view element -> wrap and unwrap.
  237. element = typeof element == 'string' ? new ViewAttributeElement( element ) : element;
  238. dispatcher.on( 'addAttribute:' + this._from.key, wrapItem( element ), { priority } );
  239. dispatcher.on( 'changeAttribute:' + this._from.key, wrapItem( element ), { priority } );
  240. dispatcher.on( 'removeAttribute:' + this._from.key, unwrapItem( element ), { priority } );
  241. dispatcher.on( 'selectionAttribute:' + this._from.key, convertSelectionAttribute( element ), { priority } );
  242. } else {
  243. if ( this._from.collapsed ) {
  244. // From collapsed marker to view element -> insertUIElement, removeUIElement.
  245. element = typeof element == 'string' ? new ViewUIElement( element ) : element;
  246. dispatcher.on( 'addMarker:' + this._from.name, insertUIElement( element ), { priority } );
  247. dispatcher.on( 'removeMarker:' + this._from.name, removeUIElement( element ), { priority } );
  248. } else {
  249. // From non-collapsed marker to view element -> wrapRange and unwrapRange.
  250. element = typeof element == 'string' ? new ViewAttributeElement( element ) : element;
  251. dispatcher.on( 'addMarker:' + this._from.name, wrapRange( element ), { priority } );
  252. dispatcher.on( 'removeMarker:' + this._from.name, unwrapRange( element ), { priority } );
  253. dispatcher.on( 'selectionMarker:' + this._from.name, convertSelectionMarker( element ), { priority } );
  254. }
  255. }
  256. }
  257. }
  258. /**
  259. * Registers what view attribute will be created by converter. Keep in mind, that only model attribute to
  260. * view attribute conversion is supported.
  261. *
  262. * Method accepts various ways of providing how the view attribute will be created:
  263. *
  264. * * for no passed parameter, attribute key and value will be converted 1-to-1 to view attribute,
  265. * * if you pass one `string`, it will be used as new attribute key while attribute value will be copied,
  266. * * if you pass two `string`s, first one will be used as new attribute key and second one as new attribute value,
  267. * * if you pass a function, it is expected to return an object with `key` and `value` properties representing attribute key and value.
  268. * This function will be passed model attribute value and model attribute key as first two parameters and then
  269. * all dispatcher's
  270. * {module:engine/conversion/modelconversiondispatcher~ModelConversionDispatcher#event:changeAttribute changeAttribute event}
  271. * parameters.
  272. *
  273. * buildModelConverter().for( dispatcher ).fromAttribute( 'class' ).toAttribute( '' );
  274. *
  275. * buildModelConverter().for( dispatcher ).fromAttribute( 'linkTitle' ).toAttribute( 'title' );
  276. *
  277. * buildModelConverter().for( dispatcher ).fromAttribute( 'highlighted' ).toAttribute( 'style', 'background:yellow' );
  278. *
  279. * buildModelConverter().for( dispatcher )
  280. * .fromAttribute( 'theme' )
  281. * .toAttribute( ( value ) => ( { key: 'class', value: value + '-theme' } ) );
  282. *
  283. * This method creates the converter and adds it as a callback to a proper
  284. * {@link module:engine/conversion/modelconversiondispatcher~ModelConversionDispatcher conversion dispatcher} event.
  285. *
  286. * @param {String|Function} [keyOrCreator] Attribute key or a creator function.
  287. * @param {*} [value] Attribute value.
  288. */
  289. toAttribute( keyOrCreator, value ) {
  290. if ( this._from.type != 'attribute' ) {
  291. /**
  292. * To-attribute conversion is supported only for model attributes.
  293. *
  294. * @error build-model-converter-element-to-attribute
  295. * @param {module:engine/model/range~Range} range
  296. */
  297. throw new CKEditorError( 'build-model-converter-non-attribute-to-attribute: ' +
  298. 'To-attribute conversion is supported only from model attributes.' );
  299. }
  300. let attributeCreator;
  301. if ( !keyOrCreator ) {
  302. // If `keyOrCreator` is not set, we assume default behavior which is 1:1 attribute re-write.
  303. // This is also a default behavior for `setAttribute` converter when no attribute creator is passed.
  304. attributeCreator = undefined;
  305. } else if ( typeof keyOrCreator == 'string' ) {
  306. // `keyOrCreator` is an attribute key.
  307. if ( value ) {
  308. // If value is set, create "dumb" creator that always returns the same object.
  309. attributeCreator = function() {
  310. return { key: keyOrCreator, value: value };
  311. };
  312. } else {
  313. // If value is not set, take it from the passed parameter.
  314. attributeCreator = function( value ) {
  315. return { key: keyOrCreator, value: value };
  316. };
  317. }
  318. } else {
  319. // `keyOrCreator` is an attribute creator function.
  320. attributeCreator = keyOrCreator;
  321. }
  322. for ( let dispatcher of this._dispatchers ) {
  323. const options = { priority: this._from.priority || 'normal' };
  324. dispatcher.on( 'addAttribute:' + this._from.key, setAttribute( attributeCreator ), options );
  325. dispatcher.on( 'changeAttribute:' + this._from.key, setAttribute( attributeCreator ), options );
  326. dispatcher.on( 'removeAttribute:' + this._from.key, removeAttribute( attributeCreator ), options );
  327. }
  328. }
  329. }
  330. /**
  331. * Entry point for model-to-view converters builder. This chainable API makes it easy to create basic, most common
  332. * model-to-view converters and attach them to provided dispatchers. The method returns an instance of
  333. * {@link module:engine/conversion/buildmodelconverter~ModelConverterBuilder}.
  334. */
  335. export default function buildModelConverter() {
  336. return new ModelConverterBuilder();
  337. }