definition-based-converters.js 15 KB

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