schema.js 36 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284
  1. /**
  2. * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
  3. * For licensing, see LICENSE.md.
  4. */
  5. import Schema from '../../src/model/schema';
  6. import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
  7. import Element from '../../src/model/element';
  8. import Position from '../../src/model/position';
  9. import Text from '../../src/model/text';
  10. describe( 'Schema', () => {
  11. let schema, root1, r1p1, r1p2, r1bQ, r1bQp, root2;
  12. beforeEach( () => {
  13. schema = new Schema();
  14. root1 = new Element( '$root', null, [
  15. new Element( 'paragraph', null, 'foo' ),
  16. new Element( 'paragraph', { align: 'right' }, 'bar' ),
  17. new Element( 'blockQuote', null, [
  18. new Element( 'paragraph', null, 'foo' )
  19. ] )
  20. ] );
  21. r1p1 = root1.getChild( 0 );
  22. r1p2 = root1.getChild( 1 );
  23. r1bQ = root1.getChild( 2 );
  24. r1bQp = r1bQ.getChild( 0 );
  25. root2 = new Element( '$root2' );
  26. } );
  27. describe( 'register()', () => {
  28. it( 'allows registering an item', () => {
  29. schema.register( 'foo' );
  30. expect( schema.getRule( 'foo' ) ).to.be.an( 'object' );
  31. } );
  32. it( 'copies rules', () => {
  33. const rules = {};
  34. schema.register( 'foo', rules );
  35. rules.isBlock = true;
  36. expect( schema.getRules().foo ).to.not.have.property( 'isBlock' );
  37. } );
  38. it( 'throws when trying to register for a single item twice', () => {
  39. schema.register( 'foo' );
  40. expect( () => {
  41. schema.register( 'foo' );
  42. } ).to.throw( CKEditorError, /^schema-cannot-register-item-twice:/ );
  43. } );
  44. } );
  45. describe( 'extend()', () => {
  46. it( 'allows extending item\'s rules', () => {
  47. schema.register( 'foo' );
  48. schema.extend( 'foo', {
  49. isBlock: true
  50. } );
  51. expect( schema.getRule( 'foo' ) ).to.have.property( 'isBlock', true );
  52. } );
  53. it( 'copies rules', () => {
  54. schema.register( 'foo', {} );
  55. const rules = {};
  56. schema.extend( 'foo', rules );
  57. rules.isBlock = true;
  58. expect( schema.getRules().foo ).to.not.have.property( 'isBlock' );
  59. } );
  60. it( 'throws when trying to extend a not yet registered item', () => {
  61. expect( () => {
  62. schema.extend( 'foo' );
  63. } ).to.throw( CKEditorError, /^schema-cannot-extend-missing-item:/ );
  64. } );
  65. } );
  66. describe( 'getRules()', () => {
  67. it( 'returns compiled rules', () => {
  68. schema.register( '$root' );
  69. schema.register( 'foo', {
  70. allowIn: '$root'
  71. } );
  72. schema.extend( 'foo', {
  73. isBlock: true
  74. } );
  75. const rules = schema.getRules();
  76. expect( rules.foo ).to.deep.equal( {
  77. name: 'foo',
  78. allowIn: [ '$root' ],
  79. allowAttributes: [],
  80. isBlock: true
  81. } );
  82. } );
  83. it( 'copies all is* types', () => {
  84. schema.register( 'foo', {
  85. isBlock: true,
  86. isFoo: true
  87. } );
  88. schema.extend( 'foo', {
  89. isBar: true,
  90. isFoo: false // Just to check that the last one wins.
  91. } );
  92. const rules = schema.getRules();
  93. expect( rules.foo ).to.have.property( 'isBlock', true );
  94. expect( rules.foo ).to.have.property( 'isFoo', false );
  95. expect( rules.foo ).to.have.property( 'isBar', true );
  96. } );
  97. it( 'does not recompile rules if not needed', () => {
  98. schema.register( 'foo' );
  99. expect( schema.getRules() ).to.equal( schema.getRules() );
  100. } );
  101. it( 'ensures no duplicates in allowIn', () => {
  102. schema.register( '$root' );
  103. schema.register( 'foo', {
  104. allowIn: '$root'
  105. } );
  106. schema.extend( 'foo', {
  107. allowIn: '$root'
  108. } );
  109. const rules = schema.getRules();
  110. expect( rules.foo ).to.deep.equal( {
  111. name: 'foo',
  112. allowIn: [ '$root' ],
  113. allowAttributes: []
  114. } );
  115. } );
  116. it( 'ensures no unregistered items in allowIn', () => {
  117. schema.register( 'foo', {
  118. allowIn: '$root'
  119. } );
  120. const rules = schema.getRules();
  121. expect( rules.foo ).to.deep.equal( {
  122. name: 'foo',
  123. allowIn: [],
  124. allowAttributes: []
  125. } );
  126. } );
  127. it( 'ensures no duplicates in allowAttributes', () => {
  128. schema.register( 'paragraph', {
  129. allowAttributes: 'foo'
  130. } );
  131. schema.extend( 'paragraph', {
  132. allowAttributes: 'foo'
  133. } );
  134. const rules = schema.getRules();
  135. expect( rules.paragraph ).to.deep.equal( {
  136. name: 'paragraph',
  137. allowIn: [],
  138. allowAttributes: [ 'foo' ]
  139. } );
  140. } );
  141. it( 'ensures no duplicates in allowAttributes duplicated by allowAttributesOf', () => {
  142. schema.register( 'paragraph', {
  143. allowAttributes: 'foo',
  144. allowAttributesOf: '$block'
  145. } );
  146. schema.register( '$block', {
  147. allowAttributes: 'foo'
  148. } );
  149. const rules = schema.getRules();
  150. expect( rules.paragraph ).to.deep.equal( {
  151. name: 'paragraph',
  152. allowIn: [],
  153. allowAttributes: [ 'foo' ]
  154. } );
  155. } );
  156. } );
  157. describe( 'getRule()', () => {
  158. // TODO
  159. } );
  160. describe( 'isRegistered()', () => {
  161. it( 'returns true if an item was registered', () => {
  162. schema.register( 'foo' );
  163. expect( schema.isRegistered( 'foo' ) ).to.be.true;
  164. } );
  165. it( 'returns false if an item was not registered', () => {
  166. expect( schema.isRegistered( 'foo' ) ).to.be.false;
  167. } );
  168. } );
  169. describe( 'isBlock()', () => {
  170. it( 'returns true if an item was registered as a block', () => {
  171. schema.register( 'foo', {
  172. isBlock: true
  173. } );
  174. expect( schema.isBlock( 'foo' ) ).to.be.true;
  175. } );
  176. it( 'returns false if an item was not registered as a block', () => {
  177. schema.register( 'foo' );
  178. expect( schema.isBlock( 'foo' ) ).to.be.false;
  179. } );
  180. it( 'returns false if an item was not registered at all', () => {
  181. expect( schema.isBlock( 'foo' ) ).to.be.false;
  182. } );
  183. } );
  184. describe( 'isLimit()', () => {
  185. it( 'returns true if an item was registered as a limit element', () => {
  186. schema.register( 'foo', {
  187. isLimit: true
  188. } );
  189. expect( schema.isLimit( 'foo' ) ).to.be.true;
  190. } );
  191. it( 'returns false if an item was not registered as a limit element', () => {
  192. schema.register( 'foo' );
  193. expect( schema.isLimit( 'foo' ) ).to.be.false;
  194. } );
  195. it( 'returns false if an item was not registered at all', () => {
  196. expect( schema.isLimit( 'foo' ) ).to.be.false;
  197. } );
  198. } );
  199. describe( 'isObject()', () => {
  200. it( 'returns true if an item was registered as an object', () => {
  201. schema.register( 'foo', {
  202. isObject: true
  203. } );
  204. expect( schema.isObject( 'foo' ) ).to.be.true;
  205. } );
  206. it( 'returns false if an item was not registered as an object', () => {
  207. schema.register( 'foo' );
  208. expect( schema.isObject( 'foo' ) ).to.be.false;
  209. } );
  210. it( 'returns false if an item was not registered at all', () => {
  211. expect( schema.isObject( 'foo' ) ).to.be.false;
  212. } );
  213. } );
  214. describe( 'checkChild()', () => {
  215. beforeEach( () => {
  216. schema.register( '$root' );
  217. schema.register( 'paragraph', {
  218. allowIn: '$root'
  219. } );
  220. schema.register( '$text', {
  221. allowIn: 'paragraph'
  222. } );
  223. } );
  224. it( 'accepts an element as a context and a node name as a child', () => {
  225. expect( schema.checkChild( root1, 'paragraph' ) ).to.be.true;
  226. expect( schema.checkChild( root1, '$text' ) ).to.be.false;
  227. } );
  228. it( 'accepts a position as a context', () => {
  229. const posInRoot = Position.createAt( root1 );
  230. const posInParagraph = Position.createAt( r1p1 );
  231. expect( schema.checkChild( posInRoot, 'paragraph' ) ).to.be.true;
  232. expect( schema.checkChild( posInRoot, '$text' ) ).to.be.false;
  233. expect( schema.checkChild( posInParagraph, '$text' ) ).to.be.true;
  234. expect( schema.checkChild( posInParagraph, 'paragraph' ) ).to.be.false;
  235. } );
  236. // This is a temporary feature which is needed to make the current V->M conversion works.
  237. // It should be removed once V->M conversion uses real positions.
  238. // Of course, real positions have this advantage that we know element attributes at this point.
  239. it( 'accepts an array of element names as a context', () => {
  240. const contextInRoot = [ '$root' ];
  241. const contextInParagraph = [ '$root', 'paragraph' ];
  242. expect( schema.checkChild( contextInRoot, 'paragraph' ) ).to.be.true;
  243. expect( schema.checkChild( contextInRoot, '$text' ) ).to.be.false;
  244. expect( schema.checkChild( contextInParagraph, '$text' ) ).to.be.true;
  245. expect( schema.checkChild( contextInParagraph, 'paragraph' ) ).to.be.false;
  246. } );
  247. it( 'accepts a node as a child', () => {
  248. expect( schema.checkChild( root1, r1p1 ) ).to.be.true;
  249. expect( schema.checkChild( root1, new Text( 'foo' ) ) ).to.be.false;
  250. } );
  251. } );
  252. describe( 'rules compilation', () => {
  253. describe( 'allowIn cases', () => {
  254. it( 'passes $root>paragraph', () => {
  255. schema.register( '$root' );
  256. schema.register( 'paragraph', {
  257. allowIn: '$root'
  258. } );
  259. expect( schema.checkChild( root1, r1p1 ) ).to.be.true;
  260. } );
  261. it( 'passes $root>paragraph and $root2>paragraph – support for array values', () => {
  262. schema.register( '$root' );
  263. schema.register( '$root2' );
  264. schema.register( 'paragraph', {
  265. allowIn: [ '$root', '$root2' ]
  266. } );
  267. expect( schema.checkChild( root1, r1p1 ) ).to.be.true;
  268. expect( schema.checkChild( root2, r1p1 ) ).to.be.true;
  269. } );
  270. it( 'passes $root>paragraph[align] – attributes does not matter', () => {
  271. schema.register( '$root' );
  272. schema.register( 'paragraph', {
  273. allowIn: '$root'
  274. } );
  275. expect( schema.checkChild( root1, r1p2 ) ).to.be.true;
  276. } );
  277. it( 'passes $root>div>div – in case of circular refs', () => {
  278. schema.register( '$root' );
  279. schema.register( 'div', {
  280. allowIn: [ '$root', 'div' ]
  281. } );
  282. const div = new Element( 'div' );
  283. root1.appendChildren( div );
  284. const div2 = new Element( 'div' );
  285. expect( schema.checkChild( div, div2 ) ).to.be.true;
  286. } );
  287. it( 'passes $root>div>div – in case of circular refs, when div1==div2', () => {
  288. schema.register( '$root' );
  289. schema.register( 'div', {
  290. allowIn: [ '$root', 'div' ]
  291. } );
  292. const div = new Element( 'div' );
  293. root1.appendChildren( div );
  294. expect( schema.checkChild( div, div ) ).to.be.true;
  295. } );
  296. it( 'rejects $root>paragraph – unregistered paragraph', () => {
  297. schema.register( '$root' );
  298. expect( schema.checkChild( root1, r1p1 ) ).to.be.false;
  299. } );
  300. it( 'rejects $root>paragraph – registered different item', () => {
  301. schema.register( '$root' );
  302. schema.register( 'paragraph' );
  303. schema.register( 'listItem', {
  304. allowIn: '$root'
  305. } );
  306. expect( schema.checkChild( root1, r1p1 ) ).to.be.false;
  307. } );
  308. it( 'rejects $root>paragraph – paragraph allowed in different context', () => {
  309. schema.register( '$root' );
  310. schema.register( '$fancyRoot' );
  311. schema.register( 'paragraph', {
  312. allowIn: '$fancyRoot'
  313. } );
  314. expect( schema.checkChild( root1, r1p1 ) ).to.be.false;
  315. } );
  316. it( 'rejects $root>blockQuote>paragraph – since paragraph is only allowed in $root', () => {
  317. schema.register( '$root' );
  318. schema.register( 'paragraph', {
  319. allowIn: '$root'
  320. } );
  321. expect( schema.checkChild( r1bQ, r1bQp ) ).to.be.false;
  322. } );
  323. it( 'rejects $root>blockQuote>paragraph – since paragraph is only allowed in $root v2', () => {
  324. schema.register( '$root' );
  325. schema.register( 'blockQuote', {
  326. allowIn: '$root'
  327. } );
  328. schema.register( 'paragraph', {
  329. allowIn: '$root'
  330. } );
  331. expect( schema.checkChild( r1bQ, r1bQp ) ).to.be.false;
  332. } );
  333. it( 'rejects $root>blockQuote>paragraph>$text - since paragraph is not allowed in blockQuote', () => {
  334. schema.register( '$root' );
  335. schema.register( 'paragraph', {
  336. allowIn: '$root'
  337. } );
  338. schema.register( '$text', {
  339. allowIn: 'paragraph'
  340. } );
  341. expect( schema.checkChild( root1, r1bQp.getChild( 0 ) ) ).to.be.false;
  342. } );
  343. it( 'rejects $root>blockQuote>paragraph>$text - since blockQuote is not allowed in $root', () => {
  344. schema.register( '$root' );
  345. schema.register( 'blockQuote' );
  346. schema.register( 'paragraph', {
  347. allowIn: [ 'blockQuote', '$root' ]
  348. } );
  349. schema.register( '$text', {
  350. allowIn: 'paragraph'
  351. } );
  352. expect( schema.checkChild( root1, r1bQp.getChild( 0 ) ) ).to.be.false;
  353. } );
  354. } );
  355. describe( 'allowWhere cases', () => {
  356. it( 'passes $root>paragraph – paragraph inherits from $block', () => {
  357. schema.register( '$root' );
  358. schema.register( '$block', {
  359. allowIn: '$root'
  360. } );
  361. schema.register( 'paragraph', {
  362. allowWhere: '$block'
  363. } );
  364. expect( schema.checkChild( root1, r1p1 ) ).to.be.true;
  365. } );
  366. it( 'supports the array syntax', () => {
  367. schema.register( '$root' );
  368. schema.register( '$root2' );
  369. schema.register( '$block', {
  370. allowIn: '$root'
  371. } );
  372. schema.register( '$block2', {
  373. allowIn: '$root2'
  374. } );
  375. schema.register( 'paragraph', {
  376. allowWhere: [ '$block', '$block2' ]
  377. } );
  378. expect( schema.checkChild( root1, r1p1 ), '$root' ).to.be.true;
  379. expect( schema.checkChild( root2, r1p1 ), '$root2' ).to.be.true;
  380. } );
  381. // This checks if some inapropriate caching or preprocessing isn't applied by register().
  382. it( 'passes $root>paragraph – paragraph inherits from $block, order of rules does not matter', () => {
  383. schema.register( '$root' );
  384. schema.register( 'paragraph', {
  385. allowWhere: '$block'
  386. } );
  387. schema.register( '$block', {
  388. allowIn: '$root'
  389. } );
  390. expect( schema.checkChild( root1, r1p1 ) ).to.be.true;
  391. } );
  392. it( 'passes $root>paragraph – paragraph inherits from $specialBlock which inherits from $block', () => {
  393. schema.register( '$root' );
  394. schema.register( '$block', {
  395. allowIn: '$root'
  396. } );
  397. schema.register( '$specialBlock', {
  398. allowWhere: '$block'
  399. } );
  400. schema.register( 'paragraph', {
  401. allowWhere: '$specialBlock'
  402. } );
  403. expect( schema.checkChild( root1, r1p1 ) ).to.be.true;
  404. } );
  405. it( 'rejects $root>paragraph – paragraph inherits from $block but $block is not allowed in $root', () => {
  406. schema.register( '$root' );
  407. schema.register( '$block' );
  408. schema.register( 'paragraph', {
  409. allowWhere: '$block'
  410. } );
  411. expect( schema.checkChild( root1, r1p1 ) ).to.be.false;
  412. } );
  413. it( 'rejects $root>paragraph>$text – paragraph inherits from $block but $block is not allowed in $root', () => {
  414. schema.register( '$root' );
  415. schema.register( '$block' );
  416. schema.register( 'paragraph', {
  417. allowWhere: '$block'
  418. } );
  419. schema.register( '$text', {
  420. allowIn: 'paragraph'
  421. } );
  422. expect( schema.checkChild( root1, r1p1.getChild( 0 ) ) ).to.be.false;
  423. } );
  424. } );
  425. describe( 'allowContentOf cases', () => {
  426. it( 'passes $root2>paragraph – $root2 inherits from $root', () => {
  427. schema.register( '$root' );
  428. schema.register( 'paragraph', {
  429. allowIn: '$root'
  430. } );
  431. schema.register( '$root2', {
  432. allowContentOf: '$root'
  433. } );
  434. expect( schema.checkChild( root2, r1p1 ) ).to.be.true;
  435. } );
  436. it( 'supports the array syntax', () => {
  437. schema.register( '$root' );
  438. schema.register( '$root2' );
  439. schema.register( 'paragraph', {
  440. allowIn: '$root'
  441. } );
  442. schema.register( 'heading1', {
  443. allowIn: '$root2'
  444. } );
  445. schema.register( '$root3', {
  446. allowContentOf: [ '$root', '$root2' ]
  447. } );
  448. const root3 = new Element( '$root3' );
  449. const heading1 = new Element( 'heading1' );
  450. expect( schema.checkChild( root3, r1p1 ), 'paragraph' ).to.be.true;
  451. expect( schema.checkChild( root3, heading1 ), 'heading1' ).to.be.true;
  452. } );
  453. it( 'passes $root2>paragraph – $root2 inherits from $root, order of rules does not matter', () => {
  454. schema.register( '$root' );
  455. schema.register( '$root2', {
  456. allowContentOf: '$root'
  457. } );
  458. schema.register( 'paragraph', {
  459. allowIn: '$root'
  460. } );
  461. expect( schema.checkChild( root2, r1p1 ) ).to.be.true;
  462. } );
  463. it( 'passes $root>paragraph>$text – paragraph inherits content of $block', () => {
  464. schema.register( '$root' );
  465. schema.register( '$block' );
  466. schema.register( 'paragraph', {
  467. allowIn: '$root',
  468. allowContentOf: '$block'
  469. } );
  470. schema.register( '$text', {
  471. allowIn: '$block'
  472. } );
  473. expect( schema.checkChild( r1p1, r1p1.getChild( 0 ) ) ).to.be.true;
  474. } );
  475. // TODO we need to make it possible to disallow bQ in bQ.
  476. it( 'passes $root>blockQuote>paragraph – blockQuote inherits content of $root', () => {
  477. schema.register( '$root' );
  478. schema.register( 'blockQuote', {
  479. allowIn: '$root',
  480. allowContentOf: '$root'
  481. } );
  482. schema.register( 'paragraph', {
  483. allowIn: '$root'
  484. } );
  485. expect( schema.checkChild( r1bQ, r1bQp ) ).to.be.true;
  486. } );
  487. it( 'rejects $root2>paragraph – $root2 inherits from $root, but paragraph is not allowed there anyway', () => {
  488. schema.register( '$root' );
  489. schema.register( 'paragraph' );
  490. schema.register( '$root2', {
  491. allowContentOf: '$root'
  492. } );
  493. expect( schema.checkChild( root2, r1p1 ) ).to.be.false;
  494. } );
  495. } );
  496. describe( 'mix of allowContentOf and allowWhere', () => {
  497. it( 'passes $root>paragraph>$text – paragraph inherits all from $block', () => {
  498. schema.register( '$root' );
  499. schema.register( '$block', {
  500. allowIn: '$root'
  501. } );
  502. schema.register( 'paragraph', {
  503. allowContentOf: '$block',
  504. allowWhere: '$block'
  505. } );
  506. schema.register( '$text', {
  507. allowIn: '$block'
  508. } );
  509. expect( schema.checkChild( r1p1, r1p1.getChild( 0 ) ) ).to.be.true;
  510. } );
  511. it( 'passes $root>paragraph and $root2>paragraph – where $root2 inherits content of $root' +
  512. 'and paragraph inherits allowWhere from $block', () => {
  513. schema.register( '$root' );
  514. schema.register( '$root2', {
  515. allowContentOf: '$root'
  516. } );
  517. schema.register( '$block', {
  518. allowIn: '$root'
  519. } );
  520. schema.register( 'paragraph', {
  521. allowWhere: '$block'
  522. } );
  523. expect( schema.checkChild( root1, 'paragraph' ), 'root1' ).to.be.true;
  524. expect( schema.checkChild( root2, 'paragraph' ), 'root2' ).to.be.true;
  525. } );
  526. it( 'passes d>a where d inherits content of c which inherits content of b', () => {
  527. schema.register( 'b' );
  528. schema.register( 'a', { allowIn: 'b' } );
  529. schema.register( 'c', { allowContentOf: 'b' } );
  530. schema.register( 'd', { allowContentOf: 'c' } );
  531. const d = new Element( 'd' );
  532. expect( schema.checkChild( d, 'a' ) ).to.be.true;
  533. } );
  534. // This case won't pass becuase we compile the rules in a pretty naive way.
  535. // To make chains like this work we'd need to repeat compilation of allowContentOf rules
  536. // as long as the previous iteration found something to compile.
  537. // This way, even though we'd not compile d<-c in the first run, we'd still find b<-c
  538. // and since we've found something, we'd now try d<-c which would work.
  539. //
  540. // We ignore those situations for now as they are very unlikely to happen and would
  541. // significantly raised the complexity of rule compilation.
  542. //
  543. // it( 'passes d>a where d inherits content of c which inherits content of b', () => {
  544. // schema.register( 'b' );
  545. // schema.register( 'a', { allowIn: 'b' } );
  546. // schema.register( 'd', { allowContentOf: 'c' } );
  547. // schema.register( 'c', { allowContentOf: 'b' } );
  548. //
  549. // const d = new Element( 'd' );
  550. //
  551. // expect( schema.checkChild( d, 'a' ) ).to.be.true;
  552. // } );
  553. } );
  554. describe( 'inheritAllFrom', () => {
  555. it( 'passes $root>paragraph – paragraph inherits allowIn from $block', () => {
  556. schema.register( '$root' );
  557. schema.register( '$block', {
  558. allowIn: '$root'
  559. } );
  560. schema.register( 'paragraph', {
  561. inheritAllFrom: '$block'
  562. } );
  563. expect( schema.checkChild( root1, r1p1 ) ).to.be.true;
  564. } );
  565. it( 'passes $root>paragraph>$text – paragraph inherits allowed content of $block', () => {
  566. schema.register( '$root' );
  567. schema.register( '$block', {
  568. allowIn: '$root'
  569. } );
  570. schema.register( '$text', {
  571. allowIn: '$block'
  572. } );
  573. schema.register( 'paragraph', {
  574. inheritAllFrom: '$block'
  575. } );
  576. expect( schema.checkChild( r1p1, r1p1.getChild( 0 ) ) ).to.be.true;
  577. } );
  578. it( 'passes $root>paragraph>$text – paragraph inherits allowIn from $block through $block\'s allowWhere', () => {
  579. schema.register( '$root' );
  580. schema.register( '$blockProto', {
  581. allowIn: '$root'
  582. } );
  583. schema.register( '$block', {
  584. allowWhere: '$blockProto'
  585. } );
  586. schema.register( 'paragraph', {
  587. inheritAllFrom: '$block'
  588. } );
  589. expect( schema.checkChild( root1, r1p1 ) ).to.be.true;
  590. } );
  591. it( 'passes $root>paragraph>$text – paragraph inherits allowIn from $block through $block\'s allowWhere', () => {
  592. schema.register( '$root' );
  593. schema.register( '$blockProto' );
  594. schema.register( '$block', {
  595. allowContentOf: '$blockProto',
  596. allowIn: '$root'
  597. } );
  598. schema.register( '$text', {
  599. allowIn: '$blockProto'
  600. } );
  601. schema.register( 'paragraph', {
  602. inheritAllFrom: '$block'
  603. } );
  604. expect( schema.checkChild( r1p1, r1p1.getChild( 0 ) ) ).to.be.true;
  605. } );
  606. } );
  607. // We need to handle cases where some independent features registered rules which might use
  608. // optional elements (elements which might not have been registered).
  609. describe( 'missing structure rules', () => {
  610. it( 'does not break when trying to check a child which is not registered', () => {
  611. schema.register( '$root' );
  612. expect( schema.checkChild( root1, 'foo404' ) ).to.be.false;
  613. } );
  614. it( 'does not break when trying to check registered child in a context which contains unregistered elements', () => {
  615. const foo404 = new Element( 'foo404' );
  616. root1.appendChildren( foo404 );
  617. schema.register( '$root' );
  618. schema.register( '$text', {
  619. allowIn: '$root'
  620. } );
  621. expect( schema.checkChild( foo404, '$text' ) ).to.be.false;
  622. } );
  623. it( 'does not break when used allowedIn pointing to an unregistered element', () => {
  624. schema.register( '$root' );
  625. schema.register( '$text', {
  626. allowIn: 'foo404'
  627. } );
  628. expect( schema.checkChild( root1, '$text' ) ).to.be.false;
  629. } );
  630. it( 'does not break when used allowWhere pointing to an unregistered element', () => {
  631. schema.register( '$root' );
  632. schema.register( '$text', {
  633. allowWhere: 'foo404'
  634. } );
  635. expect( schema.checkChild( root1, '$text' ) ).to.be.false;
  636. } );
  637. it( 'does not break when used allowContentOf pointing to an unregistered element', () => {
  638. schema.register( '$root', {
  639. allowContentOf: 'foo404'
  640. } );
  641. schema.register( '$text', {
  642. allowIn: '$root'
  643. } );
  644. expect( schema.checkChild( root1, '$text' ) ).to.be.true;
  645. } );
  646. it( 'checks whether allowIn uses a registered element', () => {
  647. schema.register( 'paragraph', {
  648. allowIn: '$root'
  649. } );
  650. // $root isn't registered!
  651. expect( schema.checkChild( root1, 'paragraph' ) ).to.be.false;
  652. } );
  653. it( 'does not break when inheriting all from an unregistered element', () => {
  654. schema.register( 'paragraph', {
  655. inheritAllFrom: '$block'
  656. } );
  657. expect( schema.checkChild( root1, r1p1 ) ).to.be.false;
  658. } );
  659. } );
  660. describe( 'allowAttributes', () => {
  661. it( 'passes paragraph[align]', () => {
  662. schema.register( 'paragraph', {
  663. allowAttributes: 'align'
  664. } );
  665. expect( schema.checkAttribute( r1p1, 'align' ) ).to.be.true;
  666. } );
  667. it( 'passes paragraph[align] and paragraph[dir] – support for array values', () => {
  668. schema.register( 'paragraph', {
  669. allowAttributes: [ 'align', 'dir' ]
  670. } );
  671. expect( schema.checkAttribute( r1p1, 'align' ), 'align' ).to.be.true;
  672. expect( schema.checkAttribute( r1p1, 'dir' ), 'dir' ).to.be.true;
  673. } );
  674. it( 'passes paragraph>$text[bold]', () => {
  675. schema.register( 'paragraph' );
  676. schema.register( '$text', {
  677. allowIn: 'paragraph',
  678. allowAttributes: 'bold'
  679. } );
  680. expect( schema.checkAttribute( r1p1.getChild( 0 ), 'bold' ) ).to.be.true;
  681. } );
  682. } );
  683. describe( 'allowAttributesOf', () => {
  684. it( 'passes paragraph[align] – paragraph inherits from $block', () => {
  685. schema.register( '$block', {
  686. allowAttributes: 'align'
  687. } );
  688. schema.register( 'paragraph', {
  689. allowAttributesOf: '$block'
  690. } );
  691. expect( schema.checkAttribute( r1p1, 'align' ) ).to.be.true;
  692. } );
  693. it( 'passes paragraph[align] and paragraph[dir] – support for array values', () => {
  694. schema.register( '$block', {
  695. allowAttributes: 'align'
  696. } );
  697. schema.register( '$block2', {
  698. allowAttributes: 'dir'
  699. } );
  700. schema.register( 'paragraph', {
  701. allowAttributesOf: [ '$block', '$block2' ]
  702. } );
  703. expect( schema.checkAttribute( r1p1, 'align' ), 'align' ).to.be.true;
  704. expect( schema.checkAttribute( r1p1, 'dir' ), 'dir' ).to.be.true;
  705. } );
  706. it( 'passes paragraph[align] and paragraph[dir] – support for combined allowAttributes and allowAttributesOf', () => {
  707. schema.register( '$block', {
  708. allowAttributes: 'align'
  709. } );
  710. schema.register( 'paragraph', {
  711. allowAttributes: 'dir',
  712. allowAttributesOf: '$block'
  713. } );
  714. expect( schema.checkAttribute( r1p1, 'align' ), 'align' ).to.be.true;
  715. expect( schema.checkAttribute( r1p1, 'dir' ), 'dir' ).to.be.true;
  716. } );
  717. // The support for allowAttributesOf is broken in the similar way as for allowContentOf (see the comment above).
  718. // However, those situations are rather theoretical, so we're not going to waste time on them now.
  719. } );
  720. describe( 'inheritAllFrom', () => {
  721. it( 'passes paragraph[align] – paragraph inherits attributes of $block', () => {
  722. schema.register( '$block', {
  723. allowAttributes: 'align'
  724. } );
  725. schema.register( 'paragraph', {
  726. inheritAllFrom: '$block'
  727. } );
  728. expect( schema.checkAttribute( r1p1, 'align' ) ).to.be.true;
  729. } );
  730. it( 'passes paragraph[align] – paragraph inherits attributes of $block through allowAttributesOf', () => {
  731. schema.register( '$blockProto', {
  732. allowAttributes: 'align'
  733. } );
  734. schema.register( '$block', {
  735. allowAttributesOf: '$blockProto'
  736. } );
  737. schema.register( 'paragraph', {
  738. inheritAllFrom: '$block'
  739. } );
  740. expect( schema.checkAttribute( r1p1, 'align' ) ).to.be.true;
  741. } );
  742. } );
  743. describe( 'missing attribute rules', () => {
  744. it( 'does not crash when checking an attribute of a unregistered element', () => {
  745. expect( schema.checkAttribute( r1p1, 'align' ) ).to.be.false;
  746. } );
  747. it( 'does not crash when inheriting attributes of a unregistered element', () => {
  748. schema.register( 'paragraph', {
  749. allowAttributesOf: '$block'
  750. } );
  751. expect( schema.checkAttribute( r1p1, 'whatever' ) ).to.be.false;
  752. } );
  753. it( 'does not crash when inheriting all from a unregistered element', () => {
  754. schema.register( 'paragraph', {
  755. allowAttributesOf: '$block'
  756. } );
  757. expect( schema.checkAttribute( r1p1, 'whatever' ) ).to.be.false;
  758. } );
  759. } );
  760. } );
  761. describe( 'real scenarios', () => {
  762. let r1bQi, r1i, r1lI, r1h, r1bQlI;
  763. const rules = [
  764. () => {
  765. schema.register( 'paragraph', {
  766. allowWhere: '$block',
  767. allowContentOf: '$block',
  768. allowAttributesOf: '$block'
  769. } );
  770. },
  771. () => {
  772. schema.register( 'heading1', {
  773. allowWhere: '$block',
  774. allowContentOf: '$block',
  775. allowAttributesOf: '$block'
  776. } );
  777. },
  778. () => {
  779. schema.register( 'listItem', {
  780. allowWhere: '$block',
  781. allowContentOf: '$block',
  782. allowAttributes: [ 'indent', 'type' ],
  783. allowAttributesOf: '$block'
  784. } );
  785. },
  786. () => {
  787. schema.register( 'blockQuote', {
  788. allowWhere: '$block',
  789. allowContentOf: '$root'
  790. } );
  791. // Disallow blockQuote in blockQuote.
  792. schema.on( 'checkChild', ( evt, args ) => {
  793. const context = args[ 0 ];
  794. const child = args[ 1 ];
  795. const childRule = schema.getRule( child );
  796. if ( childRule.name == 'blockQuote' && context[ context.length - 1 ].name == 'blockQuote' ) {
  797. evt.stop();
  798. evt.return = false;
  799. }
  800. }, { priority: 'high' } );
  801. },
  802. () => {
  803. schema.register( 'image', {
  804. allowWhere: '$block',
  805. allowAttributes: [ 'src', 'alt' ]
  806. } );
  807. },
  808. () => {
  809. schema.register( 'caption', {
  810. allowIn: 'image',
  811. allowContentOf: '$block'
  812. } );
  813. },
  814. () => {
  815. schema.extend( '$text', {
  816. allowAttributes: [ 'bold', 'italic' ]
  817. } );
  818. // Disallow bold in heading1.
  819. schema.on( 'checkAttribute', ( evt, args ) => {
  820. const context = args[ 0 ];
  821. const contextLast = context[ context.length - 1 ];
  822. const contextSecondToLast = context[ context.length - 2 ];
  823. const attributeName = args[ 1 ];
  824. if ( contextLast.name == '$text' && contextSecondToLast.name == 'heading1' && attributeName == 'bold' ) {
  825. evt.stop();
  826. evt.return = false;
  827. }
  828. }, { priority: 'high' } );
  829. },
  830. () => {
  831. schema.extend( '$block', {
  832. allowAttributes: 'alignment'
  833. } );
  834. }
  835. ];
  836. beforeEach( () => {
  837. schema.register( '$root' );
  838. schema.register( '$block', {
  839. allowIn: '$root'
  840. } );
  841. schema.register( '$text', {
  842. allowIn: '$block'
  843. } );
  844. for ( const rule of rules ) {
  845. rule();
  846. }
  847. // or...
  848. //
  849. // Use the below code to shuffle the rules.
  850. // Don't look here, Szymon!
  851. //
  852. // const rulesCopy = rules.slice();
  853. //
  854. // while ( rulesCopy.length ) {
  855. // const r = Math.floor( Math.random() * rulesCopy.length );
  856. // rulesCopy.splice( r, 1 )[ 0 ]();
  857. // }
  858. root1 = new Element( '$root', null, [
  859. new Element( 'paragraph', null, 'foo' ),
  860. new Element( 'paragraph', { alignment: 'right' }, 'bar' ),
  861. new Element( 'listItem', { type: 'x', indent: 0 }, 'foo' ),
  862. new Element( 'heading1', null, 'foo' ),
  863. new Element( 'blockQuote', null, [
  864. new Element( 'paragraph', null, 'foo' ),
  865. new Element( 'listItem', { type: 'x', indent: 0 }, 'foo' ),
  866. new Element( 'image', null, [
  867. new Element( 'caption', null, 'foo' )
  868. ] )
  869. ] ),
  870. new Element( 'image', null, [
  871. new Element( 'caption', null, 'foo' )
  872. ] )
  873. ] );
  874. r1p1 = root1.getChild( 0 );
  875. r1p2 = root1.getChild( 1 );
  876. r1lI = root1.getChild( 2 );
  877. r1h = root1.getChild( 3 );
  878. r1bQ = root1.getChild( 4 );
  879. r1i = root1.getChild( 5 );
  880. r1bQp = r1bQ.getChild( 0 );
  881. r1bQlI = r1bQ.getChild( 1 );
  882. r1bQi = r1bQ.getChild( 2 );
  883. } );
  884. it( 'passes $root>paragraph', () => {
  885. expect( schema.checkChild( root1, 'paragraph' ) ).to.be.true;
  886. } );
  887. it( 'passes $root>paragraph>$text', () => {
  888. expect( schema.checkChild( r1p1, '$text' ), 'paragraph' ).to.be.true;
  889. expect( schema.checkChild( r1p2, '$text' ), 'paragraph[alignment]' ).to.be.true;
  890. } );
  891. it( 'passes $root>listItem', () => {
  892. expect( schema.checkChild( root1, 'listItem' ) ).to.be.true;
  893. } );
  894. it( 'passes $root>listItem>$text', () => {
  895. expect( schema.checkChild( r1lI, '$text' ) ).to.be.true;
  896. } );
  897. it( 'passes $root>blockQuote>paragraph', () => {
  898. expect( schema.checkChild( r1bQ, 'paragraph' ) ).to.be.true;
  899. } );
  900. it( 'passes $root>blockQuote>paragraph>$text', () => {
  901. expect( schema.checkChild( r1bQp, '$text' ) ).to.be.true;
  902. } );
  903. it( 'passes $root>blockQuote>listItem', () => {
  904. expect( schema.checkChild( r1bQ, 'listItem' ) ).to.be.true;
  905. } );
  906. it( 'passes $root>blockQuote>listItem>$text', () => {
  907. expect( schema.checkChild( r1bQlI, '$text' ) ).to.be.true;
  908. } );
  909. it( 'passes $root>blockQuote>image', () => {
  910. expect( schema.checkChild( r1bQ, 'image' ) ).to.be.true;
  911. } );
  912. it( 'passes $root>blockQuote>image>caption', () => {
  913. expect( schema.checkChild( r1bQi, 'caption' ) ).to.be.true;
  914. } );
  915. it( 'passes $root>blockQuote>image>caption>$text', () => {
  916. expect( schema.checkChild( r1bQi.getChild( 0 ), '$text' ) ).to.be.true;
  917. } );
  918. it( 'passes $root>image', () => {
  919. expect( schema.checkChild( root1, 'image' ) ).to.be.true;
  920. } );
  921. it( 'passes $root>image>caption', () => {
  922. expect( schema.checkChild( r1i, 'caption' ) ).to.be.true;
  923. } );
  924. it( 'passes $root>image>caption>$text', () => {
  925. expect( schema.checkChild( r1i.getChild( 0 ), '$text' ) ).to.be.true;
  926. } );
  927. it( 'rejects $root>$root', () => {
  928. expect( schema.checkChild( root1, '$root' ) ).to.be.false;
  929. } );
  930. it( 'rejects $root>$text', () => {
  931. expect( schema.checkChild( root1, '$text' ) ).to.be.false;
  932. } );
  933. it( 'rejects $root>caption', () => {
  934. expect( schema.checkChild( root1, 'caption' ) ).to.be.false;
  935. } );
  936. it( 'rejects $root>paragraph>paragraph', () => {
  937. expect( schema.checkChild( r1p1, 'paragraph' ) ).to.be.false;
  938. } );
  939. it( 'rejects $root>paragraph>paragraph>$text', () => {
  940. // Edge case because p>p should not exist in the first place.
  941. // But it's good to know that it blocks also this.
  942. const p = new Element( 'p' );
  943. r1p1.appendChildren( p );
  944. expect( schema.checkChild( p, '$text' ) ).to.be.false;
  945. } );
  946. it( 'rejects $root>paragraph>$block', () => {
  947. expect( schema.checkChild( r1p1, '$block' ) ).to.be.false;
  948. } );
  949. it( 'rejects $root>paragraph>blockQuote', () => {
  950. expect( schema.checkChild( r1p1, 'blockQuote' ) ).to.be.false;
  951. } );
  952. it( 'rejects $root>paragraph>image', () => {
  953. expect( schema.checkChild( r1p1, 'image' ) ).to.be.false;
  954. } );
  955. it( 'rejects $root>paragraph>caption', () => {
  956. expect( schema.checkChild( r1p1, 'caption' ) ).to.be.false;
  957. } );
  958. it( 'rejects $root>blockQuote>blockQuote', () => {
  959. expect( schema.checkChild( r1bQ, 'blockQuote' ) ).to.be.false;
  960. } );
  961. it( 'rejects $root>blockQuote>caption', () => {
  962. expect( schema.checkChild( r1p1, 'image' ) ).to.be.false;
  963. } );
  964. it( 'rejects $root>blockQuote>$text', () => {
  965. expect( schema.checkChild( r1bQ, '$text' ) ).to.be.false;
  966. } );
  967. it( 'rejects $root>image>$text', () => {
  968. expect( schema.checkChild( r1i, '$text' ) ).to.be.false;
  969. } );
  970. it( 'rejects $root>image>paragraph', () => {
  971. expect( schema.checkChild( r1i, 'paragraph' ) ).to.be.false;
  972. } );
  973. it( 'rejects $root>image>caption>paragraph', () => {
  974. expect( schema.checkChild( r1i.getChild( 0 ), 'paragraph' ) ).to.be.false;
  975. } );
  976. it( 'rejects $root>image>caption>blockQuote', () => {
  977. expect( schema.checkChild( r1i.getChild( 0 ), 'blockQuote' ) ).to.be.false;
  978. } );
  979. it( 'accepts attribute $root>paragraph[alignment]', () => {
  980. expect( schema.checkAttribute( r1p1, 'alignment' ) ).to.be.true;
  981. } );
  982. it( 'accepts attribute $root>paragraph>$text[bold]', () => {
  983. expect( schema.checkAttribute( r1p1.getChild( 0 ), 'bold' ) ).to.be.true;
  984. } );
  985. it( 'accepts attribute $root>heading1>$text[italic]', () => {
  986. expect( schema.checkAttribute( r1h.getChild( 0 ), 'italic' ) ).to.be.true;
  987. } );
  988. it( 'accepts attribute $root>blockQuote>paragraph>$text[bold]', () => {
  989. expect( schema.checkAttribute( r1bQp.getChild( 0 ), 'bold' ) ).to.be.true;
  990. } );
  991. it( 'accepts attribute $root>listItem[alignment]', () => {
  992. expect( schema.checkAttribute( r1lI, 'alignment' ) ).to.be.true;
  993. } );
  994. it( 'accepts attribute $root>listItem[indent]', () => {
  995. expect( schema.checkAttribute( r1lI, 'indent' ) ).to.be.true;
  996. } );
  997. it( 'accepts attribute $root>listItem[type]', () => {
  998. expect( schema.checkAttribute( r1lI, 'type' ) ).to.be.true;
  999. } );
  1000. it( 'accepts attribute $root>image[src]', () => {
  1001. expect( schema.checkAttribute( r1i, 'src' ) ).to.be.true;
  1002. } );
  1003. it( 'accepts attribute $root>image[alt]', () => {
  1004. expect( schema.checkAttribute( r1i, 'alt' ) ).to.be.true;
  1005. } );
  1006. it( 'accepts attribute $root>image>caption>$text[bold]', () => {
  1007. expect( schema.checkAttribute( r1i.getChild( 0 ).getChild( 0 ), 'bold' ) ).to.be.true;
  1008. } );
  1009. it( 'rejects attribute $root[indent]', () => {
  1010. expect( schema.checkAttribute( root1, 'indent' ) ).to.be.false;
  1011. } );
  1012. it( 'rejects attribute $root>paragraph[indent]', () => {
  1013. expect( schema.checkAttribute( r1p1, 'indent' ) ).to.be.false;
  1014. } );
  1015. it( 'accepts attribute $root>heading1>$text[bold]', () => {
  1016. expect( schema.checkAttribute( r1h.getChild( 0 ), 'bold' ) ).to.be.false;
  1017. } );
  1018. it( 'rejects attribute $root>paragraph>$text[alignment]', () => {
  1019. expect( schema.checkAttribute( r1p1.getChild( 0 ), 'alignment' ) ).to.be.false;
  1020. } );
  1021. it( 'rejects attribute $root>blockQuote[indent]', () => {
  1022. expect( schema.checkAttribute( r1bQ, 'indent' ) ).to.be.false;
  1023. } );
  1024. it( 'rejects attribute $root>blockQuote[alignment]', () => {
  1025. expect( schema.checkAttribute( r1bQ, 'alignment' ) ).to.be.false;
  1026. } );
  1027. it( 'rejects attribute $root>image[indent]', () => {
  1028. expect( schema.checkAttribute( r1i, 'indent' ) ).to.be.false;
  1029. } );
  1030. it( 'rejects attribute $root>image[alignment]', () => {
  1031. expect( schema.checkAttribute( r1i, 'alignment' ) ).to.be.false;
  1032. } );
  1033. } );
  1034. // TODO:
  1035. // * getValidRanges
  1036. // * checkAttributeInSelection
  1037. // * getLimitElement
  1038. // * removeDisallowedAttributes
  1039. // * test checkChild()'s both params normalization
  1040. // * and see insertContent's _checkIsObject()
  1041. // * add normalization to isObject(), isLimit(), isBlock(), isRegistered() and improve the existing code
  1042. // * inheritAllFrom should also inherit is* props (see tests documentselection getNearestSelectionRange())
  1043. // * test the default abstract entities (in model.js)
  1044. // * see clipboardHolder definition (and rename it to the pastebin)
  1045. } );