downcasthelpers.js 67 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661
  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. /**
  6. * Contains downcast (model-to-view) converters for {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher}.
  7. *
  8. * @module engine/conversion/downcasthelpers
  9. */
  10. import ModelRange from '../model/range';
  11. import ModelSelection from '../model/selection';
  12. import ModelElement from '../model/element';
  13. import ViewAttributeElement from '../view/attributeelement';
  14. import DocumentSelection from '../model/documentselection';
  15. import ConversionHelpers from './conversionhelpers';
  16. import { cloneDeep } from 'lodash-es';
  17. import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
  18. /**
  19. * Downcast conversion helper functions.
  20. *
  21. * @extends module:engine/conversion/conversionhelpers~ConversionHelpers
  22. */
  23. export default class DowncastHelpers extends ConversionHelpers {
  24. /**
  25. * Model element to view element conversion helper.
  26. *
  27. * This conversion results in creating a view element. For example, model `<paragraph>Foo</paragraph>` becomes `<p>Foo</p>` in the view.
  28. *
  29. * editor.conversion.for( 'downcast' ).elementToElement( {
  30. * model: 'paragraph',
  31. * view: 'p'
  32. * } );
  33. *
  34. * editor.conversion.for( 'downcast' ).elementToElement( {
  35. * model: 'paragraph',
  36. * view: 'div',
  37. * converterPriority: 'high'
  38. * } );
  39. *
  40. * editor.conversion.for( 'downcast' ).elementToElement( {
  41. * model: 'fancyParagraph',
  42. * view: {
  43. * name: 'p',
  44. * classes: 'fancy'
  45. * }
  46. * } );
  47. *
  48. * editor.conversion.for( 'downcast' ).elementToElement( {
  49. * model: 'heading',
  50. * view: ( modelElement, conversionApi ) => {
  51. * const { writer } = conversionApi;
  52. *
  53. * return writer.createContainerElement( 'h' + modelElement.getAttribute( 'level' ) );
  54. * }
  55. * } );
  56. *
  57. * See {@link module:engine/conversion/conversion~Conversion#for `conversion.for()`} to learn how to add a converter
  58. * to the conversion process.
  59. *
  60. * You can read more about element-to-element conversion in the
  61. * {@glink framework/guides/deep-dive/conversion/custom-element-conversion Custom element conversion} guide.
  62. *
  63. * @method #elementToElement
  64. * @param {Object} config Conversion configuration.
  65. * @param {String} config.model The name of the model element to convert.
  66. * @param {module:engine/view/elementdefinition~ElementDefinition|Function} config.view A view element definition or a function
  67. * that takes the model element and {@link module:engine/conversion/downcastdispatcher~DowncastConversionApi downcast conversion API}
  68. * as parameters and returns a view container element.
  69. * @returns {module:engine/conversion/downcasthelpers~DowncastHelpers}
  70. */
  71. elementToElement( config ) {
  72. return this.add( downcastElementToElement( config ) );
  73. }
  74. /**
  75. * Model attribute to view element conversion helper.
  76. *
  77. * This conversion results in wrapping view nodes with a view attribute element. For example, a model text node with
  78. * `"Foo"` as data and the `bold` attribute becomes `<strong>Foo</strong>` in the view.
  79. *
  80. * editor.conversion.for( 'downcast' ).attributeToElement( {
  81. * model: 'bold',
  82. * view: 'strong'
  83. * } );
  84. *
  85. * editor.conversion.for( 'downcast' ).attributeToElement( {
  86. * model: 'bold',
  87. * view: 'b',
  88. * converterPriority: 'high'
  89. * } );
  90. *
  91. * editor.conversion.for( 'downcast' ).attributeToElement( {
  92. * model: 'invert',
  93. * view: {
  94. * name: 'span',
  95. * classes: [ 'font-light', 'bg-dark' ]
  96. * }
  97. * } );
  98. *
  99. * editor.conversion.for( 'downcast' ).attributeToElement( {
  100. * model: {
  101. * key: 'fontSize',
  102. * values: [ 'big', 'small' ]
  103. * },
  104. * view: {
  105. * big: {
  106. * name: 'span',
  107. * styles: {
  108. * 'font-size': '1.2em'
  109. * }
  110. * },
  111. * small: {
  112. * name: 'span',
  113. * styles: {
  114. * 'font-size': '0.8em'
  115. * }
  116. * }
  117. * }
  118. * } );
  119. *
  120. * editor.conversion.for( 'downcast' ).attributeToElement( {
  121. * model: 'bold',
  122. * view: ( modelAttributeValue, conversionApi ) => {
  123. * const { writer } = conversionApi;
  124. *
  125. * return writer.createAttributeElement( 'span', {
  126. * style: 'font-weight:' + modelAttributeValue
  127. * } );
  128. * }
  129. * } );
  130. *
  131. * editor.conversion.for( 'downcast' ).attributeToElement( {
  132. * model: {
  133. * key: 'color',
  134. * name: '$text'
  135. * },
  136. * view: ( modelAttributeValue, conversionApi ) => {
  137. * const { writer } = conversionApi;
  138. *
  139. * return writer.createAttributeElement( 'span', {
  140. * style: 'color:' + modelAttributeValue
  141. * } );
  142. * }
  143. * } );
  144. *
  145. * See {@link module:engine/conversion/conversion~Conversion#for `conversion.for()`} to learn how to add a converter
  146. * to the conversion process.
  147. *
  148. * @method #attributeToElement
  149. * @param {Object} config Conversion configuration.
  150. * @param {String|Object} config.model The key of the attribute to convert from or a `{ key, values }` object. `values` is an array
  151. * of `String`s with possible values if the model attribute is an enumerable.
  152. * @param {module:engine/view/elementdefinition~ElementDefinition|Function|Object} config.view A view element definition or a function
  153. * that takes the model attribute value and
  154. * {@link module:engine/conversion/downcastdispatcher~DowncastConversionApi downcast conversion API} as parameters and returns a view
  155. * attribute element. If `config.model.values` is given, `config.view` should be an object assigning values from `config.model.values`
  156. * to view element definitions or functions.
  157. * @param {module:utils/priorities~PriorityString} [config.converterPriority='normal'] Converter priority.
  158. * @returns {module:engine/conversion/downcasthelpers~DowncastHelpers}
  159. */
  160. attributeToElement( config ) {
  161. return this.add( downcastAttributeToElement( config ) );
  162. }
  163. /**
  164. * Model attribute to view attribute conversion helper.
  165. *
  166. * This conversion results in adding an attribute to a view node, basing on an attribute from a model node. For example,
  167. * `<image src='foo.jpg'></image>` is converted to `<img src='foo.jpg'></img>`.
  168. *
  169. * editor.conversion.for( 'downcast' ).attributeToAttribute( {
  170. * model: 'source',
  171. * view: 'src'
  172. * } );
  173. *
  174. * editor.conversion.for( 'downcast' ).attributeToAttribute( {
  175. * model: 'source',
  176. * view: 'href',
  177. * converterPriority: 'high'
  178. * } );
  179. *
  180. * editor.conversion.for( 'downcast' ).attributeToAttribute( {
  181. * model: {
  182. * name: 'image',
  183. * key: 'source'
  184. * },
  185. * view: 'src'
  186. * } );
  187. *
  188. * editor.conversion.for( 'downcast' ).attributeToAttribute( {
  189. * model: {
  190. * name: 'styled',
  191. * values: [ 'dark', 'light' ]
  192. * },
  193. * view: {
  194. * dark: {
  195. * key: 'class',
  196. * value: [ 'styled', 'styled-dark' ]
  197. * },
  198. * light: {
  199. * key: 'class',
  200. * value: [ 'styled', 'styled-light' ]
  201. * }
  202. * }
  203. * } );
  204. *
  205. * editor.conversion.for( 'downcast' ).attributeToAttribute( {
  206. * model: 'styled',
  207. * view: modelAttributeValue => ( {
  208. * key: 'class',
  209. * value: 'styled-' + modelAttributeValue
  210. * } )
  211. * } );
  212. *
  213. * **Note**: Downcasting to a style property requires providing `value` as an object:
  214. *
  215. * editor.conversion.for( 'downcast' ).attributeToAttribute( {
  216. * model: 'lineHeight',
  217. * view: modelAttributeValue => ( {
  218. * key: 'style',
  219. * value: {
  220. * 'line-height': modelAttributeValue,
  221. * 'border-bottom': '1px dotted #ba2'
  222. * }
  223. * } )
  224. * } );
  225. *
  226. * See {@link module:engine/conversion/conversion~Conversion#for `conversion.for()`} to learn how to add a converter
  227. * to the conversion process.
  228. *
  229. * @method #attributeToAttribute
  230. * @param {Object} config Conversion configuration.
  231. * @param {String|Object} config.model The key of the attribute to convert from or a `{ key, values, [ name ] }` object describing
  232. * the attribute key, possible values and, optionally, an element name to convert from.
  233. * @param {String|Object|Function} config.view A view attribute key, or a `{ key, value }` object or a function that takes
  234. * the model attribute value and {@link module:engine/conversion/downcastdispatcher~DowncastConversionApi downcast conversion API}
  235. * as parameters and returns a `{ key, value }` object. If `key` is `'class'`, `value` can be a `String` or an
  236. * array of `String`s. If `key` is `'style'`, `value` is an object with key-value pairs. In other cases, `value` is a `String`.
  237. * If `config.model.values` is set, `config.view` should be an object assigning values from `config.model.values` to
  238. * `{ key, value }` objects or a functions.
  239. * @param {module:utils/priorities~PriorityString} [config.converterPriority='normal'] Converter priority.
  240. * @returns {module:engine/conversion/downcasthelpers~DowncastHelpers}
  241. */
  242. attributeToAttribute( config ) {
  243. return this.add( downcastAttributeToAttribute( config ) );
  244. }
  245. /**
  246. * Model marker to view element conversion helper.
  247. *
  248. * **Note**: This method should be used only for editing downcast. For data downcast, use
  249. * {@link #markerToData `#markerToData()`} that produces valid HTML data.
  250. *
  251. * This conversion results in creating a view element on the boundaries of the converted marker. If the converted marker
  252. * is collapsed, only one element is created. For example, model marker set like this: `<paragraph>F[oo b]ar</paragraph>`
  253. * becomes `<p>F<span data-marker="search"></span>oo b<span data-marker="search"></span>ar</p>` in the view.
  254. *
  255. * editor.conversion.for( 'editingDowncast' ).markerToElement( {
  256. * model: 'search',
  257. * view: 'marker-search'
  258. * } );
  259. *
  260. * editor.conversion.for( 'editingDowncast' ).markerToElement( {
  261. * model: 'search',
  262. * view: 'search-result',
  263. * converterPriority: 'high'
  264. * } );
  265. *
  266. * editor.conversion.for( 'editingDowncast' ).markerToElement( {
  267. * model: 'search',
  268. * view: {
  269. * name: 'span',
  270. * attributes: {
  271. * 'data-marker': 'search'
  272. * }
  273. * }
  274. * } );
  275. *
  276. * editor.conversion.for( 'editingDowncast' ).markerToElement( {
  277. * model: 'search',
  278. * view: ( markerData, conversionApi ) => {
  279. * const { writer } = conversionApi;
  280. *
  281. * return writer.createUIElement( 'span', {
  282. * 'data-marker': 'search',
  283. * 'data-start': markerData.isOpening
  284. * } );
  285. * }
  286. * } );
  287. *
  288. * If a function is passed as the `config.view` parameter, it will be used to generate both boundary elements. The function
  289. * receives the `data` object and {@link module:engine/conversion/downcastdispatcher~DowncastConversionApi downcast conversion API}
  290. * as a parameters and should return an instance of the
  291. * {@link module:engine/view/uielement~UIElement view UI element}. The `data` object and
  292. * {@link module:engine/conversion/downcastdispatcher~DowncastConversionApi `conversionApi`} are passed from
  293. * {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher#event:addMarker}. Additionally,
  294. * the `data.isOpening` parameter is passed, which is set to `true` for the marker start boundary element, and `false` to
  295. * the marker end boundary element.
  296. *
  297. * See {@link module:engine/conversion/conversion~Conversion#for `conversion.for()`} to learn how to add a converter
  298. * to the conversion process.
  299. *
  300. * @method #markerToElement
  301. * @param {Object} config Conversion configuration.
  302. * @param {String} config.model The name of the model marker (or model marker group) to convert.
  303. * @param {module:engine/view/elementdefinition~ElementDefinition|Function} config.view A view element definition or a function that
  304. * takes the model marker data and {@link module:engine/conversion/downcastdispatcher~DowncastConversionApi downcast conversion API}
  305. * as a parameters and returns a view UI element.
  306. * @param {module:utils/priorities~PriorityString} [config.converterPriority='normal'] Converter priority.
  307. * @returns {module:engine/conversion/downcasthelpers~DowncastHelpers}
  308. */
  309. markerToElement( config ) {
  310. return this.add( downcastMarkerToElement( config ) );
  311. }
  312. /**
  313. * Model marker to highlight conversion helper.
  314. *
  315. * This conversion results in creating a highlight on view nodes. For this kind of conversion,
  316. * {@link module:engine/conversion/downcasthelpers~HighlightDescriptor} should be provided.
  317. *
  318. * For text nodes, a `<span>` {@link module:engine/view/attributeelement~AttributeElement} is created and it wraps all text nodes
  319. * in the converted marker range. For example, a model marker set like this: `<paragraph>F[oo b]ar</paragraph>` becomes
  320. * `<p>F<span class="comment">oo b</span>ar</p>` in the view.
  321. *
  322. * {@link module:engine/view/containerelement~ContainerElement} may provide a custom way of handling highlight. Most often,
  323. * the element itself is given classes and attributes described in the highlight descriptor (instead of being wrapped in `<span>`).
  324. * For example, a model marker set like this: `[<image src="foo.jpg"></image>]` becomes `<img src="foo.jpg" class="comment"></img>`
  325. * in the view.
  326. *
  327. * For container elements, the conversion is two-step. While the converter processes the highlight descriptor and passes it
  328. * to a container element, it is the container element instance itself that applies values from the highlight descriptor.
  329. * So, in a sense, the converter takes care of stating what should be applied on what, while the element decides how to apply that.
  330. *
  331. * editor.conversion.for( 'downcast' ).markerToHighlight( { model: 'comment', view: { classes: 'comment' } } );
  332. *
  333. * editor.conversion.for( 'downcast' ).markerToHighlight( {
  334. * model: 'comment',
  335. * view: { classes: 'new-comment' },
  336. * converterPriority: 'high'
  337. * } );
  338. *
  339. * editor.conversion.for( 'downcast' ).markerToHighlight( {
  340. * model: 'comment',
  341. * view: ( data, converstionApi ) => {
  342. * // Assuming that the marker name is in a form of comment:commentType.
  343. * const commentType = data.markerName.split( ':' )[ 1 ];
  344. *
  345. * return {
  346. * classes: [ 'comment', 'comment-' + commentType ]
  347. * };
  348. * }
  349. * } );
  350. *
  351. * If a function is passed as the `config.view` parameter, it will be used to generate the highlight descriptor. The function
  352. * receives the `data` object and {@link module:engine/conversion/downcastdispatcher~DowncastConversionApi downcast conversion API}
  353. * as a parameters and should return a
  354. * {@link module:engine/conversion/downcasthelpers~HighlightDescriptor highlight descriptor}.
  355. * The `data` object properties are passed from {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher#event:addMarker}.
  356. *
  357. * See {@link module:engine/conversion/conversion~Conversion#for `conversion.for()`} to learn how to add a converter
  358. * to the conversion process.
  359. *
  360. * @method #markerToHighlight
  361. * @param {Object} config Conversion configuration.
  362. * @param {String} config.model The name of the model marker (or model marker group) to convert.
  363. * @param {module:engine/conversion/downcasthelpers~HighlightDescriptor|Function} config.view A highlight descriptor
  364. * that will be used for highlighting or a function that takes the model marker data and
  365. * {@link module:engine/conversion/downcastdispatcher~DowncastConversionApi downcast conversion API} as a parameters
  366. * and returns a highlight descriptor.
  367. * @param {module:utils/priorities~PriorityString} [config.converterPriority='normal'] Converter priority.
  368. * @returns {module:engine/conversion/downcasthelpers~DowncastHelpers}
  369. */
  370. markerToHighlight( config ) {
  371. return this.add( downcastMarkerToHighlight( config ) );
  372. }
  373. /**
  374. * Model marker converter for data downcast.
  375. *
  376. * This conversion creates a representation for model marker boundaries in the view:
  377. *
  378. * * If the marker boundary is at a position where text nodes are allowed, then a view element with the specified tag name
  379. * and `name` attribute is added at this position.
  380. * * In other cases, a specified attribute is set on a view element that is before or after the marker boundary.
  381. *
  382. * Typically, marker names use the `group:uniqueId:otherData` convention. For example: `comment:e34zfk9k2n459df53sjl34:zx32c`.
  383. * The default configuration for this conversion is that the first part is the `group` part and the rest of
  384. * the marker name becomes the `name` part.
  385. *
  386. * Tag and attribute names and values are generated from the marker name:
  387. *
  388. * * Templates for attributes are `data-[group]-start-before="[name]"`, `data-[group]-start-after="[name]"`,
  389. * `data-[group]-end-before="[name]"` and `data-[group]-end-after="[name]"`.
  390. * * Templates for view elements are `<[group]-start name="[name]">` and `<[group]-end name="[name]">`.
  391. *
  392. * Attributes mark whether the given marker's start or end boundary is before or after the given element.
  393. * Attributes `data-[group]-start-before` and `data-[group]-end-after` are favored.
  394. * The other two are used when the former two cannot be used.
  395. *
  396. * The conversion configuration can take a function that will generate different group and name parts.
  397. * If such function is set as the `config.view` parameter, it is passed a marker name and it is expected to return an object with two
  398. * properties: `group` and `name`. If the function returns a falsy value, the conversion will not take place.
  399. *
  400. * Basic usage:
  401. *
  402. * // Using the default conversion.
  403. * // In this case, all markers whose name starts with 'comment:' will be converted.
  404. * // The `group` parameter will be set to `comment`.
  405. * // The `name` parameter will be the rest of the marker name (without `:`).
  406. * editor.conversion.for( 'dataDowncast' ).markerToData( {
  407. * model: 'comment'
  408. * } );
  409. *
  410. * An example of a view that may be generated by this conversion (assuming a marker with the name `comment:commentId:uid` marked
  411. * by `[]`):
  412. *
  413. * // Model:
  414. * <paragraph>Foo[bar</paragraph>
  415. * <image src="abc.jpg"></image>]
  416. *
  417. * // View:
  418. * <p>Foo<comment-start name="commentId:uid"></comment-start>bar</p>
  419. * <figure data-comment-end-after="commentId:uid" class="image"><img src="abc.jpg" /></figure>
  420. *
  421. * In the example above, the comment starts before "bar" and ends after the image.
  422. *
  423. * If the `name` part is empty, the following view may be generated:
  424. *
  425. * <p>Foo <myMarker-start></myMarker-start>bar</p>
  426. * <figure data-myMarker-end-after="" class="image"><img src="abc.jpg" /></figure>
  427. *
  428. * **Note:** A situation where some markers have the `name` part and some do not have it is incorrect and should be avoided.
  429. *
  430. * Examples where `data-group-start-after` and `data-group-end-before` are used:
  431. *
  432. * // Model:
  433. * <blockQuote>[]<paragraph>Foo</paragraph></blockQuote>
  434. *
  435. * // View:
  436. * <blockquote><p data-group-end-before="name" data-group-start-before="name">Foo</p></blockquote>
  437. *
  438. * Similarly, when a marker is collapsed after the last element:
  439. *
  440. * // Model:
  441. * <blockQuote><paragraph>Foo</paragraph>[]</blockQuote>
  442. *
  443. * // View:
  444. * <blockquote><p data-group-end-after="name" data-group-start-after="name">Foo</p></blockquote>
  445. *
  446. * When there are multiple markers from the same group stored in the same attribute of the same element, their
  447. * name parts are put together in the attribute value, for example: `data-group-start-before="name1,name2,name3"`.
  448. *
  449. * Other examples of usage:
  450. *
  451. * // Using a custom function which is the same as the default conversion:
  452. * editor.conversion.for( 'dataDowncast' ).markerToData( {
  453. * model: 'comment'
  454. * view: markerName => ( {
  455. * group: 'comment',
  456. * name: markerName.substr( 8 ) // Removes 'comment:' part.
  457. * } )
  458. * } );
  459. *
  460. * // Using the converter priority:
  461. * editor.conversion.for( 'dataDowncast' ).markerToData( {
  462. * model: 'comment'
  463. * view: markerName => ( {
  464. * group: 'comment',
  465. * name: markerName.substr( 8 ) // Removes 'comment:' part.
  466. * } ),
  467. * converterPriority: 'high'
  468. * } );
  469. *
  470. * This kind of conversion is useful for saving data into the database, so it should be used in the data conversion pipeline.
  471. *
  472. * See {@link module:engine/conversion/conversion~Conversion#for `conversion.for()`} to learn how to add a converter
  473. * to the conversion process.
  474. *
  475. * @method #markerToData
  476. * @param {Object} config Conversion configuration.
  477. * @param {String} config.model The name of the model marker (or model marker group) to convert.
  478. * @param {Function} [config.view] A function that takes the model marker name and
  479. * {@link module:engine/conversion/downcastdispatcher~DowncastConversionApi downcast conversion API} as a parameters
  480. * and returns an object with the `group` and `name` properties.
  481. * @param {module:utils/priorities~PriorityString} [config.converterPriority='normal'] Converter priority.
  482. * @returns {module:engine/conversion/downcasthelpers~DowncastHelpers}
  483. */
  484. markerToData( config ) {
  485. return this.add( downcastMarkerToData( config ) );
  486. }
  487. }
  488. /**
  489. * Function factory that creates a default downcast converter for text insertion changes.
  490. *
  491. * The converter automatically consumes the corresponding value from the consumables list and stops the event (see
  492. * {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher}).
  493. *
  494. * modelDispatcher.on( 'insert:$text', insertText() );
  495. *
  496. * @returns {Function} Insert text event converter.
  497. */
  498. export function insertText() {
  499. return ( evt, data, conversionApi ) => {
  500. if ( !conversionApi.consumable.consume( data.item, 'insert' ) ) {
  501. return;
  502. }
  503. const viewWriter = conversionApi.writer;
  504. const viewPosition = conversionApi.mapper.toViewPosition( data.range.start );
  505. const viewText = viewWriter.createText( data.item.data );
  506. viewWriter.insert( viewPosition, viewText );
  507. };
  508. }
  509. /**
  510. * Function factory that creates a default downcast converter for node remove changes.
  511. *
  512. * modelDispatcher.on( 'remove', remove() );
  513. *
  514. * @returns {Function} Remove event converter.
  515. */
  516. export function remove() {
  517. return ( evt, data, conversionApi ) => {
  518. // Find view range start position by mapping model position at which the remove happened.
  519. const viewStart = conversionApi.mapper.toViewPosition( data.position );
  520. const modelEnd = data.position.getShiftedBy( data.length );
  521. const viewEnd = conversionApi.mapper.toViewPosition( modelEnd, { isPhantom: true } );
  522. const viewRange = conversionApi.writer.createRange( viewStart, viewEnd );
  523. // Trim the range to remove in case some UI elements are on the view range boundaries.
  524. const removed = conversionApi.writer.remove( viewRange.getTrimmed() );
  525. // After the range is removed, unbind all view elements from the model.
  526. // Range inside view document fragment is used to unbind deeply.
  527. for ( const child of conversionApi.writer.createRangeIn( removed ).getItems() ) {
  528. conversionApi.mapper.unbindViewElement( child );
  529. }
  530. };
  531. }
  532. /**
  533. * Creates a `<span>` {@link module:engine/view/attributeelement~AttributeElement view attribute element} from the information
  534. * provided by the {@link module:engine/conversion/downcasthelpers~HighlightDescriptor highlight descriptor} object. If a priority
  535. * is not provided in the descriptor, the default priority will be used.
  536. *
  537. * @param {module:engine/view/downcastwriter~DowncastWriter} writer
  538. * @param {module:engine/conversion/downcasthelpers~HighlightDescriptor} descriptor
  539. * @returns {module:engine/view/attributeelement~AttributeElement}
  540. */
  541. export function createViewElementFromHighlightDescriptor( writer, descriptor ) {
  542. const viewElement = writer.createAttributeElement( 'span', descriptor.attributes );
  543. if ( descriptor.classes ) {
  544. viewElement._addClass( descriptor.classes );
  545. }
  546. if ( descriptor.priority ) {
  547. viewElement._priority = descriptor.priority;
  548. }
  549. viewElement._id = descriptor.id;
  550. return viewElement;
  551. }
  552. /**
  553. * Function factory that creates a converter which converts a non-collapsed {@link module:engine/model/selection~Selection model selection}
  554. * to a {@link module:engine/view/documentselection~DocumentSelection view selection}. The converter consumes appropriate
  555. * value from the `consumable` object and maps model positions from the selection to view positions.
  556. *
  557. * modelDispatcher.on( 'selection', convertRangeSelection() );
  558. *
  559. * @returns {Function} Selection converter.
  560. */
  561. export function convertRangeSelection() {
  562. return ( evt, data, conversionApi ) => {
  563. const selection = data.selection;
  564. if ( selection.isCollapsed ) {
  565. return;
  566. }
  567. if ( !conversionApi.consumable.consume( selection, 'selection' ) ) {
  568. return;
  569. }
  570. const viewRanges = [];
  571. for ( const range of selection.getRanges() ) {
  572. const viewRange = conversionApi.mapper.toViewRange( range );
  573. viewRanges.push( viewRange );
  574. }
  575. conversionApi.writer.setSelection( viewRanges, { backward: selection.isBackward } );
  576. };
  577. }
  578. /**
  579. * Function factory that creates a converter which converts a collapsed {@link module:engine/model/selection~Selection model selection} to
  580. * a {@link module:engine/view/documentselection~DocumentSelection view selection}. The converter consumes appropriate
  581. * value from the `consumable` object, maps the model selection position to the view position and breaks
  582. * {@link module:engine/view/attributeelement~AttributeElement attribute elements} at the selection position.
  583. *
  584. * modelDispatcher.on( 'selection', convertCollapsedSelection() );
  585. *
  586. * An example of the view state before and after converting the collapsed selection:
  587. *
  588. * <p><strong>f^oo<strong>bar</p>
  589. * -> <p><strong>f</strong>^<strong>oo</strong>bar</p>
  590. *
  591. * By breaking attribute elements like `<strong>`, the selection is in a correct element. Then, when the selection attribute is
  592. * converted, broken attributes might be merged again, or the position where the selection is may be wrapped
  593. * with different, appropriate attribute elements.
  594. *
  595. * See also {@link module:engine/conversion/downcasthelpers~clearAttributes} which does a clean-up
  596. * by merging attributes.
  597. *
  598. * @returns {Function} Selection converter.
  599. */
  600. export function convertCollapsedSelection() {
  601. return ( evt, data, conversionApi ) => {
  602. const selection = data.selection;
  603. if ( !selection.isCollapsed ) {
  604. return;
  605. }
  606. if ( !conversionApi.consumable.consume( selection, 'selection' ) ) {
  607. return;
  608. }
  609. const viewWriter = conversionApi.writer;
  610. const modelPosition = selection.getFirstPosition();
  611. const viewPosition = conversionApi.mapper.toViewPosition( modelPosition );
  612. const brokenPosition = viewWriter.breakAttributes( viewPosition );
  613. viewWriter.setSelection( brokenPosition );
  614. };
  615. }
  616. /**
  617. * Function factory that creates a converter which clears artifacts after the previous
  618. * {@link module:engine/model/selection~Selection model selection} conversion. It removes all empty
  619. * {@link module:engine/view/attributeelement~AttributeElement view attribute elements} and merges sibling attributes at all start and end
  620. * positions of all ranges.
  621. *
  622. * <p><strong>^</strong></p>
  623. * -> <p>^</p>
  624. *
  625. * <p><strong>foo</strong>^<strong>bar</strong>bar</p>
  626. * -> <p><strong>foo^bar<strong>bar</p>
  627. *
  628. * <p><strong>foo</strong><em>^</em><strong>bar</strong>bar</p>
  629. * -> <p><strong>foo^bar<strong>bar</p>
  630. *
  631. * This listener should be assigned before any converter for the new selection:
  632. *
  633. * modelDispatcher.on( 'selection', clearAttributes() );
  634. *
  635. * See {@link module:engine/conversion/downcasthelpers~convertCollapsedSelection}
  636. * which does the opposite by breaking attributes in the selection position.
  637. *
  638. * @returns {Function} Selection converter.
  639. */
  640. export function clearAttributes() {
  641. return ( evt, data, conversionApi ) => {
  642. const viewWriter = conversionApi.writer;
  643. const viewSelection = viewWriter.document.selection;
  644. for ( const range of viewSelection.getRanges() ) {
  645. // Not collapsed selection should not have artifacts.
  646. if ( range.isCollapsed ) {
  647. // Position might be in the node removed by the view writer.
  648. if ( range.end.parent.isAttached() ) {
  649. conversionApi.writer.mergeAttributes( range.start );
  650. }
  651. }
  652. }
  653. viewWriter.setSelection( null );
  654. };
  655. }
  656. /**
  657. * Function factory that creates a converter which converts set/change/remove attribute changes from the model to the view.
  658. * It can also be used to convert selection attributes. In that case, an empty attribute element will be created and the
  659. * selection will be put inside it.
  660. *
  661. * Attributes from the model are converted to a view element that will be wrapping these view nodes that are bound to
  662. * model elements having the given attribute. This is useful for attributes like `bold` that may be set on text nodes in the model
  663. * but are represented as an element in the view:
  664. *
  665. * [paragraph] MODEL ====> VIEW <p>
  666. * |- a {bold: true} |- <b>
  667. * |- b {bold: true} | |- ab
  668. * |- c |- c
  669. *
  670. * Passed `Function` will be provided with the attribute value and then all the parameters of the
  671. * {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher#event:attribute `attribute` event}.
  672. * It is expected that the function returns an {@link module:engine/view/element~Element}.
  673. * The result of the function will be the wrapping element.
  674. * When the provided `Function` does not return any element, no conversion will take place.
  675. *
  676. * The converter automatically consumes the corresponding value from the consumables list and stops the event (see
  677. * {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher}).
  678. *
  679. * modelDispatcher.on( 'attribute:bold', wrap( ( modelAttributeValue, { writer } ) => {
  680. * return writer.createAttributeElement( 'strong' );
  681. * } );
  682. *
  683. * @protected
  684. * @param {Function} elementCreator Function returning a view element that will be used for wrapping.
  685. * @returns {Function} Set/change attribute converter.
  686. */
  687. export function wrap( elementCreator ) {
  688. return ( evt, data, conversionApi ) => {
  689. // Recreate current wrapping node. It will be used to unwrap view range if the attribute value has changed
  690. // or the attribute was removed.
  691. const oldViewElement = elementCreator( data.attributeOldValue, conversionApi );
  692. // Create node to wrap with.
  693. const newViewElement = elementCreator( data.attributeNewValue, conversionApi );
  694. if ( !oldViewElement && !newViewElement ) {
  695. return;
  696. }
  697. if ( !conversionApi.consumable.consume( data.item, evt.name ) ) {
  698. return;
  699. }
  700. const viewWriter = conversionApi.writer;
  701. const viewSelection = viewWriter.document.selection;
  702. if ( data.item instanceof ModelSelection || data.item instanceof DocumentSelection ) {
  703. // Selection attribute conversion.
  704. viewWriter.wrap( viewSelection.getFirstRange(), newViewElement );
  705. } else {
  706. // Node attribute conversion.
  707. let viewRange = conversionApi.mapper.toViewRange( data.range );
  708. // First, unwrap the range from current wrapper.
  709. if ( data.attributeOldValue !== null && oldViewElement ) {
  710. viewRange = viewWriter.unwrap( viewRange, oldViewElement );
  711. }
  712. if ( data.attributeNewValue !== null && newViewElement ) {
  713. viewWriter.wrap( viewRange, newViewElement );
  714. }
  715. }
  716. };
  717. }
  718. /**
  719. * Function factory that creates a converter which converts node insertion changes from the model to the view.
  720. * The function passed will be provided with all the parameters of the dispatcher's
  721. * {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher#event:insert `insert` event}.
  722. * It is expected that the function returns an {@link module:engine/view/element~Element}.
  723. * The result of the function will be inserted into the view.
  724. *
  725. * The converter automatically consumes the corresponding value from the consumables list, stops the event (see
  726. * {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher}) and binds the model and view elements.
  727. *
  728. * downcastDispatcher.on(
  729. * 'insert:myElem',
  730. * insertElement( ( modelItem, { writer } ) => {
  731. * const text = writer.createText( 'myText' );
  732. * const myElem = writer.createElement( 'myElem', { myAttr: 'my-' + modelItem.getAttribute( 'myAttr' ) }, text );
  733. *
  734. * // Do something fancy with `myElem` using `modelItem` or other parameters.
  735. *
  736. * return myElem;
  737. * }
  738. * ) );
  739. *
  740. * @protected
  741. * @param {Function} elementCreator Function returning a view element, which will be inserted.
  742. * @returns {Function} Insert element event converter.
  743. */
  744. export function insertElement( elementCreator ) {
  745. return ( evt, data, conversionApi ) => {
  746. const viewElement = elementCreator( data.item, conversionApi );
  747. if ( !viewElement ) {
  748. return;
  749. }
  750. if ( !conversionApi.consumable.consume( data.item, 'insert' ) ) {
  751. return;
  752. }
  753. const viewPosition = conversionApi.mapper.toViewPosition( data.range.start );
  754. conversionApi.mapper.bindElements( data.item, viewElement );
  755. conversionApi.writer.insert( viewPosition, viewElement );
  756. };
  757. }
  758. /**
  759. * Function factory that creates a converter which converts marker adding change to the
  760. * {@link module:engine/view/uielement~UIElement view UI element}.
  761. *
  762. * The view UI element that will be added to the view depends on the passed parameter. See {@link ~insertElement}.
  763. * In case of a non-collapsed range, the UI element will not wrap nodes but separate elements will be placed at the beginning
  764. * and at the end of the range.
  765. *
  766. * This converter binds created UI elements with the marker name using {@link module:engine/conversion/mapper~Mapper#bindElementToMarker}.
  767. *
  768. * @protected
  769. * @param {module:engine/view/uielement~UIElement|Function} elementCreator A view UI element or a function returning the view element
  770. * that will be inserted.
  771. * @returns {Function} Insert element event converter.
  772. */
  773. export function insertUIElement( elementCreator ) {
  774. return ( evt, data, conversionApi ) => {
  775. // Create two view elements. One will be inserted at the beginning of marker, one at the end.
  776. // If marker is collapsed, only "opening" element will be inserted.
  777. data.isOpening = true;
  778. const viewStartElement = elementCreator( data, conversionApi );
  779. data.isOpening = false;
  780. const viewEndElement = elementCreator( data, conversionApi );
  781. if ( !viewStartElement || !viewEndElement ) {
  782. return;
  783. }
  784. const markerRange = data.markerRange;
  785. // Marker that is collapsed has consumable build differently that non-collapsed one.
  786. // For more information see `addMarker` event description.
  787. // If marker's range is collapsed - check if it can be consumed.
  788. if ( markerRange.isCollapsed && !conversionApi.consumable.consume( markerRange, evt.name ) ) {
  789. return;
  790. }
  791. // If marker's range is not collapsed - consume all items inside.
  792. for ( const value of markerRange ) {
  793. if ( !conversionApi.consumable.consume( value.item, evt.name ) ) {
  794. return;
  795. }
  796. }
  797. const mapper = conversionApi.mapper;
  798. const viewWriter = conversionApi.writer;
  799. // Add "opening" element.
  800. viewWriter.insert( mapper.toViewPosition( markerRange.start ), viewStartElement );
  801. conversionApi.mapper.bindElementToMarker( viewStartElement, data.markerName );
  802. // Add "closing" element only if range is not collapsed.
  803. if ( !markerRange.isCollapsed ) {
  804. viewWriter.insert( mapper.toViewPosition( markerRange.end ), viewEndElement );
  805. conversionApi.mapper.bindElementToMarker( viewEndElement, data.markerName );
  806. }
  807. evt.stop();
  808. };
  809. }
  810. // Function factory that returns a default downcast converter for removing a {@link module:engine/view/uielement~UIElement UI element}
  811. // based on marker remove change.
  812. //
  813. // This converter unbinds elements from the marker name.
  814. //
  815. // @returns {Function} Removed UI element converter.
  816. function removeUIElement() {
  817. return ( evt, data, conversionApi ) => {
  818. const elements = conversionApi.mapper.markerNameToElements( data.markerName );
  819. if ( !elements ) {
  820. return;
  821. }
  822. for ( const element of elements ) {
  823. conversionApi.mapper.unbindElementFromMarkerName( element, data.markerName );
  824. conversionApi.writer.clear( conversionApi.writer.createRangeOn( element ), element );
  825. }
  826. conversionApi.writer.clearClonedElementsGroup( data.markerName );
  827. evt.stop();
  828. };
  829. }
  830. // Function factory that creates a default converter for model markers.
  831. //
  832. // See {@link DowncastHelpers#markerToData} for more information what type of view is generated.
  833. //
  834. // This converter binds created UI elements and affected view elements with the marker name
  835. // using {@link module:engine/conversion/mapper~Mapper#bindElementToMarker}.
  836. //
  837. // @returns {Function} Add marker converter.
  838. function insertMarkerData( viewCreator ) {
  839. return ( evt, data, conversionApi ) => {
  840. const viewMarkerData = viewCreator( data.markerName, conversionApi );
  841. if ( !viewMarkerData ) {
  842. return;
  843. }
  844. const markerRange = data.markerRange;
  845. if ( !conversionApi.consumable.consume( markerRange, evt.name ) ) {
  846. return;
  847. }
  848. // Adding closing data first to keep the proper order in the view.
  849. handleMarkerBoundary( markerRange, false, conversionApi, data, viewMarkerData );
  850. handleMarkerBoundary( markerRange, true, conversionApi, data, viewMarkerData );
  851. evt.stop();
  852. };
  853. }
  854. // Helper function for `insertMarkerData()` that marks a marker boundary at the beginning or end of given `range`.
  855. function handleMarkerBoundary( range, isStart, conversionApi, data, viewMarkerData ) {
  856. const modelPosition = isStart ? range.start : range.end;
  857. const canInsertElement = conversionApi.schema.checkChild( modelPosition, '$text' );
  858. if ( canInsertElement ) {
  859. const viewPosition = conversionApi.mapper.toViewPosition( modelPosition );
  860. insertMarkerAsElement( viewPosition, isStart, conversionApi, data, viewMarkerData );
  861. } else {
  862. let modelElement;
  863. let isBefore;
  864. // If possible, we want to add `data-group-start-before` and `data-group-end-after` attributes.
  865. // Below `if` is constructed in a way that will favor adding these attributes.
  866. //
  867. // Also, I assume that there will be always an element either after or before the position.
  868. // If not, then it is a case when we are not in a position where text is allowed and also there are no elements around...
  869. if ( isStart && modelPosition.nodeAfter || !isStart && !modelPosition.nodeBefore ) {
  870. modelElement = modelPosition.nodeAfter;
  871. isBefore = true;
  872. } else {
  873. modelElement = modelPosition.nodeBefore;
  874. isBefore = false;
  875. }
  876. const viewElement = conversionApi.mapper.toViewElement( modelElement );
  877. insertMarkerAsAttribute( viewElement, isStart, isBefore, conversionApi, data, viewMarkerData );
  878. }
  879. }
  880. // Helper function for `insertMarkerData()` that marks a marker boundary in the view as an attribute on a view element.
  881. function insertMarkerAsAttribute( viewElement, isStart, isBefore, conversionApi, data, viewMarkerData ) {
  882. const attributeName = `data-${ viewMarkerData.group }-${ isStart ? 'start' : 'end' }-${ isBefore ? 'before' : 'after' }`;
  883. const markerNames = viewElement.hasAttribute( attributeName ) ? viewElement.getAttribute( attributeName ).split( ',' ) : [];
  884. // Adding marker name at the beginning to have the same order in the attribute as there is with marker elements.
  885. markerNames.unshift( viewMarkerData.name );
  886. conversionApi.writer.setAttribute( attributeName, markerNames.join( ',' ), viewElement );
  887. conversionApi.mapper.bindElementToMarker( viewElement, data.markerName );
  888. }
  889. // Helper function for `insertMarkerData()` that marks a marker boundary in the view as a separate view ui element.
  890. function insertMarkerAsElement( position, isStart, conversionApi, data, viewMarkerData ) {
  891. const viewElementName = `${ viewMarkerData.group }-${ isStart ? 'start' : 'end' }`;
  892. const attrs = viewMarkerData.name ? { 'name': viewMarkerData.name } : null;
  893. const viewElement = conversionApi.writer.createUIElement( viewElementName, attrs );
  894. conversionApi.writer.insert( position, viewElement );
  895. conversionApi.mapper.bindElementToMarker( viewElement, data.markerName );
  896. }
  897. // Function factory that creates a converter for removing a model marker data added by the {@link #insertMarkerData} converter.
  898. //
  899. // @returns {Function} Remove marker converter.
  900. function removeMarkerData( viewCreator ) {
  901. return ( evt, data, conversionApi ) => {
  902. const viewData = viewCreator( data.markerName, conversionApi );
  903. if ( !viewData ) {
  904. return;
  905. }
  906. const elements = conversionApi.mapper.markerNameToElements( data.markerName );
  907. if ( !elements ) {
  908. return;
  909. }
  910. for ( const element of elements ) {
  911. conversionApi.mapper.unbindElementFromMarkerName( element, data.markerName );
  912. if ( element.is( 'containerElement' ) ) {
  913. removeMarkerFromAttribute( `data-${ viewData.group }-start-before`, element );
  914. removeMarkerFromAttribute( `data-${ viewData.group }-start-after`, element );
  915. removeMarkerFromAttribute( `data-${ viewData.group }-end-before`, element );
  916. removeMarkerFromAttribute( `data-${ viewData.group }-end-after`, element );
  917. } else {
  918. conversionApi.writer.clear( conversionApi.writer.createRangeOn( element ), element );
  919. }
  920. }
  921. conversionApi.writer.clearClonedElementsGroup( data.markerName );
  922. evt.stop();
  923. function removeMarkerFromAttribute( attributeName, element ) {
  924. if ( element.hasAttribute( attributeName ) ) {
  925. const markerNames = new Set( element.getAttribute( attributeName ).split( ',' ) );
  926. markerNames.delete( viewData.name );
  927. if ( markerNames.size == 0 ) {
  928. conversionApi.writer.removeAttribute( attributeName, element );
  929. } else {
  930. conversionApi.writer.setAttribute( attributeName, Array.from( markerNames ).join( ',' ), element );
  931. }
  932. }
  933. }
  934. };
  935. }
  936. // Function factory that creates a converter which converts set/change/remove attribute changes from the model to the view.
  937. //
  938. // Attributes from the model are converted to the view element attributes in the view. You may provide a custom function to generate
  939. // a key-value attribute pair to add/change/remove. If not provided, model attributes will be converted to view element
  940. // attributes on a one-to-one basis.
  941. //
  942. // *Note:** The provided attribute creator should always return the same `key` for a given attribute from the model.
  943. //
  944. // The converter automatically consumes the corresponding value from the consumables list and stops the event (see
  945. // {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher}).
  946. //
  947. // modelDispatcher.on( 'attribute:customAttr:myElem', changeAttribute( ( value, data ) => {
  948. // // Change attribute key from `customAttr` to `class` in the view.
  949. // const key = 'class';
  950. // let value = data.attributeNewValue;
  951. //
  952. // // Force attribute value to 'empty' if the model element is empty.
  953. // if ( data.item.childCount === 0 ) {
  954. // value = 'empty';
  955. // }
  956. //
  957. // // Return the key-value pair.
  958. // return { key, value };
  959. // } ) );
  960. //
  961. // @param {Function} [attributeCreator] Function returning an object with two properties: `key` and `value`, which
  962. // represent the attribute key and attribute value to be set on a {@link module:engine/view/element~Element view element}.
  963. // The function is passed the model attribute value as the first parameter and additional data about the change as the second parameter.
  964. // @returns {Function} Set/change attribute converter.
  965. function changeAttribute( attributeCreator ) {
  966. return ( evt, data, conversionApi ) => {
  967. const oldAttribute = attributeCreator( data.attributeOldValue, conversionApi );
  968. const newAttribute = attributeCreator( data.attributeNewValue, conversionApi );
  969. if ( !oldAttribute && !newAttribute ) {
  970. return;
  971. }
  972. if ( !conversionApi.consumable.consume( data.item, evt.name ) ) {
  973. return;
  974. }
  975. const viewElement = conversionApi.mapper.toViewElement( data.item );
  976. const viewWriter = conversionApi.writer;
  977. // If model item cannot be mapped to a view element, it means item is not an `Element` instance but a `TextProxy` node.
  978. // Only elements can have attributes in a view so do not proceed for anything else (#1587).
  979. if ( !viewElement ) {
  980. /**
  981. * This error occurs when a {@link module:engine/model/textproxy~TextProxy text node's} attribute is to be downcasted
  982. * by {@link module:engine/conversion/conversion~Conversion#attributeToAttribute `Attribute to Attribute converter`}.
  983. * In most cases it is caused by converters misconfiguration when only "generic" converter is defined:
  984. *
  985. * editor.conversion.for( 'downcast' ).attributeToAttribute( {
  986. * model: 'attribute-name',
  987. * view: 'attribute-name'
  988. * } ) );
  989. *
  990. * and given attribute is used on text node, for example:
  991. *
  992. * model.change( writer => {
  993. * writer.insertText( 'Foo', { 'attribute-name': 'bar' }, parent, 0 );
  994. * } );
  995. *
  996. * In such cases, to convert the same attribute for both {@link module:engine/model/element~Element}
  997. * and {@link module:engine/model/textproxy~TextProxy `Text`} nodes, text specific
  998. * {@link module:engine/conversion/conversion~Conversion#attributeToElement `Attribute to Element converter`}
  999. * with higher {@link module:utils/priorities~PriorityString priority} must also be defined:
  1000. *
  1001. * editor.conversion.for( 'downcast' ).attributeToElement( {
  1002. * model: {
  1003. * key: 'attribute-name',
  1004. * name: '$text'
  1005. * },
  1006. * view: ( value, { writer } ) => {
  1007. * return writer.createAttributeElement( 'span', { 'attribute-name': value } );
  1008. * },
  1009. * converterPriority: 'high'
  1010. * } ) );
  1011. *
  1012. * @error conversion-attribute-to-attribute-on-text
  1013. */
  1014. throw new CKEditorError(
  1015. 'conversion-attribute-to-attribute-on-text',
  1016. [ data, conversionApi ]
  1017. );
  1018. }
  1019. // First remove the old attribute if there was one.
  1020. if ( data.attributeOldValue !== null && oldAttribute ) {
  1021. if ( oldAttribute.key == 'class' ) {
  1022. const classes = Array.isArray( oldAttribute.value ) ? oldAttribute.value : [ oldAttribute.value ];
  1023. for ( const className of classes ) {
  1024. viewWriter.removeClass( className, viewElement );
  1025. }
  1026. } else if ( oldAttribute.key == 'style' ) {
  1027. const keys = Object.keys( oldAttribute.value );
  1028. for ( const key of keys ) {
  1029. viewWriter.removeStyle( key, viewElement );
  1030. }
  1031. } else {
  1032. viewWriter.removeAttribute( oldAttribute.key, viewElement );
  1033. }
  1034. }
  1035. // Then set the new attribute.
  1036. if ( data.attributeNewValue !== null && newAttribute ) {
  1037. if ( newAttribute.key == 'class' ) {
  1038. const classes = Array.isArray( newAttribute.value ) ? newAttribute.value : [ newAttribute.value ];
  1039. for ( const className of classes ) {
  1040. viewWriter.addClass( className, viewElement );
  1041. }
  1042. } else if ( newAttribute.key == 'style' ) {
  1043. const keys = Object.keys( newAttribute.value );
  1044. for ( const key of keys ) {
  1045. viewWriter.setStyle( key, newAttribute.value[ key ], viewElement );
  1046. }
  1047. } else {
  1048. viewWriter.setAttribute( newAttribute.key, newAttribute.value, viewElement );
  1049. }
  1050. }
  1051. };
  1052. }
  1053. // Function factory that creates a converter which converts the text inside marker's range. The converter wraps the text with
  1054. // {@link module:engine/view/attributeelement~AttributeElement} created from the provided descriptor.
  1055. // See {link module:engine/conversion/downcasthelpers~createViewElementFromHighlightDescriptor}.
  1056. //
  1057. // It can also be used to convert the selection that is inside a marker. In that case, an empty attribute element will be
  1058. // created and the selection will be put inside it.
  1059. //
  1060. // If the highlight descriptor does not provide the `priority` property, `10` will be used.
  1061. //
  1062. // If the highlight descriptor does not provide the `id` property, the name of the marker will be used.
  1063. //
  1064. // This converter binds the created {@link module:engine/view/attributeelement~AttributeElement attribute elemens} with the marker name
  1065. // using the {@link module:engine/conversion/mapper~Mapper#bindElementToMarker} method.
  1066. //
  1067. // @param {module:engine/conversion/downcasthelpers~HighlightDescriptor|Function} highlightDescriptor
  1068. // @returns {Function}
  1069. function highlightText( highlightDescriptor ) {
  1070. return ( evt, data, conversionApi ) => {
  1071. if ( !data.item ) {
  1072. return;
  1073. }
  1074. if ( !( data.item instanceof ModelSelection || data.item instanceof DocumentSelection ) && !data.item.is( '$textProxy' ) ) {
  1075. return;
  1076. }
  1077. const descriptor = prepareDescriptor( highlightDescriptor, data, conversionApi );
  1078. if ( !descriptor ) {
  1079. return;
  1080. }
  1081. if ( !conversionApi.consumable.consume( data.item, evt.name ) ) {
  1082. return;
  1083. }
  1084. const viewWriter = conversionApi.writer;
  1085. const viewElement = createViewElementFromHighlightDescriptor( viewWriter, descriptor );
  1086. const viewSelection = viewWriter.document.selection;
  1087. if ( data.item instanceof ModelSelection || data.item instanceof DocumentSelection ) {
  1088. viewWriter.wrap( viewSelection.getFirstRange(), viewElement, viewSelection );
  1089. } else {
  1090. const viewRange = conversionApi.mapper.toViewRange( data.range );
  1091. const rangeAfterWrap = viewWriter.wrap( viewRange, viewElement );
  1092. for ( const element of rangeAfterWrap.getItems() ) {
  1093. if ( element.is( 'attributeElement' ) && element.isSimilar( viewElement ) ) {
  1094. conversionApi.mapper.bindElementToMarker( element, data.markerName );
  1095. // One attribute element is enough, because all of them are bound together by the view writer.
  1096. // Mapper uses this binding to get all the elements no matter how many of them are registered in the mapper.
  1097. break;
  1098. }
  1099. }
  1100. }
  1101. };
  1102. }
  1103. // Converter function factory. It creates a function which applies the marker's highlight to an element inside the marker's range.
  1104. //
  1105. // The converter checks if an element has the `addHighlight` function stored as a
  1106. // {@link module:engine/view/element~Element#_setCustomProperty custom property} and, if so, uses it to apply the highlight.
  1107. // In such case the converter will consume all element's children, assuming that they were handled by the element itself.
  1108. //
  1109. // When the `addHighlight` custom property is not present, the element is not converted in any special way.
  1110. // This means that converters will proceed to convert the element's child nodes.
  1111. //
  1112. // If the highlight descriptor does not provide the `priority` property, `10` will be used.
  1113. //
  1114. // If the highlight descriptor does not provide the `id` property, the name of the marker will be used.
  1115. //
  1116. // This converter binds altered {@link module:engine/view/containerelement~ContainerElement container elements} with the marker name using
  1117. // the {@link module:engine/conversion/mapper~Mapper#bindElementToMarker} method.
  1118. //
  1119. // @param {module:engine/conversion/downcasthelpers~HighlightDescriptor|Function} highlightDescriptor
  1120. // @returns {Function}
  1121. function highlightElement( highlightDescriptor ) {
  1122. return ( evt, data, conversionApi ) => {
  1123. if ( !data.item ) {
  1124. return;
  1125. }
  1126. if ( !( data.item instanceof ModelElement ) ) {
  1127. return;
  1128. }
  1129. const descriptor = prepareDescriptor( highlightDescriptor, data, conversionApi );
  1130. if ( !descriptor ) {
  1131. return;
  1132. }
  1133. if ( !conversionApi.consumable.test( data.item, evt.name ) ) {
  1134. return;
  1135. }
  1136. const viewElement = conversionApi.mapper.toViewElement( data.item );
  1137. if ( viewElement && viewElement.getCustomProperty( 'addHighlight' ) ) {
  1138. // Consume element itself.
  1139. conversionApi.consumable.consume( data.item, evt.name );
  1140. // Consume all children nodes.
  1141. for ( const value of ModelRange._createIn( data.item ) ) {
  1142. conversionApi.consumable.consume( value.item, evt.name );
  1143. }
  1144. viewElement.getCustomProperty( 'addHighlight' )( viewElement, descriptor, conversionApi.writer );
  1145. conversionApi.mapper.bindElementToMarker( viewElement, data.markerName );
  1146. }
  1147. };
  1148. }
  1149. // Function factory that creates a converter which converts the removing model marker to the view.
  1150. //
  1151. // Both text nodes and elements are handled by this converter but they are handled a bit differently.
  1152. //
  1153. // Text nodes are unwrapped using the {@link module:engine/view/attributeelement~AttributeElement attribute element} created from the
  1154. // provided highlight descriptor. See {link module:engine/conversion/downcasthelpers~HighlightDescriptor}.
  1155. //
  1156. // For elements, the converter checks if an element has the `removeHighlight` function stored as a
  1157. // {@link module:engine/view/element~Element#_setCustomProperty custom property}. If so, it uses it to remove the highlight.
  1158. // In such case, the children of that element will not be converted.
  1159. //
  1160. // When `removeHighlight` is not present, the element is not converted in any special way.
  1161. // The converter will proceed to convert the element's child nodes instead.
  1162. //
  1163. // If the highlight descriptor does not provide the `priority` property, `10` will be used.
  1164. //
  1165. // If the highlight descriptor does not provide the `id` property, the name of the marker will be used.
  1166. //
  1167. // This converter unbinds elements from the marker name.
  1168. //
  1169. // @param {module:engine/conversion/downcasthelpers~HighlightDescriptor|Function} highlightDescriptor
  1170. // @returns {Function}
  1171. function removeHighlight( highlightDescriptor ) {
  1172. return ( evt, data, conversionApi ) => {
  1173. // This conversion makes sense only for non-collapsed range.
  1174. if ( data.markerRange.isCollapsed ) {
  1175. return;
  1176. }
  1177. const descriptor = prepareDescriptor( highlightDescriptor, data, conversionApi );
  1178. if ( !descriptor ) {
  1179. return;
  1180. }
  1181. // View element that will be used to unwrap `AttributeElement`s.
  1182. const viewHighlightElement = createViewElementFromHighlightDescriptor( conversionApi.writer, descriptor );
  1183. // Get all elements bound with given marker name.
  1184. const elements = conversionApi.mapper.markerNameToElements( data.markerName );
  1185. if ( !elements ) {
  1186. return;
  1187. }
  1188. for ( const element of elements ) {
  1189. conversionApi.mapper.unbindElementFromMarkerName( element, data.markerName );
  1190. if ( element.is( 'attributeElement' ) ) {
  1191. conversionApi.writer.unwrap( conversionApi.writer.createRangeOn( element ), viewHighlightElement );
  1192. } else {
  1193. // if element.is( 'containerElement' ).
  1194. element.getCustomProperty( 'removeHighlight' )( element, descriptor.id, conversionApi.writer );
  1195. }
  1196. }
  1197. conversionApi.writer.clearClonedElementsGroup( data.markerName );
  1198. evt.stop();
  1199. };
  1200. }
  1201. // Model element to view element conversion helper.
  1202. //
  1203. // See {@link ~DowncastHelpers#elementToElement `.elementToElement()` downcast helper} for examples.
  1204. //
  1205. // @param {Object} config Conversion configuration.
  1206. // @param {String} config.model The name of the model element to convert.
  1207. // @param {module:engine/view/elementdefinition~ElementDefinition|Function} config.view A view element definition or a function
  1208. // that takes the model element and {@link module:engine/view/downcastwriter~DowncastWriter view downcast writer}
  1209. // as parameters and returns a view container element.
  1210. // @returns {Function} Conversion helper.
  1211. function downcastElementToElement( config ) {
  1212. config = cloneDeep( config );
  1213. config.view = normalizeToElementConfig( config.view, 'container' );
  1214. return dispatcher => {
  1215. dispatcher.on( 'insert:' + config.model, insertElement( config.view ), { priority: config.converterPriority || 'normal' } );
  1216. };
  1217. }
  1218. // Model attribute to view element conversion helper.
  1219. //
  1220. // See {@link ~DowncastHelpers#attributeToElement `.attributeToElement()` downcast helper} for examples.
  1221. //
  1222. // @param {Object} config Conversion configuration.
  1223. // @param {String|Object} config.model The key of the attribute to convert from or a `{ key, values }` object. `values` is an array
  1224. // of `String`s with possible values if the model attribute is an enumerable.
  1225. // @param {module:engine/view/elementdefinition~ElementDefinition|Function|Object} config.view A view element definition or a function
  1226. // that takes the model attribute value and {@link module:engine/view/downcastwriter~DowncastWriter view downcast writer}
  1227. // as parameters and returns a view attribute element. If `config.model.values` is
  1228. // given, `config.view` should be an object assigning values from `config.model.values` to view element definitions or functions.
  1229. // @param {module:utils/priorities~PriorityString} [config.converterPriority='normal'] Converter priority.
  1230. // @returns {Function} Conversion helper.
  1231. function downcastAttributeToElement( config ) {
  1232. config = cloneDeep( config );
  1233. const modelKey = config.model.key ? config.model.key : config.model;
  1234. let eventName = 'attribute:' + modelKey;
  1235. if ( config.model.name ) {
  1236. eventName += ':' + config.model.name;
  1237. }
  1238. if ( config.model.values ) {
  1239. for ( const modelValue of config.model.values ) {
  1240. config.view[ modelValue ] = normalizeToElementConfig( config.view[ modelValue ], 'attribute' );
  1241. }
  1242. } else {
  1243. config.view = normalizeToElementConfig( config.view, 'attribute' );
  1244. }
  1245. const elementCreator = getFromAttributeCreator( config );
  1246. return dispatcher => {
  1247. dispatcher.on( eventName, wrap( elementCreator ), { priority: config.converterPriority || 'normal' } );
  1248. };
  1249. }
  1250. // Model attribute to view attribute conversion helper.
  1251. //
  1252. // See {@link ~DowncastHelpers#attributeToAttribute `.attributeToAttribute()` downcast helper} for examples.
  1253. //
  1254. // @param {Object} config Conversion configuration.
  1255. // @param {String|Object} config.model The key of the attribute to convert from or a `{ key, values, [ name ] }` object describing
  1256. // the attribute key, possible values and, optionally, an element name to convert from.
  1257. // @param {String|Object|Function} config.view A view attribute key, or a `{ key, value }` object or a function that takes
  1258. // the model attribute value and returns a `{ key, value }` object. If `key` is `'class'`, `value` can be a `String` or an
  1259. // array of `String`s. If `key` is `'style'`, `value` is an object with key-value pairs. In other cases, `value` is a `String`.
  1260. // If `config.model.values` is set, `config.view` should be an object assigning values from `config.model.values` to
  1261. // `{ key, value }` objects or a functions.
  1262. // @param {module:utils/priorities~PriorityString} [config.converterPriority='normal'] Converter priority.
  1263. // @returns {Function} Conversion helper.
  1264. function downcastAttributeToAttribute( config ) {
  1265. config = cloneDeep( config );
  1266. const modelKey = config.model.key ? config.model.key : config.model;
  1267. let eventName = 'attribute:' + modelKey;
  1268. if ( config.model.name ) {
  1269. eventName += ':' + config.model.name;
  1270. }
  1271. if ( config.model.values ) {
  1272. for ( const modelValue of config.model.values ) {
  1273. config.view[ modelValue ] = normalizeToAttributeConfig( config.view[ modelValue ] );
  1274. }
  1275. } else {
  1276. config.view = normalizeToAttributeConfig( config.view );
  1277. }
  1278. const elementCreator = getFromAttributeCreator( config );
  1279. return dispatcher => {
  1280. dispatcher.on( eventName, changeAttribute( elementCreator ), { priority: config.converterPriority || 'normal' } );
  1281. };
  1282. }
  1283. // Model marker to view element conversion helper.
  1284. //
  1285. // See {@link ~DowncastHelpers#markerToElement `.markerToElement()` downcast helper} for examples.
  1286. //
  1287. // @param {Object} config Conversion configuration.
  1288. // @param {String} config.model The name of the model marker (or model marker group) to convert.
  1289. // @param {module:engine/view/elementdefinition~ElementDefinition|Function} config.view A view element definition or a function
  1290. // that takes the model marker data as a parameter and returns a view UI element.
  1291. // @param {module:utils/priorities~PriorityString} [config.converterPriority='normal'] Converter priority.
  1292. // @returns {Function} Conversion helper.
  1293. function downcastMarkerToElement( config ) {
  1294. config = cloneDeep( config );
  1295. config.view = normalizeToElementConfig( config.view, 'ui' );
  1296. return dispatcher => {
  1297. dispatcher.on( 'addMarker:' + config.model, insertUIElement( config.view ), { priority: config.converterPriority || 'normal' } );
  1298. dispatcher.on( 'removeMarker:' + config.model, removeUIElement( config.view ), { priority: config.converterPriority || 'normal' } );
  1299. };
  1300. }
  1301. // Model marker to view data conversion helper.
  1302. //
  1303. // See {@link ~DowncastHelpers#markerToData `markerToData()` downcast helper} to learn more.
  1304. //
  1305. // @param {Object} config
  1306. // @param {String} config.model
  1307. // @param {Function} [config.view]
  1308. // @param {module:utils/priorities~PriorityString} [config.converterPriority='normal']
  1309. // @returns {Function} Conversion helper.
  1310. function downcastMarkerToData( config ) {
  1311. config = cloneDeep( config );
  1312. const group = config.model;
  1313. // Default conversion.
  1314. if ( !config.view ) {
  1315. config.view = markerName => ( {
  1316. group,
  1317. name: markerName.substr( config.model.length + 1 )
  1318. } );
  1319. }
  1320. return dispatcher => {
  1321. dispatcher.on( 'addMarker:' + group, insertMarkerData( config.view ), { priority: config.converterPriority || 'normal' } );
  1322. dispatcher.on( 'removeMarker:' + group, removeMarkerData( config.view ), { priority: config.converterPriority || 'normal' } );
  1323. };
  1324. }
  1325. // Model marker to highlight conversion helper.
  1326. //
  1327. // See {@link ~DowncastHelpers#markerToElement `.markerToElement()` downcast helper} for examples.
  1328. //
  1329. // @param {Object} config Conversion configuration.
  1330. // @param {String} config.model The name of the model marker (or model marker group) to convert.
  1331. // @param {module:engine/conversion/downcasthelpers~HighlightDescriptor|Function} config.view A highlight descriptor
  1332. // that will be used for highlighting or a function that takes the model marker data as a parameter and returns a highlight descriptor.
  1333. // @param {module:utils/priorities~PriorityString} [config.converterPriority='normal'] Converter priority.
  1334. // @returns {Function} Conversion helper.
  1335. function downcastMarkerToHighlight( config ) {
  1336. return dispatcher => {
  1337. dispatcher.on( 'addMarker:' + config.model, highlightText( config.view ), { priority: config.converterPriority || 'normal' } );
  1338. dispatcher.on( 'addMarker:' + config.model, highlightElement( config.view ), { priority: config.converterPriority || 'normal' } );
  1339. dispatcher.on( 'removeMarker:' + config.model, removeHighlight( config.view ), { priority: config.converterPriority || 'normal' } );
  1340. };
  1341. }
  1342. // Takes `config.view`, and if it is an {@link module:engine/view/elementdefinition~ElementDefinition}, converts it
  1343. // to a function (because lower level converters accept only element creator functions).
  1344. //
  1345. // @param {module:engine/view/elementdefinition~ElementDefinition|Function} view View configuration.
  1346. // @param {'container'|'attribute'|'ui'} viewElementType View element type to create.
  1347. // @returns {Function} Element creator function to use in lower level converters.
  1348. function normalizeToElementConfig( view, viewElementType ) {
  1349. if ( typeof view == 'function' ) {
  1350. // If `view` is already a function, don't do anything.
  1351. return view;
  1352. }
  1353. return ( modelData, conversionApi ) => createViewElementFromDefinition( view, conversionApi, viewElementType );
  1354. }
  1355. // Creates a view element instance from the provided {@link module:engine/view/elementdefinition~ElementDefinition} and class.
  1356. //
  1357. // @param {module:engine/view/elementdefinition~ElementDefinition} viewElementDefinition
  1358. // @param {module:engine/view/downcastwriter~DowncastWriter} viewWriter
  1359. // @param {'container'|'attribute'|'ui'} viewElementType
  1360. // @returns {module:engine/view/element~Element}
  1361. function createViewElementFromDefinition( viewElementDefinition, conversionApi, viewElementType ) {
  1362. if ( typeof viewElementDefinition == 'string' ) {
  1363. // If `viewElementDefinition` is given as a `String`, normalize it to an object with `name` property.
  1364. viewElementDefinition = { name: viewElementDefinition };
  1365. }
  1366. let element;
  1367. const viewWriter = conversionApi.writer;
  1368. const attributes = Object.assign( {}, viewElementDefinition.attributes );
  1369. if ( viewElementType == 'container' ) {
  1370. element = viewWriter.createContainerElement( viewElementDefinition.name, attributes );
  1371. } else if ( viewElementType == 'attribute' ) {
  1372. const options = {
  1373. priority: viewElementDefinition.priority || ViewAttributeElement.DEFAULT_PRIORITY
  1374. };
  1375. element = viewWriter.createAttributeElement( viewElementDefinition.name, attributes, options );
  1376. } else {
  1377. // 'ui'.
  1378. element = viewWriter.createUIElement( viewElementDefinition.name, attributes );
  1379. }
  1380. if ( viewElementDefinition.styles ) {
  1381. const keys = Object.keys( viewElementDefinition.styles );
  1382. for ( const key of keys ) {
  1383. viewWriter.setStyle( key, viewElementDefinition.styles[ key ], element );
  1384. }
  1385. }
  1386. if ( viewElementDefinition.classes ) {
  1387. const classes = viewElementDefinition.classes;
  1388. if ( typeof classes == 'string' ) {
  1389. viewWriter.addClass( classes, element );
  1390. } else {
  1391. for ( const className of classes ) {
  1392. viewWriter.addClass( className, element );
  1393. }
  1394. }
  1395. }
  1396. return element;
  1397. }
  1398. function getFromAttributeCreator( config ) {
  1399. if ( config.model.values ) {
  1400. return ( modelAttributeValue, conversionApi ) => {
  1401. const view = config.view[ modelAttributeValue ];
  1402. if ( view ) {
  1403. return view( modelAttributeValue, conversionApi );
  1404. }
  1405. return null;
  1406. };
  1407. } else {
  1408. return config.view;
  1409. }
  1410. }
  1411. // Takes the configuration, adds default parameters if they do not exist and normalizes other parameters to be used in downcast converters
  1412. // for generating a view attribute.
  1413. //
  1414. // @param {Object} view View configuration.
  1415. function normalizeToAttributeConfig( view ) {
  1416. if ( typeof view == 'string' ) {
  1417. return modelAttributeValue => ( { key: view, value: modelAttributeValue } );
  1418. } else if ( typeof view == 'object' ) {
  1419. // { key, value, ... }
  1420. if ( view.value ) {
  1421. return () => view;
  1422. }
  1423. // { key, ... }
  1424. else {
  1425. return modelAttributeValue => ( { key: view.key, value: modelAttributeValue } );
  1426. }
  1427. } else {
  1428. // function.
  1429. return view;
  1430. }
  1431. }
  1432. // Helper function for `highlight`. Prepares the actual descriptor object using value passed to the converter.
  1433. function prepareDescriptor( highlightDescriptor, data, conversionApi ) {
  1434. // If passed descriptor is a creator function, call it. If not, just use passed value.
  1435. const descriptor = typeof highlightDescriptor == 'function' ?
  1436. highlightDescriptor( data, conversionApi ) :
  1437. highlightDescriptor;
  1438. if ( !descriptor ) {
  1439. return null;
  1440. }
  1441. // Apply default descriptor priority.
  1442. if ( !descriptor.priority ) {
  1443. descriptor.priority = 10;
  1444. }
  1445. // Default descriptor id is marker name.
  1446. if ( !descriptor.id ) {
  1447. descriptor.id = data.markerName;
  1448. }
  1449. return descriptor;
  1450. }
  1451. /**
  1452. * An object describing how the marker highlight should be represented in the view.
  1453. *
  1454. * Each text node contained in a highlighted range will be wrapped in a `<span>`
  1455. * {@link module:engine/view/attributeelement~AttributeElement view attribute element} with CSS class(es), attributes and a priority
  1456. * described by this object.
  1457. *
  1458. * Additionally, each {@link module:engine/view/containerelement~ContainerElement container element} can handle displaying the highlight
  1459. * separately by providing the `addHighlight` and `removeHighlight` custom properties. In this case:
  1460. *
  1461. * * The `HighlightDescriptor` object is passed to the `addHighlight` function upon conversion and should be used to apply the highlight to
  1462. * the element.
  1463. * * The descriptor `id` is passed to the `removeHighlight` function upon conversion and should be used to remove the highlight with the
  1464. * given ID from the element.
  1465. *
  1466. * @typedef {Object} module:engine/conversion/downcasthelpers~HighlightDescriptor
  1467. *
  1468. * @property {String|Array.<String>} classes A CSS class or an array of classes to set. If the descriptor is used to
  1469. * create an {@link module:engine/view/attributeelement~AttributeElement attribute element} over text nodes, these classes will be set
  1470. * on that attribute element. If the descriptor is applied to an element, usually these classes will be set on that element, however,
  1471. * this depends on how the element converts the descriptor.
  1472. *
  1473. * @property {String} [id] Descriptor identifier. If not provided, it defaults to the converted marker's name.
  1474. *
  1475. * @property {Number} [priority] Descriptor priority. If not provided, it defaults to `10`. If the descriptor is used to create
  1476. * an {@link module:engine/view/attributeelement~AttributeElement attribute element}, it will be that element's
  1477. * {@link module:engine/view/attributeelement~AttributeElement#priority priority}. If the descriptor is applied to an element,
  1478. * the priority will be used to determine which descriptor is more important.
  1479. *
  1480. * @property {Object} [attributes] Attributes to set. If the descriptor is used to create
  1481. * an {@link module:engine/view/attributeelement~AttributeElement attribute element} over text nodes, these attributes will be set on that
  1482. * attribute element. If the descriptor is applied to an element, usually these attributes will be set on that element, however,
  1483. * this depends on how the element converts the descriptor.
  1484. */