downcasthelpers.js 69 KB

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