index.js 8.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291
  1. /**
  2. * @typedef {import('unist').Node} Node
  3. * @typedef {import('unist').Parent} Parent
  4. */
  5. /**
  6. * @template Fn
  7. * @template Fallback
  8. * @typedef {Fn extends (value: any) => value is infer Thing ? Thing : Fallback} Predicate
  9. */
  10. /**
  11. * @callback Check
  12. * Check that an arbitrary value is a node.
  13. * @param {unknown} this
  14. * The given context.
  15. * @param {unknown} [node]
  16. * Anything (typically a node).
  17. * @param {number | null | undefined} [index]
  18. * The node’s position in its parent.
  19. * @param {Parent | null | undefined} [parent]
  20. * The node’s parent.
  21. * @returns {boolean}
  22. * Whether this is a node and passes a test.
  23. *
  24. * @typedef {Record<string, unknown> | Node} Props
  25. * Object to check for equivalence.
  26. *
  27. * Note: `Node` is included as it is common but is not indexable.
  28. *
  29. * @typedef {Array<Props | TestFunction | string> | Props | TestFunction | string | null | undefined} Test
  30. * Check for an arbitrary node.
  31. *
  32. * @callback TestFunction
  33. * Check if a node passes a test.
  34. * @param {unknown} this
  35. * The given context.
  36. * @param {Node} node
  37. * A node.
  38. * @param {number | undefined} [index]
  39. * The node’s position in its parent.
  40. * @param {Parent | undefined} [parent]
  41. * The node’s parent.
  42. * @returns {boolean | undefined | void}
  43. * Whether this node passes the test.
  44. *
  45. * Note: `void` is included until TS sees no return as `undefined`.
  46. */
  47. /**
  48. * Check if `node` is a `Node` and whether it passes the given test.
  49. *
  50. * @param {unknown} node
  51. * Thing to check, typically `Node`.
  52. * @param {Test} test
  53. * A check for a specific node.
  54. * @param {number | null | undefined} index
  55. * The node’s position in its parent.
  56. * @param {Parent | null | undefined} parent
  57. * The node’s parent.
  58. * @param {unknown} context
  59. * Context object (`this`) to pass to `test` functions.
  60. * @returns {boolean}
  61. * Whether `node` is a node and passes a test.
  62. */
  63. export const is =
  64. // Note: overloads in JSDoc can’t yet use different `@template`s.
  65. /**
  66. * @type {(
  67. * (<Condition extends string>(node: unknown, test: Condition, index?: number | null | undefined, parent?: Parent | null | undefined, context?: unknown) => node is Node & {type: Condition}) &
  68. * (<Condition extends Props>(node: unknown, test: Condition, index?: number | null | undefined, parent?: Parent | null | undefined, context?: unknown) => node is Node & Condition) &
  69. * (<Condition extends TestFunction>(node: unknown, test: Condition, index?: number | null | undefined, parent?: Parent | null | undefined, context?: unknown) => node is Node & Predicate<Condition, Node>) &
  70. * ((node?: null | undefined) => false) &
  71. * ((node: unknown, test?: null | undefined, index?: number | null | undefined, parent?: Parent | null | undefined, context?: unknown) => node is Node) &
  72. * ((node: unknown, test?: Test, index?: number | null | undefined, parent?: Parent | null | undefined, context?: unknown) => boolean)
  73. * )}
  74. */
  75. (
  76. /**
  77. * @param {unknown} [node]
  78. * @param {Test} [test]
  79. * @param {number | null | undefined} [index]
  80. * @param {Parent | null | undefined} [parent]
  81. * @param {unknown} [context]
  82. * @returns {boolean}
  83. */
  84. // eslint-disable-next-line max-params
  85. function (node, test, index, parent, context) {
  86. const check = convert(test)
  87. if (
  88. index !== undefined &&
  89. index !== null &&
  90. (typeof index !== 'number' ||
  91. index < 0 ||
  92. index === Number.POSITIVE_INFINITY)
  93. ) {
  94. throw new Error('Expected positive finite index')
  95. }
  96. if (
  97. parent !== undefined &&
  98. parent !== null &&
  99. (!is(parent) || !parent.children)
  100. ) {
  101. throw new Error('Expected parent node')
  102. }
  103. if (
  104. (parent === undefined || parent === null) !==
  105. (index === undefined || index === null)
  106. ) {
  107. throw new Error('Expected both parent and index')
  108. }
  109. return looksLikeANode(node)
  110. ? check.call(context, node, index, parent)
  111. : false
  112. }
  113. )
  114. /**
  115. * Generate an assertion from a test.
  116. *
  117. * Useful if you’re going to test many nodes, for example when creating a
  118. * utility where something else passes a compatible test.
  119. *
  120. * The created function is a bit faster because it expects valid input only:
  121. * a `node`, `index`, and `parent`.
  122. *
  123. * @param {Test} test
  124. * * when nullish, checks if `node` is a `Node`.
  125. * * when `string`, works like passing `(node) => node.type === test`.
  126. * * when `function` checks if function passed the node is true.
  127. * * when `object`, checks that all keys in test are in node, and that they have (strictly) equal values.
  128. * * when `array`, checks if any one of the subtests pass.
  129. * @returns {Check}
  130. * An assertion.
  131. */
  132. export const convert =
  133. // Note: overloads in JSDoc can’t yet use different `@template`s.
  134. /**
  135. * @type {(
  136. * (<Condition extends string>(test: Condition) => (node: unknown, index?: number | null | undefined, parent?: Parent | null | undefined, context?: unknown) => node is Node & {type: Condition}) &
  137. * (<Condition extends Props>(test: Condition) => (node: unknown, index?: number | null | undefined, parent?: Parent | null | undefined, context?: unknown) => node is Node & Condition) &
  138. * (<Condition extends TestFunction>(test: Condition) => (node: unknown, index?: number | null | undefined, parent?: Parent | null | undefined, context?: unknown) => node is Node & Predicate<Condition, Node>) &
  139. * ((test?: null | undefined) => (node?: unknown, index?: number | null | undefined, parent?: Parent | null | undefined, context?: unknown) => node is Node) &
  140. * ((test?: Test) => Check)
  141. * )}
  142. */
  143. (
  144. /**
  145. * @param {Test} [test]
  146. * @returns {Check}
  147. */
  148. function (test) {
  149. if (test === null || test === undefined) {
  150. return ok
  151. }
  152. if (typeof test === 'function') {
  153. return castFactory(test)
  154. }
  155. if (typeof test === 'object') {
  156. return Array.isArray(test) ? anyFactory(test) : propsFactory(test)
  157. }
  158. if (typeof test === 'string') {
  159. return typeFactory(test)
  160. }
  161. throw new Error('Expected function, string, or object as test')
  162. }
  163. )
  164. /**
  165. * @param {Array<Props | TestFunction | string>} tests
  166. * @returns {Check}
  167. */
  168. function anyFactory(tests) {
  169. /** @type {Array<Check>} */
  170. const checks = []
  171. let index = -1
  172. while (++index < tests.length) {
  173. checks[index] = convert(tests[index])
  174. }
  175. return castFactory(any)
  176. /**
  177. * @this {unknown}
  178. * @type {TestFunction}
  179. */
  180. function any(...parameters) {
  181. let index = -1
  182. while (++index < checks.length) {
  183. if (checks[index].apply(this, parameters)) return true
  184. }
  185. return false
  186. }
  187. }
  188. /**
  189. * Turn an object into a test for a node with a certain fields.
  190. *
  191. * @param {Props} check
  192. * @returns {Check}
  193. */
  194. function propsFactory(check) {
  195. const checkAsRecord = /** @type {Record<string, unknown>} */ (check)
  196. return castFactory(all)
  197. /**
  198. * @param {Node} node
  199. * @returns {boolean}
  200. */
  201. function all(node) {
  202. const nodeAsRecord = /** @type {Record<string, unknown>} */ (
  203. /** @type {unknown} */ (node)
  204. )
  205. /** @type {string} */
  206. let key
  207. for (key in check) {
  208. if (nodeAsRecord[key] !== checkAsRecord[key]) return false
  209. }
  210. return true
  211. }
  212. }
  213. /**
  214. * Turn a string into a test for a node with a certain type.
  215. *
  216. * @param {string} check
  217. * @returns {Check}
  218. */
  219. function typeFactory(check) {
  220. return castFactory(type)
  221. /**
  222. * @param {Node} node
  223. */
  224. function type(node) {
  225. return node && node.type === check
  226. }
  227. }
  228. /**
  229. * Turn a custom test into a test for a node that passes that test.
  230. *
  231. * @param {TestFunction} testFunction
  232. * @returns {Check}
  233. */
  234. function castFactory(testFunction) {
  235. return check
  236. /**
  237. * @this {unknown}
  238. * @type {Check}
  239. */
  240. function check(value, index, parent) {
  241. return Boolean(
  242. looksLikeANode(value) &&
  243. testFunction.call(
  244. this,
  245. value,
  246. typeof index === 'number' ? index : undefined,
  247. parent || undefined
  248. )
  249. )
  250. }
  251. }
  252. function ok() {
  253. return true
  254. }
  255. /**
  256. * @param {unknown} value
  257. * @returns {value is Node}
  258. */
  259. function looksLikeANode(value) {
  260. return value !== null && typeof value === 'object' && 'type' in value
  261. }