8
0

buildviewconverter.js 21 KB

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