upcasthelpers.js 40 KB

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