Skip to content

Commit

Permalink
Add JSDoc based types
Browse files Browse the repository at this point in the history
Closes GH-18.

Reviewed-by: Christian Murphy <christian.murphy.42@gmail.com>
  • Loading branch information
wooorm authored Apr 19, 2021
1 parent 131ee3d commit f78c229
Show file tree
Hide file tree
Showing 8 changed files with 461 additions and 170 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
.DS_Store
*.d.ts
*.log
coverage/
node_modules/
Expand Down
69 changes: 0 additions & 69 deletions index.d.ts

This file was deleted.

287 changes: 212 additions & 75 deletions index.js
Original file line number Diff line number Diff line change
@@ -1,105 +1,242 @@
// Assert if `test` passes for `node`.
// When a `parent` node is known the `index` of node should also be given.
// eslint-disable-next-line max-params
export function is(node, test, index, parent, context) {
var check = convert(test)

if (
index !== undefined &&
index !== null &&
(typeof index !== 'number' ||
index < 0 ||
index === Number.POSITIVE_INFINITY)
) {
throw new Error('Expected positive finite index')
}

if (
parent !== undefined &&
parent !== null &&
(!is(parent) || !parent.children)
) {
throw new Error('Expected parent node')
}

if (
(parent === undefined || parent === null) !==
(index === undefined || index === null)
) {
throw new Error('Expected both parent and index')
}

return node && node.type && typeof node.type === 'string'
? Boolean(check.call(context, node, index, parent))
: false
}

