definition-based-converters.js 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484
  1. /**
  2. * @license Copyright (c) 2003-2018, 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, context, schema;
  75. beforeEach( () => {
  76. model = new Model();
  77. } );
  78. function setupViewToModelTests() {
  79. 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. // Set name of view root the same as dom root.
  88. // This is a mock of attaching view root to dom root.
  89. controller.view.getRoot()._name = 'div';
  90. viewRoot = controller.view.getRoot();
  91. dispatcher = controller.modelToView;
  92. }
  93. describe( 'Attribute converters', () => {
  94. function testModelConversion( definition, expectedConversion ) {
  95. modelAttributeToViewAttributeElement( 'foo', [ definition ], [ dispatcher ] );
  96. const modelElement = new ModelText( 'foo', { foo: 'bar' } );
  97. model.change( writer => {
  98. writer.insert( modelElement, ModelPosition.createAt( modelRoot, 0 ) );
  99. } );
  100. expect( viewToString( viewRoot ) ).to.equal( expectedConversion );
  101. model.change( writer => {
  102. writer.removeAttribute( 'foo', modelElement );
  103. } );
  104. expect( viewToString( viewRoot ) ).to.equal( '<div>foo</div>' );
  105. }
  106. describe( 'model to view conversion', () => {
  107. beforeEach( () => {
  108. setupModelToViewTests();
  109. } );
  110. it( 'using passed view element name', () => {
  111. testModelConversion( { model: 'bar', view: 'strong' }, '<div><strong>foo</strong></div>' );
  112. } );
  113. it( 'using passed view element object', () => {
  114. testModelConversion( { model: 'bar', view: { name: 'strong' } }, '<div><strong>foo</strong></div>' );
  115. } );
  116. it( 'using passed view element object with style object', () => {
  117. testModelConversion( {
  118. model: 'bar',
  119. view: { name: 'span', style: { 'font-weight': 'bold' } }
  120. }, '<div><span style="font-weight:bold;">foo</span></div>' );
  121. } );
  122. it( 'using passed view element object with class string', () => {
  123. testModelConversion( { model: 'bar', view: { name: 'span', class: 'foo' } }, '<div><span class="foo">foo</span></div>' );
  124. } );
  125. it( 'using passed view element object with class array', () => {
  126. testModelConversion( {
  127. model: 'bar',
  128. view: { name: 'span', class: [ 'foo', 'foo-bar' ] }
  129. }, '<div><span class="foo foo-bar">foo</span></div>' );
  130. } );
  131. it( 'using passed view element object with attributes', () => {
  132. testModelConversion( {
  133. model: 'bar',
  134. view: { name: 'span', attribute: { 'data-foo': 'bar' } }
  135. }, '<div><span data-foo="bar">foo</span></div>' );
  136. } );
  137. it( 'should convert when changing attribute', () => {
  138. const definition1 = { model: 'bar', view: { name: 'span', class: 'bar' } };
  139. const definition2 = { model: 'baz', view: { name: 'span', class: 'baz' } };
  140. modelAttributeToViewAttributeElement( 'foo', [ definition1, definition2 ], [ dispatcher ] );
  141. const modelElement = new ModelText( 'foo', { foo: 'bar' } );
  142. model.change( writer => {
  143. writer.insert( modelElement, ModelPosition.createAt( modelRoot, 0 ) );
  144. } );
  145. expect( viewToString( viewRoot ) ).to.equal( '<div><span class="bar">foo</span></div>' );
  146. model.change( writer => {
  147. writer.setAttribute( 'foo', 'baz', modelElement );
  148. } );
  149. expect( viewToString( viewRoot ) ).to.equal( '<div><span class="baz">foo</span></div>' );
  150. } );
  151. it( 'should do nothing for undefined value', () => {
  152. modelAttributeToViewAttributeElement( 'foo', [ { model: 'bar', view: 'strong' } ], [ dispatcher ] );
  153. const modelElement = new ModelText( 'foo', { foo: 'baz' } );
  154. model.change( writer => {
  155. writer.insert( modelElement, ModelPosition.createAt( modelRoot, 0 ) );
  156. } );
  157. expect( viewToString( viewRoot ) ).to.equal( '<div>foo</div>' );
  158. } );
  159. } );
  160. describe( 'view to model conversion', () => {
  161. beforeEach( () => {
  162. setupViewToModelTests();
  163. schema.register( 'div', { inheritAllFrom: '$block' } );
  164. schema.extend( '$text', {
  165. allowIn: '$root',
  166. allowAttributes: 'foo'
  167. } );
  168. dispatcher.on( 'text', convertText() );
  169. } );
  170. it( 'should convert using element name', () => {
  171. viewToModelAttribute( 'foo', { model: 'bar', view: 'strong' }, [ dispatcher ] );
  172. const conversionResult = dispatcher.convert(
  173. new ViewAttributeElement( 'strong', null, new ViewText( 'foo' ) ), context
  174. );
  175. expect( modelToString( conversionResult ) ).to.equal( '<$text foo="bar">foo</$text>' );
  176. } );
  177. it( 'should convert using object', () => {
  178. viewToModelAttribute( 'foo', { model: 'bar', view: { name: 'strong' } }, [ dispatcher ] );
  179. const conversionResult = dispatcher.convert(
  180. new ViewAttributeElement( 'strong', null, new ViewText( 'foo' ) ), context
  181. );
  182. expect( modelToString( conversionResult ) ).to.equal( '<$text foo="bar">foo</$text>' );
  183. } );
  184. it( 'should convert using class string', () => {
  185. viewToModelAttribute( 'foo', { model: 'bar', view: { name: 'span', class: 'foo' } }, [ dispatcher ] );
  186. const conversionResult = dispatcher.convert(
  187. new ViewAttributeElement( 'span', { class: 'foo' }, new ViewText( 'foo' ) ), context
  188. );
  189. expect( modelToString( conversionResult ) ).to.equal( '<$text foo="bar">foo</$text>' );
  190. } );
  191. it( 'should convert using class array', () => {
  192. viewToModelAttribute( 'foo', {
  193. model: 'bar',
  194. view: { name: 'span', class: [ 'foo', 'bar' ] }
  195. }, [ dispatcher ] );
  196. const conversionResult = dispatcher.convert(
  197. new ViewAttributeElement( 'span', { class: 'foo bar' }, new ViewText( 'foo' ) ), context
  198. );
  199. expect( modelToString( conversionResult ) ).to.equal( '<$text foo="bar">foo</$text>' );
  200. } );
  201. it( 'should convert using style object', () => {
  202. viewToModelAttribute( 'foo', {
  203. model: 'bar',
  204. view: { name: 'span', style: { 'font-weight': 'bold' } }
  205. }, [ dispatcher ] );
  206. const conversionResult = dispatcher.convert(
  207. new ViewAttributeElement( 'span', { style: 'font-weight:bold' }, new ViewText( 'foo' ) ), context
  208. );
  209. expect( modelToString( conversionResult ) ).to.equal( '<$text foo="bar">foo</$text>' );
  210. } );
  211. it( 'should convert using attributes object', () => {
  212. viewToModelAttribute( 'foo', {
  213. model: 'bar',
  214. view: { name: 'span', attribute: { 'data-foo': 'bar' } }
  215. }, [ dispatcher ] );
  216. const conversionResult = dispatcher.convert(
  217. new ViewAttributeElement( 'span', { 'data-foo': 'bar' }, new ViewText( 'foo' ) ), context
  218. );
  219. expect( modelToString( conversionResult ) ).to.equal( '<$text foo="bar">foo</$text>' );
  220. } );
  221. it( 'should convert using acceptAlso array', () => {
  222. viewToModelAttribute( 'foo', {
  223. model: 'bar',
  224. view: 'strong',
  225. acceptsAlso: [
  226. { name: 'span', class: [ 'foo', 'bar' ] },
  227. { name: 'span', attribute: { 'data-foo': 'bar' } }
  228. ]
  229. }, [ dispatcher ] );
  230. const conversionResult = dispatcher.convert(
  231. new ViewAttributeElement( 'span', { 'data-foo': 'bar' }, new ViewText( 'foo' ) ), context
  232. );
  233. expect( modelToString( conversionResult ) ).to.equal( '<$text foo="bar">foo</$text>' );
  234. } );
  235. it( 'should convert using priority', () => {
  236. viewToModelAttribute( 'foo', { model: 'baz', view: 'strong' }, [ dispatcher ] );
  237. viewToModelAttribute( 'foo', { model: 'bar', view: { name: 'strong', priority: 'high' } }, [ dispatcher ] );
  238. const conversionResult = dispatcher.convert(
  239. new ViewAttributeElement( 'strong', null, new ViewText( 'foo' ) ), context
  240. );
  241. expect( modelToString( conversionResult ) ).to.equal( '<$text foo="bar">foo</$text>' );
  242. } );
  243. } );
  244. } );
  245. describe( 'Element converters', () => {
  246. function testModelConversion( definition, expectedResult ) {
  247. modelElementToViewContainerElement( definition, [ dispatcher ] );
  248. const modelElement = new ModelElement( 'foo', null, new ModelText( 'bar' ) );
  249. model.change( writer => {
  250. writer.insert( modelElement, ModelPosition.createAt( modelRoot, 0 ) );
  251. } );
  252. expect( viewToString( viewRoot ) ).to.equal( '<div>' + expectedResult + '</div>' );
  253. }
  254. describe( 'model to view conversion', () => {
  255. beforeEach( () => {
  256. setupModelToViewTests();
  257. } );
  258. it( 'using passed view element name', () => {
  259. testModelConversion( { model: 'foo', view: 'code' }, '<code>bar</code>' );
  260. } );
  261. it( 'using passed view element object', () => {
  262. testModelConversion( { model: 'foo', view: { name: 'code' } }, '<code>bar</code>' );
  263. } );
  264. it( 'using passed view element object with style object', () => {
  265. testModelConversion( {
  266. model: 'foo',
  267. view: { name: 'span', style: { 'font-weight': 'bold' } }
  268. }, '<span style="font-weight:bold;">bar</span>' );
  269. } );
  270. it( 'using passed view element object with class string', () => {
  271. testModelConversion( { model: 'foo', view: { name: 'span', class: 'foo' } }, '<span class="foo">bar</span>' );
  272. } );
  273. it( 'using passed view element object with class array', () => {
  274. testModelConversion( {
  275. model: 'foo',
  276. view: { name: 'span', class: [ 'foo', 'foo-bar' ] }
  277. }, '<span class="foo foo-bar">bar</span>' );
  278. } );
  279. it( 'using passed view element object with attributes', () => {
  280. testModelConversion( {
  281. model: 'foo',
  282. view: { name: 'span', attribute: { 'data-foo': 'bar' } }
  283. }, '<span data-foo="bar">bar</span>' );
  284. } );
  285. } );
  286. describe( 'view to model conversion', () => {
  287. beforeEach( () => {
  288. setupViewToModelTests();
  289. schema.register( 'div', { inheritAllFrom: '$block' } );
  290. schema.register( 'bar', { inheritAllFrom: '$block' } );
  291. schema.register( 'baz', { inheritAllFrom: '$block' } );
  292. schema.extend( '$text', {
  293. allowIn: '$root',
  294. allowAttributes: 'foo'
  295. } );
  296. dispatcher.on( 'text', convertText() );
  297. } );
  298. it( 'should convert using element name', () => {
  299. viewToModelElement( { model: 'bar', view: 'strong' }, [ dispatcher ] );
  300. const conversionResult = dispatcher.convert(
  301. new ViewElement( 'strong', null, new ViewText( 'foo' ) ), context
  302. );
  303. expect( modelToString( conversionResult ) ).to.equal( '<bar>foo</bar>' );
  304. } );
  305. it( 'should convert using object', () => {
  306. viewToModelElement( { model: 'bar', view: { name: 'strong' } }, [ dispatcher ] );
  307. const conversionResult = dispatcher.convert(
  308. new ViewElement( 'strong', null, new ViewText( 'foo' ) ), context
  309. );
  310. expect( modelToString( conversionResult ) ).to.equal( '<bar>foo</bar>' );
  311. } );
  312. it( 'should convert using class string', () => {
  313. viewToModelElement( { model: 'bar', view: { name: 'span', class: 'foo' } }, [ dispatcher ] );
  314. const conversionResult = dispatcher.convert(
  315. new ViewElement( 'span', { class: 'foo' }, new ViewText( 'foo' ) ), context
  316. );
  317. expect( modelToString( conversionResult ) ).to.equal( '<bar>foo</bar>' );
  318. } );
  319. it( 'should convert using class array', () => {
  320. viewToModelElement( { model: 'bar', view: { name: 'span', class: [ 'foo', 'bar' ] } }, [ dispatcher ] );
  321. const conversionResult = dispatcher.convert(
  322. new ViewElement( 'span', { class: 'foo bar' }, new ViewText( 'foo' ) ), context
  323. );
  324. expect( modelToString( conversionResult ) ).to.equal( '<bar>foo</bar>' );
  325. } );
  326. it( 'should convert using style object', () => {
  327. viewToModelElement( { model: 'bar', view: { name: 'span', style: { 'font-weight': 'bold' } } }, [ dispatcher ] );
  328. const conversionResult = dispatcher.convert(
  329. new ViewElement( 'span', { style: 'font-weight:bold' }, new ViewText( 'foo' ) ), context
  330. );
  331. expect( modelToString( conversionResult ) ).to.equal( '<bar>foo</bar>' );
  332. } );
  333. it( 'should convert using attributes object', () => {
  334. viewToModelElement( { model: 'bar', view: { name: 'span', attribute: { 'data-foo': 'bar' } } }, [ dispatcher ] );
  335. const conversionResult = dispatcher.convert(
  336. new ViewElement( 'span', { 'data-foo': 'bar' }, new ViewText( 'foo' ) ), context
  337. );
  338. expect( modelToString( conversionResult ) ).to.equal( '<bar>foo</bar>' );
  339. } );
  340. it( 'should convert using acceptAlso array', () => {
  341. viewToModelElement( {
  342. model: 'bar',
  343. view: 'strong',
  344. acceptsAlso: [
  345. { name: 'span', class: [ 'foo', 'bar' ] },
  346. { name: 'span', attribute: { 'data-foo': 'bar' } }
  347. ]
  348. }, [ dispatcher ] );
  349. const conversionResult = dispatcher.convert(
  350. new ViewElement( 'span', { 'data-foo': 'bar' }, new ViewText( 'foo' ) ), context
  351. );
  352. expect( modelToString( conversionResult ) ).to.equal( '<bar>foo</bar>' );
  353. } );
  354. it( 'should convert using priority', () => {
  355. viewToModelElement( { model: 'baz', view: 'strong' }, [ dispatcher ] );
  356. viewToModelElement( { model: 'bar', view: { name: 'strong', priority: 'high' } }, [ dispatcher ] );
  357. const conversionResult = dispatcher.convert(
  358. new ViewElement( 'strong', null, new ViewText( 'foo' ) ), context
  359. );
  360. expect( modelToString( conversionResult ) ).to.equal( '<bar>foo</bar>' );
  361. } );
  362. } );
  363. } );
  364. } );