8
0

buildviewconverter.js 22 KB

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