conversion.js 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816
  1. /**
  2. * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
  3. * For licensing, see LICENSE.md.
  4. */
  5. import Conversion from '../../src/conversion/conversion';
  6. import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
  7. import UpcastDispatcher from '../../src/conversion/upcastdispatcher';
  8. import UpcastHelpers, { convertText, convertToModelFragment } from '../../src/conversion/upcasthelpers';
  9. import DowncastHelpers from '../../src/conversion/downcasthelpers';
  10. import EditingController from '../../src/controller/editingcontroller';
  11. import Model from '../../src/model/model';
  12. import { parse as viewParse, stringify as viewStringify } from '../../src/dev-utils/view';
  13. import { setData, stringify as modelStringify } from '../../src/dev-utils/model';
  14. import ViewDocumentFragment from '../../src/view/documentfragment';
  15. import ViewText from '../../src/view/text';
  16. import ViewUIElement from '../../src/view/uielement';
  17. describe( 'Conversion', () => {
  18. let conversion, dispA, dispB;
  19. beforeEach( () => {
  20. conversion = new Conversion();
  21. // Placeholders. Will be used only to see if their were given as attribute for a spy function.
  22. dispA = Symbol( 'dispA' );
  23. dispB = Symbol( 'dispB' );
  24. conversion.register( 'ab', new UpcastHelpers( [ dispA, dispB ] ) );
  25. conversion.register( 'a', new UpcastHelpers( dispA ) );
  26. conversion.register( 'b', new UpcastHelpers( dispB ) );
  27. } );
  28. describe( 'register()', () => {
  29. it( 'should throw when trying to use same group name twice', () => {
  30. expect( () => {
  31. conversion.register( 'ab' );
  32. } ).to.throw( CKEditorError, /conversion-register-group-exists/ );
  33. } );
  34. } );
  35. describe( 'for()', () => {
  36. it( 'should return object with .add() method', () => {
  37. const forResult = conversion.for( 'ab' );
  38. expect( forResult.add ).to.be.instanceof( Function );
  39. } );
  40. it( 'should throw if non-existing group name has been used', () => {
  41. expect( () => {
  42. conversion.for( 'foo' );
  43. } ).to.throw( CKEditorError, /conversion-for-unknown-group/ );
  44. } );
  45. } );
  46. describe( 'add()', () => {
  47. let helperA, helperB;
  48. beforeEach( () => {
  49. helperA = sinon.stub();
  50. helperB = sinon.stub();
  51. } );
  52. it( 'should be chainable', () => {
  53. const forResult = conversion.for( 'ab' );
  54. const addResult = forResult.add( () => {} );
  55. expect( addResult ).to.equal( addResult.add( () => {} ) );
  56. } );
  57. it( 'should fire given helper for every dispatcher in given group', () => {
  58. conversion.for( 'ab' ).add( helperA );
  59. expect( helperA.calledWithExactly( dispA ) ).to.be.true;
  60. expect( helperA.calledWithExactly( dispB ) ).to.be.true;
  61. conversion.for( 'b' ).add( helperB );
  62. expect( helperB.calledWithExactly( dispA ) ).to.be.false;
  63. expect( helperB.calledWithExactly( dispB ) ).to.be.true;
  64. } );
  65. } );
  66. describe( 'converters', () => {
  67. let viewDispatcher, model, schema, conversion, modelRoot, viewRoot;
  68. beforeEach( () => {
  69. model = new Model();
  70. const controller = new EditingController( model );
  71. const modelDoc = model.document;
  72. modelRoot = modelDoc.createRoot();
  73. viewRoot = controller.view.document.getRoot();
  74. // Set name of view root the same as dom root.
  75. // This is a mock of attaching view root to dom root.
  76. viewRoot._name = 'div';
  77. schema = model.schema;
  78. schema.extend( '$text', {
  79. allowIn: '$root',
  80. allowAttributes: [ 'bold' ]
  81. } );
  82. schema.register( 'paragraph', {
  83. inheritAllFrom: '$block'
  84. } );
  85. viewDispatcher = new UpcastDispatcher( model, { schema } );
  86. viewDispatcher.on( 'text', convertText() );
  87. viewDispatcher.on( 'element', convertToModelFragment(), { priority: 'lowest' } );
  88. viewDispatcher.on( 'documentFragment', convertToModelFragment(), { priority: 'lowest' } );
  89. conversion = new Conversion();
  90. conversion.register( 'upcast', new UpcastHelpers( [ viewDispatcher ] ) );
  91. conversion.register( 'downcast', new DowncastHelpers( controller.downcastDispatcher ) );
  92. } );
  93. describe( 'elementToElement', () => {
  94. it( 'config.view is a string', () => {
  95. conversion.elementToElement( { model: 'paragraph', view: 'p' } );
  96. test( '<p>Foo</p>', '<paragraph>Foo</paragraph>' );
  97. } );
  98. it( 'config.converterPriority is defined', () => {
  99. conversion.elementToElement( { model: 'paragraph', view: 'p' } );
  100. conversion.elementToElement( { model: 'paragraph', view: 'div', converterPriority: 'high' } );
  101. test( '<div>Foo</div>', '<paragraph>Foo</paragraph>' );
  102. test( '<p>Foo</p>', '<paragraph>Foo</paragraph>', '<div>Foo</div>' );
  103. } );
  104. it( 'config.view is an object', () => {
  105. schema.register( 'fancyParagraph', {
  106. inheritAllFrom: 'paragraph'
  107. } );
  108. conversion.elementToElement( {
  109. model: 'fancyParagraph',
  110. view: {
  111. name: 'p',
  112. classes: 'fancy'
  113. }
  114. } );
  115. test( '<p class="fancy">Foo</p>', '<fancyParagraph>Foo</fancyParagraph>' );
  116. } );
  117. it( 'config.view is an object with upcastAlso defined', () => {
  118. conversion.elementToElement( {
  119. model: 'paragraph',
  120. view: 'p',
  121. upcastAlso: [
  122. 'div',
  123. {
  124. // Any element with `display: block` style.
  125. styles: {
  126. display: 'block'
  127. }
  128. }
  129. ]
  130. } );
  131. test( '<p>Foo</p>', '<paragraph>Foo</paragraph>' );
  132. test( '<div>Foo</div>', '<paragraph>Foo</paragraph>', '<p>Foo</p>' );
  133. test( '<span style="display:block">Foo</span>', '<paragraph>Foo</paragraph>', '<p>Foo</p>' );
  134. } );
  135. it( 'upcastAlso given as a function', () => {
  136. schema.register( 'heading', {
  137. inheritAllFrom: '$block'
  138. } );
  139. conversion.elementToElement( {
  140. model: 'heading',
  141. view: 'h2',
  142. upcastAlso: viewElement => {
  143. const fontSize = viewElement.getStyle( 'font-size' );
  144. if ( !fontSize ) {
  145. return null;
  146. }
  147. const match = fontSize.match( /(\d+)\s*px/ );
  148. if ( !match ) {
  149. return null;
  150. }
  151. const size = Number( match[ 1 ] );
  152. if ( size >= 26 ) {
  153. return { name: true, style: [ 'font-size' ] };
  154. }
  155. return null;
  156. }
  157. } );
  158. conversion.elementToElement( {
  159. model: 'paragraph',
  160. view: 'p'
  161. } );
  162. test( '<p></p>', '<paragraph></paragraph>' );
  163. test( '<p style="font-size:20px"></p>', '<paragraph></paragraph>', '<p></p>' );
  164. test( '<h2></h2>', '<heading></heading>' );
  165. test( '<p style="font-size:26px"></p>', '<heading></heading>', '<h2></h2>' );
  166. } );
  167. } );
  168. describe( 'attributeToElement', () => {
  169. beforeEach( () => {
  170. conversion.elementToElement( { model: 'paragraph', view: 'p' } );
  171. } );
  172. it( 'config.view is a string', () => {
  173. conversion.attributeToElement( { model: 'bold', view: 'strong' } );
  174. test( '<p><strong>Foo</strong> bar</p>', '<paragraph><$text bold="true">Foo</$text> bar</paragraph>' );
  175. } );
  176. it( 'config.converterPriority is defined', () => {
  177. conversion.attributeToElement( { model: 'bold', view: 'strong' } );
  178. conversion.attributeToElement( { model: 'bold', view: 'b', converterPriority: 'high' } );
  179. test( '<p><b>Foo</b></p>', '<paragraph><$text bold="true">Foo</$text></paragraph>' );
  180. test( '<p><strong>Foo</strong></p>', '<paragraph><$text bold="true">Foo</$text></paragraph>', '<p><b>Foo</b></p>' );
  181. } );
  182. it( 'config.view is an object', () => {
  183. conversion.attributeToElement( {
  184. model: 'bold',
  185. view: {
  186. name: 'span',
  187. classes: 'bold'
  188. }
  189. } );
  190. test( '<p><span class="bold">Foo</span> bar</p>', '<paragraph><$text bold="true">Foo</$text> bar</paragraph>' );
  191. } );
  192. it( 'config.view is an object with upcastAlso defined', () => {
  193. conversion.attributeToElement( {
  194. model: 'bold',
  195. view: 'strong',
  196. upcastAlso: [
  197. 'b',
  198. {
  199. name: 'span',
  200. classes: 'bold'
  201. },
  202. {
  203. name: 'span',
  204. styles: {
  205. 'font-weight': 'bold'
  206. }
  207. },
  208. viewElement => {
  209. const fontWeight = viewElement.getStyle( 'font-weight' );
  210. if ( viewElement.is( 'span' ) && fontWeight && /\d+/.test( fontWeight ) && Number( fontWeight ) > 500 ) {
  211. return {
  212. name: true,
  213. styles: [ 'font-weight' ]
  214. };
  215. }
  216. }
  217. ]
  218. } );
  219. test(
  220. '<p><strong>Foo</strong></p>',
  221. '<paragraph><$text bold="true">Foo</$text></paragraph>'
  222. );
  223. test(
  224. '<p><b>Foo</b></p>',
  225. '<paragraph><$text bold="true">Foo</$text></paragraph>',
  226. '<p><strong>Foo</strong></p>'
  227. );
  228. test(
  229. '<p><span class="bold">Foo</span></p>',
  230. '<paragraph><$text bold="true">Foo</$text></paragraph>',
  231. '<p><strong>Foo</strong></p>'
  232. );
  233. test(
  234. '<p><span style="font-weight: bold;">Foo</span></p>',
  235. '<paragraph><$text bold="true">Foo</$text></paragraph>',
  236. '<p><strong>Foo</strong></p>'
  237. );
  238. test(
  239. '<p><span style="font-weight: 500;">Foo</span></p>',
  240. '<paragraph>Foo</paragraph>',
  241. '<p>Foo</p>'
  242. );
  243. test(
  244. '<p><span style="font-weight: 600;">Foo</span></p>',
  245. '<paragraph><$text bold="true">Foo</$text></paragraph>',
  246. '<p><strong>Foo</strong></p>'
  247. );
  248. } );
  249. it( 'model attribute value is enumerable', () => {
  250. schema.extend( '$text', {
  251. allowAttributes: [ 'fontSize' ]
  252. } );
  253. conversion.attributeToElement( {
  254. model: {
  255. key: 'fontSize',
  256. values: [ 'big', 'small' ]
  257. },
  258. view: {
  259. big: {
  260. name: 'span',
  261. styles: {
  262. 'font-size': '1.2em'
  263. }
  264. },
  265. small: {
  266. name: 'span',
  267. styles: {
  268. 'font-size': '0.8em'
  269. }
  270. }
  271. },
  272. upcastAlso: {
  273. big: viewElement => {
  274. const fontSize = viewElement.getStyle( 'font-size' );
  275. if ( !fontSize ) {
  276. return null;
  277. }
  278. const match = fontSize.match( /(\d+)\s*px/ );
  279. if ( !match ) {
  280. return null;
  281. }
  282. const size = Number( match[ 1 ] );
  283. if ( viewElement.is( 'span' ) && size > 10 ) {
  284. return { name: true, style: [ 'font-size' ] };
  285. }
  286. return null;
  287. },
  288. small: viewElement => {
  289. const fontSize = viewElement.getStyle( 'font-size' );
  290. if ( !fontSize ) {
  291. return null;
  292. }
  293. const match = fontSize.match( /(\d+)\s*px/ );
  294. if ( !match ) {
  295. return null;
  296. }
  297. const size = Number( match[ 1 ] );
  298. if ( viewElement.is( 'span' ) && size < 10 ) {
  299. return { name: true, style: [ 'font-size' ] };
  300. }
  301. return null;
  302. }
  303. }
  304. } );
  305. test(
  306. '<p><span style="font-size:1.2em">Foo</span> bar</p>',
  307. '<paragraph><$text fontSize="big">Foo</$text> bar</paragraph>'
  308. );
  309. test(
  310. '<p><span style="font-size:12px">Foo</span> bar</p>',
  311. '<paragraph><$text fontSize="big">Foo</$text> bar</paragraph>',
  312. '<p><span style="font-size:1.2em">Foo</span> bar</p>'
  313. );
  314. test(
  315. '<p><span style="font-size:0.8em">Foo</span> bar</p>',
  316. '<paragraph><$text fontSize="small">Foo</$text> bar</paragraph>'
  317. );
  318. test(
  319. '<p><span style="font-size:8px">Foo</span> bar</p>',
  320. '<paragraph><$text fontSize="small">Foo</$text> bar</paragraph>',
  321. '<p><span style="font-size:0.8em">Foo</span> bar</p>'
  322. );
  323. test(
  324. '<p><span style="font-size:10px">Foo</span> bar</p>',
  325. '<paragraph>Foo bar</paragraph>',
  326. '<p>Foo bar</p>'
  327. );
  328. } );
  329. it( 'config.model.name is given', () => {
  330. schema.extend( '$text', {
  331. allowAttributes: [ 'textDecoration' ]
  332. } );
  333. conversion.attributeToElement( {
  334. model: {
  335. key: 'textDecoration',
  336. values: [ 'underline', 'lineThrough' ],
  337. name: '$text'
  338. },
  339. view: {
  340. underline: {
  341. name: 'span',
  342. styles: {
  343. 'text-decoration': 'underline'
  344. }
  345. },
  346. lineThrough: {
  347. name: 'span',
  348. styles: {
  349. 'text-decoration': 'line-through'
  350. }
  351. }
  352. }
  353. } );
  354. test(
  355. '<p><span style="text-decoration:underline">Foo</span></p>',
  356. '<paragraph><$text textDecoration="underline">Foo</$text></paragraph>'
  357. );
  358. test(
  359. '<p><span style="text-decoration:line-through">Foo</span></p>',
  360. '<paragraph><$text textDecoration="lineThrough">Foo</$text></paragraph>'
  361. );
  362. test(
  363. '<p><span style="text-decoration:underline">Foo</span></p>',
  364. '<paragraph><$text textDecoration="underline">Foo</$text></paragraph>'
  365. );
  366. } );
  367. } );
  368. describe( 'attributeToAttribute', () => {
  369. beforeEach( () => {
  370. conversion.elementToElement( { model: 'image', view: 'img' } );
  371. schema.register( 'image', {
  372. inheritAllFrom: '$block',
  373. } );
  374. } );
  375. it( 'config.view and config.model are strings', () => {
  376. schema.extend( 'image', {
  377. allowAttributes: [ 'source' ]
  378. } );
  379. conversion.attributeToAttribute( { model: 'source', view: 'src' } );
  380. test( '<img src="foo.jpg"></img>', '<image source="foo.jpg"></image>' );
  381. } );
  382. it( 'config.view and config.model are objects', () => {
  383. schema.extend( 'image', {
  384. allowAttributes: [ 'aside' ]
  385. } );
  386. conversion.attributeToAttribute( {
  387. model: {
  388. name: 'image',
  389. key: 'aside',
  390. values: [ 'aside' ]
  391. },
  392. view: {
  393. aside: {
  394. name: 'img',
  395. key: 'class',
  396. value: [ 'aside', 'half-size' ]
  397. }
  398. }
  399. } );
  400. conversion.elementToElement( { model: 'paragraph', view: 'p' } );
  401. test( '<img class="aside half-size"></img>', '<image aside="aside"></image>' );
  402. test( '<p class="aside half-size"></p>', '<paragraph></paragraph>', '<p></p>' );
  403. } );
  404. it( 'config.view and config.model are objects - convert to style attribute', () => {
  405. schema.extend( 'image', {
  406. allowAttributes: [ 'aside' ]
  407. } );
  408. conversion.attributeToAttribute( {
  409. model: {
  410. name: 'image',
  411. key: 'aside',
  412. values: [ 'aside' ]
  413. },
  414. view: {
  415. aside: {
  416. name: 'img',
  417. key: 'style',
  418. value: {
  419. float: 'right',
  420. width: '50%',
  421. margin: '5px'
  422. }
  423. }
  424. }
  425. } );
  426. conversion.elementToElement( { model: 'paragraph', view: 'p' } );
  427. test( '<img style="float:right;margin:5px;width:50%"></img>', '<image aside="aside"></image>' );
  428. test( '<p style="float:right;margin:5px;width:50%"></p>', '<paragraph></paragraph>', '<p></p>' );
  429. } );
  430. it( 'config is an array with upcastAlso defined', () => {
  431. conversion.elementToElement( { model: 'paragraph', view: 'p' } );
  432. schema.extend( 'paragraph', {
  433. allowAttributes: [ 'align' ]
  434. } );
  435. conversion.attributeToAttribute( {
  436. model: {
  437. key: 'align',
  438. values: [ 'right', 'center' ]
  439. },
  440. view: {
  441. right: {
  442. key: 'class',
  443. value: 'align-right'
  444. },
  445. center: {
  446. key: 'class',
  447. value: 'align-center'
  448. }
  449. },
  450. upcastAlso: {
  451. right: {
  452. styles: {
  453. 'text-align': 'right'
  454. }
  455. },
  456. center: {
  457. styles: {
  458. 'text-align': 'center'
  459. }
  460. }
  461. }
  462. } );
  463. test(
  464. '<p class="align-right">Foo</p>',
  465. '<paragraph align="right">Foo</paragraph>'
  466. );
  467. test(
  468. '<p style="text-align:right">Foo</p>',
  469. '<paragraph align="right">Foo</paragraph>',
  470. '<p class="align-right">Foo</p>'
  471. );
  472. test(
  473. '<p class="align-center">Foo</p>',
  474. '<paragraph align="center">Foo</paragraph>'
  475. );
  476. test(
  477. '<p style="text-align:center">Foo</p>',
  478. '<paragraph align="center">Foo</paragraph>',
  479. '<p class="align-center">Foo</p>'
  480. );
  481. } );
  482. it( 'config.view and config.model have name and key set', () => {
  483. schema.extend( 'image', {
  484. allowAttributes: [ 'source' ]
  485. } );
  486. conversion.attributeToAttribute( {
  487. model: {
  488. name: 'image',
  489. key: 'source'
  490. },
  491. view: {
  492. name: 'img',
  493. key: 'src'
  494. }
  495. } );
  496. test( '<img src="foo.jpg"></img>', '<image source="foo.jpg"></image>' );
  497. } );
  498. // #1443.
  499. it( 'should not set attributes on the element\'s children', () => {
  500. schema.register( 'div', {
  501. inheritAllFrom: '$root',
  502. allowWhere: '$block',
  503. isLimit: true,
  504. allowAttributes: [ 'border', 'shade' ]
  505. } );
  506. conversion.elementToElement(
  507. { model: 'div', view: 'div' }
  508. );
  509. conversion.attributeToAttribute( { model: 'border', view: { key: 'class', value: 'border' } } );
  510. conversion.attributeToAttribute( { model: 'shade', view: { key: 'class', value: 'shade' } } );
  511. test(
  512. '<div class="border"><div class="shade"></div></div>',
  513. '<div border="border"><div shade="shade"></div></div>'
  514. );
  515. } );
  516. } );
  517. describe( 'for( \'downcast\' )', () => {
  518. describe( 'elementToElement()', () => {
  519. it( 'adds downcast converter', () => {
  520. conversion.for( 'downcast' ).elementToElement( { model: 'paragraph', view: 'p' } );
  521. testDowncast( '<paragraph>foo</paragraph>', '<p>foo</p>' );
  522. } );
  523. } );
  524. describe( 'attributeToElement()', () => {
  525. it( 'adds downcast converter', () => {
  526. conversion.for( 'downcast' ).elementToElement( { model: 'paragraph', view: 'p' } );
  527. conversion.for( 'downcast' ).attributeToElement( { model: 'bold', view: 'strong' } );
  528. testDowncast( '<paragraph><$text bold="true">Foo</$text> bar</paragraph>', '<p><strong>Foo</strong> bar</p>' );
  529. } );
  530. } );
  531. describe( 'attributeToAttribute()', () => {
  532. it( 'adds downcast converter', () => {
  533. schema.register( 'image', {
  534. inheritAllFrom: '$block',
  535. allowAttributes: [ 'source' ]
  536. } );
  537. conversion.for( 'downcast' ).elementToElement( { model: 'image', view: 'img' } );
  538. conversion.for( 'downcast' ).attributeToAttribute( { model: 'source', view: 'src' } );
  539. testDowncast( '<image source="foo.jpg"></image>', '<img src="foo.jpg"></img>' );
  540. } );
  541. } );
  542. describe( 'markerToElement()', () => {
  543. it( 'adds downcast converter', () => {
  544. conversion.for( 'downcast' ).markerToElement( { model: 'search', view: 'marker-search' } );
  545. model.change( writer => {
  546. writer.insertText( 'foo', modelRoot, 0 );
  547. const range = writer.createRange(
  548. writer.createPositionAt( modelRoot, 1 ),
  549. writer.createPositionAt( modelRoot, 2 )
  550. );
  551. writer.addMarker( 'search', { range, usingOperation: false } );
  552. } );
  553. expect( viewStringify( viewRoot, null, { ignoreRoot: true } ) )
  554. .to.equal( 'f<marker-search></marker-search>o<marker-search></marker-search>o' );
  555. } );
  556. } );
  557. describe( 'markerToHighlight()', () => {
  558. it( 'adds downcast converter', () => {
  559. conversion.for( 'downcast' ).markerToHighlight( { model: 'comment', view: { classes: 'comment' } } );
  560. model.change( writer => {
  561. writer.insertText( 'foo', modelRoot, 0 );
  562. const range = writer.createRange(
  563. writer.createPositionAt( modelRoot, 0 ),
  564. writer.createPositionAt( modelRoot, 3 )
  565. );
  566. writer.addMarker( 'comment', { range, usingOperation: false } );
  567. } );
  568. expect( viewStringify( viewRoot, null, { ignoreRoot: true } ) )
  569. .to.equal( '<span class="comment">foo</span>' );
  570. } );
  571. } );
  572. } );
  573. describe( 'for( \'upcast\' )', () => {
  574. describe( 'elementToElement()', () => {
  575. it( 'adds upcast converter', () => {
  576. conversion.for( 'upcast' ).elementToElement( { model: 'paragraph', view: 'p' } );
  577. // TODO this shouldn't be required
  578. conversion.for( 'downcast' ).elementToElement( { model: 'paragraph', view: 'p' } );
  579. testUpcast( '<p>foo</p>', '<paragraph>foo</paragraph>' );
  580. } );
  581. } );
  582. describe( 'elementToAttribute()', () => {
  583. it( 'adds upcast converter', () => {
  584. conversion.for( 'upcast' ).elementToElement( { model: 'paragraph', view: 'p' } );
  585. conversion.for( 'downcast' ).elementToElement( { model: 'paragraph', view: 'p' } );
  586. conversion.for( 'upcast' ).elementToAttribute( { model: 'bold', view: 'strong' } );
  587. conversion.for( 'downcast' ).attributeToElement( { model: 'bold', view: 'strong' } );
  588. testUpcast( '<p><strong>Foo</strong> bar</p>', '<paragraph><$text bold="true">Foo</$text> bar</paragraph>' );
  589. } );
  590. } );
  591. describe( 'attributeToAttribute()', () => {
  592. it( 'adds upcast converter', () => {
  593. schema.register( 'image', {
  594. inheritAllFrom: '$block',
  595. allowAttributes: [ 'source' ]
  596. } );
  597. conversion.for( 'downcast' ).elementToElement( { model: 'image', view: 'img' } );
  598. conversion.for( 'downcast' ).attributeToAttribute( { model: 'source', view: 'src' } );
  599. conversion.for( 'upcast' ).elementToElement( { model: 'image', view: 'img' } );
  600. conversion.for( 'upcast' ).attributeToAttribute( { model: 'source', view: 'src' } );
  601. testUpcast( '<img src="foo.jpg"></img>', '<image source="foo.jpg"></image>' );
  602. } );
  603. } );
  604. describe( 'markerToElement()', () => {
  605. it( 'adds upcast converter', () => {
  606. conversion.for( 'upcast' ).elementToMarker( { model: 'search', view: 'marker-search' } );
  607. conversion.for( 'downcast' ).markerToElement( { model: 'search', view: 'marker-search' } );
  608. const frag = new ViewDocumentFragment( [
  609. new ViewText( 'fo' ),
  610. new ViewUIElement( 'marker-search' ),
  611. new ViewText( 'oba' ),
  612. new ViewUIElement( 'marker-search' ),
  613. new ViewText( 'r' )
  614. ] );
  615. const marker = { name: 'search', start: [ 2 ], end: [ 5 ] };
  616. testUpcastMarker( frag, 'foobar', marker );
  617. } );
  618. } );
  619. } );
  620. function testDowncast( input, expectedView ) {
  621. setData( model, input );
  622. expect( viewStringify( viewRoot, null, { ignoreRoot: true } ) ).to.equal( expectedView );
  623. }
  624. function testUpcast( input, expectedModel ) {
  625. loadData( input );
  626. expect( modelStringify( model.document.getRoot() ) ).to.equal( expectedModel );
  627. }
  628. function test( input, expectedModel, expectedView = null ) {
  629. loadData( input );
  630. expect( modelStringify( model.document.getRoot() ) ).to.equal( expectedModel );
  631. expect( viewStringify( viewRoot, null, { ignoreRoot: true } ) ).to.equal( expectedView || input );
  632. }
  633. function loadData( input ) {
  634. const parsedView = viewParse( input );
  635. let convertedModel;
  636. model.change( writer => {
  637. convertedModel = viewDispatcher.convert( parsedView, writer );
  638. } );
  639. model.change( writer => {
  640. writer.remove( writer.createRange(
  641. writer.createPositionAt( modelRoot, 0 ),
  642. writer.createPositionAt( modelRoot, modelRoot.maxOffset ) )
  643. );
  644. writer.insert( convertedModel, modelRoot, 0 );
  645. } );
  646. }
  647. function testUpcastMarker( viewToConvert, modelString, marker ) {
  648. const conversionResult = model.change( writer => viewDispatcher.convert( viewToConvert, writer ) );
  649. if ( marker ) {
  650. expect( conversionResult.markers.has( marker.name ) ).to.be.true;
  651. const convertedMarker = conversionResult.markers.get( marker.name );
  652. expect( convertedMarker.start.path ).to.deep.equal( marker.start );
  653. expect( convertedMarker.end.path ).to.deep.equal( marker.end );
  654. }
  655. expect( viewStringify( conversionResult ) ).to.equal( modelString );
  656. }
  657. } );
  658. } );