configurationdefinedconverters.js 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472
  1. /**
  2. * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
  3. * For licensing, see LICENSE.md.
  4. */
  5. import ModelDocument from '../../src/model/document';
  6. import ModelElement from '../../src/model/element';
  7. import ModelText from '../../src/model/text';
  8. import ModelRange from '../../src/model/range';
  9. import ViewDocument from '../../src/view/document';
  10. import ViewElement from '../../src/view/element';
  11. import ViewAttributeElement from '../../src/view/attributeelement';
  12. import ViewText from '../../src/view/text';
  13. import Mapper from '../../src/conversion/mapper';
  14. import ModelConversionDispatcher from '../../src/conversion/modelconversiondispatcher';
  15. import { insertText, remove } from '../../src/conversion/model-to-view-converters';
  16. import { convertText } from '../../src/conversion/view-to-model-converters';
  17. import {
  18. attributeElementToViewConverter,
  19. viewToAttributeElementConverter,
  20. containerElementToView,
  21. viewToContainerElement
  22. } from '../../src/conversion/configurationdefinedconverters';
  23. import ViewConversionDispatcher from '../../src/conversion/viewconversiondispatcher';
  24. import ModelSchema from '../../src/model/schema';
  25. import ModelWalker from '../../src/model/treewalker';
  26. import ModelTextProxy from '../../src/model/textproxy';
  27. function viewAttributesToString( item ) {
  28. let result = '';
  29. for ( const key of item.getAttributeKeys() ) {
  30. const value = item.getAttribute( key );
  31. if ( value ) {
  32. result += ' ' + key + '="' + value + '"';
  33. }
  34. }
  35. return result;
  36. }
  37. function modelToString( item ) {
  38. let result = '';
  39. if ( item instanceof ModelTextProxy ) {
  40. const attributes = modelAttributesToString( item );
  41. result = attributes ? '<$text' + attributes + '>' + item.data + '</$text>' : item.data;
  42. } else {
  43. const walker = new ModelWalker( { boundaries: ModelRange.createIn( item ), shallow: true } );
  44. for ( const value of walker ) {
  45. result += modelToString( value.item );
  46. }
  47. if ( item instanceof ModelElement ) {
  48. const attributes = modelAttributesToString( item );
  49. result = '<' + item.name + attributes + '>' + result + '</' + item.name + '>';
  50. }
  51. }
  52. return result;
  53. }
  54. function modelAttributesToString( item ) {
  55. let result = '';
  56. for ( const attr of item.getAttributes() ) {
  57. result += ' ' + attr[ 0 ] + '="' + attr[ 1 ] + '"';
  58. }
  59. return result;
  60. }
  61. function viewToString( item ) {
  62. let result = '';
  63. if ( item instanceof ViewText ) {
  64. result = item.data;
  65. } else {
  66. // ViewElement or ViewDocumentFragment.
  67. for ( const child of item.getChildren() ) {
  68. result += viewToString( child );
  69. }
  70. if ( item instanceof ViewElement ) {
  71. result = '<' + item.name + viewAttributesToString( item ) + '>' + result + '</' + item.name + '>';
  72. }
  73. }
  74. return result;
  75. }
  76. describe( 'Configuration defined converters', () => {
  77. let dispatcher, mapper, modelDoc, modelRoot, viewDoc, viewRoot, viewSelection, batch;
  78. beforeEach( () => {
  79. modelDoc = new ModelDocument();
  80. modelRoot = modelDoc.createRoot( 'root', 'root' );
  81. batch = modelDoc.batch();
  82. viewDoc = new ViewDocument();
  83. viewRoot = viewDoc.createRoot( 'div' );
  84. viewSelection = viewDoc.selection;
  85. mapper = new Mapper();
  86. mapper.bindElements( modelRoot, viewRoot );
  87. dispatcher = new ModelConversionDispatcher( modelDoc, { mapper, viewSelection } );
  88. dispatcher.on( 'insert:$text', insertText() );
  89. dispatcher.on( 'remove', remove() );
  90. } );
  91. afterEach( () => {
  92. viewDoc.destroy();
  93. } );
  94. describe( 'Attribute converter', () => {
  95. function testConversion( definition, expectedConversion ) {
  96. attributeElementToViewConverter( 'foo', definition, [ dispatcher ] );
  97. const modelElement = new ModelText( 'foo', { foo: 'bar' } );
  98. modelRoot.appendChildren( modelElement );
  99. dispatcher.convertInsertion( ModelRange.createIn( modelRoot ) );
  100. expect( viewToString( viewRoot ) ).to.equal( expectedConversion );
  101. batch.removeAttribute( 'bold', modelRoot );
  102. dispatcher.convertAttribute( 'removeAttribute', ModelRange.createIn( modelRoot ), 'foo', 'bar', null );
  103. expect( viewToString( viewRoot ) ).to.equal( '<div>foo</div>' );
  104. }
  105. describe( 'model to view conversion', () => {
  106. it( 'using passed view element name', () => {
  107. testConversion( { model: 'bar', view: 'strong' }, '<div><strong>foo</strong></div>' );
  108. } );
  109. it( 'using passed view element object', () => {
  110. testConversion( { model: 'bar', view: { name: 'strong' } }, '<div><strong>foo</strong></div>' );
  111. } );
  112. it( 'using passed view element object with styles object', () => {
  113. testConversion( {
  114. model: 'bar',
  115. view: { name: 'span', styles: { 'font-weight': 'bold' } }
  116. }, '<div><span style="font-weight:bold;">foo</span></div>' );
  117. } );
  118. it( 'using passed view element object with class string', () => {
  119. testConversion( { model: 'bar', view: { name: 'span', classes: 'foo' } }, '<div><span class="foo">foo</span></div>' );
  120. } );
  121. it( 'using passed view element object with class array', () => {
  122. testConversion( {
  123. model: 'bar',
  124. view: { name: 'span', classes: [ 'foo', 'foo-bar' ] }
  125. }, '<div><span class="foo foo-bar">foo</span></div>' );
  126. } );
  127. it( 'using passed view element object with attributes', () => {
  128. testConversion( {
  129. model: 'bar',
  130. view: { name: 'span', attributes: { 'data-foo': 'bar' } }
  131. }, '<div><span data-foo="bar">foo</span></div>' );
  132. } );
  133. it( 'should do nothing for undefined value', () => {
  134. attributeElementToViewConverter( 'foo', { model: 'bar', view: 'strong' }, [ dispatcher ] );
  135. const modelElement = new ModelText( 'foo', { foo: 'baz' } );
  136. modelRoot.appendChildren( modelElement );
  137. dispatcher.convertInsertion( ModelRange.createIn( modelRoot ) );
  138. expect( viewToString( viewRoot ) ).to.equal( '<div>foo</div>' );
  139. } );
  140. } );
  141. describe( 'view to model conversion', () => {
  142. let dispatcher, schema, additionalData, batch;
  143. const modelDocument = new ModelDocument();
  144. beforeEach( () => {
  145. batch = modelDocument.batch();
  146. // `additionalData` parameter for `.convert` calls.
  147. additionalData = { context: [ '$root' ] };
  148. schema = new ModelSchema();
  149. schema.registerItem( 'div', '$block' );
  150. schema.allow( { name: '$inline', attributes: [ 'foo' ], inside: '$root' } );
  151. schema.allow( { name: '$text', inside: '$root' } );
  152. dispatcher = new ViewConversionDispatcher( { schema } );
  153. dispatcher.on( 'text', convertText() );
  154. } );
  155. it( 'should convert from view element to model attribute', () => {
  156. viewToAttributeElementConverter( 'foo', { model: 'bar', view: 'strong' }, [ dispatcher ] );
  157. const conversionResult = dispatcher.convert(
  158. new ViewAttributeElement( 'strong', null, new ViewText( 'foo' ) ), batch, additionalData
  159. );
  160. expect( modelToString( conversionResult ) ).to.equal( '<$text foo="bar">foo</$text>' );
  161. } );
  162. it( 'should convert from view element to model attribute', () => {
  163. viewToAttributeElementConverter( 'foo', { model: 'bar', view: { name: 'strong' } }, [ dispatcher ] );
  164. const conversionResult = dispatcher.convert(
  165. new ViewAttributeElement( 'strong', null, new ViewText( 'foo' ) ), batch, additionalData
  166. );
  167. expect( modelToString( conversionResult ) ).to.equal( '<$text foo="bar">foo</$text>' );
  168. } );
  169. it( 'should convert from view element to model attribute', () => {
  170. viewToAttributeElementConverter( 'foo', { model: 'bar', view: { name: 'span', classes: 'foo' } }, [ dispatcher ] );
  171. const conversionResult = dispatcher.convert(
  172. new ViewAttributeElement( 'span', { class: 'foo' }, new ViewText( 'foo' ) ), batch, additionalData
  173. );
  174. expect( modelToString( conversionResult ) ).to.equal( '<$text foo="bar">foo</$text>' );
  175. } );
  176. it( 'should convert from view element to model attribute', () => {
  177. viewToAttributeElementConverter( 'foo', {
  178. model: 'bar',
  179. view: { name: 'span', classes: [ 'foo', 'bar' ] }
  180. }, [ dispatcher ] );
  181. const conversionResult = dispatcher.convert(
  182. new ViewAttributeElement( 'span', { class: 'foo bar' }, new ViewText( 'foo' ) ), batch, additionalData
  183. );
  184. expect( modelToString( conversionResult ) ).to.equal( '<$text foo="bar">foo</$text>' );
  185. } );
  186. it( 'should convert from view element to model attribute', () => {
  187. viewToAttributeElementConverter( 'foo', {
  188. model: 'bar',
  189. view: { name: 'span', styles: { 'font-weight': 'bold' } }
  190. }, [ dispatcher ] );
  191. const conversionResult = dispatcher.convert(
  192. new ViewAttributeElement( 'span', { style: 'font-weight:bold' }, new ViewText( 'foo' ) ), batch, additionalData
  193. );
  194. expect( modelToString( conversionResult ) ).to.equal( '<$text foo="bar">foo</$text>' );
  195. } );
  196. it( 'should convert from view element to model attribute', () => {
  197. viewToAttributeElementConverter( 'foo', {
  198. model: 'bar',
  199. view: { name: 'span', attributes: { 'data-foo': 'bar' } }
  200. }, [ dispatcher ] );
  201. const conversionResult = dispatcher.convert(
  202. new ViewAttributeElement( 'span', { 'data-foo': 'bar' }, new ViewText( 'foo' ) ), batch, additionalData
  203. );
  204. expect( modelToString( conversionResult ) ).to.equal( '<$text foo="bar">foo</$text>' );
  205. } );
  206. it( 'should convert from view element to model attribute', () => {
  207. viewToAttributeElementConverter( 'foo', {
  208. model: 'bar',
  209. view: 'strong',
  210. acceptsAlso: [
  211. { name: 'span', classes: [ 'foo', 'bar' ] },
  212. { name: 'span', attributes: { 'data-foo': 'bar' } }
  213. ]
  214. }, [ dispatcher ] );
  215. const conversionResult = dispatcher.convert(
  216. new ViewAttributeElement( 'span', { 'data-foo': 'bar' }, new ViewText( 'foo' ) ), batch, additionalData
  217. );
  218. expect( modelToString( conversionResult ) ).to.equal( '<$text foo="bar">foo</$text>' );
  219. } );
  220. it( 'should convert from view element to model attribute', () => {
  221. viewToAttributeElementConverter( 'foo', { model: 'baz', view: 'strong' }, [ dispatcher ] );
  222. viewToAttributeElementConverter( 'foo', { model: 'bar', view: { name: 'strong', priority: 'high' } }, [ dispatcher ] );
  223. const conversionResult = dispatcher.convert(
  224. new ViewAttributeElement( 'strong', null, new ViewText( 'foo' ) ), batch, additionalData
  225. );
  226. expect( modelToString( conversionResult ) ).to.equal( '<$text foo="bar">foo</$text>' );
  227. } );
  228. } );
  229. } );
  230. describe( 'Element converter', () => {
  231. function testModelConversion( definition, expectedResult ) {
  232. containerElementToView( definition, [ dispatcher ] );
  233. const modelElement = new ModelElement( 'foo', null, new ModelText( 'bar' ) );
  234. modelRoot.appendChildren( modelElement );
  235. dispatcher.convertInsertion( ModelRange.createIn( modelRoot ) );
  236. expect( viewToString( viewRoot ) ).to.equal( '<div>' + expectedResult + '</div>' );
  237. }
  238. describe( 'model to view conversion', () => {
  239. it( 'using passed view element name', () => {
  240. testModelConversion( { model: 'foo', view: 'strong' }, '<strong>bar</strong>' );
  241. } );
  242. it( 'using passed view element object', () => {
  243. testModelConversion( { model: 'foo', view: { name: 'strong' } }, '<strong>bar</strong>' );
  244. } );
  245. it( 'using passed view element object with styles object', () => {
  246. testModelConversion( {
  247. model: 'foo',
  248. view: { name: 'span', styles: { 'font-weight': 'bold' } }
  249. }, '<span style="font-weight:bold;">bar</span>' );
  250. } );
  251. it( 'using passed view element object with class string', () => {
  252. testModelConversion( { model: 'foo', view: { name: 'span', classes: 'foo' } }, '<span class="foo">bar</span>' );
  253. } );
  254. it( 'using passed view element object with class array', () => {
  255. testModelConversion( {
  256. model: 'foo',
  257. view: { name: 'span', classes: [ 'foo', 'foo-bar' ] }
  258. }, '<span class="foo foo-bar">bar</span>' );
  259. } );
  260. it( 'using passed view element object with attributes', () => {
  261. testModelConversion( {
  262. model: 'foo',
  263. view: { name: 'span', attributes: { 'data-foo': 'bar' } }
  264. }, '<span data-foo="bar">bar</span>' );
  265. } );
  266. } );
  267. describe( 'view to model conversion', () => {
  268. let dispatcher, schema, additionalData, batch;
  269. const modelDocument = new ModelDocument();
  270. beforeEach( () => {
  271. batch = modelDocument.batch();
  272. // `additionalData` parameter for `.convert` calls.
  273. additionalData = { context: [ '$root' ] };
  274. schema = new ModelSchema();
  275. schema.registerItem( 'div', '$block' );
  276. schema.registerItem( 'bar', '$block' );
  277. schema.registerItem( 'baz', '$block' );
  278. schema.allow( { name: '$inline', attributes: [ 'foo' ], inside: '$root' } );
  279. schema.allow( { name: '$text', inside: '$inline' } );
  280. dispatcher = new ViewConversionDispatcher( { schema } );
  281. dispatcher.on( 'text', convertText() );
  282. } );
  283. it( 'should convert from view element to model attribute', () => {
  284. viewToContainerElement( { model: 'bar', view: 'strong' }, [ dispatcher ] );
  285. const conversionResult = dispatcher.convert(
  286. new ViewElement( 'strong', null, new ViewText( 'foo' ) ), batch, additionalData
  287. );
  288. expect( modelToString( conversionResult ) ).to.equal( '<bar>foo</bar>' );
  289. } );
  290. it( 'should convert from view element to model attribute', () => {
  291. viewToContainerElement( { model: 'bar', view: { name: 'strong' } }, [ dispatcher ] );
  292. const conversionResult = dispatcher.convert(
  293. new ViewElement( 'strong', null, new ViewText( 'foo' ) ), batch, additionalData
  294. );
  295. expect( modelToString( conversionResult ) ).to.equal( '<bar>foo</bar>' );
  296. } );
  297. it( 'should convert from view element to model attribute', () => {
  298. viewToContainerElement( { model: 'bar', view: { name: 'span', classes: 'foo' } }, [ dispatcher ] );
  299. const conversionResult = dispatcher.convert(
  300. new ViewElement( 'span', { class: 'foo' }, new ViewText( 'foo' ) ), batch, additionalData
  301. );
  302. expect( modelToString( conversionResult ) ).to.equal( '<bar>foo</bar>' );
  303. } );
  304. it( 'should convert from view element to model attribute', () => {
  305. viewToContainerElement( { model: 'bar', view: { name: 'span', classes: [ 'foo', 'bar' ] } }, [ dispatcher ] );
  306. const conversionResult = dispatcher.convert(
  307. new ViewElement( 'span', { class: 'foo bar' }, new ViewText( 'foo' ) ), batch, additionalData
  308. );
  309. expect( modelToString( conversionResult ) ).to.equal( '<bar>foo</bar>' );
  310. } );
  311. it( 'should convert from view element to model attribute', () => {
  312. viewToContainerElement( { model: 'bar', view: { name: 'span', styles: { 'font-weight': 'bold' } } }, [ dispatcher ] );
  313. const conversionResult = dispatcher.convert(
  314. new ViewElement( 'span', { style: 'font-weight:bold' }, new ViewText( 'foo' ) ), batch, additionalData
  315. );
  316. expect( modelToString( conversionResult ) ).to.equal( '<bar>foo</bar>' );
  317. } );
  318. it( 'should convert from view element to model attribute', () => {
  319. viewToContainerElement( { model: 'bar', view: { name: 'span', attributes: { 'data-foo': 'bar' } } }, [ dispatcher ] );
  320. const conversionResult = dispatcher.convert(
  321. new ViewElement( 'span', { 'data-foo': 'bar' }, new ViewText( 'foo' ) ), batch, additionalData
  322. );
  323. expect( modelToString( conversionResult ) ).to.equal( '<bar>foo</bar>' );
  324. } );
  325. it( 'should convert from view element to model attribute', () => {
  326. viewToContainerElement( {
  327. model: 'bar',
  328. view: 'strong',
  329. acceptsAlso: [
  330. { name: 'span', classes: [ 'foo', 'bar' ] },
  331. { name: 'span', attributes: { 'data-foo': 'bar' } }
  332. ]
  333. }, [ dispatcher ] );
  334. const conversionResult = dispatcher.convert(
  335. new ViewElement( 'span', { 'data-foo': 'bar' }, new ViewText( 'foo' ) ), batch, additionalData
  336. );
  337. expect( modelToString( conversionResult ) ).to.equal( '<bar>foo</bar>' );
  338. } );
  339. it( 'should convert from view element to model attribute', () => {
  340. viewToContainerElement( { model: 'baz', view: 'strong' }, [ dispatcher ] );
  341. viewToContainerElement( { model: 'bar', view: { name: 'strong', priority: 'high' } }, [ dispatcher ] );
  342. const conversionResult = dispatcher.convert(
  343. new ViewElement( 'strong', null, new ViewText( 'foo' ) ), batch, additionalData
  344. );
  345. expect( modelToString( conversionResult ) ).to.equal( '<bar>foo</bar>' );
  346. } );
  347. } );
  348. } );
  349. } );