Skip to content

Commit d9a7cc4

Browse files
committed
Add convert, support for functions as tests
Related-to syntax-tree/hast-util-find-and-replace#1. Related-to syntax-tree/hast-util-find-and-replace#2.
1 parent 0d91b0f commit d9a7cc4

File tree

5 files changed

+260
-44
lines changed

5 files changed

+260
-44
lines changed

convert.js

+82
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
'use strict'
2+
3+
module.exports = convert
4+
5+
function convert(test) {
6+
if (typeof test === 'string') {
7+
return tagNameFactory(test)
8+
}
9+
10+
if (test === null || test === undefined) {
11+
return element
12+
}
13+
14+
if (typeof test === 'object') {
15+
return any(test)
16+
}
17+
18+
if (typeof test === 'function') {
19+
return callFactory(test)
20+
}
21+
22+
throw new Error('Expected function, string, or array as test')
23+
}
24+
25+
function convertAll(tests) {
26+
var length = tests.length
27+
var index = -1
28+
var results = []
29+
30+
while (++index < length) {
31+
results[index] = convert(tests[index])
32+
}
33+
34+
return results
35+
}
36+
37+
function any(tests) {
38+
var checks = convertAll(tests)
39+
var length = checks.length
40+
41+
return matches
42+
43+
function matches() {
44+
var index = -1
45+
46+
while (++index < length) {
47+
if (checks[index].apply(this, arguments)) {
48+
return true
49+
}
50+
}
51+
52+
return false
53+
}
54+
}
55+
56+
// Utility to convert a string a tag name check.
57+
function tagNameFactory(test) {
58+
return tagName
59+
60+
function tagName(node) {
61+
return element(node) && node.tagName === test
62+
}
63+
}
64+
65+
// Utility to convert a function check.
66+
function callFactory(test) {
67+
return call
68+
69+
function call(node) {
70+
return element(node) && Boolean(test.apply(this, arguments))
71+
}
72+
}
73+
74+
// Utility to return true if this is an element.
75+
function element(node) {
76+
return (
77+
node &&
78+
typeof node === 'object' &&
79+
node.type === 'element' &&
80+
typeof node.tagName === 'string'
81+
)
82+
}

index.js

+19-29
Original file line numberDiff line numberDiff line change
@@ -1,45 +1,35 @@
11
'use strict'
22

3+
var convert = require('./convert')
4+
35
module.exports = isElement
46

