schema.js 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761
  1. /**
  2. * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
  3. * For licensing, see LICENSE.md.
  4. */
  5. /**
  6. * @module engine/model/schema
  7. */
  8. import Position from './position';
  9. import Element from './element';
  10. import Range from './range';
  11. import DocumentSelection from './documentselection';
  12. import clone from '@ckeditor/ckeditor5-utils/src/lib/lodash/clone';
  13. import isArray from '@ckeditor/ckeditor5-utils/src/lib/lodash/isArray';
  14. import isString from '@ckeditor/ckeditor5-utils/src/lib/lodash/isString';
  15. import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
  16. /**
  17. * Schema is a definition of the structure of the document. It allows to define which tree model items (element, text, etc.)
  18. * can be nested within which ones and which attributes can be applied to them. It's created during the run-time of the application,
  19. * typically by features. Also, the features can query the schema to learn what structure is allowed and act accordingly.
  20. *
  21. * For instance, if a feature wants to define that an attribute bold is allowed on the text it needs to register this rule like this:
  22. *
  23. * editor.document.schema.allow( '$text', 'bold' );
  24. *
  25. * Note: items prefixed with `$` are special group of items. By default, `Schema` defines three special items:
  26. *
  27. * * `$inline` represents all inline elements,
  28. * * `$text` is a sub-group of `$inline` and represents text nodes,
  29. * * `$block` represents block elements,
  30. * * `$root` represents default editing roots (those that allow only `$block`s inside them).
  31. *
  32. * When registering an item it's possible to tell that this item should inherit from some other existing item.
  33. * E.g. `p` can inherit from `$block`, so whenever given attribute is allowed on the `$block` it will automatically be
  34. * also allowed on the `p` element. By default, `$text` item already inherits from `$inline`.
  35. */
  36. export default class Schema {
  37. /**
  38. * Creates Schema instance.
  39. */
  40. constructor() {
  41. /**
  42. * Names of elements which have "object" nature. This means that these
  43. * elements should be treated as whole, never merged, can be selected from outside, etc.
  44. * Just like images, placeholder widgets, etc.
  45. *
  46. * @member {Set.<String>} module:engine/model/schema~Schema#objects
  47. */
  48. this.objects = new Set();
  49. /**
  50. * Names of elements to which editing operations should be limited.
  51. * For example, the <kbd>Enter</kbd> should not split such elements and
  52. * <kbd>Backspace</kbd> should not be able to leave or modify such elements.
  53. *
  54. * @member {Set.<String>} module:engine/model/schema~Schema#limits
  55. */
  56. this.limits = new Set();
  57. /**
  58. * Schema items registered in the schema.
  59. *
  60. * @private
  61. * @member {Map} module:engine/model/schema~Schema#_items
  62. */
  63. this._items = new Map();
  64. /**
  65. * Description of what entities are a base for given entity.
  66. *
  67. * @private
  68. * @member {Map} module:engine/model/schema~Schema#_extensionChains
  69. */
  70. this._extensionChains = new Map();
  71. // Register some default abstract entities.
  72. this.registerItem( '$root' );
  73. this.registerItem( '$block' );
  74. this.registerItem( '$inline' );
  75. this.registerItem( '$text', '$inline' );
  76. this.allow( { name: '$block', inside: '$root' } );
  77. this.allow( { name: '$inline', inside: '$block' } );
  78. this.limits.add( '$root' );
  79. // TMP!
  80. // Create an "all allowed" context in the schema for processing the pasted content.
  81. // Read: https://github.com/ckeditor/ckeditor5-engine/issues/638#issuecomment-255086588
  82. this.registerItem( '$clipboardHolder', '$root' );
  83. this.allow( { name: '$inline', inside: '$clipboardHolder' } );
  84. }
  85. /**
  86. * Allows given query in the schema.
  87. *
  88. * // Allow text with bold attribute in all P elements.
  89. * schema.registerItem( 'p', '$block' );
  90. * schema.allow( { name: '$text', attributes: 'bold', inside: 'p' } );
  91. *
  92. * // Allow header in Ps that are in DIVs
  93. * schema.registerItem( 'header', '$block' );
  94. * schema.registerItem( 'div', '$block' );
  95. * schema.allow( { name: 'header', inside: 'div p' } ); // inside: [ 'div', 'p' ] would also work.
  96. *
  97. * @param {module:engine/model/schema~SchemaQuery} query Allowed query.
  98. */
  99. allow( query ) {
  100. this._getItem( query.name ).allow( Schema._normalizeQueryPath( query.inside ), query.attributes );
  101. }
  102. /**
  103. * Disallows given query in the schema.
  104. *
  105. * @see #allow
  106. * @param {module:engine/model/schema~SchemaQuery} query Disallowed query.
  107. */
  108. disallow( query ) {
  109. this._getItem( query.name ).disallow( Schema._normalizeQueryPath( query.inside ), query.attributes );
  110. }
  111. /**
  112. * Makes a requirement in schema that entity represented by given item has to have given set of attributes. Some
  113. * elements in the model might require some attributes to be set. If multiple sets of attributes are required it
  114. * is enough that the entity fulfills only one set.
  115. *
  116. * // "a" element must either have "href" attribute or "name" attribute
  117. * schema.requireAttributes( 'a', [ 'href' ] );
  118. * schema.requireAttributes( 'a', [ 'name' ] );
  119. * // "img" element must have both "src" and "alt" attributes
  120. * schema.requireAttributes( 'img', [ 'src', 'alt' ] );
  121. *
  122. * @param {String} name Entity name.
  123. * @param {Array.<String>} attributes Attributes that has to be set on the entity to make it valid.
  124. */
  125. requireAttributes( name, attributes ) {
  126. this._getItem( name ).requireAttributes( attributes );
  127. }
  128. /**
  129. * Checks whether given query is allowed in schema.
  130. *
  131. * // Check whether bold text is allowed in header element.
  132. * let query = {
  133. * name: '$text',
  134. * attributes: 'bold',
  135. * inside: 'header'
  136. * };
  137. * if ( schema.check( query ) ) { ... }
  138. *
  139. * // Check whether bold and italic text can be placed at caret position.
  140. * let caretPos = editor.document.selection.getFirstPosition();
  141. * let query = {
  142. * name: '$text',
  143. * attributes: [ 'bold', 'italic' ],
  144. * inside: caretPos
  145. * };
  146. * if ( schema.check( query ) ) { ... }
  147. *
  148. * // Check whether image with alt, src and title is allowed in given elements path.
  149. * let quoteElement = new Element( 'quote' );
  150. * let query = {
  151. * name: 'img',
  152. * attributes: [ 'alt', 'src', 'title' ],
  153. * // It is possible to mix strings with elements.
  154. * // Query will check whether "img" can be inside "quoteElement" that is inside a block element.
  155. * inside: [ '$block', quoteElement ]
  156. * };
  157. * if ( schema.check( query ) ) { ... }
  158. *
  159. * @param {module:engine/model/schema~SchemaQuery} query Query to check.
  160. * @returns {Boolean} `true` if given query is allowed in schema, `false` otherwise.
  161. */
  162. check( query ) {
  163. if ( !this.hasItem( query.name ) ) {
  164. return false;
  165. }
  166. // If attributes property is a string or undefined, wrap it in an array for easier processing.
  167. if ( !isArray( query.attributes ) ) {
  168. query.attributes = [ query.attributes ];
  169. } else if ( query.attributes.length === 0 ) {
  170. // To simplify algorithms, when a SchemaItem path is added "without" attribute, it is added with
  171. // attribute equal to undefined. This means that algorithms can work the same way for specified attributes
  172. // and no-atrtibutes, but we have to fill empty array with "fake" undefined value for algorithms reasons.
  173. query.attributes.push( undefined );
  174. }
  175. // Normalize the path to an array of strings.
  176. const path = Schema._normalizeQueryPath( query.inside );
  177. // Get extension chain of given item and retrieve all schema items that are extended by given item.
  178. const schemaItems = this._extensionChains.get( query.name ).map( name => {
  179. return this._getItem( name );
  180. } );
  181. // First check if the query meets at required attributes for this item.
  182. if ( !this._getItem( query.name )._checkRequiredAttributes( query.attributes ) ) {
  183. return false;
  184. }
  185. // If there is matching disallow path, this query is not valid with schema.
  186. for ( const attribute of query.attributes ) {
  187. for ( const schemaItem of schemaItems ) {
  188. if ( schemaItem._hasMatchingPath( 'disallow', path, attribute ) ) {
  189. return false;
  190. }
  191. }
  192. }
  193. // At this point, the query is not disallowed.
  194. // If there are correct allow paths that match the query, this query is valid with schema.
  195. // Since we are supporting multiple attributes, we have to make sure that if attributes are set,
  196. // we have allowed paths for all of them.
  197. // Keep in mind that if the query has no attributes, query.attribute was converted to an array
  198. // with a single `undefined` value. This fits the algorithm well.
  199. for ( const attribute of query.attributes ) {
  200. // Skip all attributes that are stored in elements.
  201. // This isn't perfect solution but we have to deal with it for now.
  202. // `attribute` may have `undefined` value.
  203. if ( attribute && DocumentSelection._isStoreAttributeKey( attribute ) ) {
  204. continue;
  205. }
  206. let matched = false;
  207. for ( const schemaItem of schemaItems ) {
  208. if ( schemaItem._hasMatchingPath( 'allow', path, attribute ) ) {
  209. matched = true;
  210. break;
  211. }
  212. }
  213. // The attribute has not been matched, so it is not allowed by any schema item.
  214. // The query is disallowed.
  215. if ( !matched ) {
  216. return false;
  217. }
  218. }
  219. return true;
  220. }
  221. /**
  222. * Checks whether there is an item registered under given name in schema.
  223. *
  224. * @param itemName
  225. * @returns {Boolean}
  226. */
  227. hasItem( itemName ) {
  228. return this._items.has( itemName );
  229. }
  230. /**
  231. * Registers given item name in schema.
  232. *
  233. * // Register P element that should be treated like all block elements.
  234. * schema.registerItem( 'p', '$block' );
  235. *
  236. * @param {String} itemName Name to register.
  237. * @param [isExtending] If set, new item will extend item with given name.
  238. */
  239. registerItem( itemName, isExtending ) {
  240. if ( this.hasItem( itemName ) ) {
  241. /**
  242. * Item with specified name already exists in schema.
  243. *
  244. * @error model-schema-item-exists
  245. */
  246. throw new CKEditorError( 'model-schema-item-exists: Item with specified name already exists in schema.' );
  247. }
  248. if ( !!isExtending && !this.hasItem( isExtending ) ) {
  249. /**
  250. * Item with specified name does not exist in schema.
  251. *
  252. * @error model-schema-no-item
  253. */
  254. throw new CKEditorError( 'model-schema-no-item: Item with specified name does not exist in schema.' );
  255. }
  256. // Create new SchemaItem and add it to the items store.
  257. this._items.set( itemName, new SchemaItem( this ) );
  258. // Create an extension chain.
  259. // Extension chain has all item names that should be checked when that item is on path to check.
  260. // This simply means, that if item is not extending anything, it should have only itself in it's extension chain.
  261. // Since extending is not dynamic, we can simply get extension chain of extended item and expand it with registered name,
  262. // if the registered item is extending something.
  263. const chain = this.hasItem( isExtending ) ? this._extensionChains.get( isExtending ).concat( itemName ) : [ itemName ];
  264. this._extensionChains.set( itemName, chain );
  265. }
  266. /**
  267. * Checks whether item of given name is extending item of another given name.
  268. *
  269. * @param {String} childItemName Name of the child item.
  270. * @param {String} parentItemName Name of the parent item.
  271. * @returns {Boolean} `true` if child item extends parent item, `false` otherwise.
  272. */
  273. itemExtends( childItemName, parentItemName ) {
  274. if ( !this.hasItem( childItemName ) || !this.hasItem( parentItemName ) ) {
  275. /**
  276. * Item with specified name does not exist in schema.
  277. *
  278. * @error model-schema-no-item
  279. */
  280. throw new CKEditorError( 'model-schema-no-item: Item with specified name does not exist in schema.' );
  281. }
  282. const chain = this._extensionChains.get( childItemName );
  283. return chain.some( itemName => itemName == parentItemName );
  284. }
  285. /**
  286. * Checks whether the attribute is allowed in selection:
  287. *
  288. * * if the selection is not collapsed, then checks if the attribute is allowed on any of nodes in that range,
  289. * * if the selection is collapsed, then checks if on the selection position there's a text with the
  290. * specified attribute allowed.
  291. *
  292. * @param {module:engine/model/selection~Selection} selection Selection which will be checked.
  293. * @param {String} attribute The name of the attribute to check.
  294. * @returns {Boolean}
  295. */
  296. checkAttributeInSelection( selection, attribute ) {
  297. if ( selection.isCollapsed ) {
  298. // Check whether schema allows for a text with the attribute in the selection.
  299. return this.check( { name: '$text', inside: selection.getFirstPosition(), attributes: attribute } );
  300. } else {
  301. const ranges = selection.getRanges();
  302. // For all ranges, check nodes in them until you find a node that is allowed to have the attribute.
  303. for ( const range of ranges ) {
  304. for ( const value of range ) {
  305. // If returned item does not have name property, it is a TextFragment.
  306. const name = value.item.name || '$text';
  307. // Attribute should be checked together with existing attributes.
  308. // See https://github.com/ckeditor/ckeditor5-engine/issues/1110.
  309. const attributes = Array.from( value.item.getAttributeKeys() ).concat( attribute );
  310. if ( this.check( { name, inside: value.previousPosition, attributes } ) ) {
  311. // If we found a node that is allowed to have the attribute, return true.
  312. return true;
  313. }
  314. }
  315. }
  316. }
  317. // If we haven't found such node, return false.
  318. return false;
  319. }
  320. /**
  321. * Transforms the given set ranges into a set of ranges where the given attribute is allowed (and can be applied).
  322. *
  323. * @param {Array.<module:engine/model/range~Range>} ranges Ranges to be validated.
  324. * @param {String} attribute The name of the attribute to check.
  325. * @returns {Array.<module:engine/model/range~Range>} Ranges in which the attribute is allowed.
  326. */
  327. getValidRanges( ranges, attribute ) {
  328. const validRanges = [];
  329. for ( const range of ranges ) {
  330. let last = range.start;
  331. let from = range.start;
  332. const to = range.end;
  333. for ( const value of range.getWalker() ) {
  334. const name = value.item.name || '$text';
  335. const itemPosition = Position.createBefore( value.item );
  336. if ( !this.check( { name, inside: itemPosition, attributes: attribute } ) ) {
  337. if ( !from.isEqual( last ) ) {
  338. validRanges.push( new Range( from, last ) );
  339. }
  340. from = value.nextPosition;
  341. }
  342. last = value.nextPosition;
  343. }
  344. if ( from && !from.isEqual( to ) ) {
  345. validRanges.push( new Range( from, to ) );
  346. }
  347. }
  348. return validRanges;
  349. }
  350. /**
  351. * Returns the lowest {@link module:engine/model/schema~Schema#limits limit element} containing the entire
  352. * selection or the root otherwise.
  353. *
  354. * @param {module:engine/model/selection~Selection} selection Selection which returns the common ancestor.
  355. * @returns {module:engine/model/element~Element}
  356. */
  357. getLimitElement( selection ) {
  358. // Find the common ancestor for all selection's ranges.
  359. let element = Array.from( selection.getRanges() )
  360. .reduce( ( node, range ) => {
  361. if ( !node ) {
  362. return range.getCommonAncestor();
  363. }
  364. return node.getCommonAncestor( range.getCommonAncestor() );
  365. }, null );
  366. while ( !this.limits.has( element.name ) ) {
  367. if ( element.parent ) {
  368. element = element.parent;
  369. } else {
  370. break;
  371. }
  372. }
  373. return element;
  374. }
  375. /**
  376. * Removes disallowed by {@link module:engine/model/schema~Schema schema} attributes from given nodes.
  377. * When {@link module:engine/model/batch~Batch batch} parameter is provided then attributes will be removed
  378. * using that batch, by creating {@link module:engine/model/delta/attributedelta~AttributeDelta attribute deltas}.
  379. * Otherwise, attributes will be removed directly from provided nodes using {@link module:engine/model/node~Node node} API.
  380. *
  381. * @param {Iterable.<module:engine/model/node~Node>} nodes Nodes that will be filtered.
  382. * @param {module:engine/model/schema~SchemaPath} inside Path inside which schema will be checked.
  383. * @param {module:engine/model/batch~Batch} [batch] Batch to which the deltas will be added.
  384. */
  385. removeDisallowedAttributes( nodes, inside, batch ) {
  386. for ( const node of nodes ) {
  387. const name = node.is( 'text' ) ? '$text' : node.name;
  388. const attributes = Array.from( node.getAttributeKeys() );
  389. const queryPath = Schema._normalizeQueryPath( inside );
  390. // When node with attributes is not allowed in current position.
  391. if ( !this.check( { name, attributes, inside: queryPath } ) ) {
  392. // Let's remove attributes one by one.
  393. // TODO: this should be improved to check all combination of attributes.
  394. for ( const attribute of node.getAttributeKeys() ) {
  395. if ( !this.check( { name, attributes: attribute, inside: queryPath } ) ) {
  396. if ( batch ) {
  397. batch.removeAttribute( node, attribute );
  398. } else {
  399. node.removeAttribute( attribute );
  400. }
  401. }
  402. }
  403. }
  404. if ( node.is( 'element' ) ) {
  405. this.removeDisallowedAttributes( node.getChildren(), queryPath.concat( node.name ), batch );
  406. }
  407. }
  408. }
  409. /**
  410. * Returns {@link module:engine/model/schema~SchemaItem schema item} that was registered in the schema under given name.
  411. * If item has not been found, throws error.
  412. *
  413. * @private
  414. * @param {String} itemName Name to look for in schema.
  415. * @returns {module:engine/model/schema~SchemaItem} Schema item registered under given name.
  416. */
  417. _getItem( itemName ) {
  418. if ( !this.hasItem( itemName ) ) {
  419. /**
  420. * Item with specified name does not exist in schema.
  421. *
  422. * @error model-schema-no-item
  423. */
  424. throw new CKEditorError( 'model-schema-no-item: Item with specified name does not exist in schema.' );
  425. }
  426. return this._items.get( itemName );
  427. }
  428. /**
  429. * Normalizes a path to an entity by converting it from {@link module:engine/model/schema~SchemaPath} to an array of strings.
  430. *
  431. * @protected
  432. * @param {module:engine/model/schema~SchemaPath} path Path to normalize.
  433. * @returns {Array.<String>} Normalized path.
  434. */
  435. static _normalizeQueryPath( path ) {
  436. let normalized = [];
  437. if ( isArray( path ) ) {
  438. for ( const pathItem of path ) {
  439. if ( pathItem instanceof Element ) {
  440. normalized.push( pathItem.name );
  441. } else if ( isString( pathItem ) ) {
  442. normalized.push( pathItem );
  443. }
  444. }
  445. } else if ( path instanceof Position ) {
  446. let parent = path.parent;
  447. while ( parent !== null ) {
  448. normalized.push( parent.name );
  449. parent = parent.parent;
  450. }
  451. normalized.reverse();
  452. } else if ( isString( path ) ) {
  453. normalized = path.split( ' ' );
  454. }
  455. return normalized;
  456. }
  457. }
  458. /**
  459. * SchemaItem is a singular registry item in {@link module:engine/model/schema~Schema} that groups and holds allow/disallow rules for
  460. * one entity. This class is used internally in {@link module:engine/model/schema~Schema} and should not be used outside it.
  461. *
  462. * @see module:engine/model/schema~Schema
  463. * @protected
  464. */
  465. export class SchemaItem {
  466. /**
  467. * Creates SchemaItem instance.
  468. *
  469. * @param {module:engine/model/schema~Schema} schema Schema instance that owns this item.
  470. */
  471. constructor( schema ) {
  472. /**
  473. * Schema instance that owns this item.
  474. *
  475. * @private
  476. * @member {module:engine/model/schema~Schema} module:engine/model/schema~SchemaItem#_schema
  477. */
  478. this._schema = schema;
  479. /**
  480. * Paths in which the entity, represented by this item, is allowed.
  481. *
  482. * @private
  483. * @member {Array} module:engine/model/schema~SchemaItem#_allowed
  484. */
  485. this._allowed = [];
  486. /**
  487. * Paths in which the entity, represented by this item, is disallowed.
  488. *
  489. * @private
  490. * @member {Array} module:engine/model/schema~SchemaItem#_disallowed
  491. */
  492. this._disallowed = [];
  493. /**
  494. * Attributes that are required by the entity represented by this item.
  495. *
  496. * @protected
  497. * @member {Array} module:engine/model/schema~SchemaItem#_requiredAttributes
  498. */
  499. this._requiredAttributes = [];
  500. }
  501. /**
  502. * Allows entity, represented by this item, to be in given path.
  503. *
  504. * @param {Array.<String>} path Path in which entity is allowed.
  505. * @param {Array.<String>|String} [attributes] If set, this path will be used only for entities that have attribute(s) with this key.
  506. */
  507. allow( path, attributes ) {
  508. this._addPath( '_allowed', path, attributes );
  509. }
  510. /**
  511. * Disallows entity, represented by this item, to be in given path.
  512. *
  513. * @param {Array.<String>} path Path in which entity is disallowed.
  514. * @param {Array.<String>|String} [attributes] If set, this path will be used only for entities that have an attribute(s) with this key.
  515. */
  516. disallow( path, attributes ) {
  517. this._addPath( '_disallowed', path, attributes );
  518. }
  519. /**
  520. * Specifies that the entity, to be valid, requires given attributes set. It is possible to register multiple
  521. * different attributes set. If there are more than one attributes set required, the entity will be valid if
  522. * at least one of them is fulfilled.
  523. *
  524. * @param {Array.<String>} attributes Attributes that has to be set on the entity to make it valid.
  525. */
  526. requireAttributes( attributes ) {
  527. this._requiredAttributes.push( attributes );
  528. }
  529. /**
  530. * Custom toJSON method to solve child-parent circular dependencies.
  531. *
  532. * @returns {Object} Clone of this object with the parent property replaced with its name.
  533. */
  534. toJSON() {
  535. const json = clone( this );
  536. // Due to circular references we need to remove parent reference.
  537. json._schema = '[model.Schema]';
  538. return json;
  539. }
  540. /**
  541. * Adds path to the SchemaItem instance.
  542. *
  543. * @private
  544. * @param {String} member Name of the array member into which the path will be added. Possible values are `_allowed` or `_disallowed`.
  545. * @param {Array.<String>} path Path to add.
  546. * @param {Array.<String>|String} [attributes] If set, this path will be used only for entities that have attribute(s) with this key.
  547. */
  548. _addPath( member, path, attributes ) {
  549. path = path.slice();
  550. if ( !isArray( attributes ) ) {
  551. attributes = [ attributes ];
  552. }
  553. for ( const attribute of attributes ) {
  554. this[ member ].push( { path, attribute } );
  555. }
  556. }
  557. /**
  558. * Returns all paths of given type that were previously registered in the item.
  559. *
  560. * @private
  561. * @param {String} type Paths' type. Possible values are `allow` or `disallow`.
  562. * @param {String} [attribute] If set, only paths registered for given attribute will be returned.
  563. * @returns {Array} Paths registered in the item.
  564. */
  565. _getPaths( type, attribute ) {
  566. const source = type === 'allow' ? this._allowed : this._disallowed;
  567. const paths = [];
  568. for ( const item of source ) {
  569. if ( item.attribute === attribute ) {
  570. paths.push( item.path );
  571. }
  572. }
  573. return paths;
  574. }
  575. /**
  576. * Checks whether given set of attributes fulfills required attributes of this item.
  577. *
  578. * @protected
  579. * @see module:engine/model/schema~SchemaItem#requireAttributes
  580. * @param {Array.<String>} attributesToCheck Attributes to check.
  581. * @returns {Boolean} `true` if given set or attributes fulfills required attributes, `false` otherwise.
  582. */
  583. _checkRequiredAttributes( attributesToCheck ) {
  584. let found = true;
  585. for ( const attributeSet of this._requiredAttributes ) {
  586. found = true;
  587. for ( const attribute of attributeSet ) {
  588. if ( attributesToCheck.indexOf( attribute ) == -1 ) {
  589. found = false;
  590. break;
  591. }
  592. }
  593. if ( found ) {
  594. break;
  595. }
  596. }
  597. return found;
  598. }
  599. /**
  600. * Checks whether this item has any registered path of given type that matches the provided path.
  601. *
  602. * @protected
  603. * @param {String} type Paths' type. Possible values are `allow` or `disallow`.
  604. * @param {Array.<String>} pathToCheck Path to check.
  605. * @param {String} [attribute] If set, only paths registered for given attribute will be checked.
  606. * @returns {Boolean} `true` if item has any registered matching path, `false` otherwise.
  607. */
  608. _hasMatchingPath( type, pathToCheck, attribute ) {
  609. const registeredPaths = this._getPaths( type, attribute );
  610. for ( const registeredPathPath of registeredPaths ) {
  611. if ( matchPaths( this._schema, pathToCheck, registeredPathPath ) ) {
  612. return true;
  613. }
  614. }
  615. return false;
  616. }
  617. }
  618. /**
  619. * Object with query used by {@link module:engine/model/schema~Schema} to query schema or add allow/disallow rules to schema.
  620. *
  621. * @typedef {Object} module:engine/model/schema~SchemaQuery
  622. * @property {String} name Entity name.
  623. * @property {module:engine/model/schema~SchemaPath} inside Path inside which the entity is placed.
  624. * @property {Array.<String>|String} [attributes] If set, the query applies only to entities that has attribute(s) with given key.
  625. */
  626. /**
  627. * Path to an entity, begins from the top-most ancestor. Can be passed in multiple formats. Internally, normalized to
  628. * an array of strings. If string is passed, entities from the path should be divided by ` ` (space character). If
  629. * an array is passed, unrecognized items are skipped. If position is passed, it is assumed that the entity is at given position.
  630. *
  631. * @typedef {String|Array.<String|module:engine/model/element~Element>|module:engine/model/position~Position}
  632. * module:engine/model/schema~SchemaPath
  633. */
  634. // Checks whether the given pathToCheck and registeredPath right ends match.
  635. //
  636. // pathToCheck: C, D
  637. // registeredPath: A, B, C, D
  638. // result: OK
  639. //
  640. // pathToCheck: A, B, C
  641. // registeredPath: A, B, C, D
  642. // result: NOK
  643. //
  644. // Note – when matching paths, element extension chains (inheritance) are taken into consideration.
  645. //
  646. // @param {Schema} schema
  647. // @param {Array.<String>} pathToCheck
  648. // @param {Array.<String>} registeredPath
  649. function matchPaths( schema, pathToCheck, registeredPath ) {
  650. // Start checking from the right end of both tables.
  651. let registeredPathIndex = registeredPath.length - 1;
  652. let pathToCheckIndex = pathToCheck.length - 1;
  653. // And finish once reaching an end of the shorter table.
  654. while ( registeredPathIndex >= 0 && pathToCheckIndex >= 0 ) {
  655. const checkName = pathToCheck[ pathToCheckIndex ];
  656. // Fail when checking a path which contains element which aren't even registered to the schema.
  657. if ( !schema.hasItem( checkName ) ) {
  658. return false;
  659. }
  660. const extChain = schema._extensionChains.get( checkName );
  661. if ( extChain.includes( registeredPath[ registeredPathIndex ] ) ) {
  662. registeredPathIndex--;
  663. pathToCheckIndex--;
  664. } else {
  665. return false;
  666. }
  667. }
  668. return true;
  669. }