@bquery/bquery 1.5.0 → 1.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (380) hide show
  1. package/README.md +193 -23
  2. package/dist/a11y/announce.d.ts +43 -0
  3. package/dist/a11y/announce.d.ts.map +1 -0
  4. package/dist/a11y/audit.d.ts +42 -0
  5. package/dist/a11y/audit.d.ts.map +1 -0
  6. package/dist/a11y/index.d.ts +53 -0
  7. package/dist/a11y/index.d.ts.map +1 -0
  8. package/dist/a11y/media-preferences.d.ts +77 -0
  9. package/dist/a11y/media-preferences.d.ts.map +1 -0
  10. package/dist/a11y/roving-tab-index.d.ts +38 -0
  11. package/dist/a11y/roving-tab-index.d.ts.map +1 -0
  12. package/dist/a11y/skip-link.d.ts +37 -0
  13. package/dist/a11y/skip-link.d.ts.map +1 -0
  14. package/dist/a11y/trap-focus.d.ts +49 -0
  15. package/dist/a11y/trap-focus.d.ts.map +1 -0
  16. package/dist/a11y/types.d.ts +152 -0
  17. package/dist/a11y/types.d.ts.map +1 -0
  18. package/dist/a11y-C5QOVvRn.js +421 -0
  19. package/dist/a11y-C5QOVvRn.js.map +1 -0
  20. package/dist/a11y.es.mjs +14 -0
  21. package/dist/component/component.d.ts +13 -5
  22. package/dist/component/component.d.ts.map +1 -1
  23. package/dist/component/html.d.ts +40 -3
  24. package/dist/component/html.d.ts.map +1 -1
  25. package/dist/component/index.d.ts +3 -2
  26. package/dist/component/index.d.ts.map +1 -1
  27. package/dist/component/library.d.ts.map +1 -1
  28. package/dist/component/scope.d.ts +138 -0
  29. package/dist/component/scope.d.ts.map +1 -0
  30. package/dist/component/types.d.ts +184 -17
  31. package/dist/component/types.d.ts.map +1 -1
  32. package/dist/component-CuuTijA6.js +684 -0
  33. package/dist/component-CuuTijA6.js.map +1 -0
  34. package/dist/component.es.mjs +10 -6
  35. package/dist/{config-DRmZZno3.js → config-BW35FKuA.js} +4 -4
  36. package/dist/config-BW35FKuA.js.map +1 -0
  37. package/dist/constraints-3lV9yyBw.js +100 -0
  38. package/dist/constraints-3lV9yyBw.js.map +1 -0
  39. package/dist/core/collection.d.ts +48 -0
  40. package/dist/core/collection.d.ts.map +1 -1
  41. package/dist/core/element.d.ts +92 -0
  42. package/dist/core/element.d.ts.map +1 -1
  43. package/dist/core/env.d.ts +18 -0
  44. package/dist/core/env.d.ts.map +1 -0
  45. package/dist/core/index.d.ts +1 -0
  46. package/dist/core/index.d.ts.map +1 -1
  47. package/dist/core/shared.d.ts +8 -0
  48. package/dist/core/shared.d.ts.map +1 -1
  49. package/dist/core/utils/index.d.ts +52 -41
  50. package/dist/core/utils/index.d.ts.map +1 -1
  51. package/dist/core-Cjl7GUu8.js +717 -0
  52. package/dist/core-Cjl7GUu8.js.map +1 -0
  53. package/dist/{core-DPdbItcq.js → core-DnlyjbF2.js} +1 -1
  54. package/dist/{core-DPdbItcq.js.map → core-DnlyjbF2.js.map} +1 -1
  55. package/dist/core.es.mjs +45 -44
  56. package/dist/custom-directives-7wAShnnd.js +9 -0
  57. package/dist/custom-directives-7wAShnnd.js.map +1 -0
  58. package/dist/devtools/devtools.d.ts +212 -0
  59. package/dist/devtools/devtools.d.ts.map +1 -0
  60. package/dist/devtools/index.d.ts +20 -0
  61. package/dist/devtools/index.d.ts.map +1 -0
  62. package/dist/devtools/types.d.ts +69 -0
  63. package/dist/devtools/types.d.ts.map +1 -0
  64. package/dist/devtools-D2fQLhDN.js +122 -0
  65. package/dist/devtools-D2fQLhDN.js.map +1 -0
  66. package/dist/devtools.es.mjs +19 -0
  67. package/dist/dnd/draggable.d.ts +51 -0
  68. package/dist/dnd/draggable.d.ts.map +1 -0
  69. package/dist/dnd/droppable.d.ts +38 -0
  70. package/dist/dnd/droppable.d.ts.map +1 -0
  71. package/dist/dnd/index.d.ts +47 -0
  72. package/dist/dnd/index.d.ts.map +1 -0
  73. package/dist/dnd/sortable.d.ts +43 -0
  74. package/dist/dnd/sortable.d.ts.map +1 -0
  75. package/dist/dnd/types.d.ts +250 -0
  76. package/dist/dnd/types.d.ts.map +1 -0
  77. package/dist/dnd-B8EgyzaI.js +244 -0
  78. package/dist/dnd-B8EgyzaI.js.map +1 -0
  79. package/dist/dnd.es.mjs +6 -0
  80. package/dist/env-NeVmr4Gf.js +19 -0
  81. package/dist/env-NeVmr4Gf.js.map +1 -0
  82. package/dist/forms/create-form.d.ts +49 -0
  83. package/dist/forms/create-form.d.ts.map +1 -0
  84. package/dist/forms/index.d.ts +39 -0
  85. package/dist/forms/index.d.ts.map +1 -0
  86. package/dist/forms/types.d.ts +139 -0
  87. package/dist/forms/types.d.ts.map +1 -0
  88. package/dist/forms/validators.d.ts +179 -0
  89. package/dist/forms/validators.d.ts.map +1 -0
  90. package/dist/forms-C3yovgH9.js +141 -0
  91. package/dist/forms-C3yovgH9.js.map +1 -0
  92. package/dist/forms.es.mjs +14 -0
  93. package/dist/full.d.ts +37 -9
  94. package/dist/full.d.ts.map +1 -1
  95. package/dist/full.es.mjs +186 -91
  96. package/dist/full.iife.js +47 -31
  97. package/dist/full.iife.js.map +1 -1
  98. package/dist/full.umd.js +47 -31
  99. package/dist/full.umd.js.map +1 -1
  100. package/dist/i18n/formatting.d.ts +40 -0
  101. package/dist/i18n/formatting.d.ts.map +1 -0
  102. package/dist/i18n/i18n.d.ts +48 -0
  103. package/dist/i18n/i18n.d.ts.map +1 -0
  104. package/dist/i18n/index.d.ts +57 -0
  105. package/dist/i18n/index.d.ts.map +1 -0
  106. package/dist/i18n/translate.d.ts +83 -0
  107. package/dist/i18n/translate.d.ts.map +1 -0
  108. package/dist/i18n/types.d.ts +156 -0
  109. package/dist/i18n/types.d.ts.map +1 -0
  110. package/dist/i18n-BnnhTFOS.js +89 -0
  111. package/dist/i18n-BnnhTFOS.js.map +1 -0
  112. package/dist/i18n.es.mjs +6 -0
  113. package/dist/index.d.ts +11 -0
  114. package/dist/index.d.ts.map +1 -1
  115. package/dist/index.es.mjs +233 -138
  116. package/dist/media/battery.d.ts +35 -0
  117. package/dist/media/battery.d.ts.map +1 -0
  118. package/dist/media/breakpoints.d.ts +51 -0
  119. package/dist/media/breakpoints.d.ts.map +1 -0
  120. package/dist/media/clipboard.d.ts +30 -0
  121. package/dist/media/clipboard.d.ts.map +1 -0
  122. package/dist/media/device-sensors.d.ts +54 -0
  123. package/dist/media/device-sensors.d.ts.map +1 -0
  124. package/dist/media/geolocation.d.ts +38 -0
  125. package/dist/media/geolocation.d.ts.map +1 -0
  126. package/dist/media/index.d.ts +42 -0
  127. package/dist/media/index.d.ts.map +1 -0
  128. package/dist/media/media-query.d.ts +36 -0
  129. package/dist/media/media-query.d.ts.map +1 -0
  130. package/dist/media/network.d.ts +35 -0
  131. package/dist/media/network.d.ts.map +1 -0
  132. package/dist/media/types.d.ts +173 -0
  133. package/dist/media/types.d.ts.map +1 -0
  134. package/dist/media/viewport.d.ts +32 -0
  135. package/dist/media/viewport.d.ts.map +1 -0
  136. package/dist/media-Di2Ta22s.js +340 -0
  137. package/dist/media-Di2Ta22s.js.map +1 -0
  138. package/dist/media.es.mjs +12 -0
  139. package/dist/motion/index.d.ts +7 -3
  140. package/dist/motion/index.d.ts.map +1 -1
  141. package/dist/motion/morph.d.ts +27 -0
  142. package/dist/motion/morph.d.ts.map +1 -0
  143. package/dist/motion/parallax.d.ts +30 -0
  144. package/dist/motion/parallax.d.ts.map +1 -0
  145. package/dist/motion/reduced-motion.d.ts +36 -3
  146. package/dist/motion/reduced-motion.d.ts.map +1 -1
  147. package/dist/motion/types.d.ts +58 -0
  148. package/dist/motion/types.d.ts.map +1 -1
  149. package/dist/motion/typewriter.d.ts +31 -0
  150. package/dist/motion/typewriter.d.ts.map +1 -0
  151. package/dist/motion-qPj_TYGv.js +530 -0
  152. package/dist/motion-qPj_TYGv.js.map +1 -0
  153. package/dist/motion.es.mjs +27 -23
  154. package/dist/mount-SM07RUa6.js +403 -0
  155. package/dist/mount-SM07RUa6.js.map +1 -0
  156. package/dist/{object-qGpWr6-J.js → object-BCk-1c8T.js} +5 -4
  157. package/dist/{object-qGpWr6-J.js.map → object-BCk-1c8T.js.map} +1 -1
  158. package/dist/{platform-B7JhGBc7.js → platform-CPbCprb6.js} +3 -3
  159. package/dist/platform-CPbCprb6.js.map +1 -0
  160. package/dist/platform.es.mjs +2 -2
  161. package/dist/plugin/index.d.ts +22 -0
  162. package/dist/plugin/index.d.ts.map +1 -0
  163. package/dist/plugin/registry.d.ts +108 -0
  164. package/dist/plugin/registry.d.ts.map +1 -0
  165. package/dist/plugin/types.d.ts +110 -0
  166. package/dist/plugin/types.d.ts.map +1 -0
  167. package/dist/plugin-cPoOHFLY.js +64 -0
  168. package/dist/plugin-cPoOHFLY.js.map +1 -0
  169. package/dist/plugin.es.mjs +9 -0
  170. package/dist/reactive/computed.d.ts +7 -0
  171. package/dist/reactive/computed.d.ts.map +1 -1
  172. package/dist/reactive-Cfv0RK6x.js +233 -0
  173. package/dist/reactive-Cfv0RK6x.js.map +1 -0
  174. package/dist/reactive.es.mjs +18 -17
  175. package/dist/registry-CWf368tT.js +26 -0
  176. package/dist/registry-CWf368tT.js.map +1 -0
  177. package/dist/router/bq-link.d.ts +112 -0
  178. package/dist/router/bq-link.d.ts.map +1 -0
  179. package/dist/router/constraints.d.ts +9 -0
  180. package/dist/router/constraints.d.ts.map +1 -0
  181. package/dist/router/index.d.ts +14 -6
  182. package/dist/router/index.d.ts.map +1 -1
  183. package/dist/router/match.d.ts +0 -1
  184. package/dist/router/match.d.ts.map +1 -1
  185. package/dist/router/path-pattern.d.ts +14 -0
  186. package/dist/router/path-pattern.d.ts.map +1 -0
  187. package/dist/router/query.d.ts.map +1 -1
  188. package/dist/router/router.d.ts +3 -1
  189. package/dist/router/router.d.ts.map +1 -1
  190. package/dist/router/types.d.ts +48 -4
  191. package/dist/router/types.d.ts.map +1 -1
  192. package/dist/router/use-route.d.ts +50 -0
  193. package/dist/router/use-route.d.ts.map +1 -0
  194. package/dist/router/utils.d.ts +3 -0
  195. package/dist/router/utils.d.ts.map +1 -1
  196. package/dist/router-BrthaP_z.js +473 -0
  197. package/dist/router-BrthaP_z.js.map +1 -0
  198. package/dist/router.es.mjs +13 -10
  199. package/dist/{sanitize-jyJ2ryE2.js → sanitize-B1V4JswB.js} +95 -83
  200. package/dist/sanitize-B1V4JswB.js.map +1 -0
  201. package/dist/security/index.d.ts +2 -0
  202. package/dist/security/index.d.ts.map +1 -1
  203. package/dist/security/sanitize.d.ts +4 -1
  204. package/dist/security/sanitize.d.ts.map +1 -1
  205. package/dist/security/trusted-html.d.ts +53 -0
  206. package/dist/security/trusted-html.d.ts.map +1 -0
  207. package/dist/security.es.mjs +10 -9
  208. package/dist/ssr/hydrate.d.ts +65 -0
  209. package/dist/ssr/hydrate.d.ts.map +1 -0
  210. package/dist/ssr/index.d.ts +59 -0
  211. package/dist/ssr/index.d.ts.map +1 -0
  212. package/dist/ssr/render.d.ts +62 -0
  213. package/dist/ssr/render.d.ts.map +1 -0
  214. package/dist/ssr/serialize.d.ts +118 -0
  215. package/dist/ssr/serialize.d.ts.map +1 -0
  216. package/dist/ssr/types.d.ts +70 -0
  217. package/dist/ssr/types.d.ts.map +1 -0
  218. package/dist/ssr-B2qd_WBB.js +248 -0
  219. package/dist/ssr-B2qd_WBB.js.map +1 -0
  220. package/dist/ssr.es.mjs +9 -0
  221. package/dist/store/create-store.d.ts.map +1 -1
  222. package/dist/store/define-store.d.ts +1 -1
  223. package/dist/store/define-store.d.ts.map +1 -1
  224. package/dist/store/index.d.ts +1 -1
  225. package/dist/store/index.d.ts.map +1 -1
  226. package/dist/store/mapping.d.ts +1 -1
  227. package/dist/store/mapping.d.ts.map +1 -1
  228. package/dist/store/persisted.d.ts +38 -4
  229. package/dist/store/persisted.d.ts.map +1 -1
  230. package/dist/store/types.d.ts +140 -3
  231. package/dist/store/types.d.ts.map +1 -1
  232. package/dist/store/utils.d.ts +2 -2
  233. package/dist/store/utils.d.ts.map +1 -1
  234. package/dist/store/watch.d.ts +1 -1
  235. package/dist/store/watch.d.ts.map +1 -1
  236. package/dist/store-DWpyH6p5.js +338 -0
  237. package/dist/store-DWpyH6p5.js.map +1 -0
  238. package/dist/store.es.mjs +11 -10
  239. package/dist/storybook/index.d.ts +37 -0
  240. package/dist/storybook/index.d.ts.map +1 -0
  241. package/dist/storybook.es.mjs +151 -0
  242. package/dist/storybook.es.mjs.map +1 -0
  243. package/dist/testing/index.d.ts +23 -0
  244. package/dist/testing/index.d.ts.map +1 -0
  245. package/dist/testing/testing.d.ts +156 -0
  246. package/dist/testing/testing.d.ts.map +1 -0
  247. package/dist/testing/types.d.ts +134 -0
  248. package/dist/testing/types.d.ts.map +1 -0
  249. package/dist/testing-CsqjNUyy.js +224 -0
  250. package/dist/testing-CsqjNUyy.js.map +1 -0
  251. package/dist/testing.es.mjs +9 -0
  252. package/dist/type-guards-Do9DWgNp.js +44 -0
  253. package/dist/type-guards-Do9DWgNp.js.map +1 -0
  254. package/dist/untrack-DJVQQ2WM.js +33 -0
  255. package/dist/untrack-DJVQQ2WM.js.map +1 -0
  256. package/dist/view/custom-directives.d.ts +20 -0
  257. package/dist/view/custom-directives.d.ts.map +1 -0
  258. package/dist/view/evaluate.d.ts.map +1 -1
  259. package/dist/view/process.d.ts.map +1 -1
  260. package/dist/view.es.mjs +11 -10
  261. package/package.json +52 -11
  262. package/src/a11y/announce.ts +131 -0
  263. package/src/a11y/audit.ts +314 -0
  264. package/src/a11y/index.ts +68 -0
  265. package/src/a11y/media-preferences.ts +255 -0
  266. package/src/a11y/roving-tab-index.ts +164 -0
  267. package/src/a11y/skip-link.ts +255 -0
  268. package/src/a11y/trap-focus.ts +184 -0
  269. package/src/a11y/types.ts +183 -0
  270. package/src/component/component.ts +345 -65
  271. package/src/component/html.ts +153 -53
  272. package/src/component/index.ts +12 -2
  273. package/src/component/library.ts +66 -28
  274. package/src/component/scope.ts +212 -0
  275. package/src/component/types.ts +238 -19
  276. package/src/core/collection.ts +707 -628
  277. package/src/core/element.ts +981 -774
  278. package/src/core/env.ts +60 -0
  279. package/src/core/index.ts +49 -48
  280. package/src/core/shared.ts +62 -13
  281. package/src/core/utils/index.ts +148 -83
  282. package/src/devtools/devtools.ts +410 -0
  283. package/src/devtools/index.ts +48 -0
  284. package/src/devtools/types.ts +104 -0
  285. package/src/dnd/draggable.ts +296 -0
  286. package/src/dnd/droppable.ts +228 -0
  287. package/src/dnd/index.ts +62 -0
  288. package/src/dnd/sortable.ts +307 -0
  289. package/src/dnd/types.ts +293 -0
  290. package/src/forms/create-form.ts +278 -0
  291. package/src/forms/index.ts +65 -0
  292. package/src/forms/types.ts +154 -0
  293. package/src/forms/validators.ts +265 -0
  294. package/src/full.ts +260 -3
  295. package/src/i18n/formatting.ts +67 -0
  296. package/src/i18n/i18n.ts +200 -0
  297. package/src/i18n/index.ts +67 -0
  298. package/src/i18n/translate.ts +182 -0
  299. package/src/i18n/types.ts +171 -0
  300. package/src/index.ts +108 -36
  301. package/src/media/battery.ts +116 -0
  302. package/src/media/breakpoints.ts +131 -0
  303. package/src/media/clipboard.ts +80 -0
  304. package/src/media/device-sensors.ts +158 -0
  305. package/src/media/geolocation.ts +119 -0
  306. package/src/media/index.ts +76 -0
  307. package/src/media/media-query.ts +92 -0
  308. package/src/media/network.ts +115 -0
  309. package/src/media/types.ts +177 -0
  310. package/src/media/viewport.ts +84 -0
  311. package/src/motion/index.ts +57 -48
  312. package/src/motion/morph.ts +151 -0
  313. package/src/motion/parallax.ts +120 -0
  314. package/src/motion/reduced-motion.ts +66 -17
  315. package/src/motion/transition.ts +97 -97
  316. package/src/motion/types.ts +63 -0
  317. package/src/motion/typewriter.ts +164 -0
  318. package/src/platform/announcer.ts +208 -208
  319. package/src/platform/config.ts +163 -163
  320. package/src/platform/cookies.ts +165 -165
  321. package/src/platform/index.ts +39 -39
  322. package/src/platform/meta.ts +168 -168
  323. package/src/plugin/index.ts +37 -0
  324. package/src/plugin/registry.ts +269 -0
  325. package/src/plugin/types.ts +137 -0
  326. package/src/reactive/async-data.ts +486 -486
  327. package/src/reactive/computed.ts +130 -92
  328. package/src/reactive/index.ts +37 -37
  329. package/src/reactive/signal.ts +29 -29
  330. package/src/router/bq-link.ts +279 -0
  331. package/src/router/constraints.ts +201 -0
  332. package/src/router/index.ts +49 -41
  333. package/src/router/match.ts +312 -106
  334. package/src/router/path-pattern.ts +52 -0
  335. package/src/router/query.ts +38 -35
  336. package/src/router/router.ts +402 -211
  337. package/src/router/types.ts +139 -93
  338. package/src/router/use-route.ts +68 -0
  339. package/src/router/utils.ts +157 -116
  340. package/src/security/constants.ts +211 -211
  341. package/src/security/index.ts +12 -10
  342. package/src/security/sanitize.ts +6 -2
  343. package/src/security/trusted-html.ts +71 -0
  344. package/src/ssr/hydrate.ts +82 -0
  345. package/src/ssr/index.ts +70 -0
  346. package/src/ssr/render.ts +508 -0
  347. package/src/ssr/serialize.ts +296 -0
  348. package/src/ssr/types.ts +81 -0
  349. package/src/store/create-store.ts +467 -329
  350. package/src/store/define-store.ts +2 -1
  351. package/src/store/index.ts +27 -22
  352. package/src/store/mapping.ts +2 -1
  353. package/src/store/persisted.ts +249 -61
  354. package/src/store/types.ts +247 -94
  355. package/src/store/utils.ts +135 -141
  356. package/src/store/watch.ts +2 -1
  357. package/src/storybook/index.ts +480 -0
  358. package/src/testing/index.ts +42 -0
  359. package/src/testing/testing.ts +593 -0
  360. package/src/testing/types.ts +170 -0
  361. package/src/view/custom-directives.ts +30 -0
  362. package/src/view/evaluate.ts +292 -290
  363. package/src/view/process.ts +108 -92
  364. package/dist/component-CY5MVoYN.js +0 -531
  365. package/dist/component-CY5MVoYN.js.map +0 -1
  366. package/dist/config-DRmZZno3.js.map +0 -1
  367. package/dist/core-CK2Mfpf4.js +0 -648
  368. package/dist/core-CK2Mfpf4.js.map +0 -1
  369. package/dist/motion-C5DRdPnO.js +0 -415
  370. package/dist/motion-C5DRdPnO.js.map +0 -1
  371. package/dist/platform-B7JhGBc7.js.map +0 -1
  372. package/dist/reactive-BDya-ia8.js +0 -253
  373. package/dist/reactive-BDya-ia8.js.map +0 -1
  374. package/dist/router-CijiICxt.js +0 -188
  375. package/dist/router-CijiICxt.js.map +0 -1
  376. package/dist/sanitize-jyJ2ryE2.js.map +0 -1
  377. package/dist/store-CPK9E62U.js +0 -262
  378. package/dist/store-CPK9E62U.js.map +0 -1
  379. package/dist/view-Cdi0g-qo.js +0 -396
  380. package/dist/view-Cdi0g-qo.js.map +0 -1