5-
// Check if if `node` is an `element` and, if `tagNames` is given, `node`
6-
// matches them `tagNames`.
7-
function isElement(node, tagNames) {
8-
var name
7+
isElement.convert = convert
98

10-
if (
11-
!(
12-
tagNames === null ||
13-
tagNames === undefined ||
14-
typeof tagNames === 'string' ||
15-
(typeof tagNames === 'object' && tagNames.length !== 0)
16-
)
17-
) {
18-
throw new Error(
19-
'Expected `string` or `Array.<string>` for `tagNames`, not `' +
20-
tagNames +
21-
'`'
22-
)
23-
}
9+
// Check if if `node` is an `element` and whether it passes the given test.
10+
function isElement(node, test, index, parent, context) {
11+
var hasParent = parent !== null && parent !== undefined
12+
var hasIndex = index !== null && index !== undefined
13+
var check = convert(test)
2414

2515
if (
26-
!node ||
27-
typeof node !== 'object' ||
28-
node.type !== 'element' ||
29-
typeof node.tagName !== 'string'
16+
hasIndex &&
17+
(typeof index !== 'number' || index < 0 || index === Infinity)
3018
) {
31-
return false
19+
throw new Error('Expected positive finite index for child node')
3220
}
3321

34-
if (tagNames === null || tagNames === undefined) {
35-
return true
22+
if (hasParent && (!parent.type || !parent.children)) {
23+
throw new Error('Expected parent node')
3624
}
3725

38-
name = node.tagName
26+
if (!node || !node.type || typeof node.type !== 'string') {
27+
return false
28+
}
3929

40-
if (typeof tagNames === 'string') {
41-
return name === tagNames
30+
if (hasParent !== hasIndex) {
31+
throw new Error('Expected both parent and index')
4232
}
4333

44-
return tagNames.indexOf(name) !== -1
34+
return check.call(context, node, index, parent)
4535
}

package.json

+4-1
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
"Titus Wormer <[email protected]> (https://wooorm.com)"
2525
],
2626
"files": [
27+
"convert.js",
2728
"index.js"
2829
],
2930
"dependencies": {},
@@ -58,7 +59,9 @@
5859
"prettier": true,
5960
"esnext": false,
6061
"rules": {
61-
"unicorn/prefer-includes": "off"
62+
"max-params": "off",
63+
"unicorn/prefer-includes": "off",
64+
"unicorn/prefer-reflect-apply": "off"
6265
},
6366
"ignores": [
6467
"hast-util-is-element.js"

readme.md

+44-13
Original file line numberDiff line numberDiff line change
@@ -33,30 +33,55 @@ is({type: 'element', tagName: 'a'}, ['a', 'area']) // => true
3333

3434
## API
3535

36-
### `isElement(node[, tagName|tagNames])`
36+
### `isElement(node[, test[, index, parent[, context]]])`
3737

3838
Check if the given value is a (certain) [*element*][element].
3939

40-
* When given a `tagName` or `tagNames`, checks that `node` is an
41-
[*element*][element] whose `tagName` field matches `tagName` or is included
42-
in `tagNames`
43-
* Otherwise checks that `node` is an [*element*][element]
40+
* `node` ([`Node`][node]) — Node to check.
41+
* `test` ([`Function`][test], `string`, or `Array.<Test>`, optional)
42+
— When `array`, checks if any one of the subtests pass.
43+
When `string`, checks that the element has that tag name.
44+
When `function`, see [`test`][test]
45+
* `index` (`number`, optional) — [Index][] of `node` in `parent`
46+
* `parent` ([`Node`][node], optional) — [Parent][] of `node`
47+
* `context` (`*`, optional) — Context object to invoke `test` with
48+
49+
###### Returns
50+
51+
`boolean` — Whether `test` passed *and* `node` is an [`Element`][element].
52+
53+
###### Throws
54+
55+
`Error` — When an incorrect `test`, `index`, or `parent` is given.
56+
A `node` that is not a node, or not an element, does not throw.
57+
58+
#### `function test(element[, index, parent])`
4459

4560
###### Parameters
4661

47-
* `node` (`*`) — Value to check, probably [`Node`][node]
48-
* `tagName` (`string`, optional) — Value that `node`s `tagName` field should
49-
match
50-
* `tagNames` (`Array.<string>`, optional) — Values that should include `node`s
51-
`tagName` field should match
62+
* `element` ([`Element`][element]) — Element to check
63+
* `index` (`number?`) — [Index][] of `node` in `parent`
64+
* `parent` ([`Node?`][node]) — [Parent][] of `node`
65+
66+
###### Context
67+
68+
`*` — The to `is` given `context`.
5269

5370
###### Returns
5471

55-
`boolean`whether `node` passes the test.
72+
`boolean?`Whether `element` matches.
5673

57-
###### Throws
74+
### `isElement.convert(test)`
75+
76+
Create a test function from `test`, that can later be called with a `node`,
77+
`index`, and `parent`.
78+
Useful if you’re going to test many nodes, for example when creating a utility
79+
where something else passes a compatible test.
80+
81+
The created function is slightly faster because it expects valid input only.
82+
Therefore, passing invalid input, yields unexpected results.
5883

59-
`Error` — When the second parameter is given but invalid.
84+
Can also be accessed with `require('hast-util-is-element/convert')`.
6085

6186
## Security
6287

@@ -156,4 +181,10 @@ abide by its terms.
156181

157182
[element]: https://github.com/syntax-tree/hast#element
158183

184+
[parent]: https://github.com/syntax-tree/unist#parent-1
185+
186+
[index]: https://github.com/syntax-tree/unist#index
187+
188+
[test]: #function-testelement-index-parent
189+
159190
[xss]: https://en.wikipedia.org/wiki/Cross-site_scripting

test.js

+111-1
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ test('isElement', function (t) {
1111
function () {
1212
isElement(null, true)
1313
},
14-
'Expected `string` or `Array.<string>` for `tagNames`, not `true`',
14+
/Expected function, string, or array as test/,
1515
'should throw when the second parameter is invalid'
1616
)
1717

@@ -99,5 +99,115 @@ test('isElement', function (t) {
9999
st.end()
100100
})
101101

102+
t.test('isElement(node, test)', function (st) {
103+
st.equal(
104+
isElement({type: 'text'}, function () {
105+
throw new Error('!')
106+
}),
107+
false,
108+
'should not call `test` if the given node is not an element'
109+
)
110+
111+
st.equal(
112+
isElement({type: 'element', tagName: 'a', children: []}, function (node) {
113+
return node.children.length === 0
114+
}),
115+
true,
116+
'should call `test` if the given node is a valid element (1)'
117+
)
118+
119+
st.equal(
120+
isElement(
121+
{type: 'element', tagName: 'a', children: [{type: 'text'}]},
122+
function (node) {
123+
return node.children.length === 0
124+
}
125+
),
126+
false,
127+
'should call `test` if the given node is a valid element (2)'
128+
)
129+
130+
var ctx = {}
131+
var root = {
132+
type: 'root',
133+
children: [{type: 'element', tagName: 'a', children: []}]
134+
}
135+
136+
st.equal(
137+
isElement(
138+
root.children[0],
139+
function (node, index, parent) {
140+
st.equal(node, root.children[0], 'should pass `node` to test')
141+
st.equal(index, 0, 'should pass `index` to test')
142+
st.equal(parent, root, 'should pass `parent` to test')
143+
st.equal(this, ctx, 'should pass `context` to test')
144+
},
145+
0,
146+
root,
147+
ctx
148+
),
149+
false,
150+
'should call `test` if the given node is a valid element (2)'
151+
)
152+
153+
st.throws(
154+
function () {
155+
isElement(root.children[0], function () {}, 0)
156+
},
157+
/Expected both parent and index/,
158+
'should throw if `index` is passed but not `parent`'
159+
)
160+
161+
st.throws(
162+
function () {
163+
isElement(root.children[0], function () {}, undefined, root)
164+
},
165+
/Expected both parent and index/,
166+
'should throw if `parent` is passed but not `index`'
167+
)
168+
169+
st.throws(
170+
function () {
171+
isElement(root.children[0], function () {}, false)
172+
},
173+
/Expected positive finite index for child node/,
174+
'should throw if `index` is not a number'
175+
)
176+
177+
st.throws(
178+
function () {
179+
isElement(root.children[0], function () {}, -1)
180+
},
181+
/Expected positive finite index for child node/,
182+
'should throw if `index` is negative'
183+
)
184+
185+
st.throws(
186+
function () {
187+
isElement(root.children[0], function () {}, Infinity)
188+
},
189+
/Expected positive finite index for child node/,
190+
'should throw if `index` is infinity'
191+
)
192+
193+
st.throws(
194+
function () {
195+
isElement(root.children[0], function () {}, 0, true)
196+
},
197+
/Expected parent node/,
198+
'should throw if `parent` is not a node'
199+
)
200+
201+
st.throws(
202+
function () {
203+
isElement(root.children[0], function () {}, 0, {type: 'root'})
204+
},
205+
/Expected parent node/,
206+
'should throw if `parent` is not a parent'
207+
)
208+
209+
st.end()
210+
})
211+
102212
t.end()
103213
})

0 commit comments

Comments
 (0)