upcasthelpers.js 30 KB

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