Severian's picture
Upload 7464 files
c211499
raw
history blame
7.44 kB
/**
* @typedef {import('unist').Parent} Parent
* @typedef {import('hast').Element} Element
*/
/**
* @typedef {null | undefined | string | TestFunctionAnything | Array<string | TestFunctionAnything>} Test
* Check for an arbitrary element, unaware of TypeScript inferral.
*
* @callback TestFunctionAnything
* Check if an element passes a test, unaware of TypeScript inferral.
* @param {Element} element
* An element.
* @param {number | null | undefined} [index]
* The element’s position in its parent.
* @param {Parent | null | undefined} [parent]
* The element’s parent.
* @returns {boolean | void}
* Whether this element passes the test.
*/
/**
* @template {Element} T
* Element type.
* @typedef {T['tagName'] | TestFunctionPredicate<T> | Array<T['tagName'] | TestFunctionPredicate<T>>} PredicateTest
* Check for an element that can be inferred by TypeScript.
*/
/**
* Check if an element passes a certain node test.
*
* @template {Element} T
* Element type.
* @callback TestFunctionPredicate
* Complex test function for an element that can be inferred by TypeScript.
* @param {Element} element
* An element.
* @param {number | null | undefined} [index]
* The element’s position in its parent.
* @param {Parent | null | undefined} [parent]
* The element’s parent.
* @returns {element is T}
* Whether this element passes the test.
*/
/**
* @callback AssertAnything
* Check that an arbitrary value is an element, unaware of TypeScript inferral.
* @param {unknown} [node]
* Anything (typically a node).
* @param {number | null | undefined} [index]
* The node’s position in its parent.
* @param {Parent | null | undefined} [parent]
* The node’s parent.
* @returns {boolean}
* Whether this is an element and passes a test.
*/
/**
* Check if a node is an element and passes a certain node test
*
* @template {Element} T
* Element type.
* @callback AssertPredicate
* Check that an arbitrary value is a specific element, aware of TypeScript.
* @param {unknown} [node]
* Anything (typically a node).
* @param {number | null | undefined} [index]
* The node’s position in its parent.
* @param {Parent | null | undefined} [parent]
* The node’s parent.
* @returns {node is T}
* Whether this is an element and passes a test.
*/
/**
* Check if `node` is an `Element` and whether it passes the given test.
*
* @param node
* Thing to check, typically `Node`.
* @param test
* A check for a specific element.
* @param index
* The node’s position in its parent.
* @param parent
* The node’s parent.
* @returns
* Whether `node` is an element and passes a test.
*/
export const isElement =
/**
* @type {(
* (() => false) &
* (<T extends Element = Element>(node: unknown, test?: PredicateTest<T>, index?: number, parent?: Parent, context?: unknown) => node is T) &
* ((node: unknown, test: Test, index?: number, parent?: Parent, context?: unknown) => boolean)
* )}
*/
(
/**
* @param {unknown} [node]
* @param {Test | undefined} [test]
* @param {number | null | undefined} [index]
* @param {Parent | null | undefined} [parent]
* @param {unknown} [context]
* @returns {boolean}
*/
// eslint-disable-next-line max-params
function (node, test, index, parent, context) {
const check = convertElement(test)
if (
index !== undefined &&
index !== null &&
(typeof index !== 'number' ||
index < 0 ||
index === Number.POSITIVE_INFINITY)
) {
throw new Error('Expected positive finite index for child node')
}
if (
parent !== undefined &&
parent !== null &&
(!parent.type || !parent.children)
) {
throw new Error('Expected parent node')
}
// @ts-expect-error Looks like a node.
if (!node || !node.type || typeof node.type !== 'string') {
return false
}
if (
(parent === undefined || parent === null) !==
(index === undefined || index === null)
) {
throw new Error('Expected both parent and index')
}
return check.call(context, node, index, parent)
}
)
/**
* Generate an assertion from a test.
*
* Useful if you’re going to test many nodes, for example when creating a
* utility where something else passes a compatible test.
*
* The created function is a bit faster because it expects valid input only:
* a `node`, `index`, and `parent`.
*
* @param test
* * When nullish, checks if `node` is an `Element`.
* * When `string`, works like passing `(element) => element.tagName === test`.
* * When `function` checks if function passed the element is true.
* * When `array`, checks any one of the subtests pass.
* @returns
* An assertion.
*/
export const convertElement =
/**
* @type {(
* (<T extends Element>(test: T['tagName'] | TestFunctionPredicate<T>) => AssertPredicate<T>) &
* ((test?: Test) => AssertAnything)
* )}
*/
(
/**
* @param {Test | null | undefined} [test]
* @returns {AssertAnything}
*/
function (test) {
if (test === undefined || test === null) {
return element
}
if (typeof test === 'string') {
return tagNameFactory(test)
}
if (typeof test === 'object') {
return anyFactory(test)
}
if (typeof test === 'function') {
return castFactory(test)
}
throw new Error('Expected function, string, or array as test')
}
)
/**
* Handle multiple tests.
*
* @param {Array<string | TestFunctionAnything>} tests
* @returns {AssertAnything}
*/
function anyFactory(tests) {
/** @type {Array<AssertAnything>} */
const checks = []
let index = -1
while (++index < tests.length) {
checks[index] = convertElement(tests[index])
}
return castFactory(any)
/**
* @this {unknown}
* @param {Array<unknown>} parameters
* @returns {boolean}
*/
function any(...parameters) {
let index = -1
while (++index < checks.length) {
if (checks[index].call(this, ...parameters)) {
return true
}
}
return false
}
}
/**
* Turn a string into a test for an element with a certain tag name.
*
* @param {string} check
* @returns {AssertAnything}
*/
function tagNameFactory(check) {
return tagName
/**
* @param {unknown} node
* @returns {boolean}
*/
function tagName(node) {
return element(node) && node.tagName === check
}
}
/**
* Turn a custom test into a test for an element that passes that test.
*
* @param {TestFunctionAnything} check
* @returns {AssertAnything}
*/
function castFactory(check) {
return assertion
/**
* @this {unknown}
* @param {unknown} node
* @param {Array<unknown>} parameters
* @returns {boolean}
*/
function assertion(node, ...parameters) {
// @ts-expect-error: fine.
return element(node) && Boolean(check.call(this, node, ...parameters))
}
}
/**
* Make sure something is an element.
*
* @param {unknown} node
* @returns {node is Element}
*/
function element(node) {
return Boolean(
node &&
typeof node === 'object' &&
// @ts-expect-error Looks like a node.
node.type === 'element' &&
// @ts-expect-error Looks like an element.
typeof node.tagName === 'string'
)
}