buildmodelconverter.js 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450
  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. highlightText,
  17. highlightElement
  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 five 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 highlight converter. This is a converter that converts model markers to view highlight
  56. * described by {@link module:engine/conversion/buildmodelconverter~HighlightDescriptor} object passed to
  57. * {@link module:engine/conversion/buildmodelconverter~ModelConverterBuilder#toHighlight} method.
  58. *
  59. * buildModelConverter().for( dispatcher ).fromMarker( 'search' ).toHighlight( {
  60. * class: 'search',
  61. * priority: 20
  62. * } );
  63. *
  64. * 5. Model marker to element converter. This is a converter that takes model marker and creates separate elements at
  65. * the beginning and at the end of the marker's range. For more information see
  66. * {@link module:engine/conversion/buildmodelconverter~ModelConverterBuilder#toElement} method.
  67. *
  68. * buildModelConverter().for( dispatcher ).fromMarker( 'search' ).toElement( 'span' );
  69. *
  70. * It is possible to provide various different parameters for
  71. * {@link module:engine/conversion/buildmodelconverter~ModelConverterBuilder#toElement},
  72. * {@link module:engine/conversion/buildmodelconverter~ModelConverterBuilder#toAttribute} and
  73. * {@link module:engine/conversion/buildmodelconverter~ModelConverterBuilder#toHighlight} methods.
  74. * See their descriptions to learn more.
  75. *
  76. * It is also possible to {@link module:engine/conversion/buildmodelconverter~ModelConverterBuilder#withPriority change default priority}
  77. * of created converters to decide which converter should be fired earlier and which later. This is useful if you have
  78. * a general converter but also want to provide different special-case converters (i.e. given model element is converted
  79. * always to given view element, but if it has given attribute it is converter to other view element). For this,
  80. * use {@link module:engine/conversion/buildmodelconverter~ModelConverterBuilder#withPriority withPriority} right after `from...` method.
  81. *
  82. * Note that `to...` methods are "terminators", which means that should be the last one used in building converter.
  83. *
  84. * You can use {@link module:engine/conversion/buildviewconverter~ViewConverterBuilder}
  85. * to create "opposite" converters - from view to model.
  86. */
  87. class ModelConverterBuilder {
  88. /**
  89. * Creates `ModelConverterBuilder` with given `dispatchers` registered to it.
  90. */
  91. constructor() {
  92. /**
  93. * Dispatchers to which converters will be attached.
  94. *
  95. * @type {Array.<module:engine/conversion/modelconversiondispatcher~ModelConversionDispatcher>}
  96. * @private
  97. */
  98. this._dispatchers = [];
  99. /**
  100. * Contains data about registered "from" query.
  101. *
  102. * @type {Object}
  103. * @private
  104. */
  105. this._from = null;
  106. }
  107. /**
  108. * Set one or more dispatchers which the built converter will be attached to.
  109. *
  110. * @chainable
  111. * @param {...module:engine/conversion/modelconversiondispatcher~ModelConversionDispatcher} dispatchers One or more dispatchers.
  112. * @returns {module:engine/conversion/buildmodelconverter~ModelConverterBuilder}
  113. */
  114. for( ...dispatchers ) {
  115. this._dispatchers = dispatchers;
  116. return this;
  117. }
  118. /**
  119. * Registers what model element should be converted.
  120. *
  121. * @chainable
  122. * @param {String} elementName Name of element to convert.
  123. * @returns {module:engine/conversion/buildmodelconverter~ModelConverterBuilder}
  124. */
  125. fromElement( elementName ) {
  126. this._from = {
  127. type: 'element',
  128. name: elementName,
  129. priority: null
  130. };
  131. return this;
  132. }
  133. /**
  134. * Registers what model attribute should be converted.
  135. *
  136. * @chainable
  137. * @param {String} key Key of attribute to convert.
  138. * @returns {module:engine/conversion/buildmodelconverter~ModelConverterBuilder}
  139. */
  140. fromAttribute( key ) {
  141. this._from = {
  142. type: 'attribute',
  143. key,
  144. priority: null
  145. };
  146. return this;
  147. }
  148. /**
  149. * Registers what type of marker should be converted.
  150. *
  151. * @chainable
  152. * @param {String} markerName Name of marker to convert.
  153. * @returns {module:engine/conversion/buildmodelconverter~ModelConverterBuilder}
  154. */
  155. fromMarker( markerName ) {
  156. this._from = {
  157. type: 'marker',
  158. name: markerName,
  159. priority: null
  160. };
  161. return this;
  162. }
  163. /**
  164. * Changes default priority for built converter. The lower the number, the earlier converter will be fired.
  165. * Default priority is `10`.
  166. *
  167. * **Note:** Keep in mind that event priority, that is set by this modifier, is used for attribute priority
  168. * when {@link module:engine/view/writer~writer} is used. This changes how view elements are ordered,
  169. * i.e.: `<strong><em>foo</em></strong>` vs `<em><strong>foo</strong></em>`. Using priority you can also
  170. * prevent node merging, i.e.: `<span class="bold"><span class="theme">foo</span><span>` vs `<span class="bold theme">foo</span>`.
  171. * If you want to prevent merging, just set different priority for both converters.
  172. *
  173. * buildModelConverter().for( dispatcher ).fromAttribute( 'bold' ).withPriority( 2 ).toElement( 'strong' );
  174. * buildModelConverter().for( dispatcher ).fromAttribute( 'italic' ).withPriority( 3 ).toElement( 'em' );
  175. *
  176. * @chainable
  177. * @param {Number} priority Converter priority.
  178. * @returns {module:engine/conversion/buildmodelconverter~ModelConverterBuilder}
  179. */
  180. withPriority( priority ) {
  181. this._from.priority = priority;
  182. return this;
  183. }
  184. /**
  185. * Registers what view element will be created by converter.
  186. *
  187. * Method accepts various ways of providing how the view element will be created. You can pass view element name as
  188. * `string`, view element instance which will be cloned and used, or creator function which returns view element that
  189. * will be used. Keep in mind that when you view element instance or creator function, it has to be/return a
  190. * proper type of view element: {@link module:engine/view/containerelement~ContainerElement ViewContainerElement} if you convert
  191. * from element, {@link module:engine/view/attributeelement~AttributeElement ViewAttributeElement} if you convert
  192. * from attribute and {@link module:engine/view/uielement~UIElement ViewUIElement} if you convert from marker.
  193. *
  194. * NOTE: When converting from model's marker, separate elements will be created at the beginning and at the end of the
  195. * marker's range. If range is collapsed then only one element will be created. See how markers
  196. * {module:engine/model/buildviewconverter~ViewConverterBuilder#toMarker view -> model serialization}
  197. * works to find out what view element format is the best for you.
  198. *
  199. * buildModelConverter().for( dispatcher ).fromElement( 'paragraph' ).toElement( 'p' );
  200. *
  201. * buildModelConverter().for( dispatcher ).fromElement( 'image' ).toElement( new ViewContainerElement( 'img' ) );
  202. *
  203. * buildModelConverter().for( dispatcher )
  204. * .fromElement( 'header' )
  205. * .toElement( ( data ) => new ViewContainerElement( 'h' + data.item.getAttribute( 'level' ) ) );
  206. *
  207. * buildModelConverter().for( dispatcher ).fromAttribute( 'bold' ).toElement( new ViewAttributeElement( 'strong' ) );
  208. *
  209. * buildModelConverter().for( dispatcher ).fromMarker( 'search' ).toElement( 'span' );
  210. *
  211. * buildModelConverter().for( dispatcher ).fromMarker( 'search' ).toElement( new ViewUIElement( 'span' ) );
  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}
  217. * parameters will be passed,
  218. * * from attribute: there is one parameter and it is attribute value,
  219. * * from marker: {@link module:engine/conversion/buildmodelconverter~MarkerViewElementCreatorData}.
  220. *
  221. * This method also registers model selection to view selection converter, if conversion is from attribute.
  222. *
  223. * This method creates the converter and adds it as a callback to a proper
  224. * {@link module:engine/conversion/modelconversiondispatcher~ModelConversionDispatcher conversion dispatcher} event.
  225. *
  226. * @param {String|module:engine/view/element~Element|Function} element Element created by converter or
  227. * a function that returns view element.
  228. */
  229. toElement( element ) {
  230. const priority = this._from.priority === null ? 'normal' : this._from.priority;
  231. for ( const dispatcher of this._dispatchers ) {
  232. if ( this._from.type == 'element' ) {
  233. // From model element to view element -> insert element.
  234. element = typeof element == 'string' ? new ViewContainerElement( element ) : element;
  235. dispatcher.on( 'insert:' + this._from.name, insertElement( element ), { priority } );
  236. } else if ( this._from.type == 'attribute' ) {
  237. // From model attribute to view element -> wrap and unwrap.
  238. element = typeof element == 'string' ? new ViewAttributeElement( element ) : element;
  239. dispatcher.on( 'addAttribute:' + this._from.key, wrapItem( element ), { priority } );
  240. dispatcher.on( 'changeAttribute:' + this._from.key, wrapItem( element ), { priority } );
  241. dispatcher.on( 'removeAttribute:' + this._from.key, unwrapItem( element ), { priority } );
  242. dispatcher.on( 'selectionAttribute:' + this._from.key, convertSelectionAttribute( element ), { priority } );
  243. } else { // From marker to element.
  244. const priority = this._from.priority === null ? 'normal' : this._from.priority;
  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. }
  249. }
  250. }
  251. /**
  252. * Registers that marker should be converted to view highlight. Markers, basically,
  253. * are {@link module:engine/model/liverange~LiveRange} instances, that are named. View highlight is
  254. * a representation of the model marker in the view:
  255. * * each {@link module:engine/view/text~Text view text node} in the marker's range will be wrapped with `span`
  256. * {@link module:engine/view/attributeelement~AttributeElement},
  257. * * each {@link module:engine/view/containerelement~ContainerElement container view element} in the marker's
  258. * range can handle highlighting individually by providing `addHighlight` and `removeHighlight`
  259. * custom properties:
  260. *
  261. * viewElement.setCustomProperty( 'addHighlight', ( element, descriptor ) => {} );
  262. * viewElement.setCustomProperty( 'removeHighlight', ( element, descriptor ) => {} );
  263. *
  264. * {@link module:engine/conversion/buildmodelconverter~HighlightDescriptor} will be used to create
  265. * spans over text nodes and also will be provided to `addHighlight` and `removeHighlight` methods
  266. * each time highlight should be set or removed from view elements.
  267. * NOTE: When `addHighlight` and `removeHighlight` custom properties are present, converter assumes
  268. * that element itself is taking care of presenting highlight on its child nodes, so it won't convert them.
  269. *
  270. * Highlight descriptor can be provided as plain object:
  271. *
  272. * buildModelConverter.for( dispatcher ).fromMarker( 'search' ).toHighlight( { class: 'search-highlight' } );
  273. *
  274. * Also, descriptor creator function can be provided:
  275. *
  276. * buildModelConverter.for( dispatcher ).fromMarker( 'search:blue' ).toHighlight( data => {
  277. * const color = data.markerName.split( ':' )[ 1 ];
  278. *
  279. * return { class: 'search-' + color };
  280. * } );
  281. *
  282. * Throws {@link module:utils/ckeditorerror~CKEditorError CKEditorError}
  283. * `build-model-converter-non-marker-to-highlight` when trying to convert not from marker.
  284. *
  285. * @param {function|module:engine/conversion/buildmodelconverter~HighlightDescriptor} highlightDescriptor
  286. */
  287. toHighlight( highlightDescriptor ) {
  288. const priority = this._from.priority === null ? 'normal' : this._from.priority;
  289. if ( this._from.type != 'marker' ) {
  290. /**
  291. * To highlight conversion is supported only for model markers.
  292. *
  293. * @error build-model-converter-non-marker-to-highlight
  294. */
  295. throw new CKEditorError(
  296. 'build-model-converter-non-marker-to-highlight: Conversion to highlight is supported ' +
  297. 'only from model markers.'
  298. );
  299. }
  300. for ( const dispatcher of this._dispatchers ) {
  301. // Separate converters for converting texts and elements inside marker's range.
  302. dispatcher.on( 'addMarker:' + this._from.name, highlightText( highlightDescriptor ), { priority } );
  303. dispatcher.on( 'addMarker:' + this._from.name, highlightElement( highlightDescriptor ), { priority } );
  304. dispatcher.on( 'removeMarker:' + this._from.name, highlightText( highlightDescriptor ), { priority } );
  305. dispatcher.on( 'removeMarker:' + this._from.name, highlightElement( highlightDescriptor ), { priority } );
  306. dispatcher.on( 'selectionMarker:' + this._from.name, convertSelectionMarker( highlightDescriptor ), { priority } );
  307. }
  308. }
  309. /**
  310. * Registers what view attribute will be created by converter. Keep in mind, that only model attribute to
  311. * view attribute conversion is supported.
  312. *
  313. * Method accepts various ways of providing how the view attribute will be created:
  314. *
  315. * * for no passed parameter, attribute key and value will be converted 1-to-1 to view attribute,
  316. * * if you pass one `string`, it will be used as new attribute key while attribute value will be copied,
  317. * * if you pass two `string`s, first one will be used as new attribute key and second one as new attribute value,
  318. * * if you pass a function, it is expected to return an object with `key` and `value` properties representing attribute key and value.
  319. * This function will be passed model attribute value and model attribute key as first two parameters and then
  320. * all dispatcher's
  321. * {module:engine/conversion/modelconversiondispatcher~ModelConversionDispatcher#event:changeAttribute changeAttribute event}
  322. * parameters.
  323. *
  324. * buildModelConverter().for( dispatcher ).fromAttribute( 'class' ).toAttribute( '' );
  325. *
  326. * buildModelConverter().for( dispatcher ).fromAttribute( 'linkTitle' ).toAttribute( 'title' );
  327. *
  328. * buildModelConverter().for( dispatcher ).fromAttribute( 'highlighted' ).toAttribute( 'style', 'background:yellow' );
  329. *
  330. * buildModelConverter().for( dispatcher )
  331. * .fromAttribute( 'theme' )
  332. * .toAttribute( ( value ) => ( { key: 'class', value: value + '-theme' } ) );
  333. *
  334. * This method creates the converter and adds it as a callback to a proper
  335. * {@link module:engine/conversion/modelconversiondispatcher~ModelConversionDispatcher conversion dispatcher} event.
  336. *
  337. * @param {String|Function} [keyOrCreator] Attribute key or a creator function.
  338. * @param {*} [value] Attribute value.
  339. */
  340. toAttribute( keyOrCreator, value ) {
  341. if ( this._from.type != 'attribute' ) {
  342. /**
  343. * To-attribute conversion is supported only for model attributes.
  344. *
  345. * @error build-model-converter-element-to-attribute
  346. */
  347. throw new CKEditorError( 'build-model-converter-non-attribute-to-attribute: ' +
  348. 'To-attribute conversion is supported only from model attributes.' );
  349. }
  350. let attributeCreator;
  351. if ( !keyOrCreator ) {
  352. // If `keyOrCreator` is not set, we assume default behavior which is 1:1 attribute re-write.
  353. // This is also a default behavior for `setAttribute` converter when no attribute creator is passed.
  354. attributeCreator = undefined;
  355. } else if ( typeof keyOrCreator == 'string' ) {
  356. // `keyOrCreator` is an attribute key.
  357. if ( value ) {
  358. // If value is set, create "dumb" creator that always returns the same object.
  359. attributeCreator = function() {
  360. return { key: keyOrCreator, value };
  361. };
  362. } else {
  363. // If value is not set, take it from the passed parameter.
  364. attributeCreator = function( value ) {
  365. return { key: keyOrCreator, value };
  366. };
  367. }
  368. } else {
  369. // `keyOrCreator` is an attribute creator function.
  370. attributeCreator = keyOrCreator;
  371. }
  372. for ( const dispatcher of this._dispatchers ) {
  373. const options = { priority: this._from.priority || 'normal' };
  374. dispatcher.on( 'addAttribute:' + this._from.key, setAttribute( attributeCreator ), options );
  375. dispatcher.on( 'changeAttribute:' + this._from.key, setAttribute( attributeCreator ), options );
  376. dispatcher.on( 'removeAttribute:' + this._from.key, removeAttribute( attributeCreator ), options );
  377. }
  378. }
  379. }
  380. /**
  381. * Entry point for model-to-view converters builder. This chainable API makes it easy to create basic, most common
  382. * model-to-view converters and attach them to provided dispatchers. The method returns an instance of
  383. * {@link module:engine/conversion/buildmodelconverter~ModelConverterBuilder}.
  384. */
  385. export default function buildModelConverter() {
  386. return new ModelConverterBuilder();
  387. }
  388. /**
  389. * @typedef MarkerViewElementCreatorData
  390. * @param {Object} data Additional information about the change.
  391. * @param {String} data.markerName Marker name.
  392. * @param {module:engine/model/range~Range} data.markerRange Marker range.
  393. * @param {Boolean} data.isOpening Defines if currently converted element is a beginning or end of the marker range.
  394. * @param {module:engine/conversion/modelconsumable~ModelConsumable} consumable Values to consume.
  395. * @param {Object} conversionApi Conversion interface to be used by callback, passed in `ModelConversionDispatcher` constructor.
  396. */
  397. /**
  398. * @typedef HighlightDescriptor
  399. * Object describing how content highlight should be created in the view. Each text node contained in highlight
  400. * will be wrapped with `span` element with CSS class, attributes and priority described by this object. Each element
  401. * can handle displaying highlight separately by providing `addHighlight` and `removeHighlight` custom
  402. * properties.
  403. *
  404. * @property {String|Array.<String>} class CSS class or array of classes that will be added to `span`
  405. * {@link module:engine/view/attributeelement~AttributeElement} wrapping each text node in the highlighted content.
  406. * @property {String} [id] Descriptor identifier. If not provided, marker's name from which given highlight is created
  407. * will be used.
  408. * @property {Number} [priority] {@link module:engine/view/attributeelement~AttributeElement#priority} of the `span`
  409. * wrapping each text node in the highlighted content. If not provided, default 10 priority will be used.
  410. * @property {Object} [attributes] Attributes that will be added to `span`
  411. * {@link module:engine/view/attributeelement~AttributeElement} wrapping each text node it the highlighted content.
  412. */