@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.
- package/README.md +21 -32
- package/lib/index.d.ts +17 -0
- package/lib/index.js +18 -0
- package/lib/index.js.map +1 -0
- package/lib/{es2015/match-element.d.ts → match-element.d.ts} +1 -1
- package/lib/match-element.js +18 -0
- package/lib/match-element.js.map +1 -0
- package/lib/{es2015/match.d.ts → match.d.ts} +1 -1
- package/lib/match.js +18 -0
- package/lib/match.js.map +1 -0
- package/lib/{es2015/matchers → matchers}/any-of.d.ts +1 -1
- package/lib/matchers/any-of.js +6 -0
- package/lib/matchers/any-of.js.map +1 -0
- package/lib/{es2015/matchers → matchers}/child-nodes.d.ts +1 -1
- package/lib/matchers/child-nodes.js +24 -0
- package/lib/matchers/child-nodes.js.map +1 -0
- package/lib/{es2015/matchers → matchers}/children.d.ts +1 -1
- package/lib/matchers/children.js +24 -0
- package/lib/matchers/children.js.map +1 -0
- package/lib/{es2015/matchers → matchers}/css.d.ts +1 -1
- package/lib/matchers/css.js +14 -0
- package/lib/matchers/css.js.map +1 -0
- package/lib/{es2015/matchers → matchers}/element.d.ts +1 -1
- package/lib/matchers/element.js +30 -0
- package/lib/matchers/element.js.map +1 -0
- package/lib/{es2018/matchers → matchers}/multiple.d.ts +1 -1
- package/lib/matchers/multiple.js +39 -0
- package/lib/matchers/multiple.js.map +1 -0
- package/lib/{es2015/matchers → matchers}/node.d.ts +1 -1
- package/lib/matchers/node.js +27 -0
- package/lib/matchers/node.js.map +1 -0
- package/lib/{es2015/matchers → matchers}/optional.d.ts +1 -1
- package/lib/matchers/optional.js +5 -0
- package/lib/matchers/optional.js.map +1 -0
- package/lib/{es2015/matchers → matchers}/repeat.d.ts +1 -1
- package/lib/matchers/repeat.js +16 -0
- package/lib/matchers/repeat.js.map +1 -0
- package/lib/{es2015/matchers → matchers}/tap.d.ts +1 -1
- package/lib/matchers/tap.js +8 -0
- package/lib/matchers/tap.js.map +1 -0
- package/lib/{es2018/matchers → matchers}/text-content-equals.d.ts +1 -1
- package/lib/matchers/text-content-equals.js +16 -0
- package/lib/matchers/text-content-equals.js.map +1 -0
- package/lib/{es2018/matchers → matchers}/text-content-includes.d.ts +1 -1
- package/lib/matchers/text-content-includes.js +16 -0
- package/lib/matchers/text-content-includes.js.map +1 -0
- package/lib/{es2015/matchers → matchers}/text-content-matches.d.ts +1 -1
- package/lib/matchers/text-content-matches.js +12 -0
- package/lib/matchers/text-content-matches.js.map +1 -0
- package/lib/{es2015/matchers → matchers}/text-node.d.ts +1 -1
- package/lib/matchers/text-node.js +30 -0
- package/lib/matchers/text-node.js.map +1 -0
- package/lib/{es2015/matchers → matchers}/xpath.d.ts +1 -1
- package/lib/{es2015/matchers → matchers}/xpath.js +7 -11
- package/lib/matchers/xpath.js.map +1 -0
- package/lib/types.d.ts +18 -0
- package/lib/types.js +2 -0
- package/lib/types.js.map +1 -0
- package/lib/{es2015/utils → utils}/concat.js +1 -5
- package/lib/utils/concat.js.map +1 -0
- package/lib/{es2015/utils → utils}/match-multiple.d.ts +4 -1
- package/lib/utils/match-multiple.js +20 -0
- package/lib/utils/match-multiple.js.map +1 -0
- package/lib/{es2015/utils → utils}/match-one-by-one.d.ts +1 -1
- package/lib/{es2015/utils → utils}/match-one-by-one.js +9 -9
- package/lib/utils/match-one-by-one.js.map +1 -0
- package/lib/utils/merge-in-place.d.ts +1 -0
- package/lib/{es2015/utils → utils}/merge-in-place.js +5 -9
- package/lib/utils/merge-in-place.js.map +1 -0
- package/lib/utils/next-element-sibling.js +7 -0
- package/lib/utils/next-element-sibling.js.map +1 -0
- package/lib/utils/next-sibling.js +7 -0
- package/lib/utils/next-sibling.js.map +1 -0
- package/package.json +34 -49
- package/src/index.ts +18 -0
- package/src/match-element.ts +23 -0
- package/src/match.ts +23 -0
- package/src/matchers/any-of.ts +13 -0
- package/src/matchers/child-nodes.ts +33 -0
- package/src/matchers/children.ts +35 -0
- package/src/matchers/css.ts +24 -0
- package/src/matchers/element.ts +49 -0
- package/src/matchers/multiple.ts +59 -0
- package/src/matchers/node.ts +50 -0
- package/src/matchers/optional.ts +6 -0
- package/src/matchers/repeat.ts +25 -0
- package/src/matchers/tap.ts +12 -0
- package/src/matchers/text-content-equals.ts +28 -0
- package/src/matchers/text-content-includes.ts +28 -0
- package/src/matchers/text-content-matches.ts +20 -0
- package/src/matchers/text-node.ts +52 -0
- package/src/matchers/xpath.ts +54 -0
- package/src/types.ts +39 -0
- package/src/utils/concat.ts +9 -0
- package/src/utils/match-multiple.ts +27 -0
- package/src/utils/match-one-by-one.ts +47 -0
- package/src/utils/merge-in-place.ts +26 -0
- package/src/utils/next-element-sibling.ts +10 -0
- package/src/utils/next-sibling.ts +7 -0
- package/dist/es2015/index.min.mjs +0 -2
- package/dist/es2015/index.min.mjs.map +0 -1
- package/dist/es2015/index.mjs +0 -4445
- package/dist/es2015/index.mjs.map +0 -1
- package/dist/es2015/index.umd.js +0 -4471
- package/dist/es2015/index.umd.js.map +0 -1
- package/dist/es2015/index.umd.min.js +0 -2
- package/dist/es2015/index.umd.min.js.map +0 -1
- package/dist/es2018/index.min.mjs +0 -2
- package/dist/es2018/index.min.mjs.map +0 -1
- package/dist/es2018/index.mjs +0 -4453
- package/dist/es2018/index.mjs.map +0 -1
- package/dist/es2018/index.umd.js +0 -4479
- package/dist/es2018/index.umd.js.map +0 -1
- package/dist/es2018/index.umd.min.js +0 -2
- package/dist/es2018/index.umd.min.js.map +0 -1
- package/lib/es2015/index.d.ts +0 -17
- package/lib/es2015/index.js +0 -38
- package/lib/es2015/index.js.map +0 -1
- package/lib/es2015/match-element.js +0 -22
- package/lib/es2015/match-element.js.map +0 -1
- package/lib/es2015/match.js +0 -22
- package/lib/es2015/match.js.map +0 -1
- package/lib/es2015/matchers/any-of.js +0 -10
- package/lib/es2015/matchers/any-of.js.map +0 -1
- package/lib/es2015/matchers/child-nodes.js +0 -23
- package/lib/es2015/matchers/child-nodes.js.map +0 -1
- package/lib/es2015/matchers/children.js +0 -23
- package/lib/es2015/matchers/children.js.map +0 -1
- package/lib/es2015/matchers/css.js +0 -18
- package/lib/es2015/matchers/css.js.map +0 -1
- package/lib/es2015/matchers/element.js +0 -34
- package/lib/es2015/matchers/element.js.map +0 -1
- package/lib/es2015/matchers/multiple.d.ts +0 -10
- package/lib/es2015/matchers/multiple.js +0 -42
- package/lib/es2015/matchers/multiple.js.map +0 -1
- package/lib/es2015/matchers/node.js +0 -31
- package/lib/es2015/matchers/node.js.map +0 -1
- package/lib/es2015/matchers/optional.js +0 -9
- package/lib/es2015/matchers/optional.js.map +0 -1
- package/lib/es2015/matchers/repeat.js +0 -20
- package/lib/es2015/matchers/repeat.js.map +0 -1
- package/lib/es2015/matchers/tap.js +0 -12
- package/lib/es2015/matchers/tap.js.map +0 -1
- package/lib/es2015/matchers/text-content-equals.d.ts +0 -7
- package/lib/es2015/matchers/text-content-equals.js +0 -20
- package/lib/es2015/matchers/text-content-equals.js.map +0 -1
- package/lib/es2015/matchers/text-content-includes.d.ts +0 -7
- package/lib/es2015/matchers/text-content-includes.js +0 -20
- package/lib/es2015/matchers/text-content-includes.js.map +0 -1
- package/lib/es2015/matchers/text-content-matches.js +0 -16
- package/lib/es2015/matchers/text-content-matches.js.map +0 -1
- package/lib/es2015/matchers/text-node.js +0 -34
- package/lib/es2015/matchers/text-node.js.map +0 -1
- package/lib/es2015/matchers/xpath.js.map +0 -1
- package/lib/es2015/types.d.ts +0 -18
- package/lib/es2015/types.js +0 -3
- package/lib/es2015/types.js.map +0 -1
- package/lib/es2015/utils/concat.js.map +0 -1
- package/lib/es2015/utils/match-multiple.js +0 -21
- package/lib/es2015/utils/match-multiple.js.map +0 -1
- package/lib/es2015/utils/match-one-by-one.js.map +0 -1
- package/lib/es2015/utils/merge-in-place.d.ts +0 -5
- package/lib/es2015/utils/merge-in-place.js.map +0 -1
- package/lib/es2015/utils/next-element-sibling.js +0 -11
- package/lib/es2015/utils/next-element-sibling.js.map +0 -1
- package/lib/es2015/utils/next-sibling.js +0 -11
- package/lib/es2015/utils/next-sibling.js.map +0 -1
- package/lib/es2018/index.d.ts +0 -17
- package/lib/es2018/index.js +0 -38
- package/lib/es2018/index.js.map +0 -1
- package/lib/es2018/match-element.d.ts +0 -4
- package/lib/es2018/match-element.js +0 -22
- package/lib/es2018/match-element.js.map +0 -1
- package/lib/es2018/match.d.ts +0 -4
- package/lib/es2018/match.js +0 -22
- package/lib/es2018/match.js.map +0 -1
- package/lib/es2018/matchers/any-of.d.ts +0 -6
- package/lib/es2018/matchers/any-of.js +0 -10
- package/lib/es2018/matchers/any-of.js.map +0 -1
- package/lib/es2018/matchers/child-nodes.d.ts +0 -2
- package/lib/es2018/matchers/child-nodes.js +0 -27
- package/lib/es2018/matchers/child-nodes.js.map +0 -1
- package/lib/es2018/matchers/children.d.ts +0 -2
- package/lib/es2018/matchers/children.js +0 -27
- package/lib/es2018/matchers/children.js.map +0 -1
- package/lib/es2018/matchers/css.d.ts +0 -3
- package/lib/es2018/matchers/css.js +0 -18
- package/lib/es2018/matchers/css.js.map +0 -1
- package/lib/es2018/matchers/element.d.ts +0 -4
- package/lib/es2018/matchers/element.js +0 -34
- package/lib/es2018/matchers/element.js.map +0 -1
- package/lib/es2018/matchers/multiple.js +0 -42
- package/lib/es2018/matchers/multiple.js.map +0 -1
- package/lib/es2018/matchers/node.d.ts +0 -4
- package/lib/es2018/matchers/node.js +0 -31
- package/lib/es2018/matchers/node.js.map +0 -1
- package/lib/es2018/matchers/optional.d.ts +0 -2
- package/lib/es2018/matchers/optional.js +0 -9
- package/lib/es2018/matchers/optional.js.map +0 -1
- package/lib/es2018/matchers/repeat.d.ts +0 -2
- package/lib/es2018/matchers/repeat.js +0 -20
- package/lib/es2018/matchers/repeat.js.map +0 -1
- package/lib/es2018/matchers/tap.d.ts +0 -2
- package/lib/es2018/matchers/tap.js +0 -12
- package/lib/es2018/matchers/tap.js.map +0 -1
- package/lib/es2018/matchers/text-content-equals.js +0 -20
- package/lib/es2018/matchers/text-content-equals.js.map +0 -1
- package/lib/es2018/matchers/text-content-includes.js +0 -20
- package/lib/es2018/matchers/text-content-includes.js.map +0 -1
- package/lib/es2018/matchers/text-content-matches.d.ts +0 -6
- package/lib/es2018/matchers/text-content-matches.js +0 -16
- package/lib/es2018/matchers/text-content-matches.js.map +0 -1
- package/lib/es2018/matchers/text-node.d.ts +0 -4
- package/lib/es2018/matchers/text-node.js +0 -34
- package/lib/es2018/matchers/text-node.js.map +0 -1
- package/lib/es2018/matchers/xpath.d.ts +0 -3
- package/lib/es2018/matchers/xpath.js +0 -34
- package/lib/es2018/matchers/xpath.js.map +0 -1
- package/lib/es2018/types.d.ts +0 -18
- package/lib/es2018/types.js +0 -3
- package/lib/es2018/types.js.map +0 -1
- package/lib/es2018/utils/concat.d.ts +0 -1
- package/lib/es2018/utils/concat.js +0 -14
- package/lib/es2018/utils/concat.js.map +0 -1
- package/lib/es2018/utils/match-multiple.d.ts +0 -2
- package/lib/es2018/utils/match-multiple.js +0 -21
- package/lib/es2018/utils/match-multiple.js.map +0 -1
- package/lib/es2018/utils/match-one-by-one.d.ts +0 -2
- package/lib/es2018/utils/match-one-by-one.js +0 -38
- package/lib/es2018/utils/match-one-by-one.js.map +0 -1
- package/lib/es2018/utils/merge-in-place.d.ts +0 -5
- package/lib/es2018/utils/merge-in-place.js +0 -31
- package/lib/es2018/utils/merge-in-place.js.map +0 -1
- package/lib/es2018/utils/next-element-sibling.d.ts +0 -1
- package/lib/es2018/utils/next-element-sibling.js +0 -11
- package/lib/es2018/utils/next-element-sibling.js.map +0 -1
- package/lib/es2018/utils/next-sibling.d.ts +0 -1
- package/lib/es2018/utils/next-sibling.js +0 -11
- package/lib/es2018/utils/next-sibling.js.map +0 -1
- /package/lib/{es2015/utils → utils}/concat.d.ts +0 -0
- /package/lib/{es2015/utils → utils}/next-element-sibling.d.ts +0 -0
- /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
|
+
}
|