matcher.js 11 KB

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