upcasthelpers.js 41 KB

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