@blackglory/match 0.3.5 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (242) hide show
  1. package/README.md +21 -32
  2. package/lib/index.d.ts +17 -0
  3. package/lib/index.js +18 -0
  4. package/lib/index.js.map +1 -0
  5. package/lib/{es2015/match-element.d.ts → match-element.d.ts} +1 -1
  6. package/lib/match-element.js +18 -0
  7. package/lib/match-element.js.map +1 -0
  8. package/lib/{es2015/match.d.ts → match.d.ts} +1 -1
  9. package/lib/match.js +18 -0
  10. package/lib/match.js.map +1 -0
  11. package/lib/{es2015/matchers → matchers}/any-of.d.ts +1 -1
  12. package/lib/matchers/any-of.js +6 -0
  13. package/lib/matchers/any-of.js.map +1 -0
  14. package/lib/{es2015/matchers → matchers}/child-nodes.d.ts +1 -1
  15. package/lib/matchers/child-nodes.js +24 -0
  16. package/lib/matchers/child-nodes.js.map +1 -0
  17. package/lib/{es2015/matchers → matchers}/children.d.ts +1 -1
  18. package/lib/matchers/children.js +24 -0
  19. package/lib/matchers/children.js.map +1 -0
  20. package/lib/{es2015/matchers → matchers}/css.d.ts +1 -1
  21. package/lib/matchers/css.js +14 -0
  22. package/lib/matchers/css.js.map +1 -0
  23. package/lib/{es2015/matchers → matchers}/element.d.ts +1 -1
  24. package/lib/matchers/element.js +30 -0
  25. package/lib/matchers/element.js.map +1 -0
  26. package/lib/{es2018/matchers → matchers}/multiple.d.ts +1 -1
  27. package/lib/matchers/multiple.js +39 -0
  28. package/lib/matchers/multiple.js.map +1 -0
  29. package/lib/{es2015/matchers → matchers}/node.d.ts +1 -1
  30. package/lib/matchers/node.js +27 -0
  31. package/lib/matchers/node.js.map +1 -0
  32. package/lib/{es2015/matchers → matchers}/optional.d.ts +1 -1
  33. package/lib/matchers/optional.js +5 -0
  34. package/lib/matchers/optional.js.map +1 -0
  35. package/lib/{es2015/matchers → matchers}/repeat.d.ts +1 -1
  36. package/lib/matchers/repeat.js +16 -0
  37. package/lib/matchers/repeat.js.map +1 -0
  38. package/lib/{es2015/matchers → matchers}/tap.d.ts +1 -1
  39. package/lib/matchers/tap.js +8 -0
  40. package/lib/matchers/tap.js.map +1 -0
  41. package/lib/{es2018/matchers → matchers}/text-content-equals.d.ts +1 -1
  42. package/lib/matchers/text-content-equals.js +16 -0
  43. package/lib/matchers/text-content-equals.js.map +1 -0
  44. package/lib/{es2018/matchers → matchers}/text-content-includes.d.ts +1 -1
  45. package/lib/matchers/text-content-includes.js +16 -0
  46. package/lib/matchers/text-content-includes.js.map +1 -0
  47. package/lib/{es2015/matchers → matchers}/text-content-matches.d.ts +1 -1
  48. package/lib/matchers/text-content-matches.js +12 -0
  49. package/lib/matchers/text-content-matches.js.map +1 -0
  50. package/lib/{es2015/matchers → matchers}/text-node.d.ts +1 -1
  51. package/lib/matchers/text-node.js +30 -0
  52. package/lib/matchers/text-node.js.map +1 -0
  53. package/lib/{es2015/matchers → matchers}/xpath.d.ts +1 -1
  54. package/lib/{es2015/matchers → matchers}/xpath.js +7 -11
  55. package/lib/matchers/xpath.js.map +1 -0
  56. package/lib/types.d.ts +18 -0
  57. package/lib/types.js +2 -0
  58. package/lib/types.js.map +1 -0
  59. package/lib/{es2015/utils → utils}/concat.js +1 -5
  60. package/lib/utils/concat.js.map +1 -0
  61. package/lib/{es2015/utils → utils}/match-multiple.d.ts +4 -1
  62. package/lib/utils/match-multiple.js +20 -0
  63. package/lib/utils/match-multiple.js.map +1 -0
  64. package/lib/{es2015/utils → utils}/match-one-by-one.d.ts +1 -1
  65. package/lib/{es2015/utils → utils}/match-one-by-one.js +9 -9
  66. package/lib/utils/match-one-by-one.js.map +1 -0
  67. package/lib/utils/merge-in-place.d.ts +1 -0
  68. package/lib/{es2015/utils → utils}/merge-in-place.js +5 -9
  69. package/lib/utils/merge-in-place.js.map +1 -0
  70. package/lib/utils/next-element-sibling.js +7 -0
  71. package/lib/utils/next-element-sibling.js.map +1 -0
  72. package/lib/utils/next-sibling.js +7 -0
  73. package/lib/utils/next-sibling.js.map +1 -0
  74. package/package.json +34 -49
  75. package/src/index.ts +18 -0
  76. package/src/match-element.ts +23 -0
  77. package/src/match.ts +23 -0
  78. package/src/matchers/any-of.ts +13 -0
  79. package/src/matchers/child-nodes.ts +33 -0
  80. package/src/matchers/children.ts +35 -0
  81. package/src/matchers/css.ts +24 -0
  82. package/src/matchers/element.ts +49 -0
  83. package/src/matchers/multiple.ts +59 -0
  84. package/src/matchers/node.ts +50 -0
  85. package/src/matchers/optional.ts +6 -0
  86. package/src/matchers/repeat.ts +25 -0
  87. package/src/matchers/tap.ts +12 -0
  88. package/src/matchers/text-content-equals.ts +28 -0
  89. package/src/matchers/text-content-includes.ts +28 -0
  90. package/src/matchers/text-content-matches.ts +20 -0
  91. package/src/matchers/text-node.ts +52 -0
  92. package/src/matchers/xpath.ts +54 -0
  93. package/src/types.ts +39 -0
  94. package/src/utils/concat.ts +9 -0
  95. package/src/utils/match-multiple.ts +27 -0
  96. package/src/utils/match-one-by-one.ts +47 -0
  97. package/src/utils/merge-in-place.ts +26 -0
  98. package/src/utils/next-element-sibling.ts +10 -0
  99. package/src/utils/next-sibling.ts +7 -0
  100. package/dist/es2015/index.min.mjs +0 -2
  101. package/dist/es2015/index.min.mjs.map +0 -1
  102. package/dist/es2015/index.mjs +0 -4445
  103. package/dist/es2015/index.mjs.map +0 -1
  104. package/dist/es2015/index.umd.js +0 -4471
  105. package/dist/es2015/index.umd.js.map +0 -1
  106. package/dist/es2015/index.umd.min.js +0 -2
  107. package/dist/es2015/index.umd.min.js.map +0 -1
  108. package/dist/es2018/index.min.mjs +0 -2
  109. package/dist/es2018/index.min.mjs.map +0 -1
  110. package/dist/es2018/index.mjs +0 -4453
  111. package/dist/es2018/index.mjs.map +0 -1
  112. package/dist/es2018/index.umd.js +0 -4479
  113. package/dist/es2018/index.umd.js.map +0 -1
  114. package/dist/es2018/index.umd.min.js +0 -2
  115. package/dist/es2018/index.umd.min.js.map +0 -1
  116. package/lib/es2015/index.d.ts +0 -17
  117. package/lib/es2015/index.js +0 -38
  118. package/lib/es2015/index.js.map +0 -1
  119. package/lib/es2015/match-element.js +0 -22
  120. package/lib/es2015/match-element.js.map +0 -1
  121. package/lib/es2015/match.js +0 -22
  122. package/lib/es2015/match.js.map +0 -1
  123. package/lib/es2015/matchers/any-of.js +0 -10
  124. package/lib/es2015/matchers/any-of.js.map +0 -1
  125. package/lib/es2015/matchers/child-nodes.js +0 -23
  126. package/lib/es2015/matchers/child-nodes.js.map +0 -1
  127. package/lib/es2015/matchers/children.js +0 -23
  128. package/lib/es2015/matchers/children.js.map +0 -1
  129. package/lib/es2015/matchers/css.js +0 -18
  130. package/lib/es2015/matchers/css.js.map +0 -1
  131. package/lib/es2015/matchers/element.js +0 -34
  132. package/lib/es2015/matchers/element.js.map +0 -1
  133. package/lib/es2015/matchers/multiple.d.ts +0 -10
  134. package/lib/es2015/matchers/multiple.js +0 -42
  135. package/lib/es2015/matchers/multiple.js.map +0 -1
  136. package/lib/es2015/matchers/node.js +0 -31
  137. package/lib/es2015/matchers/node.js.map +0 -1
  138. package/lib/es2015/matchers/optional.js +0 -9
  139. package/lib/es2015/matchers/optional.js.map +0 -1
  140. package/lib/es2015/matchers/repeat.js +0 -20
  141. package/lib/es2015/matchers/repeat.js.map +0 -1
  142. package/lib/es2015/matchers/tap.js +0 -12
  143. package/lib/es2015/matchers/tap.js.map +0 -1
  144. package/lib/es2015/matchers/text-content-equals.d.ts +0 -7
  145. package/lib/es2015/matchers/text-content-equals.js +0 -20
  146. package/lib/es2015/matchers/text-content-equals.js.map +0 -1
  147. package/lib/es2015/matchers/text-content-includes.d.ts +0 -7
  148. package/lib/es2015/matchers/text-content-includes.js +0 -20
  149. package/lib/es2015/matchers/text-content-includes.js.map +0 -1
  150. package/lib/es2015/matchers/text-content-matches.js +0 -16
  151. package/lib/es2015/matchers/text-content-matches.js.map +0 -1
  152. package/lib/es2015/matchers/text-node.js +0 -34
  153. package/lib/es2015/matchers/text-node.js.map +0 -1
  154. package/lib/es2015/matchers/xpath.js.map +0 -1
  155. package/lib/es2015/types.d.ts +0 -18
  156. package/lib/es2015/types.js +0 -3
  157. package/lib/es2015/types.js.map +0 -1
  158. package/lib/es2015/utils/concat.js.map +0 -1
  159. package/lib/es2015/utils/match-multiple.js +0 -21
  160. package/lib/es2015/utils/match-multiple.js.map +0 -1
  161. package/lib/es2015/utils/match-one-by-one.js.map +0 -1
  162. package/lib/es2015/utils/merge-in-place.d.ts +0 -5
  163. package/lib/es2015/utils/merge-in-place.js.map +0 -1
  164. package/lib/es2015/utils/next-element-sibling.js +0 -11
  165. package/lib/es2015/utils/next-element-sibling.js.map +0 -1
  166. package/lib/es2015/utils/next-sibling.js +0 -11
  167. package/lib/es2015/utils/next-sibling.js.map +0 -1
  168. package/lib/es2018/index.d.ts +0 -17
  169. package/lib/es2018/index.js +0 -38
  170. package/lib/es2018/index.js.map +0 -1
  171. package/lib/es2018/match-element.d.ts +0 -4
  172. package/lib/es2018/match-element.js +0 -22
  173. package/lib/es2018/match-element.js.map +0 -1
  174. package/lib/es2018/match.d.ts +0 -4
  175. package/lib/es2018/match.js +0 -22
  176. package/lib/es2018/match.js.map +0 -1
  177. package/lib/es2018/matchers/any-of.d.ts +0 -6
  178. package/lib/es2018/matchers/any-of.js +0 -10
  179. package/lib/es2018/matchers/any-of.js.map +0 -1
  180. package/lib/es2018/matchers/child-nodes.d.ts +0 -2
  181. package/lib/es2018/matchers/child-nodes.js +0 -27
  182. package/lib/es2018/matchers/child-nodes.js.map +0 -1
  183. package/lib/es2018/matchers/children.d.ts +0 -2
  184. package/lib/es2018/matchers/children.js +0 -27
  185. package/lib/es2018/matchers/children.js.map +0 -1
  186. package/lib/es2018/matchers/css.d.ts +0 -3
  187. package/lib/es2018/matchers/css.js +0 -18
  188. package/lib/es2018/matchers/css.js.map +0 -1
  189. package/lib/es2018/matchers/element.d.ts +0 -4
  190. package/lib/es2018/matchers/element.js +0 -34
  191. package/lib/es2018/matchers/element.js.map +0 -1
  192. package/lib/es2018/matchers/multiple.js +0 -42
  193. package/lib/es2018/matchers/multiple.js.map +0 -1
  194. package/lib/es2018/matchers/node.d.ts +0 -4
  195. package/lib/es2018/matchers/node.js +0 -31
  196. package/lib/es2018/matchers/node.js.map +0 -1
  197. package/lib/es2018/matchers/optional.d.ts +0 -2
  198. package/lib/es2018/matchers/optional.js +0 -9
  199. package/lib/es2018/matchers/optional.js.map +0 -1
  200. package/lib/es2018/matchers/repeat.d.ts +0 -2
  201. package/lib/es2018/matchers/repeat.js +0 -20
  202. package/lib/es2018/matchers/repeat.js.map +0 -1
  203. package/lib/es2018/matchers/tap.d.ts +0 -2
  204. package/lib/es2018/matchers/tap.js +0 -12
  205. package/lib/es2018/matchers/tap.js.map +0 -1
  206. package/lib/es2018/matchers/text-content-equals.js +0 -20
  207. package/lib/es2018/matchers/text-content-equals.js.map +0 -1
  208. package/lib/es2018/matchers/text-content-includes.js +0 -20
  209. package/lib/es2018/matchers/text-content-includes.js.map +0 -1
  210. package/lib/es2018/matchers/text-content-matches.d.ts +0 -6
  211. package/lib/es2018/matchers/text-content-matches.js +0 -16
  212. package/lib/es2018/matchers/text-content-matches.js.map +0 -1
  213. package/lib/es2018/matchers/text-node.d.ts +0 -4
  214. package/lib/es2018/matchers/text-node.js +0 -34
  215. package/lib/es2018/matchers/text-node.js.map +0 -1
  216. package/lib/es2018/matchers/xpath.d.ts +0 -3
  217. package/lib/es2018/matchers/xpath.js +0 -34
  218. package/lib/es2018/matchers/xpath.js.map +0 -1
  219. package/lib/es2018/types.d.ts +0 -18
  220. package/lib/es2018/types.js +0 -3
  221. package/lib/es2018/types.js.map +0 -1
  222. package/lib/es2018/utils/concat.d.ts +0 -1
  223. package/lib/es2018/utils/concat.js +0 -14
  224. package/lib/es2018/utils/concat.js.map +0 -1
  225. package/lib/es2018/utils/match-multiple.d.ts +0 -2
  226. package/lib/es2018/utils/match-multiple.js +0 -21
  227. package/lib/es2018/utils/match-multiple.js.map +0 -1
  228. package/lib/es2018/utils/match-one-by-one.d.ts +0 -2
  229. package/lib/es2018/utils/match-one-by-one.js +0 -38
  230. package/lib/es2018/utils/match-one-by-one.js.map +0 -1
  231. package/lib/es2018/utils/merge-in-place.d.ts +0 -5
  232. package/lib/es2018/utils/merge-in-place.js +0 -31
  233. package/lib/es2018/utils/merge-in-place.js.map +0 -1
  234. package/lib/es2018/utils/next-element-sibling.d.ts +0 -1
  235. package/lib/es2018/utils/next-element-sibling.js +0 -11
  236. package/lib/es2018/utils/next-element-sibling.js.map +0 -1
  237. package/lib/es2018/utils/next-sibling.d.ts +0 -1
  238. package/lib/es2018/utils/next-sibling.js +0 -11
  239. package/lib/es2018/utils/next-sibling.js.map +0 -1
  240. /package/lib/{es2015/utils → utils}/concat.d.ts +0 -0
  241. /package/lib/{es2015/utils → utils}/next-element-sibling.d.ts +0 -0
  242. /package/lib/{es2015/utils → utils}/next-sibling.d.ts +0 -0
