downcasthelpers.js 66 KB

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