8
0

view-converter-builder.js 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409
  1. /**
  2. * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
  3. * For licensing, see LICENSE.md.
  4. */
  5. 'use strict';
  6. import Matcher from '../treeview/matcher.js';
  7. import ModelElement from '../treemodel/element.js';
  8. import isIterable from '../../utils/isiterable.js';
  9. /**
  10. * Provides chainable, high-level API to easily build basic view-to-model converters that are appended to given
  11. * dispatchers. View-to-model converters are used when external data is added to the editor, i.e. when a user pastes
  12. * HTML content to the editor. Then, converters are used to translate this structure, possibly removing unknown/incorrect
  13. * nodes, and add it to the model. Also multiple, different elements might be translated into the same thing in the
  14. * model, i.e. `<b>` and `<strong>` elements might be converted to `bold` attribute (even though `bold` attribute will
  15. * be then converted only to `<strong>` tag). Instances of this class are created by {@link engine.treeController.BuildViewConverterFor}.
  16. *
  17. * If you need more complex converters, see {@link engine.treeController.ViewConversionDispatcher},
  18. * {@link engine.treeController.viewToModel}, {@link engine.treeController.ViewConsumable}.
  19. *
  20. * Using this API it is possible to create various kind of converters:
  21. *
  22. * 1. View element to model element:
  23. *
  24. * BuildViewConverterFor( dispatcher ).fromElement( 'p' ).toElement( 'paragraph' );
  25. *
  26. * 2. View element to model attribute:
  27. *
  28. * BuildViewConverterFor( dispatcher ).fromElement( 'b' ).fromElement( 'strong' ).toAttributes( { bold: true } );
  29. *
  30. * 3. View attribute to model attribute:
  31. *
  32. * BuildViewConverterFor( dispatcher ).fromAttribute( 'style', { 'font-weight': 'bold' } ).toAttributes( { bold: true } );
  33. * BuildViewConverterFor( dispatcher )
  34. * .fromAttribute( 'class' )
  35. * .toAttributes( ( viewElement ) => ( { class: viewElement.getAttribute( 'class' ) } ) );
  36. *
  37. * 4. View elements and attributes to model attribute:
  38. *
  39. * BuildViewConverterFor( dispatcher )
  40. * .fromElement( 'b' ).fromElement( 'strong' ).fromAttribute( 'style', { 'font-weight': 'bold' } )
  41. * .toAttributes( { bold: true } );
  42. *
  43. * 5. View {@link engine.treeView.Matcher view element matcher instance} or {@link engine.treeView.Matcher#add matcher pattern}
  44. * to model element or attribute:
  45. *
  46. * const matcher = new ViewMatcher();
  47. * matcher.add( 'div', { class: 'quote' } );
  48. * BuildViewConverterFor( dispatcher ).from( matcher ).toElement( 'quote' );
  49. *
  50. * BuildViewConverterFor( dispatcher ).from( { name: 'span', class: 'bold' } ).toAttributes( { bold: true } );
  51. *
  52. * Note, that converters built using `ViewConverterBuilder` automatically check {@link engine.treeModel.Schema schema}
  53. * if created model structure is valid. If given conversion would be invalid according to schema, it is ignored.
  54. *
  55. * It is possible to provide creator functions as parameters for {@link engine.treeController.ViewConverterBuilder#toElement}
  56. * and {@link engine.treeController.ViewConverterBuilder#toAttributes} methods. See their descriptions to learn more.
  57. *
  58. * By default, converter will {@link engine.treeController.ViewConsumable#consume consume} every value specified in
  59. * given `from...` query, i.e. `.from( { name: 'span', class: 'bold' } )` will make converter consume both `span` name
  60. * and `bold` class. It is possible to change this behavior using {@link engine.treeController.ViewConverterBuilder#consuming consuming}
  61. * modifier. The modifier alters the last `fromXXX` query used before it. To learn more about consuming values,
  62. * see {@link engine.treeController.ViewConsumable}.
  63. *
  64. * It is also possible to {@link engine.treeController.ViewConverterBuilder#withPriority change default priority}
  65. * of created converters to decide which converter should be fired earlier and which later. This is useful if you provide
  66. * a general converter but want to provide different converter for a specific-case (i.e. given view element is converted
  67. * always to given model element, but if it has given class it is converter to other model element). For this,
  68. * use {@link engine.treeController.ViewConverterBuilder#withPriority withPriority} modifier. The modifier alters
  69. * the last `from...` query used before it.
  70. *
  71. * Note that `to...` methods are "terminators", which means that should be the last one used in building converter.
  72. *
  73. * You can use {@link engine.treeController.ModelConverterBuilder} to create "opposite" converters - from model to view.
  74. *
  75. * @memberOf engine.treeController
  76. */
  77. class ViewConverterBuilder {
  78. /**
  79. * Creates `ViewConverterBuilder` with given `dispatchers` registered to it.
  80. *
  81. * @param {Array.<engine.treeController.ViewConversionDispatcher>} dispatchers Dispatchers to which converters will
  82. * be attached.
  83. */
  84. constructor( dispatchers ) {
  85. /**
  86. * Dispatchers to which converters will be attached.
  87. *
  88. * @type {Array.<engine.treeController.ViewConversionDispatcher>}
  89. * @private
  90. */
  91. this._dispatchers = dispatchers;
  92. /**
  93. * Stores "from" queries.
  94. *
  95. * @type {Array}
  96. * @private
  97. */
  98. this._from = [];
  99. }
  100. /**
  101. * Registers what view element should be converted.
  102. *
  103. * BuildViewConverterFor( dispatcher ).fromElement( 'p' ).toElement( 'paragraph' );
  104. *
  105. * @chainable
  106. * @param {String} elementName View element name.
  107. * @returns {engine.treeController.ViewConverterBuilder}
  108. */
  109. fromElement( elementName ) {
  110. return this.from( { name: elementName } );
  111. }
  112. /**
  113. * Registers what view attribute should be converted.
  114. *
  115. * BuildViewConverterFor( dispatcher ).fromAttribute( 'style', { 'font-weight': 'bold' } ).toAttributes( { bold: true } );
  116. *
  117. * @chainable
  118. * @param {String|RegExp} key View attribute key.
  119. * @param {String|RegExp} [value] View attribute value.
  120. * @returns {engine.treeController.ViewConverterBuilder}
  121. */
  122. fromAttribute( key, value = /.*/ ) {
  123. let pattern = {};
  124. pattern[ key ] = value;
  125. return this.from( pattern );
  126. }
  127. /**
  128. * Registers what view pattern should be converted. The method accepts either {@link engine.treeView.Matcher view matcher}
  129. * or view matcher pattern.
  130. *
  131. * const matcher = new ViewMatcher();
  132. * matcher.add( 'div', { class: 'quote' } );
  133. * BuildViewConverterFor( dispatcher ).from( matcher ).toElement( 'quote' );
  134. *
  135. * BuildViewConverterFor( dispatcher ).from( { name: 'span', class: 'bold' } ).toAttributes( { bold: true } );
  136. *
  137. * @chainable
  138. * @param {Object|engine.treeView.Matcher} matcher View matcher or view matcher pattern.
  139. * @returns {engine.treeController.ViewConverterBuilder}
  140. */
  141. from( matcher ) {
  142. if ( !( matcher instanceof Matcher ) ) {
  143. matcher = new Matcher( matcher );
  144. }
  145. this._from.push( {
  146. matcher: matcher,
  147. consume: false,
  148. priority: null
  149. } );
  150. return this;
  151. }
  152. /**
  153. * Modifies which consumable values will be {@link engine.treeController.ViewConsumable#consume consumed} by built converter.
  154. * It modifies the last `from...` query. Can be used after each `from...` query in given chain. Useful for providing
  155. * more specific matches.
  156. *
  157. * // This converter will only handle class bold conversion (to proper attribute) but span element
  158. * // conversion will have to be done in separate converter.
  159. * // Without consuming modifier, the converter would consume both class and name, so a converter for
  160. * // span element would not be fired.
  161. * BuildViewConverterFor( dispatcher )
  162. * .from( { name: 'span', class: 'bold' } ).consuming( { class: 'bold' } )
  163. * .toAttribute( { bold: true } );
  164. *
  165. * BuildViewConverterFor( dispatcher )
  166. * .fromElement( 'img' ).consuming( { name: true, attributes: [ 'src', 'title' ] } )
  167. * .toElement( ( viewElement ) => new ModelElement( 'image', { src: viewElement.getAttribute( 'src' ),
  168. * title: viewElement.getAttribute( 'title' ) } );
  169. *
  170. * **Note:** All and only values from passed object has to be consumable on converted view element. This means that
  171. * using `consuming` method, you can either make looser conversion conditions (like in first example) or tighter
  172. * conversion conditions (like in second example). So, the view element, to be converter, has to match query of
  173. * `from...` method and then have to have enough consumable values to consume.
  174. *
  175. * @see engine.treeController.ViewConsumable
  176. * @chainable
  177. * @param {Object} consume Values to consume.
  178. * @returns {engine.treeController.ViewConverterBuilder}
  179. */
  180. consuming( consume ) {
  181. let lastFrom = this._from[ this._from.length - 1 ];
  182. lastFrom.consume = consume;
  183. return this;
  184. }
  185. /**
  186. * Changes default priority for built converter. It modifies the last `from...` query. Can be used after each
  187. * `from...` query in given chain. Useful for overwriting converters. The lower the number, the earlier converter will be fired.
  188. *
  189. * BuildViewConverterFor( dispatcher ).fromElement( 'p' ).toElement( 'paragraph' );
  190. * // Register converter with proper priority, otherwise "p" element would get consumed by first
  191. * // converter and the second converter would not be fired.
  192. * BuildViewConverterFor( dispatcher )
  193. * .from( { name: 'p', class: 'custom' } ).withPriority( 9 )
  194. * .toElement( 'customParagraph' );
  195. *
  196. * **Note:** `ViewConverterBuilder` takes care so all `toElement` conversions takes place before all `toAttributes`
  197. * conversions. This is done by setting default `toElement` priority to `10` and `toAttributes` priority to `1000`.
  198. * It is recommended to set converter priority for `toElement` conversions below `500` and `toAttributes` priority
  199. * above `500`. It is important that model elements are created before attributes, otherwise attributes would
  200. * not be applied or other errors may occur.
  201. *
  202. * @chainable
  203. * @param {Number} priority Converter priority.
  204. * @returns {engine.treeController.ViewConverterBuilder}
  205. */
  206. withPriority( priority ) {
  207. let lastFrom = this._from[ this._from.length - 1 ];
  208. lastFrom.priority = priority;
  209. return this;
  210. }
  211. /**
  212. * Registers what model element will be created by converter.
  213. *
  214. * Method accepts two ways of providing what kind of model element will be created. You can pass model element
  215. * name as a `string` or a function that will return model element instance. If you provide creator function,
  216. * it will be passed converted view element as first and only parameter.
  217. *
  218. * BuildViewConverterFor( dispatcher ).fromElement( 'p' ).toElement( 'paragraph' );
  219. * BuildViewConverterFor( dispatcher )
  220. * .fromElement( 'img' )
  221. * .toElement( ( viewElement ) => new ModelElement( 'image', { src: viewElement.getAttribute( 'src' ) } );
  222. *
  223. * @param {String|Function} element Model element name or model element creator function.
  224. */
  225. toElement( element ) {
  226. const eventCallbackGen = function( from ) {
  227. return ( evt, data, consumable, conversionApi ) => {
  228. // There is one callback for all patterns in the matcher.
  229. // This will be usually just one pattern but we support matchers with many patterns too.
  230. let matchAll = from.matcher.matchAll( data.input );
  231. // If there is no match, this callback should not do anything.
  232. if ( !matchAll ) {
  233. return;
  234. }
  235. // Now, for every match between matcher and actual element, we will try to consume the match.
  236. for ( let match of matchAll ) {
  237. // Create model element basing on creator function or element name.
  238. const modelElement = element instanceof Function ? element( data.input ) : new ModelElement( element );
  239. // Check whether generated structure is okay with `Schema`.
  240. // TODO: Make it more sane after .getAttributeKeys() is available for ModelElement.
  241. const keys = Array.from( modelElement.getAttributes() ).map( ( attribute ) => attribute[ 0 ] );
  242. if ( !conversionApi.schema.check( { name: modelElement.name, attributes: keys, inside: data.context } ) ) {
  243. continue;
  244. }
  245. // Try to consume appropriate values from consumable values list.
  246. if ( !consumable.consume( data.input, from.consume || match.match ) ) {
  247. continue;
  248. }
  249. // If everything is fine, we are ready to start the conversion.
  250. // Add newly created `modelElement` to the parents stack.
  251. data.context.push( modelElement );
  252. // Convert children of converted view element and append them to `modelElement`.
  253. modelElement.appendChildren( conversionApi.convertChildren( data.input, consumable, data ) );
  254. // Remove created `modelElement` from the parents stack.
  255. data.context.pop();
  256. // Add `modelElement` as a result.
  257. data.output = modelElement;
  258. // Prevent multiple conversion if there are other correct matches.
  259. break;
  260. }
  261. };
  262. };
  263. this._setCallback( eventCallbackGen, 10 );
  264. }
  265. /**
  266. * Registers what model attribute will be created by converter.
  267. *
  268. * Method accepts two ways of providing what kind of model attribute will be created. You can either pass two strings
  269. * representing attribute key and attribute value or a function that returns an object with `key` and `value` properties.
  270. * If you provide creator function, it will be passed converted view element as first and only parameter.
  271. *
  272. * BuildViewConverterFor( dispatcher ).fromAttribute( 'style', { 'font-weight': 'bold' } ).toAttribute( 'bold', true );
  273. * BuildViewConverterFor( dispatcher )
  274. * .fromAttribute( 'class' )
  275. * .toAttribute( ( viewElement ) => ( { key: 'class', value: viewElement.getAttribute( 'class' ) } ) );
  276. *
  277. * @param {String|Function} keyOrCreator Attribute key or a creator function.
  278. * @param {String} [value] Attribute value. Required if `keyOrCreator` is a `string`. Ignored otherwise.
  279. */
  280. toAttribute( keyOrCreator, value ) {
  281. const eventCallbackGen = function( from ) {
  282. return ( evt, data, consumable, conversionApi ) => {
  283. // There is one callback for all patterns in the matcher.
  284. // This will be usually just one pattern but we support matchers with many patterns too.
  285. let matchAll = from.matcher.matchAll( data.input );
  286. // If there is no match, this callback should not do anything.
  287. if ( !matchAll ) {
  288. return;
  289. }
  290. // Now, for every match between matcher and actual element, we will try to consume the match.
  291. for ( let match of matchAll ) {
  292. // Try to consume appropriate values from consumable values list.
  293. if ( !consumable.consume( data.input, from.consume || match.match ) ) {
  294. continue;
  295. }
  296. // Since we are converting to attribute we need an output on which we will set the attribute.
  297. // If the output is not created yet, we will create it.
  298. if ( !data.output ) {
  299. data.output = conversionApi.convertChildren( data.input, consumable, data );
  300. }
  301. // Use attribute creator function, if provided.
  302. let attribute = keyOrCreator instanceof Function ? keyOrCreator( data.input ) : { key: keyOrCreator, value: value };
  303. // Set attribute on current `output`. `Schema` is checked inside this helper function.
  304. setAttributeOn( data.output, attribute, data, conversionApi );
  305. // Prevent multiple conversion if there are other correct matches.
  306. break;
  307. }
  308. };
  309. };
  310. this._setCallback( eventCallbackGen, 1000 );
  311. }
  312. /**
  313. * Helper function that uses given callback generator to created callback function and sets it on registered dispatchers.
  314. *
  315. * @param eventCallbackGen
  316. * @param defaultPriority
  317. * @private
  318. */
  319. _setCallback( eventCallbackGen, defaultPriority ) {
  320. // We will add separate event callback for each registered `from` entry.
  321. for ( let from of this._from ) {
  322. // We have to figure out event name basing on matcher's patterns.
  323. // If there is exactly one pattern and it has `name` property we will used that name.
  324. const matcherElementName = from.matcher.getElementName();
  325. const eventName = matcherElementName ? 'element:' + matcherElementName : 'element';
  326. const eventCallback = eventCallbackGen( from );
  327. const priority = from.priority === null ? defaultPriority : from.priority;
  328. // Add event to each registered dispatcher.
  329. for ( let dispatcher of this._dispatchers ) {
  330. dispatcher.on( eventName, eventCallback, null, priority );
  331. }
  332. }
  333. }
  334. }
  335. // Helper function that sets given attributes on given `engine.treeModel.Item` or `engine.treeModel.DocumentFragment`.
  336. function setAttributeOn( toChange, attribute, data, conversionApi ) {
  337. if ( isIterable( toChange ) ) {
  338. for ( let node of toChange ) {
  339. setAttributeOn( node, attribute, data, conversionApi );
  340. }
  341. return;
  342. }
  343. // TODO: Make it more sane after .getAttributeKeys() is available for ModelElement.
  344. const keys = Array.from( toChange.getAttributes() ).map( ( attribute ) => attribute[ 0 ] ).concat( attribute.key );
  345. const schemaQuery = {
  346. name: toChange.name || '$text',
  347. attributes: keys,
  348. inside: data.context
  349. };
  350. if ( conversionApi.schema.check( schemaQuery ) ) {
  351. toChange.setAttribute( attribute.key, attribute.value );
  352. }
  353. }
  354. /**
  355. * Entry point for view-to-model converters builder. This chainable API makes it easy to create basic, most common
  356. * view-to-model converters and attach them to provided dispatchers. The method returns an instance of
  357. * {@link engine.treeController.ViewConverterBuilder}.
  358. *
  359. * @external engine.treeController.BuildViewConverterFor
  360. * @memberOf engine.treeController
  361. * @param {...engine.treeController.ViewConversionDispatcher} dispatchers One or more dispatchers to which
  362. * the built converter will be attached.
  363. */
  364. export default function BuildViewConverterFor( ...dispatchers ) {
  365. return new ViewConverterBuilder( dispatchers );
  366. }