upcasthelpers.js 38 KB

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