8
0

upcast-converters.js 34 KB

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