@@ -1,106 +1,312 @@
1
- /**
2
- * Route matching helpers.
3
- * @module bquery/router
4
- */
5
-
6
- import { parseQuery } from './query';
7
- import type { Route, RouteDefinition } from './types';
8
-
9
- // ============================================================================
10
- // Route Matching
11
- // ============================================================================
12
-
13
- /**
14
- * Converts a route path pattern to a RegExp for matching.
15
- * Uses placeholder approach to preserve :param and * patterns during escaping.
16
- * Returns positional capture groups for maximum compatibility.
17
- * @internal
18
- */
19
- const pathToRegex = (path: string): RegExp => {
20
- // Handle wildcard-only route
21
- if (path === '*') {
22
- return /^.*$/;
23
- }
24
-
25
- // Unique placeholders using null chars (won't appear in normal paths)
26
- const PARAM_MARKER = '\u0000P\u0000';
27
- const WILDCARD_MARKER = '\u0000W\u0000';
28
-
29
- // Step 1: Extract :param patterns before escaping
30
- let pattern = path.replace(/:([a-zA-Z_][a-zA-Z0-9_]*)/g, () => {
31
- return PARAM_MARKER;
32
- });
33
-
34
- // Step 2: Extract * wildcards before escaping
35
- pattern = pattern.replace(/\*/g, WILDCARD_MARKER);
36
-
37
- // Step 3: Escape ALL regex metacharacters: \ ^ $ . * + ? ( ) [ ] { } |
38
- pattern = pattern.replace(/[\\^$.*+?()[\]{}|]/g, '\\$&');
39
-
40
- // Step 4: Restore param capture groups (positional, not named)
41
- pattern = pattern.replace(/\u0000P\u0000/g, () => `([^/]+)`);
42
-
43
- // Step 5: Restore wildcards as .*
44
- pattern = pattern.replace(/\u0000W\u0000/g, '.*');
45
-
46
- return new RegExp(`^${pattern}$`);
47
- };
48
-
49
- /**
50
- * Extracts param names from a route path.
51
- * @internal
52
- */
53
- const extractParamNames = (path: string): string[] => {
54
- const matches = path.match(/:([a-zA-Z_][a-zA-Z0-9_]*)/g);
55
- return matches ? matches.map((m) => m.slice(1)) : [];
56
- };
57
-
58
- /**
59
- * Matches a path against route definitions and extracts params.
60
- * Uses positional captures for maximum compatibility.
61
- * @internal
62
- */
63
- export const matchRoute = (
64
- path: string,
65
- routes: RouteDefinition[]
66
- ): { matched: RouteDefinition; params: Record<string, string> } | null => {
67
- for (const route of routes) {
68
- const regex = pathToRegex(route.path);
69
- const match = path.match(regex);
70
-
71
- if (match) {
72
- const paramNames = extractParamNames(route.path);
73
- const params: Record<string, string> = {};
74
-
75
- // Map positional captures to param names
76
- paramNames.forEach((name, index) => {
77
- params[name] = match[index + 1] || '';
78
- });
79
-
80
- return { matched: route, params };
81
- }
82
- }
83
-
84
- return null;
85
- };
86
-
87
- /**
88
- * Creates a Route object from the current URL.
89
- * @internal
90
- */
91
- export const createRoute = (
92
- pathname: string,
93
- search: string,
94
- hash: string,
95
- routes: RouteDefinition[]
96
- ): Route => {
97
- const result = matchRoute(pathname, routes);
98
-
99
- return {
100
- path: pathname,
101
- params: result?.params ?? {},
102
- query: parseQuery(search),
103
- matched: result?.matched ?? null,
104
- hash: hash.replace(/^#/, ''),
105
- };
106
- };
1
+ /**
2
+ * Route matching helpers.
3
+ * @module bquery/router
4
+ */
5
+
6
+ import { parseQuery } from './query';
7
+ import { getNormalizedRouteConstraint, getRouteConstraintRegex } from './constraints';
8
+ import { isParamChar, isParamStart, readConstraint } from './path-pattern';
9
+ import type { Route, RouteDefinition } from './types';
10
+
11
+ const readConstraintOrThrow = (
12
+ path: string,
13
+ startIndex: number
14
+ ): { constraint: string; endIndex: number } => {
15
+ const parsedConstraint = readConstraint(path, startIndex);
16
+ if (!parsedConstraint) {
17
+ throw new Error(
18
+ `bQuery router: Invalid route param constraint syntax in path "${path}" at index ${startIndex}.`
19
+ );
20
+ }
21
+ return parsedConstraint;
22
+ };
23
+
24
+ type RouteParamDescriptor = {
25
+ name: string;
26
+ constraint?: string;
27
+ nextIndex: number;
28
+ };
29
+
30
+ const validatedRoutePathCache = new Set<string>();
31
+
32
+ const readParamDescriptor = (path: string, index: number): RouteParamDescriptor | null => {
33
+ if (path[index] !== ':' || !isParamStart(path[index + 1])) {
34
+ return null;
35
+ }
36
+
37
+ let nameEnd = index + 2;
38
+ while (nameEnd < path.length && isParamChar(path[nameEnd])) {
39
+ nameEnd++;
40
+ }
41
+
42
+ let nextIndex = nameEnd;
43
+ let constraint: string | undefined;
44
+
45
+ if (path[nameEnd] === '(') {
46
+ const parsedConstraint = readConstraintOrThrow(path, nameEnd);
47
+ constraint = parsedConstraint.constraint;
48
+ nextIndex = parsedConstraint.endIndex;
49
+ }
50
+
51
+ return {
52
+ name: path.slice(index + 1, nameEnd),
53
+ constraint,
54
+ nextIndex,
55
+ };
56
+ };
57
+
58
+ const validateRoutePathPattern = (path: string): void => {
59
+ if (validatedRoutePathCache.has(path)) {
60
+ return;
61
+ }
62
+
63
+ for (let i = 0; i < path.length; ) {
64
+ const char = path[i];
65
+
66
+ if (char === ':' && isParamStart(path[i + 1])) {
67
+ const param = readParamDescriptor(path, i);
68
+ if (param?.constraint) {
69
+ getNormalizedRouteConstraint(param.constraint);
70
+ }
71
+ i = param?.nextIndex ?? i + 1;
72
+ continue;
73
+ }
74
+
75
+ i++;
76
+ }
77
+
78
+ validatedRoutePathCache.add(path);
79
+ };
80
+
81
+ const findSegmentBoundary = (value: string, startIndex: number): number => {
82
+ const slashIndex = value.indexOf('/', startIndex);
83
+ return slashIndex === -1 ? value.length : slashIndex;
84
+ };
85
+
86
+ const readNextStaticChunk = (path: string, startIndex: number): string => {
87
+ let chunkEnd = startIndex;
88
+
89
+ while (chunkEnd < path.length) {
90
+ if (path[chunkEnd] === '*') {
91
+ break;
92
+ }
93
+
94
+ if (path[chunkEnd] === ':' && isParamStart(path[chunkEnd + 1])) {
95
+ break;
96
+ }
97
+
98
+ chunkEnd++;
99
+ }
100
+
101
+ return path.slice(startIndex, chunkEnd);
102
+ };
103
+
104
+ const findAnchoredCandidateEnds = (
105
+ actualPath: string,
106
+ startIndex: number,
107
+ limit: number,
108
+ nextStaticChunk: string
109
+ ): number[] => {
110
+ const candidates: number[] = [];
111
+ let searchIndex = startIndex;
112
+
113
+ while (searchIndex <= limit) {
114
+ const candidateEnd = actualPath.indexOf(nextStaticChunk, searchIndex);
115
+ if (candidateEnd === -1 || candidateEnd > limit) {
116
+ break;
117
+ }
118
+
119
+ candidates.push(candidateEnd);
120
+ searchIndex = candidateEnd + 1;
121
+ }
122
+
123
+ return candidates.reverse();
124
+ };
125
+
126
+ const matchPathPattern = (routePath: string, actualPath: string): Record<string, string> | null => {
127
+ // Memoization keeps wildcard/param backtracking linear for repeated subproblems
128
+ // within a single route/path match attempt.
129
+ const memo = new Map<string, Record<string, string> | null>();
130
+
131
+ const matchFrom = (routeIndex: number, pathIndex: number): Record<string, string> | null => {
132
+ const memoKey = `${routeIndex}:${pathIndex}`;
133
+ if (memo.has(memoKey)) {
134
+ return memo.get(memoKey) ?? null;
135
+ }
136
+
137
+ if (routeIndex === routePath.length) {
138
+ const result = pathIndex === actualPath.length ? {} : null;
139
+ memo.set(memoKey, result);
140
+ return result;
141
+ }
142
+
143
+ const routeChar = routePath[routeIndex];
144
+
145
+ if (routeChar === '*') {
146
+ if (routeIndex === routePath.length - 1) {
147
+ const result = {};
148
+ memo.set(memoKey, result);
149
+ return result;
150
+ }
151
+
152
+ const nextStaticChunk = readNextStaticChunk(routePath, routeIndex + 1);
153
+ const anchoredCandidateEnds =
154
+ nextStaticChunk.length > 0
155
+ ? findAnchoredCandidateEnds(actualPath, pathIndex, actualPath.length, nextStaticChunk)
156
+ : null;
157
+
158
+ const iterateCandidateEnds = anchoredCandidateEnds
159
+ ? (callback: (candidateEnd: number) => Record<string, string> | null) => {
160
+ for (const candidateEnd of anchoredCandidateEnds) {
161
+ const result = callback(candidateEnd);
162
+ if (result) {
163
+ return result;
164
+ }
165
+ }
166
+ return null;
167
+ }
168
+ : (callback: (candidateEnd: number) => Record<string, string> | null) => {
169
+ for (let candidateEnd = actualPath.length; candidateEnd >= pathIndex; candidateEnd--) {
170
+ const result = callback(candidateEnd);
171
+ if (result) {
172
+ return result;
173
+ }
174
+ }
175
+ return null;
176
+ };
177
+
178
+ const wildcardMatch = iterateCandidateEnds((candidateEnd) => {
179
+ const suffixMatch = matchFrom(routeIndex + 1, candidateEnd);
180
+ if (suffixMatch) {
181
+ memo.set(memoKey, suffixMatch);
182
+ return suffixMatch;
183
+ }
184
+ return null;
185
+ });
186
+
187
+ if (wildcardMatch) {
188
+ return wildcardMatch;
189
+ }
190
+
191
+ memo.set(memoKey, null);
192
+ return null;
193
+ }
194
+
195
+ const param = readParamDescriptor(routePath, routeIndex);
196
+ if (param) {
197
+ const constraintRegex = param.constraint
198
+ ? getRouteConstraintRegex(param.constraint)
199
+ : undefined;
200
+ const candidateLimit = param.constraint
201
+ ? actualPath.length
202
+ : findSegmentBoundary(actualPath, pathIndex);
203
+ const nextStaticChunk = readNextStaticChunk(routePath, param.nextIndex);
204
+ const anchoredCandidateEnds =
205
+ nextStaticChunk.length > 0
206
+ ? findAnchoredCandidateEnds(actualPath, pathIndex, candidateLimit, nextStaticChunk)
207
+ : null;
208
+
209
+ const iterateCandidateEnds = anchoredCandidateEnds
210
+ ? (callback: (candidateEnd: number) => Record<string, string> | null) => {
211
+ for (const candidateEnd of anchoredCandidateEnds) {
212
+ if (candidateEnd <= pathIndex) {
213
+ continue;
214
+ }
215
+ const result = callback(candidateEnd);
216
+ if (result) {
217
+ return result;
218
+ }
219
+ }
220
+ return null;
221
+ }
222
+ : (callback: (candidateEnd: number) => Record<string, string> | null) => {
223
+ for (let candidateEnd = candidateLimit; candidateEnd > pathIndex; candidateEnd--) {
224
+ const result = callback(candidateEnd);
225
+ if (result) {
226
+ return result;
227
+ }
228
+ }
229
+ return null;
230
+ };
231
+
232
+ const paramMatch = iterateCandidateEnds((candidateEnd) => {
233
+ const candidateValue = actualPath.slice(pathIndex, candidateEnd);
234
+
235
+ if (constraintRegex) {
236
+ if (!constraintRegex.test(candidateValue)) {
237
+ return null;
238
+ }
239
+ }
240
+
241
+ const suffixMatch = matchFrom(param.nextIndex, candidateEnd);
242
+ if (suffixMatch) {
243
+ const result = {
244
+ [param.name]: candidateValue,
245
+ ...suffixMatch,
246
+ };
247
+ memo.set(memoKey, result);
248
+ return result;
249
+ }
250
+ return null;
251
+ });
252
+
253
+ if (paramMatch) {
254
+ return paramMatch;
255
+ }
256
+
257
+ memo.set(memoKey, null);
258
+ return null;
259
+ }
260
+
261
+ if (pathIndex >= actualPath.length || routeChar !== actualPath[pathIndex]) {
262
+ memo.set(memoKey, null);
263
+ return null;
264
+ }
265
+
266
+ const result = matchFrom(routeIndex + 1, pathIndex + 1);
267
+ memo.set(memoKey, result);
268
+ return result;
269
+ };
270
+
271
+ return matchFrom(0, 0);
272
+ };
273
+
274
+ /**
275
+ * Matches a path against route definitions and extracts params.
276
+ * @internal
277
+ */
278
+ export const matchRoute = (
279
+ path: string,
280
+ routes: RouteDefinition[]
281
+ ): { matched: RouteDefinition; params: Record<string, string> } | null => {
282
+ for (const route of routes) {
283
+ validateRoutePathPattern(route.path);
284
+ const params = matchPathPattern(route.path, path);
285
+ if (params) {
286
+ return { matched: route, params };
287
+ }
288
+ }
289
+
290
+ return null;
291
+ };
292
+
293
+ /**
294
+ * Creates a Route object from the current URL.
295
+ * @internal
296
+ */
297
+ export const createRoute = (
298
+ pathname: string,
299
+ search: string,
300
+ hash: string,
301
+ routes: RouteDefinition[]
302
+ ): Route => {
303
+ const result = matchRoute(pathname, routes);
304
+
305
+ return {
306
+ path: pathname,
307
+ params: result?.params ?? {},
308
+ query: parseQuery(search),
309
+ matched: result?.matched ?? null,
310
+ hash: hash.replace(/^#/, ''),
311
+ };
312
+ };
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Shared helpers for parsing route path params and constraints.
3
+ * @internal
4
+ */
5
+
6
+ /** Validates whether a character can start a route param name. @internal */
7
+ export const isParamStart = (char: string | undefined): boolean =>
8
+ char !== undefined &&
9
+ ((char >= 'a' && char <= 'z') || (char >= 'A' && char <= 'Z') || char === '_');
10
+
11
+ /** Validates whether a character can appear after the start of a route param name. @internal */
12
+ export const isParamChar = (char: string | undefined): boolean =>
13
+ isParamStart(char) || (char !== undefined && char >= '0' && char <= '9');
14
+
15
+ /** Reads a route param constraint while preserving escaped chars and nested groups. @internal */
16
+ export const readConstraint = (
17
+ path: string,
18
+ startIndex: number
19
+ ): { constraint: string; endIndex: number } | null => {
20
+ let depth = 1;
21
+ let constraint = '';
22
+ let i = startIndex + 1;
23
+ let inCharacterClass = false;
24
+
25
+ while (i < path.length) {
26
+ const char = path[i];
27
+
28
+ if (char === '\\' && i + 1 < path.length) {
29
+ constraint += char + path[i + 1];
30
+ i += 2;
31
+ continue;
32
+ }
33
+
34
+ if (char === '[' && !inCharacterClass) {
35
+ inCharacterClass = true;
36
+ } else if (char === ']' && inCharacterClass) {
37
+ inCharacterClass = false;
38
+ } else if (!inCharacterClass && char === '(') {
39
+ depth++;
40
+ } else if (!inCharacterClass && char === ')') {
41
+ depth--;
42
+ if (depth === 0) {
43
+ return { constraint, endIndex: i + 1 };
44
+ }
45
+ }
46
+
47
+ constraint += char;
48
+ i++;
49
+ }
50
+
51
+ return null;
52
+ };
@@ -1,35 +1,38 @@
1
- /**
2
- * Query string helpers.
3
- * @module bquery/router
4
- */
5
-
6
- /**
7
- * Parses query string into an object.
8
- * Single values are stored as strings, duplicate keys become arrays.
9
- * @internal
10
- *
11
- * @example
12
- * parseQuery('?foo=1') // { foo: '1' }
13
- * parseQuery('?tag=a&tag=b') // { tag: ['a', 'b'] }
14
- * parseQuery('?x=1&y=2&x=3') // { x: ['1', '3'], y: '2' }
15
- */
16
- export const parseQuery = (search: string): Record<string, string | string[]> => {
17
- const query: Record<string, string | string[]> = {};
18
- const params = new URLSearchParams(search);
19
-
20
- params.forEach((value, key) => {
21
- const existing = query[key];
22
- if (existing === undefined) {
23
- // First occurrence: store as string
24
- query[key] = value;
25
- } else if (Array.isArray(existing)) {
26
- // Already an array: append
27
- existing.push(value);
28
- } else {
29
- // Second occurrence: convert to array
30
- query[key] = [existing, value];
31
- }
32
- });
33
-
34
- return query;
35
- };
1
+ /**
2
+ * Query string helpers.
3
+ * @module bquery/router
4
+ */
5
+
6
+ import { isPrototypePollutionKey } from '../core/utils/object';
7
+
8
+ /**
9
+ * Parses query string into an object.
10
+ * Single values are stored as strings, duplicate keys become arrays.
11
+ * @internal
12
+ *
13
+ * @example
14
+ * parseQuery('?foo=1') // { foo: '1' }
15
+ * parseQuery('?tag=a&tag=b') // { tag: ['a', 'b'] }
16
+ * parseQuery('?x=1&y=2&x=3') // { x: ['1', '3'], y: '2' }
17
+ */
18
+ export const parseQuery = (search: string): Record<string, string | string[]> => {
19
+ const query: Record<string, string | string[]> = {};
20
+ const params = new URLSearchParams(search);
21
+
22
+ params.forEach((value, key) => {
23
+ if (isPrototypePollutionKey(key)) return;
24
+ const existing = query[key];
25
+ if (existing === undefined) {
26
+ // First occurrence: store as string
27
+ query[key] = value;
28
+ } else if (Array.isArray(existing)) {
29
+ // Already an array: append
30
+ existing.push(value);
31
+ } else {
32
+ // Second occurrence: convert to array
33
+ query[key] = [existing, value];
34
+ }
35
+ });
36
+
37
+ return query;
38
+ };