view-to-model-helpers.js 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612
  1. /**
  2. * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
  3. * For licensing, see LICENSE.md.
  4. */
  5. import {
  6. elementToElement, elementToAttribute, attributeToAttribute, elementToMarker
  7. } from '../../src/conversion/view-to-model-helpers';
  8. import Model from '../../src/model/model';
  9. import Conversion from '../../src/conversion/conversion';
  10. import ViewConversionDispatcher from '../../src/conversion/viewconversiondispatcher';
  11. import ModelElement from '../../src/model/element';
  12. import ViewDocumentFragment from '../../src/view/documentfragment';
  13. import ViewUIElement from '../../src/view/uielement';
  14. import ViewContainerElement from '../../src/view/containerelement';
  15. import ViewAttributeElement from '../../src/view/attributeelement';
  16. import ViewText from '../../src/view/text';
  17. import { convertText, convertToModelFragment } from '../../src/conversion/view-to-model-converters';
  18. import { stringify } from '../../src/dev-utils/model';
  19. describe( 'view-to-model-helpers', () => {
  20. let dispatcher, model, schema, conversion;
  21. beforeEach( () => {
  22. model = new Model();
  23. schema = model.schema;
  24. schema.extend( '$text', {
  25. allowIn: '$root'
  26. } );
  27. schema.register( '$marker', {
  28. inheritAllFrom: '$block'
  29. } );
  30. schema.register( 'paragraph', {
  31. inheritAllFrom: '$block'
  32. } );
  33. schema.extend( '$text', {
  34. allowAttributes: [ 'bold' ]
  35. } );
  36. dispatcher = new ViewConversionDispatcher( model, { schema } );
  37. dispatcher.on( 'text', convertText() );
  38. dispatcher.on( 'element', convertToModelFragment(), { priority: 'lowest' } );
  39. dispatcher.on( 'documentFragment', convertToModelFragment(), { priority: 'lowest' } );
  40. conversion = new Conversion();
  41. conversion.register( 'view', [ dispatcher ] );
  42. } );
  43. describe( 'elementToElement', () => {
  44. it( 'config.view is a string', () => {
  45. const helper = elementToElement( { view: 'p', model: 'paragraph' } );
  46. conversion.for( 'view' ).add( helper );
  47. expectResult( new ViewContainerElement( 'p' ), '<paragraph></paragraph>' );
  48. } );
  49. it( 'can be overwritten using priority', () => {
  50. schema.register( 'p', {
  51. inheritAllFrom: '$block'
  52. } );
  53. const helperA = elementToElement( { view: 'p', model: 'p' } );
  54. const helperB = elementToElement( { view: 'p', model: 'paragraph' }, 'high' );
  55. conversion.for( 'view' ).add( helperA ).add( helperB );
  56. expectResult( new ViewContainerElement( 'p' ), '<paragraph></paragraph>' );
  57. } );
  58. it( 'config.view is an object', () => {
  59. schema.register( 'fancyParagraph', {
  60. inheritAllFrom: '$block'
  61. } );
  62. const helperParagraph = elementToElement( { view: 'p', model: 'paragraph' } );
  63. const helperFancy = elementToElement( {
  64. view: {
  65. name: 'p',
  66. class: 'fancy'
  67. },
  68. model: 'fancyParagraph'
  69. }, 'high' );
  70. conversion.for( 'view' ).add( helperParagraph ).add( helperFancy );
  71. expectResult( new ViewContainerElement( 'p' ), '<paragraph></paragraph>' );
  72. expectResult( new ViewContainerElement( 'p', { class: 'fancy' } ), '<fancyParagraph></fancyParagraph>' );
  73. } );
  74. it( 'config.model is element instance', () => {
  75. schema.extend( 'paragraph', {
  76. allowAttributes: [ 'fancy' ]
  77. } );
  78. const helper = elementToElement( {
  79. view: {
  80. name: 'p',
  81. class: 'fancy'
  82. },
  83. model: new ModelElement( 'paragraph', { fancy: true } )
  84. } );
  85. conversion.for( 'view' ).add( helper );
  86. expectResult( new ViewContainerElement( 'p', { class: 'fancy' } ), '<paragraph fancy="true"></paragraph>' );
  87. } );
  88. it( 'config.model is a function', () => {
  89. schema.register( 'heading', {
  90. inheritAllFrom: '$block',
  91. allowAttributes: [ 'level' ]
  92. } );
  93. const helper = elementToElement( {
  94. view: {
  95. name: 'p',
  96. class: 'heading'
  97. },
  98. model: viewElement => new ModelElement( 'heading', { level: viewElement.getAttribute( 'data-level' ) } )
  99. } );
  100. conversion.for( 'view' ).add( helper );
  101. expectResult( new ViewContainerElement( 'p', { class: 'heading', 'data-level': 2 } ), '<heading level="2"></heading>' );
  102. } );
  103. it( 'should fire conversion of the element children', () => {
  104. const helper = elementToElement( { view: 'p', model: 'paragraph' } );
  105. conversion.for( 'view' ).add( helper );
  106. expectResult( new ViewContainerElement( 'p', null, new ViewText( 'foo' ) ), '<paragraph>foo</paragraph>' );
  107. } );
  108. it( 'should not insert a model element if it is not allowed by schema', () => {
  109. const helper = elementToElement( { view: 'h2', model: 'heading' } );
  110. conversion.for( 'view' ).add( helper );
  111. expectResult( new ViewContainerElement( 'h2' ), '' );
  112. } );
  113. it( 'should auto-break elements', () => {
  114. schema.register( 'heading', {
  115. inheritAllFrom: '$block'
  116. } );
  117. const helperParagraph = elementToElement( { view: 'p', model: 'paragraph' } );
  118. const helperHeading = elementToElement( { view: 'h2', model: 'heading' } );
  119. conversion.for( 'view' ).add( helperParagraph ).add( helperHeading );
  120. expectResult(
  121. new ViewContainerElement( 'p', null, [
  122. new ViewText( 'Foo' ),
  123. new ViewContainerElement( 'h2', null, new ViewText( 'Xyz' ) ),
  124. new ViewText( 'Bar' )
  125. ] ),
  126. '<paragraph>Foo</paragraph><heading>Xyz</heading><paragraph>Bar</paragraph>'
  127. );
  128. } );
  129. it( 'should not do anything if returned model element is null', () => {
  130. const helperA = elementToElement( { view: 'p', model: 'paragraph' } );
  131. const helperB = elementToElement( { view: 'p', model: () => null }, 'high' );
  132. conversion.for( 'view' ).add( helperA ).add( helperB );
  133. expectResult( new ViewContainerElement( 'p' ), '<paragraph></paragraph>' );
  134. } );
  135. } );
  136. describe( 'elementToAttribute', () => {
  137. it( 'config.view is string', () => {
  138. const helper = elementToAttribute( { view: 'strong', model: 'bold' } );
  139. conversion.for( 'view' ).add( helper );
  140. expectResult(
  141. new ViewAttributeElement( 'strong', null, new ViewText( 'foo' ) ),
  142. '<$text bold="true">foo</$text>'
  143. );
  144. } );
  145. it( 'can be overwritten using priority', () => {
  146. const helperA = elementToAttribute( { view: 'strong', model: 'strong' } );
  147. const helperB = elementToAttribute( { view: 'strong', model: 'bold' }, 'high' );
  148. conversion.for( 'view' ).add( helperA ).add( helperB );
  149. expectResult(
  150. new ViewAttributeElement( 'strong', null, new ViewText( 'foo' ) ),
  151. '<$text bold="true">foo</$text>'
  152. );
  153. } );
  154. it( 'config.view is an object', () => {
  155. const helper = elementToAttribute( {
  156. view: {
  157. name: 'span',
  158. class: 'bold'
  159. },
  160. model: 'bold'
  161. } );
  162. conversion.for( 'view' ).add( helper );
  163. expectResult(
  164. new ViewAttributeElement( 'span', { class: 'bold' }, new ViewText( 'foo' ) ),
  165. '<$text bold="true">foo</$text>'
  166. );
  167. } );
  168. it( 'model attribute value is given', () => {
  169. schema.extend( '$text', {
  170. allowAttributes: [ 'styled' ]
  171. } );
  172. const helper = elementToAttribute( {
  173. view: {
  174. name: 'span',
  175. class: [ 'styled', 'styled-dark' ]
  176. },
  177. model: {
  178. key: 'styled',
  179. value: 'dark'
  180. }
  181. } );
  182. conversion.for( 'view' ).add( helper );
  183. expectResult(
  184. new ViewAttributeElement( 'span', { class: 'styled styled-dark' }, new ViewText( 'foo' ) ),
  185. '<$text styled="dark">foo</$text>'
  186. );
  187. } );
  188. it( 'model attribute value is a function', () => {
  189. schema.extend( '$text', {
  190. allowAttributes: [ 'fontSize' ]
  191. } );
  192. const helper = elementToAttribute( {
  193. view: {
  194. name: 'span',
  195. style: {
  196. 'font-size': /[\s\S]+/
  197. }
  198. },
  199. model: {
  200. key: 'fontSize',
  201. value: viewElement => {
  202. const fontSize = viewElement.getStyle( 'font-size' );
  203. const value = fontSize.substr( 0, fontSize.length - 2 );
  204. if ( value <= 10 ) {
  205. return 'small';
  206. } else if ( value > 12 ) {
  207. return 'big';
  208. }
  209. return null;
  210. }
  211. }
  212. } );
  213. conversion.for( 'view' ).add( helper );
  214. expectResult(
  215. new ViewAttributeElement( 'span', { style: 'font-size:9px' }, new ViewText( 'foo' ) ),
  216. '<$text fontSize="small">foo</$text>'
  217. );
  218. expectResult(
  219. new ViewAttributeElement( 'span', { style: 'font-size:12px' }, new ViewText( 'foo' ) ),
  220. 'foo'
  221. );
  222. expectResult(
  223. new ViewAttributeElement( 'span', { style: 'font-size:14px' }, new ViewText( 'foo' ) ),
  224. '<$text fontSize="big">foo</$text>'
  225. );
  226. } );
  227. it( 'should not set an attribute if it is not allowed by schema', () => {
  228. const helper = elementToAttribute( { view: 'em', model: 'italic' } );
  229. conversion.for( 'view' ).add( helper );
  230. expectResult(
  231. new ViewAttributeElement( 'em', null, new ViewText( 'foo' ) ),
  232. 'foo'
  233. );
  234. } );
  235. it( 'should not do anything if returned model attribute is null', () => {
  236. const helperA = elementToAttribute( { view: 'strong', model: 'bold' } );
  237. const helperB = elementToAttribute( {
  238. view: 'strong',
  239. model: {
  240. key: 'bold',
  241. value: () => null
  242. }
  243. }, 'high' );
  244. conversion.for( 'view' ).add( helperA ).add( helperB );
  245. expectResult(
  246. new ViewAttributeElement( 'strong', null, new ViewText( 'foo' ) ),
  247. '<$text bold="true">foo</$text>'
  248. );
  249. } );
  250. } );
  251. describe( 'attributeToAttribute', () => {
  252. beforeEach( () => {
  253. conversion.for( 'view' ).add( elementToElement( { view: 'img', model: 'image' } ) );
  254. schema.register( 'image', {
  255. inheritAllFrom: '$block'
  256. } );
  257. } );
  258. it( 'config.view is a string', () => {
  259. schema.extend( 'image', {
  260. allowAttributes: [ 'source' ]
  261. } );
  262. const helper = attributeToAttribute( { view: 'src', model: 'source' } );
  263. conversion.for( 'view' ).add( helper );
  264. expectResult(
  265. new ViewAttributeElement( 'img', { src: 'foo.jpg' } ),
  266. '<image source="foo.jpg"></image>'
  267. );
  268. } );
  269. it( 'config.view has only key set', () => {
  270. schema.extend( 'image', {
  271. allowAttributes: [ 'source' ]
  272. } );
  273. const helper = attributeToAttribute( { view: { key: 'src' }, model: 'source' } );
  274. conversion.for( 'view' ).add( helper );
  275. expectResult(
  276. new ViewAttributeElement( 'img', { src: 'foo.jpg' } ),
  277. '<image source="foo.jpg"></image>'
  278. );
  279. } );
  280. it( 'can be overwritten using priority', () => {
  281. schema.extend( 'image', {
  282. allowAttributes: [ 'src', 'source' ]
  283. } );
  284. const helperA = attributeToAttribute( { view: { key: 'src' }, model: 'src' } );
  285. const helperB = attributeToAttribute( { view: { key: 'src' }, model: 'source' }, 'normal' );
  286. conversion.for( 'view' ).add( helperA ).add( helperB );
  287. expectResult(
  288. new ViewAttributeElement( 'img', { src: 'foo.jpg' } ),
  289. '<image source="foo.jpg"></image>'
  290. );
  291. } );
  292. it( 'config.view has value set', () => {
  293. schema.extend( 'image', {
  294. allowAttributes: [ 'styled' ]
  295. } );
  296. const helper = attributeToAttribute( {
  297. view: {
  298. key: 'data-style',
  299. value: /[\s\S]*/
  300. },
  301. model: 'styled'
  302. } );
  303. conversion.for( 'view' ).add( helper );
  304. expectResult(
  305. new ViewAttributeElement( 'img', { 'data-style': 'dark' } ),
  306. '<image styled="dark"></image>'
  307. );
  308. } );
  309. it( 'model attribute value is a string', () => {
  310. schema.extend( 'image', {
  311. allowAttributes: [ 'styled' ]
  312. } );
  313. const helper = attributeToAttribute( {
  314. view: {
  315. name: 'img',
  316. key: 'class',
  317. value: 'styled-dark'
  318. },
  319. model: {
  320. key: 'styled',
  321. value: 'dark'
  322. }
  323. } );
  324. conversion.for( 'view' )
  325. .add( helper )
  326. .add( elementToElement( { view: 'p', model: 'paragraph' } ) );
  327. expectResult(
  328. new ViewContainerElement( 'img', { class: 'styled-dark' } ),
  329. '<image styled="dark"></image>'
  330. );
  331. expectResult(
  332. new ViewContainerElement( 'img', { class: 'styled-xxx' } ),
  333. '<image></image>'
  334. );
  335. expectResult(
  336. new ViewContainerElement( 'p', { class: 'styled-dark' } ),
  337. '<paragraph></paragraph>'
  338. );
  339. } );
  340. it( 'model attribute value is a function', () => {
  341. schema.extend( 'image', {
  342. allowAttributes: [ 'styled' ]
  343. } );
  344. const helper = attributeToAttribute( {
  345. view: {
  346. key: 'class',
  347. value: /styled-[\S]+/
  348. },
  349. model: {
  350. key: 'styled',
  351. value: viewElement => {
  352. const regexp = /styled-([\S]+)/;
  353. const match = viewElement.getAttribute( 'class' ).match( regexp );
  354. return match[ 1 ];
  355. }
  356. }
  357. } );
  358. conversion.for( 'view' ).add( helper );
  359. expectResult(
  360. new ViewAttributeElement( 'img', { 'class': 'styled-dark' } ),
  361. '<image styled="dark"></image>'
  362. );
  363. } );
  364. it( 'should not set an attribute if it is not allowed by schema', () => {
  365. const helper = attributeToAttribute( { view: 'src', model: 'source' } );
  366. conversion.for( 'view' ).add( helper );
  367. expectResult(
  368. new ViewAttributeElement( 'img', { src: 'foo.jpg' } ),
  369. '<image></image>'
  370. );
  371. } );
  372. it( 'should not do anything if returned model attribute is null', () => {
  373. schema.extend( 'image', {
  374. allowAttributes: [ 'styled' ]
  375. } );
  376. const helperA = attributeToAttribute( {
  377. view: {
  378. key: 'class',
  379. value: 'styled'
  380. },
  381. model: {
  382. key: 'styled',
  383. value: true
  384. }
  385. } );
  386. const helperB = attributeToAttribute( {
  387. view: {
  388. key: 'class',
  389. value: 'styled'
  390. },
  391. model: {
  392. key: 'styled',
  393. value: () => null
  394. }
  395. } );
  396. conversion.for( 'view' ).add( helperA ).add( helperB );
  397. expectResult(
  398. new ViewAttributeElement( 'img', { class: 'styled' } ),
  399. '<image styled="true"></image>'
  400. );
  401. } );
  402. } );
  403. describe( 'elementToMarker', () => {
  404. it( 'config.view is a string', () => {
  405. const helper = elementToMarker( { view: 'marker-search', model: 'search' } );
  406. conversion.for( 'view' ).add( helper );
  407. const frag = new ViewDocumentFragment( [
  408. new ViewText( 'fo' ),
  409. new ViewUIElement( 'marker-search' ),
  410. new ViewText( 'oba' ),
  411. new ViewUIElement( 'marker-search' ),
  412. new ViewText( 'r' )
  413. ] );
  414. const marker = { name: 'search', start: [ 2 ], end: [ 5 ] };
  415. expectResult( frag, 'foobar', marker );
  416. } );
  417. it( 'can be overwritten using priority', () => {
  418. const helperA = elementToMarker( { view: 'marker-search', model: 'search-result' } );
  419. const helperB = elementToMarker( { view: 'marker-search', model: 'search' }, 'high' );
  420. conversion.for( 'view' ).add( helperA ).add( helperB );
  421. const frag = new ViewDocumentFragment( [
  422. new ViewText( 'fo' ),
  423. new ViewUIElement( 'marker-search' ),
  424. new ViewText( 'oba' ),
  425. new ViewUIElement( 'marker-search' ),
  426. new ViewText( 'r' )
  427. ] );
  428. const marker = { name: 'search', start: [ 2 ], end: [ 5 ] };
  429. expectResult( frag, 'foobar', marker );
  430. } );
  431. it( 'config.view is an object', () => {
  432. const helper = elementToMarker( {
  433. view: {
  434. name: 'span',
  435. 'data-marker': 'search'
  436. },
  437. model: 'search'
  438. } );
  439. conversion.for( 'view' ).add( helper );
  440. const frag = new ViewDocumentFragment( [
  441. new ViewText( 'f' ),
  442. new ViewUIElement( 'span', { 'data-marker': 'search' } ),
  443. new ViewText( 'oob' ),
  444. new ViewUIElement( 'span', { 'data-marker': 'search' } ),
  445. new ViewText( 'ar' )
  446. ] );
  447. const marker = { name: 'search', start: [ 1 ], end: [ 4 ] };
  448. expectResult( frag, 'foobar', marker );
  449. } );
  450. it( 'config.model is a function', () => {
  451. const helper = elementToMarker( {
  452. view: 'comment',
  453. model: viewElement => 'comment:' + viewElement.getAttribute( 'data-comment-id' )
  454. } );
  455. conversion.for( 'view' ).add( helper );
  456. const frag = new ViewDocumentFragment( [
  457. new ViewText( 'foo' ),
  458. new ViewUIElement( 'comment', { 'data-comment-id': 4 } ),
  459. new ViewText( 'b' ),
  460. new ViewUIElement( 'comment', { 'data-comment-id': 4 } ),
  461. new ViewText( 'ar' )
  462. ] );
  463. const marker = { name: 'comment:4', start: [ 3 ], end: [ 4 ] };
  464. expectResult( frag, 'foobar', marker );
  465. } );
  466. } );
  467. function expectResult( viewToConvert, modelString, marker ) {
  468. const model = dispatcher.convert( viewToConvert );
  469. if ( marker ) {
  470. expect( model.markers.has( marker.name ) ).to.be.true;
  471. const convertedMarker = model.markers.get( marker.name );
  472. expect( convertedMarker.start.path ).to.deep.equal( marker.start );
  473. expect( convertedMarker.end.path ).to.deep.equal( marker.end );
  474. }
  475. expect( stringify( model ) ).to.equal( modelString );
  476. }
  477. } );