@@ -0,0 +1,24 @@
1
+ import { concat } from '@utils/concat.js'
2
+ import { ITerminalMatcher } from '@src/types.js'
3
+ import { isString } from '@blackglory/prelude'
4
+
5
+ export function css(
6
+ strings: TemplateStringsArray
7
+ , ...values: string[]
8
+ ): ITerminalMatcher<Element>
9
+ export function css(selector: string): ITerminalMatcher<Element>
10
+ export function css(...args:
11
+ | [selector: string]
12
+ | [strings: TemplateStringsArray, ...values: string[]]
13
+ ): ITerminalMatcher<Element> {
14
+ if (isString(args[0])) {
15
+ const [selector] = args
16
+
17
+ return (element: Element) => element.matches(selector)
18
+ } else {
19
+ const [strings, ...values] = args
20
+ const selector = concat(strings, values).join('')
21
+
22
+ return css(selector)
23
+ }
24
+ }
@@ -0,0 +1,49 @@
1
+ import { isntElement } from 'extra-dom'
2
+ import { INestedMatcher, ITerminalMatcher, IReadonlyContext } from '@src/types.js'
3
+ import { isArray, isString } from '@blackglory/prelude'
4
+ import { concat } from '@utils/concat.js'
5
+ import { mergeInPlace } from '@utils/merge-in-place.js'
6
+
7
+ export function element(
8
+ strings: TemplateStringsArray
9
+ , ...values: string[]
10
+ ): (...matchers: Array<INestedMatcher<Element> | ITerminalMatcher<Element>>) => INestedMatcher<Node>
11
+ export function element(name: string, ...matchers: Array<INestedMatcher<Element>>):
12
+ INestedMatcher<Node>
13
+ export function element(...matchers: Array<INestedMatcher<Element>>):
14
+ INestedMatcher<Node>
15
+ export function element(...args:
16
+ | [strings: TemplateStringsArray, ...values: string[]]
17
+ | [name: string, ...matchers: Array<INestedMatcher<Element> | ITerminalMatcher<Element>>]
18
+ | [...matchers: Array<INestedMatcher<Element> | ITerminalMatcher<Element>>]
19
+ ) {
20
+ if (isArray(args[0])) {
21
+ const [strings, ...values] =
22
+ args as [strings: TemplateStringsArray, ...values: string[]]
23
+ const name = concat(strings, values).join('')
24
+
25
+ return (...matchers: Array<INestedMatcher<Element> | ITerminalMatcher<Element>>) => element(name, ...matchers)
26
+ }
27
+
28
+ if (isString(args[0])) {
29
+ const [name, ...matchers] =
30
+ args as [name: string, ...matchers: Array<INestedMatcher<Element> | ITerminalMatcher<Element>>]
31
+
32
+ return function (this: IReadonlyContext, _element: Element) {
33
+ const result = element(...matchers).call(this, _element)
34
+ if (result) {
35
+ mergeInPlace(this.collection, { [name]: _element })
36
+ }
37
+ return result
38
+ }
39
+ }
40
+
41
+ const [...matchers] = args as [...matchers: Array<INestedMatcher<Element> | ITerminalMatcher<Element>>]
42
+
43
+ return function (this: IReadonlyContext, element: Element) {
44
+ if (isntElement(element)) return false
45
+ if (matchers.length === 0) return true
46
+
47
+ return matchers.every(match => match.call(this, element))
48
+ }
49
+ }
@@ -0,0 +1,59 @@
1
+ import { INestedMatcher, ITerminalMatcher, ISkipMatcher, IReadonlyContext } from '@src/types.js'
2
+ import { countup } from 'extra-generator'
3
+ import { assert } from '@blackglory/prelude'
4
+ import { matchMultiple } from '@utils/match-multiple.js'
5
+
6
+ export enum Range {
7
+ Min = 0
8
+ , Max = 1
9
+ }
10
+
11
+ interface IMultipleOptions {
12
+ // 当开启贪婪模式时, 应该优先匹配最长的情况
13
+ greedy: boolean // = true, 默认启用贪婪模式
14
+ }
15
+
16
+ export function multiple<T extends Node>(
17
+ [min, max]: [min: number, max: number]
18
+ , matcher: INestedMatcher<T> | ITerminalMatcher<T>
19
+ , options: IMultipleOptions = { greedy: true }
20
+ ): ISkipMatcher<T> {
21
+ assert(Number.isInteger(min), 'parameter min must be an integer')
22
+ assert(Number.isInteger(max) || max === Infinity, 'parameter max must be an integer or Infinity')
23
+ assert(min >= 0, 'parameter min must be greater than or equal to 0')
24
+ assert(min <= max, 'parameter max must be greater than or equal to min')
25
+
26
+ return function* (this: IReadonlyContext, node: T) {
27
+ if (options.greedy) {
28
+ let ubound = max
29
+ while (true) {
30
+ const round = matchMultiple.call(
31
+ this
32
+ , node
33
+ , ubound
34
+ , matcher as INestedMatcher<Node> | ITerminalMatcher<Node>
35
+ )
36
+
37
+ if (round < min) break
38
+ yield round
39
+
40
+ ubound = round - 1
41
+ if (ubound < min) break
42
+ }
43
+ } else {
44
+ for (const ubound of countup(min, max)) {
45
+ const result = matchMultiple.call(
46
+ this
47
+ , node
48
+ , ubound
49
+ , matcher as INestedMatcher<Node> | INestedMatcher<Node>
50
+ )
51
+
52
+ // 如果匹配的节点数量少于ubound, 说明匹配失败, 即使尝试更长的匹配也不会成功.
53
+ if (result < ubound) break
54
+
55
+ if (result === ubound) yield ubound
56
+ }
57
+ }
58
+ }
59
+ }
@@ -0,0 +1,50 @@
1
+ import { INestedMatcher, ITerminalMatcher, IReadonlyContext } from '@src/types.js'
2
+ import { isArray, isString } from '@blackglory/prelude'
3
+ import { concat } from '@utils/concat.js'
4
+ import { mergeInPlace } from '@utils/merge-in-place.js'
5
+
6
+ export function node(
7
+ strings: TemplateStringsArray
8
+ , ...values: string[]
9
+ ): (...matchers: Array<INestedMatcher<Node> | ITerminalMatcher<Node>>) => INestedMatcher<Node>
10
+ export function node(
11
+ name: string
12
+ , ...matchers: Array<INestedMatcher<Node> | ITerminalMatcher<Node>>
13
+ ): INestedMatcher<Node>
14
+ export function node(
15
+ ...matchers: Array<INestedMatcher<Node> | ITerminalMatcher<Node>>
16
+ ): INestedMatcher<Node>
17
+ export function node(...args:
18
+ | [strings: TemplateStringsArray, ...values: string[]]
19
+ | [name: string, ...matchers: Array<INestedMatcher<Node> | ITerminalMatcher<Node>>]
20
+ | [...matchers: Array<INestedMatcher<Node> | ITerminalMatcher<Node>>]
21
+ ) {
22
+ if (isArray(args[0])) {
23
+ const [strings, ...values] =
24
+ args as [strings: TemplateStringsArray, ...values: string[]]
25
+ const name = concat(strings, values).join('')
26
+
27
+ return (...matchers: Array<INestedMatcher<Node> | ITerminalMatcher<Node>>) => node(name, ...matchers)
28
+ }
29
+
30
+ if (isString(args[0])) {
31
+ const [name, ...matchers] =
32
+ args as [name: string, ...matchers: Array<INestedMatcher<Node> | ITerminalMatcher<Node>>]
33
+
34
+ return function (this: IReadonlyContext, _node: Node) {
35
+ const result = node(...matchers).call(this, _node)
36
+ if (result) {
37
+ mergeInPlace(this.collection, { [name]: _node })
38
+ }
39
+ return result
40
+ }
41
+ }
42
+
43
+ const [...matchers] = args as [...matchers: Array<INestedMatcher<Node> | ITerminalMatcher<Node>>]
44
+
45
+ return function (this: IReadonlyContext, node: Node) {
46
+ if (matchers.length === 0) return true
47
+
48
+ return matchers.every(match => match.call(this, node))
49
+ }
50
+ }
@@ -0,0 +1,6 @@
1
+ import { INestedMatcher, ITerminalMatcher, ISkipMatcher } from '@src/types.js'
2
+ import { multiple } from './multiple.js'
3
+
4
+ export function optional<T extends Node>(matcher: INestedMatcher<T> | ITerminalMatcher<T>): ISkipMatcher<T> {
5
+ return multiple([0, 1], matcher, { greedy: true })
6
+ }
@@ -0,0 +1,25 @@
1
+ import { INestedMatcher, ITerminalMatcher, ISkipMatcher, IReadonlyContext } from '@src/types.js'
2
+ import { assert } from '@blackglory/prelude'
3
+ import { matchMultiple } from '@utils/match-multiple.js'
4
+
5
+ export function repeat<T extends Node>(
6
+ times: number
7
+ , matcher: INestedMatcher<T> | ITerminalMatcher<T>
8
+ ): ISkipMatcher<T> {
9
+ assert(Number.isInteger(times), 'parameter times must be an integer')
10
+ assert(times >= 0, 'parameter number must be greater than or equal to 0')
11
+
12
+ return function (this: IReadonlyContext, node: T) {
13
+ const result = matchMultiple.call(
14
+ this
15
+ , node
16
+ , times
17
+ , matcher as INestedMatcher<Node> | ITerminalMatcher<Node>
18
+ )
19
+ if (result === times) {
20
+ return times
21
+ } else {
22
+ return false
23
+ }
24
+ }
25
+ }
@@ -0,0 +1,12 @@
1
+ import { IReadonlyContext, IMatcher } from '@src/types.js'
2
+
3
+ export function tap<T extends Node, U extends ReturnType<IMatcher<any>>>(
4
+ matcher: (this: IReadonlyContext, node: T) => U
5
+ , callback: (value: U) => void
6
+ ): (this: IReadonlyContext, node: T) => U {
7
+ return function (this: IReadonlyContext, node: T) {
8
+ const result = matcher.call(this, node)
9
+ callback(result)
10
+ return result
11
+ }
12
+ }
@@ -0,0 +1,28 @@
1
+ import { ITerminalMatcher } from '@src/types.js'
2
+ import { isNull } from '@blackglory/prelude'
3
+
4
+ interface ITextContentEqualsOptions {
5
+ caseSensitive?: boolean
6
+ trim?: boolean
7
+ }
8
+
9
+ export function textContentEquals(
10
+ text: string
11
+ , {
12
+ caseSensitive = true
13
+ , trim = false
14
+ }: ITextContentEqualsOptions = {}
15
+ ): ITerminalMatcher<Node> {
16
+ return (node: Node) => {
17
+ if (isNull(node.textContent)) return false
18
+
19
+ let textContent = node.textContent
20
+ if (!caseSensitive) {
21
+ textContent = textContent.toLowerCase()
22
+ text = text.toLowerCase()
23
+ }
24
+ if (trim) textContent = textContent.trim()
25
+
26
+ return textContent === text
27
+ }
28
+ }
@@ -0,0 +1,28 @@
1
+ import { ITerminalMatcher } from '@src/types.js'
2
+ import { isNull } from '@blackglory/prelude'
3
+
4
+ interface ITextContentIncludesOptions {
5
+ caseSensitive?: boolean
6
+ trim?: boolean
7
+ }
8
+
9
+ export function textContentIncludes(
10
+ searchString: string
11
+ , {
12
+ caseSensitive = true
13
+ , trim = false
14
+ }: ITextContentIncludesOptions = {}
15
+ ): ITerminalMatcher<Node> {
16
+ return (node: Node) => {
17
+ if (isNull(node.textContent)) return false
18
+
19
+ let textContent = node.textContent
20
+ if (!caseSensitive) {
21
+ textContent = textContent.toLowerCase()
22
+ searchString = searchString.toLowerCase()
23
+ }
24
+ if (trim) textContent = textContent.trim()
25
+
26
+ return textContent.includes(searchString)
27
+ }
28
+ }
@@ -0,0 +1,20 @@
1
+ import { ITerminalMatcher } from '@src/types.js'
2
+ import { isNull } from '@blackglory/prelude'
3
+
4
+ interface ITextContentMatchesOptions {
5
+ trim?: boolean
6
+ }
7
+
8
+ export function textContentMatches(
9
+ pattern: RegExp
10
+ , { trim = false }: ITextContentMatchesOptions = {}
11
+ ): ITerminalMatcher<Node> {
12
+ return (node: Node) => {
13
+ if (isNull(node.textContent)) return false
14
+
15
+ let textContent = node.textContent
16
+ if (trim) textContent = textContent.trim()
17
+
18
+ return pattern.test(textContent)
19
+ }
20
+ }
@@ -0,0 +1,52 @@
1
+ import { INestedMatcher, ITerminalMatcher, IReadonlyContext } from '@src/types.js'
2
+ import { isArray, isString } from '@blackglory/prelude'
3
+ import { concat } from '@utils/concat.js'
4
+ import { isntTextNode } from 'extra-dom'
5
+ import { mergeInPlace } from '@utils/merge-in-place.js'
6
+
7
+ export function textNode(
8
+ strings: TemplateStringsArray
9
+ , ...values: string[]
10
+ ): (...matchers: Array<ITerminalMatcher<Node>>) => INestedMatcher<Node>
11
+ export function textNode(
12
+ name: string
13
+ , ...matchers: Array<ITerminalMatcher<Node>>
14
+ ): INestedMatcher<Node>
15
+ export function textNode(
16
+ ...matchers: Array<ITerminalMatcher<Node>>
17
+ ): INestedMatcher<Node>
18
+ export function textNode(...args:
19
+ | [strings: TemplateStringsArray, ...values: string[]]
20
+ | [name: string, ...matchers: Array<ITerminalMatcher<Node>>]
21
+ | [...matchers: Array<ITerminalMatcher<Node>>]
22
+ ) {
23
+ if (isArray(args[0])) {
24
+ const [strings, ...values] =
25
+ args as [strings: TemplateStringsArray, ...values: string[]]
26
+ const name = concat(strings, values).join('')
27
+
28
+ return (...matchers: Array<ITerminalMatcher<Node>>) => textNode(name, ...matchers)
29
+ }
30
+
31
+ if (isString(args[0])) {
32
+ const [name, ...matchers] =
33
+ args as [name: string, ...matchers: Array<ITerminalMatcher<Node>>]
34
+
35
+ return function (this: IReadonlyContext, node: Node) {
36
+ const result = textNode(...matchers).call(this, node)
37
+ if (result) {
38
+ mergeInPlace(this.collection, { [name]: node })
39
+ }
40
+ return result
41
+ }
42
+ }
43
+
44
+ const [...matchers] = args as [...matchers: Array<ITerminalMatcher<Node>>]
45
+
46
+ return function (this: IReadonlyContext, node: Node) {
47
+ if (isntTextNode(node)) return false
48
+ if (matchers.length === 0) return true
49
+
50
+ return matchers.every(match => match.call(this, node))
51
+ }
52
+ }
@@ -0,0 +1,54 @@
1
+ import { ITerminalMatcher, IReadonlyContext } from '@src/types.js'
2
+ import { isString } from '@blackglory/prelude'
3
+ import { concat } from '@utils/concat.js'
4
+ import { assert } from '@blackglory/prelude'
5
+
6
+ const UNORDERED_NODE_ITERATOR_TYPE =
7
+ 'XPathResult' in globalThis
8
+ ? XPathResult.UNORDERED_NODE_ITERATOR_TYPE
9
+ : 4
10
+
11
+ export function xpath(
12
+ strings: TemplateStringsArray
13
+ , ...values: string[]
14
+ ): ITerminalMatcher<Node>
15
+ export function xpath(
16
+ experssion: string
17
+ ): ITerminalMatcher<Node>
18
+ export function xpath(...args:
19
+ | [expression: string]
20
+ | [strings: TemplateStringsArray, ...values: string[]]
21
+ ): ITerminalMatcher<Node> {
22
+ if (isString(args[0])) {
23
+ const [expression] = args
24
+ assert(expression.startsWith('//*'), 'XPath expressions must start with "//*"')
25
+
26
+ return function (
27
+ this: Pick<IReadonlyContext, 'document'>
28
+ , node: Node
29
+ ): boolean {
30
+ return xpathMatches(this.document, expression, node)
31
+ }
32
+ } else {
33
+ const [strings, ...values] = args
34
+ const expression = concat(strings, values).join('')
35
+
36
+ return xpath(expression)
37
+ }
38
+ }
39
+
40
+ function xpathMatches(document: Document, expression: string, node: Node): boolean {
41
+ const iterator = document.evaluate(
42
+ expression
43
+ , node
44
+ , null
45
+ , UNORDERED_NODE_ITERATOR_TYPE
46
+ , null
47
+ )
48
+
49
+ let value
50
+ while ((value = iterator.iterateNext()) !== null) {
51
+ if (value === node) return true
52
+ }
53
+ return false
54
+ }
package/src/types.ts ADDED
@@ -0,0 +1,39 @@
1
+ export interface IContext {
2
+ readonly document: Document
3
+ readonly next: (node: Node, distance?: number) => Node | undefined
4
+ readonly collection: {
5
+ [name: string]: Node | Node[]
6
+ }
7
+ }
8
+
9
+ export interface IReadonlyContext {
10
+ readonly document: Document
11
+ readonly next: (node: Node, distance?: number) => Node | undefined
12
+ readonly collection: {
13
+ readonly [name: string]: Node | Node[]
14
+ }
15
+ }
16
+
17
+ export type IMatcher<T extends Node> =
18
+ | ITerminalMatcher<T>
19
+ | INestedMatcher<T>
20
+ | ISkipMatcher<T>
21
+ | (
22
+ <T extends Node>(this: IReadonlyContext, node: T) =>
23
+ boolean | number | Iterable<number>
24
+ )
25
+
26
+ export type INestedMatcher<T extends Node> = (
27
+ this: IReadonlyContext
28
+ , node: T
29
+ ) => boolean
30
+
31
+ export type ISkipMatcher<T extends Node> = (
32
+ this: IReadonlyContext
33
+ , node: T
34
+ ) => number | Iterable<number> | false
35
+
36
+ export type ITerminalMatcher<T extends Node> = (
37
+ this: IReadonlyContext
38
+ , node: T
39
+ ) => boolean
@@ -0,0 +1,9 @@
1
+ export function concat(strings: TemplateStringsArray, values: string[]): string[] {
2
+ const result = []
3
+ for (let i = 0, len = values.length; i < len; i++) {
4
+ result.push(strings[i])
5
+ result.push(values[i])
6
+ }
7
+ result.push(strings[strings.length - 1])
8
+ return result
9
+ }
@@ -0,0 +1,27 @@
1
+ import { countup } from 'extra-generator'
2
+ import { INestedMatcher, ITerminalMatcher, IReadonlyContext } from '@src/types.js'
3
+
4
+ /**
5
+ * @returns {number} 返回值为成功匹配的元素个数, 当此值等于ubound时, 代表匹配成功.
6
+ */
7
+ export function matchMultiple<T extends Node>(
8
+ this: IReadonlyContext
9
+ , node: T
10
+ , ubound: number
11
+ , matcher: INestedMatcher<T> | ITerminalMatcher<T>
12
+ ): number {
13
+ let currentNode: T | null = node
14
+
15
+ for (const round of countup(1, ubound)) {
16
+ if (!currentNode) return round - 1
17
+
18
+ const result = matcher.call(this, currentNode)
19
+ if (result) {
20
+ currentNode = this.next(currentNode) as T | null
21
+ } else {
22
+ return round - 1
23
+ }
24
+ }
25
+
26
+ return ubound
27
+ }
@@ -0,0 +1,47 @@
1
+ import { IMatcher, IReadonlyContext } from '@src/types.js'
2
+ import { isBoolean, isNumber, isIterable } from '@blackglory/prelude'
3
+
4
+ export function matchOneByOne<T extends Node>(
5
+ context: IReadonlyContext
6
+ , source: T | null
7
+ , ...matchers: Array<IMatcher<T>>
8
+ ): boolean {
9
+ if (matchers.length === 0) return true
10
+ if (!source) return false
11
+
12
+ const [matcher, ...otherMatchers] = matchers
13
+
14
+ const result = Reflect.apply(matcher, context, [source]) as ReturnType<typeof matcher>
15
+
16
+ // TerminalMatcher
17
+ if (isBoolean(result)) {
18
+ if (result) {
19
+ const nextNode = context.next(source) as T | null
20
+ return matchOneByOne(context, nextNode, ...otherMatchers)
21
+ } else {
22
+ return false
23
+ }
24
+ }
25
+
26
+ // 此处一定是成功匹配, 因为SkipMatcher在失败时会返回false.
27
+ if (isNumber(result)) {
28
+ const distance = result
29
+ const nextNode = context.next(source, distance) as T | null
30
+ return matchOneByOne(context, nextNode, ...otherMatchers)
31
+ }
32
+
33
+ // SkipMatcher返回Iterable意味着存在多种可能性, 可能出现失败回溯.
34
+ if (isIterable(result)) {
35
+ for (const distance of result) {
36
+ const nextNode = context.next(source, distance) as T | null
37
+ if (matchOneByOne(context, nextNode, ...otherMatchers)) {
38
+ return true
39
+ }
40
+ }
41
+
42
+ // 尝试了所有可能性, 未发现可以匹配的结果
43
+ return false
44
+ }
45
+
46
+ throw new Error('Unknown return value')
47
+ }
@@ -0,0 +1,26 @@
1
+ import { isArray } from '@blackglory/prelude'
2
+
3
+ export function mergeInPlace<T>(
4
+ target: Record<string, T | T[]>
5
+ , source: Record<string, T | T[]>
6
+ ): void {
7
+ for (const [key, value] of Object.entries(source)) {
8
+ if (target[key]) {
9
+ if (isArray(target[key])) {
10
+ if (isArray(value)) {
11
+ target[key] = [...target[key] as T[], ...value]
12
+ } else {
13
+ target[key] = [...target[key] as T[], value]
14
+ }
15
+ } else {
16
+ if (isArray(value)) {
17
+ target[key] = [target[key] as T, ...value]
18
+ } else {
19
+ target[key] = [target[key] as T, value]
20
+ }
21
+ }
22
+ } else {
23
+ target[key] = value
24
+ }
25
+ }
26
+ }
@@ -0,0 +1,10 @@
1
+ import * as ExtraDOM from 'extra-dom'
2
+
3
+ export function nextElementSibling(
4
+ node: Node
5
+ , distance: number = 1
6
+ ): Node | undefined {
7
+ if (distance === 0) return node
8
+
9
+ return ExtraDOM.nextElementSibling(node, distance)
10
+ }
@@ -0,0 +1,7 @@
1
+ import * as ExtraDOM from 'extra-dom'
2
+
3
+ export function nextSibling(node: Node, distance: number = 1): Node | undefined {
4
+ if (distance === 0) return node
5
+
6
+ return ExtraDOM.nextSibling(node, distance)
7
+ }