index.js 8.8 KB

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