8
0

definition-conversion.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537
  1. /**
  2. * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
  3. * For licensing, see LICENSE.md.
  4. */
  5. import {
  6. modelElementIsViewElement, modelAttributeIsViewElement, modelAttributeIsViewAttribute
  7. } from '../../src/conversion/definition-conversion';
  8. import Conversion from '../../src/conversion/conversion';
  9. import ViewConversionDispatcher from '../../src/conversion/viewconversiondispatcher';
  10. import { convertText, convertToModelFragment } from '../../src/conversion/view-to-model-converters';
  11. import EditingController from '../../src/controller/editingcontroller';
  12. import Model from '../../src/model/model';
  13. import ModelRange from '../../src/model/range';
  14. import { stringify as viewStringify, parse as viewParse } from '../../src/dev-utils/view';
  15. import { stringify as modelStringify } from '../../src/dev-utils/model';
  16. describe( 'definition-conversion', () => {
  17. let viewDispatcher, model, schema, conversion, modelRoot, viewRoot;
  18. beforeEach( () => {
  19. model = new Model();
  20. const controller = new EditingController( model );
  21. const modelDoc = model.document;
  22. modelRoot = modelDoc.createRoot();
  23. viewRoot = controller.view.getRoot();
  24. // Set name of view root the same as dom root.
  25. // This is a mock of attaching view root to dom root.
  26. viewRoot._name = 'div';
  27. schema = model.schema;
  28. schema.extend( '$text', {
  29. allowAttributes: [ 'bold' ]
  30. } );
  31. schema.register( 'paragraph', {
  32. inheritAllFrom: '$block'
  33. } );
  34. viewDispatcher = new ViewConversionDispatcher( model, { schema } );
  35. viewDispatcher.on( 'text', convertText() );
  36. viewDispatcher.on( 'element', convertToModelFragment(), { priority: 'lowest' } );
  37. viewDispatcher.on( 'documentFragment', convertToModelFragment(), { priority: 'lowest' } );
  38. conversion = new Conversion();
  39. conversion.register( 'view', [ viewDispatcher ] );
  40. conversion.register( 'model', [ controller.modelToView ] );
  41. } );
  42. describe( 'modelElementIsViewElement', () => {
  43. it( 'config.view is a string', () => {
  44. modelElementIsViewElement( conversion, { model: 'paragraph', view: 'p' } );
  45. test( '<p>Foo</p>', '<paragraph>Foo</paragraph>' );
  46. } );
  47. it( 'config.view is an object', () => {
  48. schema.register( 'fancyParagraph', {
  49. inheritAllFrom: 'paragraph'
  50. } );
  51. modelElementIsViewElement( conversion, {
  52. model: 'fancyParagraph',
  53. view: {
  54. name: 'p',
  55. class: 'fancy'
  56. }
  57. } );
  58. test( '<p class="fancy">Foo</p>', '<fancyParagraph>Foo</fancyParagraph>' );
  59. } );
  60. it( 'config.view is an object with upcastAlso defined', () => {
  61. modelElementIsViewElement( conversion, {
  62. model: 'paragraph',
  63. view: 'p',
  64. upcastAlso: [
  65. 'div',
  66. {
  67. // Match any name.
  68. name: /./,
  69. style: {
  70. display: 'block'
  71. }
  72. }
  73. ]
  74. } );
  75. test( '<p>Foo</p>', '<paragraph>Foo</paragraph>' );
  76. test( '<div>Foo</div>', '<paragraph>Foo</paragraph>', '<p>Foo</p>' );
  77. test( '<span style="display:block">Foo</span>', '<paragraph>Foo</paragraph>', '<p>Foo</p>' );
  78. } );
  79. it( 'upcastAlso given as a function', () => {
  80. schema.register( 'heading', {
  81. inheritAllFrom: '$block'
  82. } );
  83. modelElementIsViewElement( conversion, {
  84. model: 'heading',
  85. view: 'h2',
  86. upcastAlso: viewElement => {
  87. const fontSize = viewElement.getStyle( 'font-size' );
  88. if ( !fontSize ) {
  89. return null;
  90. }
  91. const match = fontSize.match( /(\d+)\s*px/ );
  92. if ( !match ) {
  93. return null;
  94. }
  95. const size = Number( match[ 1 ] );
  96. if ( size >= 26 ) {
  97. return { name: true, style: [ 'font-size' ] };
  98. }
  99. return null;
  100. }
  101. } );
  102. modelElementIsViewElement( conversion, {
  103. model: 'paragraph',
  104. view: 'p'
  105. } );
  106. test( '<p></p>', '<paragraph></paragraph>' );
  107. test( '<p style="font-size:20px"></p>', '<paragraph></paragraph>', '<p></p>' );
  108. test( '<h2></h2>', '<heading></heading>' );
  109. test( '<p style="font-size:26px"></p>', '<heading></heading>', '<h2></h2>' );
  110. } );
  111. } );
  112. describe( 'modelAttributeIsViewElement', () => {
  113. beforeEach( () => {
  114. modelElementIsViewElement( conversion, { model: 'paragraph', view: 'p' } );
  115. } );
  116. it( 'config.view is a string', () => {
  117. modelAttributeIsViewElement( conversion, 'bold', { view: 'strong' } );
  118. test( '<p><strong>Foo</strong> bar</p>', '<paragraph><$text bold="true">Foo</$text> bar</paragraph>' );
  119. } );
  120. it( 'config.view is an object', () => {
  121. modelAttributeIsViewElement( conversion, 'bold', {
  122. view: {
  123. name: 'span',
  124. class: 'bold'
  125. }
  126. } );
  127. test( '<p><span class="bold">Foo</span> bar</p>', '<paragraph><$text bold="true">Foo</$text> bar</paragraph>' );
  128. } );
  129. it( 'config.view is an object with upcastAlso defined', () => {
  130. modelAttributeIsViewElement( conversion, 'bold', {
  131. view: 'strong',
  132. upcastAlso: [
  133. 'b',
  134. {
  135. name: 'span',
  136. class: 'bold'
  137. },
  138. {
  139. name: 'span',
  140. style: {
  141. 'font-weight': 'bold'
  142. }
  143. },
  144. viewElement => {
  145. const fontWeight = viewElement.getStyle( 'font-weight' );
  146. if ( viewElement.is( 'span' ) && fontWeight && /\d+/.test( fontWeight ) && Number( fontWeight ) > 500 ) {
  147. return {
  148. name: true,
  149. style: [ 'font-weight' ]
  150. };
  151. }
  152. }
  153. ]
  154. } );
  155. test(
  156. '<p><strong>Foo</strong></p>',
  157. '<paragraph><$text bold="true">Foo</$text></paragraph>'
  158. );
  159. test(
  160. '<p><b>Foo</b></p>',
  161. '<paragraph><$text bold="true">Foo</$text></paragraph>',
  162. '<p><strong>Foo</strong></p>'
  163. );
  164. test(
  165. '<p><span class="bold">Foo</span></p>',
  166. '<paragraph><$text bold="true">Foo</$text></paragraph>',
  167. '<p><strong>Foo</strong></p>'
  168. );
  169. test(
  170. '<p><span style="font-weight: bold;">Foo</span></p>',
  171. '<paragraph><$text bold="true">Foo</$text></paragraph>',
  172. '<p><strong>Foo</strong></p>'
  173. );
  174. test(
  175. '<p><span style="font-weight: 500;">Foo</span></p>',
  176. '<paragraph>Foo</paragraph>',
  177. '<p>Foo</p>'
  178. );
  179. test(
  180. '<p><span style="font-weight: 600;">Foo</span></p>',
  181. '<paragraph><$text bold="true">Foo</$text></paragraph>',
  182. '<p><strong>Foo</strong></p>'
  183. );
  184. } );
  185. it( 'config.model is a string', () => {
  186. schema.extend( '$text', {
  187. allowAttributes: [ 'styled' ]
  188. } );
  189. modelAttributeIsViewElement( conversion, 'styled', {
  190. model: 'dark',
  191. view: {
  192. name: 'span',
  193. class: [ 'styled', 'styled-dark' ]
  194. }
  195. } );
  196. test( '<p><span class="styled styled-dark">Foo</span> bar</p>', '<paragraph><$text styled="dark">Foo</$text> bar</paragraph>' );
  197. } );
  198. it( 'config is an array', () => {
  199. schema.extend( '$text', {
  200. allowAttributes: [ 'fontSize' ]
  201. } );
  202. modelAttributeIsViewElement( conversion, 'fontSize', [
  203. {
  204. model: 'big',
  205. view: {
  206. name: 'span',
  207. style: {
  208. 'font-size': '1.2em'
  209. }
  210. }
  211. },
  212. {
  213. model: 'small',
  214. view: {
  215. name: 'span',
  216. style: {
  217. 'font-size': '0.8em'
  218. }
  219. }
  220. }
  221. ] );
  222. test(
  223. '<p><span style="font-size:1.2em">Foo</span> bar</p>',
  224. '<paragraph><$text fontSize="big">Foo</$text> bar</paragraph>'
  225. );
  226. test(
  227. '<p><span style="font-size:0.8em">Foo</span> bar</p>',
  228. '<paragraph><$text fontSize="small">Foo</$text> bar</paragraph>'
  229. );
  230. } );
  231. it( 'config is an array with upcastAlso defined', () => {
  232. schema.extend( '$text', {
  233. allowAttributes: [ 'fontSize' ]
  234. } );
  235. modelAttributeIsViewElement( conversion, 'fontSize', [
  236. {
  237. model: 'big',
  238. view: {
  239. name: 'span',
  240. style: {
  241. 'font-size': '1.2em'
  242. }
  243. },
  244. upcastAlso: viewElement => {
  245. const fontSize = viewElement.getStyle( 'font-size' );
  246. if ( !fontSize ) {
  247. return null;
  248. }
  249. const match = fontSize.match( /(\d+)\s*px/ );
  250. if ( !match ) {
  251. return null;
  252. }
  253. const size = Number( match[ 1 ] );
  254. if ( viewElement.is( 'span' ) && size > 10 ) {
  255. return { name: true, style: [ 'font-size' ] };
  256. }
  257. return null;
  258. }
  259. },
  260. {
  261. model: 'small',
  262. view: {
  263. name: 'span',
  264. style: {
  265. 'font-size': '0.8em'
  266. }
  267. },
  268. upcastAlso: viewElement => {
  269. const fontSize = viewElement.getStyle( 'font-size' );
  270. if ( !fontSize ) {
  271. return null;
  272. }
  273. const match = fontSize.match( /(\d+)\s*px/ );
  274. if ( !match ) {
  275. return null;
  276. }
  277. const size = Number( match[ 1 ] );
  278. if ( viewElement.is( 'span' ) && size < 10 ) {
  279. return { name: true, style: [ 'font-size' ] };
  280. }
  281. return null;
  282. }
  283. }
  284. ] );
  285. test(
  286. '<p><span style="font-size:1.2em">Foo</span> bar</p>',
  287. '<paragraph><$text fontSize="big">Foo</$text> bar</paragraph>'
  288. );
  289. test(
  290. '<p><span style="font-size:12px">Foo</span> bar</p>',
  291. '<paragraph><$text fontSize="big">Foo</$text> bar</paragraph>',
  292. '<p><span style="font-size:1.2em">Foo</span> bar</p>'
  293. );
  294. test(
  295. '<p><span style="font-size:0.8em">Foo</span> bar</p>',
  296. '<paragraph><$text fontSize="small">Foo</$text> bar</paragraph>'
  297. );
  298. test(
  299. '<p><span style="font-size:8px">Foo</span> bar</p>',
  300. '<paragraph><$text fontSize="small">Foo</$text> bar</paragraph>',
  301. '<p><span style="font-size:0.8em">Foo</span> bar</p>'
  302. );
  303. test(
  304. '<p><span style="font-size:10px">Foo</span> bar</p>',
  305. '<paragraph>Foo bar</paragraph>',
  306. '<p>Foo bar</p>'
  307. );
  308. } );
  309. } );
  310. describe( 'modelAttributeIsViewAttribute', () => {
  311. beforeEach( () => {
  312. modelElementIsViewElement( conversion, { model: 'image', view: 'img' } );
  313. schema.register( 'image', {
  314. inheritAllFrom: '$block',
  315. } );
  316. } );
  317. it( 'config is not set', () => {
  318. schema.extend( 'image', {
  319. allowAttributes: [ 'src' ]
  320. } );
  321. modelAttributeIsViewAttribute( conversion, 'src' );
  322. test( '<img src="foo.jpg"></img>', '<image src="foo.jpg"></image>' );
  323. } );
  324. it( 'config.view is a string', () => {
  325. schema.extend( 'image', {
  326. allowAttributes: [ 'source' ]
  327. } );
  328. modelAttributeIsViewAttribute( conversion, 'source', { view: 'src' } );
  329. test( '<img src="foo.jpg"></img>', '<image source="foo.jpg"></image>' );
  330. } );
  331. it( 'config.view is an object', () => {
  332. schema.extend( 'image', {
  333. allowAttributes: [ 'aside' ]
  334. } );
  335. modelAttributeIsViewAttribute( conversion, 'aside', {
  336. model: true,
  337. view: {
  338. name: 'img',
  339. key: 'class',
  340. value: 'aside half-size'
  341. }
  342. } );
  343. modelElementIsViewElement( conversion, { model: 'paragraph', view: 'p' } );
  344. test( '<img class="aside half-size"></img>', '<image aside="true"></image>' );
  345. test( '<p class="aside half-size"></p>', '<paragraph></paragraph>', '<p></p>' );
  346. } );
  347. it( 'config is an array', () => {
  348. schema.extend( 'image', {
  349. allowAttributes: [ 'styled' ]
  350. } );
  351. modelAttributeIsViewAttribute( conversion, 'styled', [
  352. {
  353. model: 'dark',
  354. view: {
  355. key: 'class',
  356. value: 'styled styled-dark'
  357. }
  358. },
  359. {
  360. model: 'light',
  361. view: {
  362. key: 'class',
  363. value: 'styled styled-light'
  364. }
  365. }
  366. ] );
  367. test( '<img class="styled styled-dark"></img>', '<image styled="dark"></image>' );
  368. test( '<img class="styled styled-light"></img>', '<image styled="light"></image>' );
  369. } );
  370. it( 'config is an array with upcastAlso defined', () => {
  371. modelElementIsViewElement( conversion, { model: 'paragraph', view: 'p' } );
  372. schema.extend( 'paragraph', {
  373. allowAttributes: [ 'align' ]
  374. } );
  375. modelAttributeIsViewAttribute( conversion, 'align', [
  376. {
  377. model: 'right',
  378. view: {
  379. key: 'class',
  380. value: 'align-right'
  381. },
  382. upcastAlso: viewElement => {
  383. if ( viewElement.getStyle( 'text-align' ) == 'right' ) {
  384. return {
  385. style: [ 'text-align' ]
  386. };
  387. }
  388. return null;
  389. }
  390. },
  391. {
  392. model: 'center',
  393. view: {
  394. key: 'class',
  395. value: 'align-center'
  396. },
  397. upcastAlso: {
  398. style: {
  399. 'text-align': 'center'
  400. }
  401. }
  402. }
  403. ] );
  404. test(
  405. '<p class="align-right">Foo</p>',
  406. '<paragraph align="right">Foo</paragraph>'
  407. );
  408. test(
  409. '<p style="text-align:right">Foo</p>',
  410. '<paragraph align="right">Foo</paragraph>',
  411. '<p class="align-right">Foo</p>'
  412. );
  413. test(
  414. '<p class="align-center">Foo</p>',
  415. '<paragraph align="center">Foo</paragraph>'
  416. );
  417. test(
  418. '<p style="text-align:center">Foo</p>',
  419. '<paragraph align="center">Foo</paragraph>',
  420. '<p class="align-center">Foo</p>'
  421. );
  422. } );
  423. } );
  424. function test( input, expectedModel, expectedView = null ) {
  425. loadData( input );
  426. expect( modelStringify( model.document.getRoot() ) ).to.equal( expectedModel );
  427. expect( viewStringify( viewRoot, null, { ignoreRoot: true } ) ).to.equal( expectedView || input );
  428. }
  429. function loadData( input ) {
  430. const parsedView = viewParse( input );
  431. const convertedModel = viewDispatcher.convert( parsedView );
  432. model.change( writer => {
  433. writer.remove( ModelRange.createFromParentsAndOffsets( modelRoot, 0, modelRoot, modelRoot.maxOffset ) );
  434. writer.insert( convertedModel, modelRoot, 0 );
  435. } );
  436. }
  437. } );