buildviewconverter.js 20 KB

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