upcasthelpers.js 30 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756
  1. /**
  2. * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
  3. * For licensing, see LICENSE.md.
  4. */
  5. import Matcher from '../view/matcher';
  6. import ModelRange from '../model/range';
  7. import ConversionHelpers from './conversionhelpers';
  8. import { cloneDeep } from 'lodash-es';
  9. import ModelSelection from '../model/selection';
  10. /**
  11. * Contains {@link module:engine/view/view view} to {@link module:engine/model/model model} converters for
  12. * {@link module:engine/conversion/upcastdispatcher~UpcastDispatcher}.
  13. *
  14. * @module engine/conversion/upcasthelpers
  15. */
  16. /**
  17. * Upcast conversion helper functions.
  18. *
  19. * @extends module:engine/conversion/conversionhelpers~ConversionHelpers
  20. */
  21. export default class UpcastHelpers extends ConversionHelpers {
  22. /**
  23. * View element to model element conversion helper.
  24. *
  25. * This conversion results in creating a model element. For example,
  26. * view `<p>Foo</p>` becomes `<paragraph>Foo</paragraph>` in the model.
  27. *
  28. * Keep in mind that the element will be inserted only if it is allowed
  29. * by {@link module:engine/model/schema~Schema schema} configuration.
  30. *
  31. * editor.conversion.for( 'upcast' ).elementToElement( {
  32. * view: 'p',
  33. * model: 'paragraph'
  34. * } );
  35. *
  36. * editor.conversion.for( 'upcast' ).elementToElement( {
  37. * view: 'p',
  38. * model: 'paragraph',
  39. * converterPriority: 'high'
  40. * } );
  41. *
  42. * editor.conversion.for( 'upcast' ).elementToElement( {
  43. * view: {
  44. * name: 'p',
  45. * classes: 'fancy'
  46. * },
  47. * model: 'fancyParagraph'
  48. * } );
  49. *
  50. * editor.conversion.for( 'upcast' ).elementToElement( {
  51. * view: {
  52. * name: 'p',
  53. * classes: 'heading'
  54. * },
  55. * model: ( viewElement, modelWriter ) => {
  56. * return modelWriter.createElement( 'heading', { level: viewElement.getAttribute( 'data-level' ) } );
  57. * }
  58. * } );
  59. *
  60. * See {@link module:engine/conversion/conversion~Conversion#for `conversion.for()`} to learn how to add a converter
  61. * to the conversion process.
  62. *
  63. * @method #elementToElement
  64. * @param {Object} config Conversion configuration.
  65. * @param {module:engine/view/matcher~MatcherPattern} config.view Pattern matching all view elements which should be converted.
  66. * @param {String|module:engine/model/element~Element|Function} config.model Name of the model element, a model element
  67. * instance or a function that takes a view element and returns a model element. The model element will be inserted in the model.
  68. * @param {module:utils/priorities~PriorityString} [config.converterPriority='normal'] Converter priority.
  69. * @returns {module:engine/conversion/upcasthelpers~UpcastHelpers}
  70. */
  71. elementToElement( config ) {
  72. return this.add( upcastElementToElement( config ) );
  73. }
  74. /**
  75. * View element to model attribute conversion helper.
  76. *
  77. * This conversion results in setting an attribute on a model node. For example, view `<strong>Foo</strong>` becomes
  78. * `Foo` {@link module:engine/model/text~Text model text node} with `bold` attribute set to `true`.
  79. *
  80. * This helper is meant to set a model attribute on all the elements that are inside the converted element:
  81. *
  82. * <strong>Foo</strong> --> <strong><p>Foo</p></strong> --> <paragraph><$text bold="true">Foo</$text></paragraph>
  83. *
  84. * Above is a sample of HTML code, that goes through autoparagraphing (first step) and then is converted (second step).
  85. * Even though `<strong>` is over `<p>` element, `bold="true"` was added to the text. See
  86. * {@link module:engine/conversion/upcasthelpers~UpcastHelpers#attributeToAttribute} for comparison.
  87. *
  88. * Keep in mind that the attribute will be set only if it is allowed by {@link module:engine/model/schema~Schema schema} configuration.
  89. *
  90. * editor.conversion.for( 'upcast' ).elementToAttribute( {
  91. * view: 'strong',
  92. * model: 'bold'
  93. * } );
  94. *
  95. * editor.conversion.for( 'upcast' ).elementToAttribute( {
  96. * view: 'strong',
  97. * model: 'bold',
  98. * converterPriority: 'high'
  99. * } );
  100. *
  101. * editor.conversion.for( 'upcast' ).elementToAttribute( {
  102. * view: {
  103. * name: 'span',
  104. * classes: 'bold'
  105. * },
  106. * model: 'bold'
  107. * } );
  108. *
  109. * editor.conversion.for( 'upcast' ).elementToAttribute( {
  110. * view: {
  111. * name: 'span',
  112. * classes: [ 'styled', 'styled-dark' ]
  113. * },
  114. * model: {
  115. * key: 'styled',
  116. * value: 'dark'
  117. * }
  118. * } );
  119. *
  120. * editor.conversion.for( 'upcast' ).elementToAttribute( {
  121. * view: {
  122. * name: 'span',
  123. * styles: {
  124. * 'font-size': /[\s\S]+/
  125. * }
  126. * },
  127. * model: {
  128. * key: 'fontSize',
  129. * value: viewElement => {
  130. * const fontSize = viewElement.getStyle( 'font-size' );
  131. * const value = fontSize.substr( 0, fontSize.length - 2 );
  132. *
  133. * if ( value <= 10 ) {
  134. * return 'small';
  135. * } else if ( value > 12 ) {
  136. * return 'big';
  137. * }
  138. *
  139. * return null;
  140. * }
  141. * }
  142. * } );
  143. *
  144. * See {@link module:engine/conversion/conversion~Conversion#for `conversion.for()`} to learn how to add a converter
  145. * to the conversion process.
  146. *
  147. * @method #elementToAttribute
  148. * @param {Object} config Conversion configuration.
  149. * @param {module:engine/view/matcher~MatcherPattern} config.view Pattern matching all view elements which should be converted.
  150. * @param {String|Object} config.model Model attribute key or an object with `key` and `value` properties, describing
  151. * the model attribute. `value` property may be set as a function that takes a view element and returns the value.
  152. * If `String` is given, the model attribute value will be set to `true`.
  153. * @param {module:utils/priorities~PriorityString} [config.converterPriority='normal'] Converter priority.
  154. * @returns {module:engine/conversion/upcasthelpers~UpcastHelpers}
  155. */
  156. elementToAttribute( config ) {
  157. return this.add( upcastElementToAttribute( config ) );
  158. }
  159. /**
  160. * View attribute to model attribute conversion helper.
  161. *
  162. * This conversion results in setting an attribute on a model node. For example, view `<img src="foo.jpg"></img>` becomes
  163. * `<image source="foo.jpg"></image>` in the model.
  164. *
  165. * This helper is meant to convert view attributes from view elements which got converted to the model, so the view attribute
  166. * is set only on the corresponding model node:
  167. *
  168. * <div class="dark"><div>foo</div></div> --> <div dark="true"><div>foo</div></div>
  169. *
  170. * Above, `class="dark"` attribute is added only to the `<div>` elements that has it. This is in contrary to
  171. * {@link module:engine/conversion/upcasthelpers~UpcastHelpers#elementToAttribute} which sets attributes for
  172. * all the children in the model:
  173. *
  174. * <strong>Foo</strong> --> <strong><p>Foo</p></strong> --> <paragraph><$text bold="true">Foo</$text></paragraph>
  175. *
  176. * Above is a sample of HTML code, that goes through autoparagraphing (first step) and then is converted (second step).
  177. * Even though `<strong>` is over `<p>` element, `bold="true"` was added to the text.
  178. *
  179. * Keep in mind that the attribute will be set only if it is allowed by {@link module:engine/model/schema~Schema schema} configuration.
  180. *
  181. * editor.conversion.for( 'upcast' ).attributeToAttribute( {
  182. * view: 'src',
  183. * model: 'source'
  184. * } );
  185. *
  186. * editor.conversion.for( 'upcast' ).attributeToAttribute( {
  187. * view: { key: 'src' },
  188. * model: 'source'
  189. * } );
  190. *
  191. * editor.conversion.for( 'upcast' ).attributeToAttribute( {
  192. * view: { key: 'src' },
  193. * model: 'source',
  194. * converterPriority: 'normal'
  195. * } );
  196. *
  197. * editor.conversion.for( 'upcast' ).attributeToAttribute( {
  198. * view: {
  199. * key: 'data-style',
  200. * value: /[\s\S]+/
  201. * },
  202. * model: 'styled'
  203. * } );
  204. *
  205. * editor.conversion.for( 'upcast' ).attributeToAttribute( {
  206. * view: {
  207. * name: 'img',
  208. * key: 'class',
  209. * value: 'styled-dark'
  210. * },
  211. * model: {
  212. * key: 'styled',
  213. * value: 'dark'
  214. * }
  215. * } );
  216. *
  217. * editor.conversion.for( 'upcast' ).attributeToAttribute( {
  218. * view: {
  219. * key: 'class',
  220. * value: /styled-[\S]+/
  221. * },
  222. * model: {
  223. * key: 'styled'
  224. * value: viewElement => {
  225. * const regexp = /styled-([\S]+)/;
  226. * const match = viewElement.getAttribute( 'class' ).match( regexp );
  227. *
  228. * return match[ 1 ];
  229. * }
  230. * }
  231. * } );
  232. *
  233. * See {@link module:engine/conversion/conversion~Conversion#for `conversion.for()`} to learn how to add a converter
  234. * to the conversion process.
  235. *
  236. * @method #attributeToAttribute
  237. * @param {Object} config Conversion configuration.
  238. * @param {String|Object} config.view Specifies which view attribute will be converted. If a `String` is passed,
  239. * attributes with given key will be converted. If an `Object` is passed, it must have a required `key` property,
  240. * specifying view attribute key, and may have an optional `value` property, specifying view attribute value and optional `name`
  241. * property specifying a view element name from/on which the attribute should be converted. `value` can be given as a `String`,
  242. * a `RegExp` or a function callback, that takes view attribute value as the only parameter and returns `Boolean`.
  243. * @param {String|Object} config.model Model attribute key or an object with `key` and `value` properties, describing
  244. * the model attribute. `value` property may be set as a function that takes a view element and returns the value.
  245. * If `String` is given, the model attribute value will be same as view attribute value.
  246. * @param {module:utils/priorities~PriorityString} [config.converterPriority='low'] Converter priority.
  247. * @returns {module:engine/conversion/upcasthelpers~UpcastHelpers}
  248. */
  249. attributeToAttribute( config ) {
  250. return this.add( upcastAttributeToAttribute( config ) );
  251. }
  252. /**
  253. * View element to model marker conversion helper.
  254. *
  255. * This conversion results in creating a model marker. For example, if the marker was stored in a view as an element:
  256. * `<p>Fo<span data-marker="comment" data-comment-id="7"></span>o</p><p>B<span data-marker="comment" data-comment-id="7"></span>ar</p>`,
  257. * after the conversion is done, the marker will be available in
  258. * {@link module:engine/model/model~Model#markers model document markers}.
  259. *
  260. * editor.conversion.for( 'upcast' ).elementToMarker( {
  261. * view: 'marker-search',
  262. * model: 'search'
  263. * } );
  264. *
  265. * editor.conversion.for( 'upcast' ).elementToMarker( {
  266. * view: 'marker-search',
  267. * model: 'search',
  268. * converterPriority: 'high'
  269. * } );
  270. *
  271. * editor.conversion.for( 'upcast' ).elementToMarker( {
  272. * view: 'marker-search',
  273. * model: viewElement => 'comment:' + viewElement.getAttribute( 'data-comment-id' )
  274. * } );
  275. *
  276. * editor.conversion.for( 'upcast' ).elementToMarker( {
  277. * view: {
  278. * name: 'span',
  279. * attributes: {
  280. * 'data-marker': 'search'
  281. * }
  282. * },
  283. * model: 'search'
  284. * } );
  285. *
  286. * See {@link module:engine/conversion/conversion~Conversion#for `conversion.for()`} to learn how to add a converter
  287. * to the conversion process.
  288. *
  289. * @method #elementToMarker
  290. * @param {Object} config Conversion configuration.
  291. * @param {module:engine/view/matcher~MatcherPattern} config.view Pattern matching all view elements which should be converted.
  292. * @param {String|Function} config.model Name of the model marker, or a function that takes a view element and returns
  293. * a model marker name.
  294. * @param {module:utils/priorities~PriorityString} [config.converterPriority='normal'] Converter priority.
  295. * @returns {module:engine/conversion/upcasthelpers~UpcastHelpers}
  296. */
  297. elementToMarker( config ) {
  298. return this.add( upcastElementToMarker( config ) );
  299. }
  300. }
  301. /**
  302. * Function factory, creates a converter that converts {@link module:engine/view/documentfragment~DocumentFragment view document fragment}
  303. * or all children of {@link module:engine/view/element~Element} into
  304. * {@link module:engine/model/documentfragment~DocumentFragment model document fragment}.
  305. * This is the "entry-point" converter for upcast (view to model conversion). This converter starts the conversion of all children
  306. * of passed view document fragment. Those children {@link module:engine/view/node~Node view nodes} are then handled by other converters.
  307. *
  308. * This also a "default", last resort converter for all view elements that has not been converted by other converters.
  309. * When a view element is being converted to the model but it does not have converter specified, that view element
  310. * will be converted to {@link module:engine/model/documentfragment~DocumentFragment model document fragment} and returned.
  311. *
  312. * @returns {Function} Universal converter for view {@link module:engine/view/documentfragment~DocumentFragment fragments} and
  313. * {@link module:engine/view/element~Element elements} that returns
  314. * {@link module:engine/model/documentfragment~DocumentFragment model fragment} with children of converted view item.
  315. */
  316. export function convertToModelFragment() {
  317. return ( evt, data, conversionApi ) => {
  318. // Second argument in `consumable.consume` is discarded for ViewDocumentFragment but is needed for ViewElement.
  319. if ( !data.modelRange && conversionApi.consumable.consume( data.viewItem, { name: true } ) ) {
  320. const { modelRange, modelCursor } = conversionApi.convertChildren( data.viewItem, data.modelCursor );
  321. data.modelRange = modelRange;
  322. data.modelCursor = modelCursor;
  323. }
  324. };
  325. }
  326. /**
  327. * Function factory, creates a converter that converts {@link module:engine/view/text~Text} to {@link module:engine/model/text~Text}.
  328. *
  329. * @returns {Function} {@link module:engine/view/text~Text View text} converter.
  330. */
  331. export function convertText() {
  332. return ( evt, data, conversionApi ) => {
  333. if ( conversionApi.schema.checkChild( data.modelCursor, '$text' ) ) {
  334. if ( conversionApi.consumable.consume( data.viewItem ) ) {
  335. const text = conversionApi.writer.createText( data.viewItem.data );
  336. conversionApi.writer.insert( text, data.modelCursor );
  337. data.modelRange = ModelRange._createFromPositionAndShift( data.modelCursor, text.offsetSize );
  338. data.modelCursor = data.modelRange.end;
  339. }
  340. }
  341. };
  342. }
  343. /**
  344. * Function factory, creates a callback function which converts a {@link module:engine/view/selection~Selection
  345. * view selection} taken from the {@link module:engine/view/document~Document#event:selectionChange} event
  346. * and sets in on the {@link module:engine/model/document~Document#selection model}.
  347. *
  348. * **Note**: because there is no view selection change dispatcher nor any other advanced view selection to model
  349. * conversion mechanism, the callback should be set directly on view document.
  350. *
  351. * view.document.on( 'selectionChange', convertSelectionChange( modelDocument, mapper ) );
  352. *
  353. * @param {module:engine/model/model~Model} model Data model.
  354. * @param {module:engine/conversion/mapper~Mapper} mapper Conversion mapper.
  355. * @returns {Function} {@link module:engine/view/document~Document#event:selectionChange} callback function.
  356. */
  357. export function convertSelectionChange( model, mapper ) {
  358. return ( evt, data ) => {
  359. const viewSelection = data.newSelection;
  360. const modelSelection = new ModelSelection();
  361. const ranges = [];
  362. for ( const viewRange of viewSelection.getRanges() ) {
  363. ranges.push( mapper.toModelRange( viewRange ) );
  364. }
  365. modelSelection.setTo( ranges, { backward: viewSelection.isBackward } );
  366. if ( !modelSelection.isEqual( model.document.selection ) ) {
  367. model.change( writer => {
  368. writer.setSelection( modelSelection );
  369. } );
  370. }
  371. };
  372. }
  373. // View element to model element conversion helper.
  374. //
  375. // See {@link ~UpcastHelpers#elementToElement `.elementToElement()` upcast helper} for examples.
  376. //
  377. // @param {Object} config Conversion configuration.
  378. // @param {module:engine/view/matcher~MatcherPattern} config.view Pattern matching all view elements which should be converted.
  379. // @param {String|module:engine/model/element~Element|Function} config.model Name of the model element, a model element
  380. // instance or a function that takes a view element and returns a model element. The model element will be inserted in the model.
  381. // @param {module:utils/priorities~PriorityString} [config.converterPriority='normal'] Converter priority.
  382. // @returns {Function} Conversion helper.
  383. function upcastElementToElement( config ) {
  384. config = cloneDeep( config );
  385. const converter = prepareToElementConverter( config );
  386. const elementName = getViewElementNameFromConfig( config );
  387. const eventName = elementName ? 'element:' + elementName : 'element';
  388. return dispatcher => {
  389. dispatcher.on( eventName, converter, { priority: config.converterPriority || 'normal' } );
  390. };
  391. }
  392. // View element to model attribute conversion helper.
  393. //
  394. // See {@link ~UpcastHelpers#elementToAttribute `.elementToAttribute()` upcast helper} for examples.
  395. //
  396. // @param {Object} config Conversion configuration.
  397. // @param {module:engine/view/matcher~MatcherPattern} config.view Pattern matching all view elements which should be converted.
  398. // @param {String|Object} config.model Model attribute key or an object with `key` and `value` properties, describing
  399. // the model attribute. `value` property may be set as a function that takes a view element and returns the value.
  400. // If `String` is given, the model attribute value will be set to `true`.
  401. // @param {module:utils/priorities~PriorityString} [config.converterPriority='normal'] Converter priority.
  402. // @returns {Function} Conversion helper.
  403. function upcastElementToAttribute( config ) {
  404. config = cloneDeep( config );
  405. normalizeModelAttributeConfig( config );
  406. const converter = prepareToAttributeConverter( config, false );
  407. const elementName = getViewElementNameFromConfig( config );
  408. const eventName = elementName ? 'element:' + elementName : 'element';
  409. return dispatcher => {
  410. dispatcher.on( eventName, converter, { priority: config.converterPriority || 'low' } );
  411. };
  412. }
  413. // View attribute to model attribute conversion helper.
  414. //
  415. // See {@link ~UpcastHelpers#attributeToAttribute `.attributeToAttribute()` upcast helper} for examples.
  416. //
  417. // @param {Object} config Conversion configuration.
  418. // @param {String|Object} config.view Specifies which view attribute will be converted. If a `String` is passed,
  419. // attributes with given key will be converted. If an `Object` is passed, it must have a required `key` property,
  420. // specifying view attribute key, and may have an optional `value` property, specifying view attribute value and optional `name`
  421. // property specifying a view element name from/on which the attribute should be converted. `value` can be given as a `String`,
  422. // a `RegExp` or a function callback, that takes view attribute value as the only parameter and returns `Boolean`.
  423. // @param {String|Object} config.model Model attribute key or an object with `key` and `value` properties, describing
  424. // the model attribute. `value` property may be set as a function that takes a view element and returns the value.
  425. // If `String` is given, the model attribute value will be same as view attribute value.
  426. // @param {module:utils/priorities~PriorityString} [config.converterPriority='low'] Converter priority.
  427. // @returns {Function} Conversion helper.
  428. function upcastAttributeToAttribute( config ) {
  429. config = cloneDeep( config );
  430. let viewKey = null;
  431. if ( typeof config.view == 'string' || config.view.key ) {
  432. viewKey = normalizeViewAttributeKeyValueConfig( config );
  433. }
  434. normalizeModelAttributeConfig( config, viewKey );
  435. const converter = prepareToAttributeConverter( config, true );
  436. return dispatcher => {
  437. dispatcher.on( 'element', converter, { priority: config.converterPriority || 'low' } );
  438. };
  439. }
  440. // View element to model marker conversion helper.
  441. //
  442. // See {@link ~UpcastHelpers#elementToMarker `.elementToMarker()` upcast helper} for examples.
  443. //
  444. // @param {Object} config Conversion configuration.
  445. // @param {module:engine/view/matcher~MatcherPattern} config.view Pattern matching all view elements which should be converted.
  446. // @param {String|Function} config.model Name of the model marker, or a function that takes a view element and returns
  447. // a model marker name.
  448. // @param {module:utils/priorities~PriorityString} [config.converterPriority='normal'] Converter priority.
  449. // @returns {Function} Conversion helper.
  450. function upcastElementToMarker( config ) {
  451. config = cloneDeep( config );
  452. normalizeToMarkerConfig( config );
  453. return upcastElementToElement( config );
  454. }
  455. // Helper function for from-view-element conversion. Checks if `config.view` directly specifies converted view element's name
  456. // and if so, returns it.
  457. //
  458. // @param {Object} config Conversion config.
  459. // @returns {String|null} View element name or `null` if name is not directly set.
  460. function getViewElementNameFromConfig( config ) {
  461. if ( typeof config.view == 'string' ) {
  462. return config.view;
  463. }
  464. if ( typeof config.view == 'object' && typeof config.view.name == 'string' ) {
  465. return config.view.name;
  466. }
  467. return null;
  468. }
  469. // Helper for to-model-element conversion. Takes a config object and returns a proper converter function.
  470. //
  471. // @param {Object} config Conversion configuration.
  472. // @returns {Function} View to model converter.
  473. function prepareToElementConverter( config ) {
  474. const matcher = new Matcher( config.view );
  475. return ( evt, data, conversionApi ) => {
  476. // This will be usually just one pattern but we support matchers with many patterns too.
  477. const match = matcher.match( data.viewItem );
  478. // If there is no match, this callback should not do anything.
  479. if ( !match ) {
  480. return;
  481. }
  482. // Force consuming element's name.
  483. match.match.name = true;
  484. // Create model element basing on config.
  485. const modelElement = getModelElement( config.model, data.viewItem, conversionApi.writer );
  486. // Do not convert if element building function returned falsy value.
  487. if ( !modelElement ) {
  488. return;
  489. }
  490. // When element was already consumed then skip it.
  491. if ( !conversionApi.consumable.test( data.viewItem, match.match ) ) {
  492. return;
  493. }
  494. // Find allowed parent for element that we are going to insert.
  495. // If current parent does not allow to insert element but one of the ancestors does
  496. // then split nodes to allowed parent.
  497. const splitResult = conversionApi.splitToAllowedParent( modelElement, data.modelCursor );
  498. // When there is no split result it means that we can't insert element to model tree, so let's skip it.
  499. if ( !splitResult ) {
  500. return;
  501. }
  502. // Insert element on allowed position.
  503. conversionApi.writer.insert( modelElement, splitResult.position );
  504. // Convert children and insert to element.
  505. const childrenResult = conversionApi.convertChildren( data.viewItem, conversionApi.writer.createPositionAt( modelElement, 0 ) );
  506. // Consume appropriate value from consumable values list.
  507. conversionApi.consumable.consume( data.viewItem, match.match );
  508. // Set conversion result range.
  509. data.modelRange = new ModelRange(
  510. // Range should start before inserted element
  511. conversionApi.writer.createPositionBefore( modelElement ),
  512. // Should end after but we need to take into consideration that children could split our
  513. // element, so we need to move range after parent of the last converted child.
  514. // before: <allowed>[]</allowed>
  515. // after: <allowed>[<converted><child></child></converted><child></child><converted>]</converted></allowed>
  516. conversionApi.writer.createPositionAfter( childrenResult.modelCursor.parent )
  517. );
  518. // Now we need to check where the modelCursor should be.
  519. // If we had to split parent to insert our element then we want to continue conversion inside split parent.
  520. //
  521. // before: <allowed><notAllowed>[]</notAllowed></allowed>
  522. // after: <allowed><notAllowed></notAllowed><converted></converted><notAllowed>[]</notAllowed></allowed>
  523. if ( splitResult.cursorParent ) {
  524. data.modelCursor = conversionApi.writer.createPositionAt( splitResult.cursorParent, 0 );
  525. // Otherwise just continue after inserted element.
  526. } else {
  527. data.modelCursor = data.modelRange.end;
  528. }
  529. };
  530. }
  531. // Helper function for upcasting-to-element converter. Takes the model configuration, the converted view element
  532. // and a writer instance and returns a model element instance to be inserted in the model.
  533. //
  534. // @param {String|Function|module:engine/model/element~Element} model Model conversion configuration.
  535. // @param {module:engine/view/node~Node} input The converted view node.
  536. // @param {module:engine/model/writer~Writer} writer A writer instance to use to create the model element.
  537. function getModelElement( model, input, writer ) {
  538. if ( model instanceof Function ) {
  539. return model( input, writer );
  540. } else {
  541. return writer.createElement( model );
  542. }
  543. }
  544. // Helper function view-attribute-to-model-attribute helper. Normalizes `config.view` which was set as `String` or
  545. // as an `Object` with `key`, `value` and `name` properties. Normalized `config.view` has is compatible with
  546. // {@link module:engine/view/matcher~MatcherPattern}.
  547. //
  548. // @param {Object} config Conversion config.
  549. // @returns {String} Key of the converted view attribute.
  550. function normalizeViewAttributeKeyValueConfig( config ) {
  551. if ( typeof config.view == 'string' ) {
  552. config.view = { key: config.view };
  553. }
  554. const key = config.view.key;
  555. let normalized;
  556. if ( key == 'class' || key == 'style' ) {
  557. const keyName = key == 'class' ? 'classes' : 'styles';
  558. normalized = {
  559. [ keyName ]: config.view.value
  560. };
  561. } else {
  562. const value = typeof config.view.value == 'undefined' ? /[\s\S]*/ : config.view.value;
  563. normalized = {
  564. attributes: {
  565. [ key ]: value
  566. }
  567. };
  568. }
  569. if ( config.view.name ) {
  570. normalized.name = config.view.name;
  571. }
  572. config.view = normalized;
  573. return key;
  574. }
  575. // Helper function that normalizes `config.model` in from-model-attribute conversion. `config.model` can be set
  576. // as a `String`, an `Object` with only `key` property or an `Object` with `key` and `value` properties. Normalized
  577. // `config.model` is an `Object` with `key` and `value` properties.
  578. //
  579. // @param {Object} config Conversion config.
  580. // @param {String} viewAttributeKeyToCopy Key of the converted view attribute. If it is set, model attribute value
  581. // will be equal to view attribute value.
  582. function normalizeModelAttributeConfig( config, viewAttributeKeyToCopy = null ) {
  583. const defaultModelValue = viewAttributeKeyToCopy === null ? true : viewElement => viewElement.getAttribute( viewAttributeKeyToCopy );
  584. const key = typeof config.model != 'object' ? config.model : config.model.key;
  585. const value = typeof config.model != 'object' || typeof config.model.value == 'undefined' ? defaultModelValue : config.model.value;
  586. config.model = { key, value };
  587. }
  588. // Helper for to-model-attribute conversion. Takes the model attribute name and conversion configuration and returns
  589. // a proper converter function.
  590. //
  591. // @param {String} modelAttributeKey The key of the model attribute to set on a model node.
  592. // @param {Object|Array.<Object>} config Conversion configuration. It is possible to provide multiple configurations in an array.
  593. // @param {Boolean} shallow If set to `true` the attribute will be set only on top-level nodes. Otherwise, it will be set
  594. // on all elements in the range.
  595. function prepareToAttributeConverter( config, shallow ) {
  596. const matcher = new Matcher( config.view );
  597. return ( evt, data, conversionApi ) => {
  598. const match = matcher.match( data.viewItem );
  599. // If there is no match, this callback should not do anything.
  600. if ( !match ) {
  601. return;
  602. }
  603. const modelKey = config.model.key;
  604. const modelValue = typeof config.model.value == 'function' ? config.model.value( data.viewItem ) : config.model.value;
  605. // Do not convert if attribute building function returned falsy value.
  606. if ( modelValue === null ) {
  607. return;
  608. }
  609. if ( onlyViewNameIsDefined( config ) ) {
  610. match.match.name = true;
  611. } else {
  612. // Do not test or consume `name` consumable.
  613. delete match.match.name;
  614. }
  615. // Try to consume appropriate values from consumable values list.
  616. if ( !conversionApi.consumable.test( data.viewItem, match.match ) ) {
  617. return;
  618. }
  619. // Since we are converting to attribute we need an range on which we will set the attribute.
  620. // If the range is not created yet, we will create it.
  621. if ( !data.modelRange ) {
  622. // Convert children and set conversion result as a current data.
  623. data = Object.assign( data, conversionApi.convertChildren( data.viewItem, data.modelCursor ) );
  624. }
  625. // Set attribute on current `output`. `Schema` is checked inside this helper function.
  626. const attributeWasSet = setAttributeOn( data.modelRange, { key: modelKey, value: modelValue }, shallow, conversionApi );
  627. if ( attributeWasSet ) {
  628. conversionApi.consumable.consume( data.viewItem, match.match );
  629. }
  630. };
  631. }
  632. // Helper function that checks if element name should be consumed in attribute converters.
  633. //
  634. // @param {Object} config Conversion config.
  635. // @returns {Boolean}
  636. function onlyViewNameIsDefined( config ) {
  637. if ( typeof config.view == 'object' && !getViewElementNameFromConfig( config ) ) {
  638. return false;
  639. }
  640. return !config.view.classes && !config.view.attributes && !config.view.styles;
  641. }
  642. // Helper function for to-model-attribute converter. Sets model attribute on given range. Checks {@link module:engine/model/schema~Schema}
  643. // to ensure proper model structure.
  644. //
  645. // @param {module:engine/model/range~Range} modelRange Model range on which attribute should be set.
  646. // @param {Object} modelAttribute Model attribute to set.
  647. // @param {module:engine/conversion/upcastdispatcher~UpcastConversionApi} conversionApi Conversion API.
  648. // @param {Boolean} shallow If set to `true` the attribute will be set only on top-level nodes. Otherwise, it will be set
  649. // on all elements in the range.
  650. // @returns {Boolean} `true` if attribute was set on at least one node from given `modelRange`.
  651. function setAttributeOn( modelRange, modelAttribute, shallow, conversionApi ) {
  652. let result = false;
  653. // Set attribute on each item in range according to Schema.
  654. for ( const node of Array.from( modelRange.getItems( { shallow } ) ) ) {
  655. if ( conversionApi.schema.checkAttribute( node, modelAttribute.key ) ) {
  656. conversionApi.writer.setAttribute( modelAttribute.key, modelAttribute.value, node );
  657. result = true;
  658. }
  659. }
  660. return result;
  661. }
  662. // Helper function for upcasting-to-marker conversion. Takes the config in a format requested by `upcastElementToMarker()`
  663. // function and converts it to a format that is supported by `_upcastElementToElement()` function.
  664. //
  665. // @param {Object} config Conversion configuration.
  666. function normalizeToMarkerConfig( config ) {
  667. const oldModel = config.model;
  668. config.model = ( viewElement, modelWriter ) => {
  669. const markerName = typeof oldModel == 'string' ? oldModel : oldModel( viewElement );
  670. return modelWriter.createElement( '$marker', { 'data-name': markerName } );
  671. };
  672. }