matcher.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385
  1. /**
  2. * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
  3. * For licensing, see LICENSE.md.
  4. */
  5. /**
  6. * @module engine/view/matcher
  7. */
  8. /**
  9. * View matcher class.
  10. * Instance of this class can be used to find {@link module:engine/view/element~Element elements} that match given pattern.
  11. */
  12. export default class Matcher {
  13. /**
  14. * Creates new instance of Matcher.
  15. *
  16. * @param {String|RegExp|Object} [pattern] Match patterns. See {@link module:engine/view/matcher~Matcher#add add method} for
  17. * more information.
  18. */
  19. constructor( ...pattern ) {
  20. this._patterns = [];
  21. this.add( ...pattern );
  22. }
  23. /**
  24. * Adds pattern or patterns to matcher instance.
  25. *
  26. * Example patterns matching element's name:
  27. *
  28. * // String.
  29. * matcher.add( 'div' );
  30. * matcher.add( { name: 'div' } );
  31. *
  32. * // Regular expression.
  33. * matcher.add( /^\w/ );
  34. * matcher.add( { name: /^\w/ } );
  35. *
  36. * Example pattern matching element's attributes:
  37. *
  38. * matcher.add( {
  39. * attribute: {
  40. * title: 'foobar', // Attribute title should equal 'foobar'.
  41. * foo: /^\w+/, // Attribute foo should match /^\w+/ regexp.
  42. * bar: true // Attribute bar should be set (can be empty).
  43. * }
  44. * } );
  45. *
  46. * Example patterns matching element's classes:
  47. *
  48. * // Single class.
  49. * matcher.add( {
  50. * class: 'foobar'
  51. * } );
  52. *
  53. * // Single class using regular expression.
  54. * matcher.add( {
  55. * class: /foo.../
  56. * } );
  57. *
  58. * // Multiple classes to match.
  59. * matcher.add( {
  60. * class: [ 'baz', 'bar', /foo.../ ]
  61. * } ):
  62. *
  63. * Example pattern matching element's styles:
  64. *
  65. * matcher.add( {
  66. * style: {
  67. * position: 'absolute',
  68. * color: /^\w*blue$/
  69. * }
  70. * } );
  71. *
  72. * Example function pattern:
  73. *
  74. * matcher.add( ( element ) => {
  75. * // Result of this function will be included in `match`
  76. * // property of the object returned from matcher.match() call.
  77. * if ( element.name === 'div' && element.childCount > 0 ) {
  78. * return { name: true };
  79. * }
  80. *
  81. * return null;
  82. * } );
  83. *
  84. * Multiple patterns can be added in one call:
  85. *
  86. * matcher.add( 'div', { class: 'foobar' } );
  87. *
  88. * @param {Object|String|RegExp|Function} pattern Object describing pattern details. If string or regular expression
  89. * is provided it will be used to match element's name. Pattern can be also provided in a form
  90. * of a function - then this function will be called with each {@link module:engine/view/element~Element element} as a parameter.
  91. * Function's return value will be stored under `match` key of the object returned from
  92. * {@link module:engine/view/matcher~Matcher#match match} or {@link module:engine/view/matcher~Matcher#matchAll matchAll} methods.
  93. * @param {String|RegExp} [pattern.name] Name or regular expression to match element's name.
  94. * @param {Object} [pattern.attribute] Object with key-value pairs representing attributes to match. Each object key
  95. * represents attribute name. Value under that key can be either:
  96. * * `true` - then attribute is just required (can be empty),
  97. * * a string - then attribute has to be equal, or
  98. * * a regular expression - then attribute has to match the expression.
  99. * @param {String|RegExp|Array} [pattern.class] Class name or array of class names to match. Each name can be
  100. * provided in a form of string or regular expression.
  101. * @param {Object} [pattern.style] Object with key-value pairs representing styles to match. Each object key
  102. * represents style name. Value under that key can be either a string or a regular expression and it will be used
  103. * to match style value.
  104. */
  105. add( ...pattern ) {
  106. for ( let item of pattern ) {
  107. // String or RegExp pattern is used as element's name.
  108. if ( typeof item == 'string' || item instanceof RegExp ) {
  109. item = { name: item };
  110. }
  111. // Single class name/RegExp can be provided.
  112. if ( item.class && ( typeof item.class == 'string' || item.class instanceof RegExp ) ) {
  113. item.class = [ item.class ];
  114. }
  115. this._patterns.push( item );
  116. }
  117. }
  118. /**
  119. * Matches elements for currently stored patterns. Returns match information about first found
  120. * {@link module:engine/view/element~Element element}, otherwise returns `null`.
  121. *
  122. * Example of returned object:
  123. *
  124. * {
  125. * element: <instance of found element>,
  126. * pattern: <pattern used to match found element>,
  127. * match: {
  128. * name: true,
  129. * attribute: [ 'title', 'href' ],
  130. * class: [ 'foo' ],
  131. * style: [ 'color', 'position' ]
  132. * }
  133. * }
  134. *
  135. * @see module:engine/view/matcher~Matcher#add
  136. * @see module:engine/view/matcher~Matcher#matchAll
  137. * @param {...module:engine/view/element~Element} element View element to match against stored patterns.
  138. * @returns {Object|null} result
  139. * @returns {module:engine/view/element~Element} result.element Matched view element.
  140. * @returns {Object|String|RegExp|Function} result.pattern Pattern that was used to find matched element.
  141. * @returns {Object} result.match Object representing matched element parts.
  142. * @returns {Boolean} [result.match.name] True if name of the element was matched.
  143. * @returns {Array} [result.match.attribute] Array with matched attribute names.
  144. * @returns {Array} [result.match.class] Array with matched class names.
  145. * @returns {Array} [result.match.style] Array with matched style names.
  146. */
  147. match( ...element ) {
  148. for ( const singleElement of element ) {
  149. for ( const pattern of this._patterns ) {
  150. const match = isElementMatching( singleElement, pattern );
  151. if ( match ) {
  152. return {
  153. element: singleElement,
  154. pattern,
  155. match
  156. };
  157. }
  158. }
  159. }
  160. return null;
  161. }
  162. /**
  163. * Matches elements for currently stored patterns. Returns array of match information with all found
  164. * {@link module:engine/view/element~Element elements}. If no element is found - returns `null`.
  165. *
  166. * @see module:engine/view/matcher~Matcher#add
  167. * @see module:engine/view/matcher~Matcher#match
  168. * @param {...module:engine/view/element~Element} element View element to match against stored patterns.
  169. * @returns {Array.<Object>|null} Array with match information about found elements or `null`. For more information
  170. * see {@link module:engine/view/matcher~Matcher#match match method} description.
  171. */
  172. matchAll( ...element ) {
  173. const results = [];
  174. for ( const singleElement of element ) {
  175. for ( const pattern of this._patterns ) {
  176. const match = isElementMatching( singleElement, pattern );
  177. if ( match ) {
  178. results.push( {
  179. element: singleElement,
  180. pattern,
  181. match
  182. } );
  183. }
  184. }
  185. }
  186. return results.length > 0 ? results : null;
  187. }
  188. /**
  189. * Returns the name of the element to match if there is exactly one pattern added to the matcher instance
  190. * and it matches element name defined by `string` (not `RegExp`). Otherwise, returns `null`.
  191. *
  192. * @returns {String|null} Element name trying to match.
  193. */
  194. getElementName() {
  195. if ( this._patterns.length !== 1 ) {
  196. return null;
  197. }
  198. const pattern = this._patterns[ 0 ];
  199. const name = pattern.name;
  200. return ( typeof pattern != 'function' && name && !( name instanceof RegExp ) ) ? name : null;
  201. }
  202. }
  203. // Returns match information if {@link module:engine/view/element~Element element} is matching provided pattern.
  204. // If element cannot be matched to provided pattern - returns `null`.
  205. //
  206. // @param {module:engine/view/element~Element} element
  207. // @param {Object|String|RegExp|Function} pattern
  208. // @returns {Object|null} Returns object with match information or null if element is not matching.
  209. function isElementMatching( element, pattern ) {
  210. // If pattern is provided as function - return result of that function;
  211. if ( typeof pattern == 'function' ) {
  212. return pattern( element );
  213. }
  214. const match = {};
  215. // Check element's name.
  216. if ( pattern.name ) {
  217. match.name = matchName( pattern.name, element.name );
  218. if ( !match.name ) {
  219. return null;
  220. }
  221. }
  222. // Check element's attributes.
  223. if ( pattern.attribute ) {
  224. match.attribute = matchAttributes( pattern.attribute, element );
  225. if ( !match.attribute ) {
  226. return null;
  227. }
  228. }
  229. // Check element's classes.
  230. if ( pattern.class ) {
  231. match.class = matchClasses( pattern.class, element );
  232. if ( !match.class ) {
  233. return false;
  234. }
  235. }
  236. // Check element's styles.
  237. if ( pattern.style ) {
  238. match.style = matchStyles( pattern.style, element );
  239. if ( !match.style ) {
  240. return false;
  241. }
  242. }
  243. return match;
  244. }
  245. // Checks if name can be matched by provided pattern.
  246. //
  247. // @param {String|RegExp} pattern
  248. // @param {String} name
  249. // @returns {Boolean} Returns `true` if name can be matched, `false` otherwise.
  250. function matchName( pattern, name ) {
  251. // If pattern is provided as RegExp - test against this regexp.
  252. if ( pattern instanceof RegExp ) {
  253. return pattern.test( name );
  254. }
  255. return pattern === name;
  256. }
  257. // Checks if attributes of provided element can be matched against provided patterns.
  258. //
  259. // @param {Object} patterns Object with information about attributes to match. Each key of the object will be
  260. // used as attribute name. Value of each key can be a string or regular expression to match against attribute value.
  261. // @param {module:engine/view/element~Element} element Element which attributes will be tested.
  262. // @returns {Array|null} Returns array with matched attribute names or `null` if no attributes were matched.
  263. function matchAttributes( patterns, element ) {
  264. const match = [];
  265. for ( const name in patterns ) {
  266. const pattern = patterns[ name ];
  267. if ( element.hasAttribute( name ) ) {
  268. const attribute = element.getAttribute( name );
  269. if ( pattern === true ) {
  270. match.push( name );
  271. } else if ( pattern instanceof RegExp ) {
  272. if ( pattern.test( attribute ) ) {
  273. match.push( name );
  274. } else {
  275. return null;
  276. }
  277. } else if ( attribute === pattern ) {
  278. match.push( name );
  279. } else {
  280. return null;
  281. }
  282. } else {
  283. return null;
  284. }
  285. }
  286. return match;
  287. }
  288. // Checks if classes of provided element can be matched against provided patterns.
  289. //
  290. // @param {Array.<String|RegExp>} patterns Array of strings or regular expressions to match against element's classes.
  291. // @param {module:engine/view/element~Element} element Element which classes will be tested.
  292. // @returns {Array|null} Returns array with matched class names or `null` if no classes were matched.
  293. function matchClasses( patterns, element ) {
  294. const match = [];
  295. for ( const pattern of patterns ) {
  296. if ( pattern instanceof RegExp ) {
  297. const classes = element.getClassNames();
  298. for ( const name of classes ) {
  299. if ( pattern.test( name ) ) {
  300. match.push( name );
  301. }
  302. }
  303. if ( match.length === 0 ) {
  304. return null;
  305. }
  306. } else if ( element.hasClass( pattern ) ) {
  307. match.push( pattern );
  308. } else {
  309. return null;
  310. }
  311. }
  312. return match;
  313. }
  314. // Checks if styles of provided element can be matched against provided patterns.
  315. //
  316. // @param {Object} patterns Object with information about styles to match. Each key of the object will be
  317. // used as style name. Value of each key can be a string or regular expression to match against style value.
  318. // @param {module:engine/view/element~Element} element Element which styles will be tested.
  319. // @returns {Array|null} Returns array with matched style names or `null` if no styles were matched.
  320. function matchStyles( patterns, element ) {
  321. const match = [];
  322. for ( const name in patterns ) {
  323. const pattern = patterns[ name ];
  324. if ( element.hasStyle( name ) ) {
  325. const style = element.getStyle( name );
  326. if ( pattern instanceof RegExp ) {
  327. if ( pattern.test( style ) ) {
  328. match.push( name );
  329. } else {
  330. return null;
  331. }
  332. } else if ( style === pattern ) {
  333. match.push( name );
  334. } else {
  335. return null;
  336. }
  337. } else {
  338. return null;
  339. }
  340. }
  341. return match;
  342. }