export function convert(test) {
if (test === undefined || test === null) {
return ok
}
/**
* @typedef {import('unist').Node} Node
* @typedef {import('unist').Parent} Parent
*
* @typedef {string} Type
* @typedef {Object<string, unknown>} Props
*/

/**
* Check if a node passes a test
*
* @callback TestFunctionAnything
* @param {Node} node
* @param {number} [index]
* @param {Parent} [parent]
* @returns {boolean|void}
*/

/**
* Check if a node passes a certain node test
*
* @template {Node} X
* @callback TestFunctionPredicate
* @param {Node} node
* @param {number} [index]
* @param {Parent} [parent]
* @returns {node is X}
*/

/**
* @callback AssertAnything
* @param {unknown} [node]
* @param {number} [index]
* @param {Parent} [parent]
* @returns {boolean}
*/

/**
* Check if a node passes a certain node test
*
* @template {Node} Y
* @callback AssertPredicate
* @param {unknown} [node]
* @param {number} [index]
* @param {Parent} [parent]
* @returns {node is Y}
*/

export var is =
/**
* Check if a node passes a test.
* When a `parent` node is known the `index` of node should also be given.
*
* @type {(
* (<T extends Node>(node: unknown, test: T['type']|Partial<T>|TestFunctionPredicate<T>|Array.<T['type']|Partial<T>|TestFunctionPredicate<T>>, index?: number, parent?: Parent, context?: unknown) => node is T) &
* ((node?: unknown, test?: null|undefined|Type|Props|TestFunctionAnything|Array.<Type|Props|TestFunctionAnything>, index?: number, parent?: Parent, context?: unknown) => boolean)
* )}
*/
(
/**
* Check if a node passes a test.
* When a `parent` node is known the `index` of node should also be given.
*
* @param {unknown} [node] Node to check
* @param {null|undefined|Type|Props|TestFunctionAnything|Array.<Type|Props|TestFunctionAnything>} [test]
* When nullish, checks if `node` is a `Node`.
* When `string`, works like passing `function (node) {return node.type === test}`.
* When `function` checks if function passed the node is true.
* When `object`, checks that all keys in test are in node, and that they have (strictly) equal values.
* When `array`, checks any one of the subtests pass.
* @param {number} [index] Position of `node` in `parent`
* @param {Parent} [parent] Parent of `node`
* @param {unknown} [context] Context object to invoke `test` with
* @returns {boolean} Whether test passed and `node` is a `Node` (object with `type` set to non-empty `string`).
*/
// eslint-disable-next-line max-params
function is(node, test, index, parent, context) {
var check = convert(test)

if (
index !== undefined &&
index !== null &&
(typeof index !== 'number' ||
index < 0 ||
index === Number.POSITIVE_INFINITY)
) {
throw new Error('Expected positive finite index')
}

if (typeof test === 'string') {
return typeFactory(test)
}
if (
parent !== undefined &&
parent !== null &&
(!is(parent) || !parent.children)
) {
throw new Error('Expected parent node')
}

if (typeof test === 'object') {
return 'length' in test ? anyFactory(test) : allFactory(test)
}
if (
(parent === undefined || parent === null) !==
(index === undefined || index === null)
) {
throw new Error('Expected both parent and index')
}

if (typeof test === 'function') {
return test
}
// @ts-ignore Looks like a node.
return node && node.type && typeof node.type === 'string'
? Boolean(check.call(context, node, index, parent))
: false
}
)

export var convert =
/**
* @type {(
* (<T extends Node>(test: T['type']|Partial<T>|TestFunctionPredicate<T>) => AssertPredicate<T>) &
* ((test?: null|undefined|Type|Props|TestFunctionAnything|Array.<Type|Props|TestFunctionAnything>) => AssertAnything)
* )}
*/
(
/**
* Generate an assertion from a check.
* @param {null|undefined|Type|Props|TestFunctionAnything|Array.<Type|Props|TestFunctionAnything>} [test]
* When nullish, checks if `node` is a `Node`.
* When `string`, works like passing `function (node) {return node.type === test}`.
* When `function` checks if function passed the node is true.
* When `object`, checks that all keys in test are in node, and that they have (strictly) equal values.
* When `array`, checks any one of the subtests pass.
* @returns {AssertAnything}
*/
function (test) {
if (test === undefined || test === null) {
return ok
}

throw new Error('Expected function, string, or object as test')
}
if (typeof test === 'string') {
return typeFactory(test)
}

// Utility to assert each property in `test` is represented in `node`, and each
// values are strictly equal.
function allFactory(test) {
return all
if (typeof test === 'object') {
// @ts-ignore looks like a list of tests / partial test object.
return 'length' in test ? anyFactory(test) : propsFactory(test)
}

function all(node) {
var key
if (typeof test === 'function') {
return castFactory(test)
}

for (key in test) {
if (node[key] !== test[key]) return false
throw new Error('Expected function, string, or object as test')
}

return true
}
}

)
/**
* @param {Array.<Type|Props|TestFunctionAnything>} tests
* @returns {AssertAnything}
*/
function anyFactory(tests) {
/** @type {Array.<AssertAnything>} */
var checks = []
var index = -1

while (++index < tests.length) {
checks[index] = convert(tests[index])
}

return any
return castFactory(any)

/**
* @this {unknown}
* @param {unknown[]} parameters
* @returns {boolean}
*/
function any(...parameters) {
var index = -1

while (++index < checks.length) {
if (checks[index].call(this, ...parameters)) {
return true
}
if (checks[index].call(this, ...parameters)) return true
}

return false
}
}

// Utility to convert a string into a function which checks a given node’s type
// for said string.
function typeFactory(test) {
return type
/**
* Utility to assert each property in `test` is represented in `node`, and each
* values are strictly equal.
*
* @param {Props} check
* @returns {AssertAnything}
*/
function propsFactory(check) {
return castFactory(all)

/**
* @param {Node} node
* @returns {boolean}
*/
function all(node) {
/** @type {string} */
var key

for (key in check) {
if (node[key] !== check[key]) return
}

return true
}
}

/**
* Utility to convert a string into a function which checks a given node’s type
* for said string.
*
* @param {Type} check
* @returns {AssertAnything}
*/
function typeFactory(check) {
return castFactory(type)

/**
* @param {Node} node
*/
function type(node) {
return Boolean(node && node.type === test)
return node && node.type === check
}
}

/**
* Utility to convert a string into a function which checks a given node’s type
* for said string.
* @param {TestFunctionAnything} check
* @returns {AssertAnything}
*/
function castFactory(check) {
return assertion

/**
* @this {unknown}
* @param {Array.<unknown>} parameters
* @returns {boolean}
*/
function assertion(...parameters) {
return Boolean(check.call(this, ...parameters))
}
}

Expand Down
Loading

0 comments on commit f78c229

Please sign in to comment.