@bquery/bquery 1.1.1 → 1.2.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.
@@ -0,0 +1,166 @@
1
+ import { computed as E, signal as S } from "./reactive.es.mjs";
2
+ let h = null;
3
+ const d = S({
4
+ path: "",
5
+ params: {},
6
+ query: {},
7
+ matched: null,
8
+ hash: ""
9
+ }), _ = E(() => d.value), z = (r) => {
10
+ if (r === "*")
11
+ return /^.*$/;
12
+ const o = "\0P\0", e = "\0W\0", a = [];
13
+ let t = r.replace(/:([a-zA-Z_][a-zA-Z0-9_]*)/g, (u, p) => (a.push(p), o));
14
+ t = t.replace(/\*/g, e), t = t.replace(/[\\^$.*+?()[\]{}|]/g, "\\$&");
15
+ let n = 0;
16
+ return t = t.replace(/\u0000P\u0000/g, () => `(?<${a[n++]}>[^/]+)`), t = t.replace(/\u0000W\u0000/g, ".*"), new RegExp(`^${t}$`);
17
+ }, N = (r) => {
18
+ const o = r.match(/:([a-zA-Z_][a-zA-Z0-9_]*)/g);
19
+ return o ? o.map((e) => e.slice(1)) : [];
20
+ }, W = (r, o) => {
21
+ for (const e of o) {
22
+ const a = z(e.path), t = r.match(a);
23
+ if (t) {
24
+ const n = N(e.path), u = {};
25
+ return t.groups ? Object.assign(u, t.groups) : n.forEach((p, w) => {
26
+ u[p] = t[w + 1] || "";
27
+ }), { matched: e, params: u };
28
+ }
29
+ }
30
+ return null;
31
+ }, C = (r) => {
32
+ const o = {};
33
+ return new URLSearchParams(r).forEach((a, t) => {
34
+ const n = o[t];
35
+ n === void 0 ? o[t] = a : Array.isArray(n) ? n.push(a) : o[t] = [n, a];
36
+ }), o;
37
+ }, y = (r, o, e, a) => {
38
+ const t = W(r, a);
39
+ return {
40
+ path: r,
41
+ params: t?.params ?? {},
42
+ query: C(o),
43
+ matched: t?.matched ?? null,
44
+ hash: e.replace(/^#/, "")
45
+ };
46
+ }, P = async (r, o = {}) => {
47
+ if (!h)
48
+ throw new Error("bQuery router: No router initialized. Call createRouter() first.");
49
+ await h[o.replace ? "replace" : "push"](r);
50
+ }, Q = () => {
51
+ h ? h.back() : history.back();
52
+ }, Z = () => {
53
+ h ? h.forward() : history.forward();
54
+ }, q = (r) => {
55
+ h && h.destroy();
56
+ const { routes: o, base: e = "", hash: a = !1 } = r, t = [], n = [], u = x(o, e), p = () => {
57
+ if (a) {
58
+ const c = window.location.hash.slice(1) || "/", [f, i = ""] = c.split("?"), [m, l = ""] = i.split("#");
59
+ return {
60
+ pathname: f,
61
+ search: m ? `?${m}` : "",
62
+ hash: l ? `#${l}` : ""
63
+ };
64
+ }
65
+ let s = window.location.pathname;
66
+ return e && s.startsWith(e) && (s = s.slice(e.length) || "/"), {
67
+ pathname: s,
68
+ search: window.location.search,
69
+ hash: window.location.hash
70
+ };
71
+ }, w = () => {
72
+ const { pathname: s, search: c, hash: f } = p(), i = y(s, c, f, u);
73
+ d.value = i;
74
+ }, $ = async (s, c) => {
75
+ const { pathname: f, search: i, hash: m } = p(), l = y(f, i, m, u), g = new URL(s, window.location.origin), R = a ? s : g.pathname, k = y(R, g.search, g.hash, u);
76
+ for (const v of t)
77
+ if (await v(k, l) === !1)
78
+ return;
79
+ const L = a ? `#${s}` : `${e}${s}`;
80
+ history[c]({}, "", L), w();
81
+ for (const v of n)
82
+ v(d.value, l);
83
+ }, b = async () => {
84
+ const { pathname: s, search: c, hash: f } = p(), i = d.value, m = y(s, c, f, u);
85
+ for (const l of t)
86
+ if (await l(m, i) === !1) {
87
+ const R = a ? `#${i.path}` : `${e}${i.path}`;
88
+ history.pushState({}, "", R);
89
+ return;
90
+ }
91
+ w();
92
+ for (const l of n)
93
+ l(d.value, i);
94
+ };
95
+ window.addEventListener("popstate", b), w();
96
+ const A = {
97
+ push: (s) => $(s, "pushState"),
98
+ replace: (s) => $(s, "replaceState"),
99
+ back: () => history.back(),
100
+ forward: () => history.forward(),
101
+ go: (s) => history.go(s),
102
+ beforeEach: (s) => (t.push(s), () => {
103
+ const c = t.indexOf(s);
104
+ c > -1 && t.splice(c, 1);
105
+ }),
106
+ afterEach: (s) => (n.push(s), () => {
107
+ const c = n.indexOf(s);
108
+ c > -1 && n.splice(c, 1);
109
+ }),
110
+ currentRoute: _,
111
+ routes: u,
112
+ destroy: () => {
113
+ window.removeEventListener("popstate", b), t.length = 0, n.length = 0, h = null;
114
+ }
115
+ };
116
+ return h = A, A;
117
+ }, x = (r, o = "") => {
118
+ const e = [];
119
+ for (const a of r) {
120
+ const t = a.path === "*" ? "*" : `${o}${a.path}`.replace(/\/+/g, "/");
121
+ e.push({
122
+ ...a,
123
+ path: t
124
+ }), a.children && e.push(...x(a.children, t));
125
+ }
126
+ return e;
127
+ }, I = (r, o = {}) => {
128
+ if (!h)
129
+ throw new Error("bQuery router: No router initialized.");
130
+ const e = h.routes.find((t) => t.name === r);
131
+ if (!e)
132
+ throw new Error(`bQuery router: Route "${r}" not found.`);
133
+ let a = e.path;
134
+ for (const [t, n] of Object.entries(o))
135
+ a = a.replace(`:${t}`, encodeURIComponent(n));
136
+ return a;
137
+ }, M = (r, o = !1) => {
138
+ const e = d.value.path;
139
+ return o ? e === r : e.startsWith(r);
140
+ }, U = (r, o = !1) => E(() => {
141
+ const e = d.value.path;
142
+ return o ? e === r : e.startsWith(r);
143
+ }), j = (r, o = {}) => (e) => {
144
+ e.preventDefault(), P(r, o);
145
+ }, H = (r = document.body) => {
146
+ const o = (e) => {
147
+ const t = e.target.closest("a");
148
+ if (!t || t.target || t.hasAttribute("download") || t.origin !== window.location.origin) return;
149
+ const n = t.pathname + t.search + t.hash;
150
+ e.preventDefault(), P(n);
151
+ };
152
+ return r.addEventListener("click", o), () => r.removeEventListener("click", o);
153
+ };
154
+ export {
155
+ Q as back,
156
+ q as createRouter,
157
+ _ as currentRoute,
158
+ Z as forward,
159
+ H as interceptLinks,
160
+ M as isActive,
161
+ U as isActiveSignal,
162
+ j as link,
163
+ P as navigate,
164
+ I as resolve
165
+ };
166
+ //# sourceMappingURL=router.es.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"router.es.mjs","sources":["../src/router/index.ts"],"sourcesContent":["/**\r\n * Minimal SPA router with History API integration.\r\n *\r\n * This module provides a lightweight, signal-based router for single-page\r\n * applications. Features include:\r\n * - History API navigation\r\n * - Route matching with params and wildcards\r\n * - Lazy route loading\r\n * - Navigation guards (beforeEach, afterEach)\r\n * - Reactive current route via signals\r\n * - Multi-value query params (e.g., `?tag=a&tag=b` → `{ tag: ['a', 'b'] }`)\r\n *\r\n * @module bquery/router\r\n *\r\n * @example\r\n * ```ts\r\n * import { createRouter, navigate, currentRoute } from 'bquery/router';\r\n * import { effect } from 'bquery/reactive';\r\n *\r\n * const router = createRouter({\r\n * routes: [\r\n * { path: '/', component: () => import('./Home') },\r\n * { path: '/user/:id', component: () => import('./User') },\r\n * { path: '*', component: () => import('./NotFound') },\r\n * ],\r\n * });\r\n *\r\n * effect(() => {\r\n * console.log('Route changed:', currentRoute.value);\r\n * });\r\n *\r\n * navigate('/user/42');\r\n * ```\r\n */\r\n\r\nimport { computed, signal, type ReadonlySignal, type Signal } from '../reactive/index';\r\n\r\n// ============================================================================\r\n// Types\r\n// ============================================================================\r\n\r\n/**\r\n * Represents a parsed route with matched params.\r\n */\r\nexport type Route = {\r\n /** The current path (e.g., '/user/42') */\r\n path: string;\r\n /** Extracted route params (e.g., { id: '42' }) */\r\n params: Record<string, string>;\r\n /**\r\n * Query string params.\r\n * Each key maps to a single string value by default.\r\n * Only keys that appear multiple times in the query string become arrays.\r\n * @example\r\n * // ?foo=1 → { foo: '1' }\r\n * // ?tag=a&tag=b → { tag: ['a', 'b'] }\r\n * // ?x=1&y=2&x=3 → { x: ['1', '3'], y: '2' }\r\n */\r\n query: Record<string, string | string[]>;\r\n /** The matched route definition */\r\n matched: RouteDefinition | null;\r\n /** Hash fragment without # */\r\n hash: string;\r\n};\r\n\r\n/**\r\n * Route definition for configuration.\r\n */\r\nexport type RouteDefinition = {\r\n /** Path pattern (e.g., '/user/:id', '/posts/*') */\r\n path: string;\r\n /** Component loader (sync or async) */\r\n component: () => unknown | Promise<unknown>;\r\n /** Optional route name for programmatic navigation */\r\n name?: string;\r\n /** Optional metadata */\r\n meta?: Record<string, unknown>;\r\n /** Nested child routes */\r\n children?: RouteDefinition[];\r\n};\r\n\r\n/**\r\n * Router configuration options.\r\n */\r\nexport type RouterOptions = {\r\n /** Array of route definitions */\r\n routes: RouteDefinition[];\r\n /** Base path for all routes (default: '') */\r\n base?: string;\r\n /** Use hash-based routing instead of history (default: false) */\r\n hash?: boolean;\r\n};\r\n\r\n/**\r\n * Navigation guard function type.\r\n */\r\nexport type NavigationGuard = (to: Route, from: Route) => boolean | void | Promise<boolean | void>;\r\n\r\n/**\r\n * Router instance returned by createRouter.\r\n */\r\nexport type Router = {\r\n /** Navigate to a path */\r\n push: (path: string) => Promise<void>;\r\n /** Replace current history entry */\r\n replace: (path: string) => Promise<void>;\r\n /** Go back in history */\r\n back: () => void;\r\n /** Go forward in history */\r\n forward: () => void;\r\n /** Go to a specific history entry */\r\n go: (delta: number) => void;\r\n /** Add a beforeEach guard */\r\n beforeEach: (guard: NavigationGuard) => () => void;\r\n /** Add an afterEach hook */\r\n afterEach: (hook: (to: Route, from: Route) => void) => () => void;\r\n /** Current route (reactive) */\r\n currentRoute: ReadonlySignal<Route>;\r\n /** All route definitions */\r\n routes: RouteDefinition[];\r\n /** Destroy the router and cleanup listeners */\r\n destroy: () => void;\r\n};\r\n\r\n// ============================================================================\r\n// Internal State\r\n// ============================================================================\r\n\r\n/** @internal */\r\nlet activeRouter: Router | null = null;\r\n\r\n/** @internal */\r\nconst routeSignal: Signal<Route> = signal<Route>({\r\n path: '',\r\n params: {},\r\n query: {},\r\n matched: null,\r\n hash: '',\r\n});\r\n\r\n/**\r\n * Reactive signal containing the current route.\r\n *\r\n * @example\r\n * ```ts\r\n * import { currentRoute } from 'bquery/router';\r\n * import { effect } from 'bquery/reactive';\r\n *\r\n * effect(() => {\r\n * document.title = `Page: ${currentRoute.value.path}`;\r\n * });\r\n * ```\r\n */\r\nexport const currentRoute: ReadonlySignal<Route> = computed(() => routeSignal.value);\r\n\r\n// ============================================================================\r\n// Route Matching\r\n// ============================================================================\r\n\r\n/**\r\n * Converts a route path pattern to a RegExp for matching.\r\n * Uses placeholder approach to preserve :param and * patterns during escaping.\r\n * @internal\r\n */\r\nconst pathToRegex = (path: string): RegExp => {\r\n // Handle wildcard-only route\r\n if (path === '*') {\r\n return /^.*$/;\r\n }\r\n\r\n // Unique placeholders using null chars (won't appear in normal paths)\r\n const PARAM_MARKER = '\\u0000P\\u0000';\r\n const WILDCARD_MARKER = '\\u0000W\\u0000';\r\n\r\n // Store param names for restoration\r\n const paramNames: string[] = [];\r\n\r\n // Step 1: Extract :param patterns before escaping\r\n let pattern = path.replace(/:([a-zA-Z_][a-zA-Z0-9_]*)/g, (_, name) => {\r\n paramNames.push(name);\r\n return PARAM_MARKER;\r\n });\r\n\r\n // Step 2: Extract * wildcards before escaping\r\n pattern = pattern.replace(/\\*/g, WILDCARD_MARKER);\r\n\r\n // Step 3: Escape ALL regex metacharacters: \\ ^ $ . * + ? ( ) [ ] { } |\r\n pattern = pattern.replace(/[\\\\^$.*+?()[\\]{}|]/g, '\\\\$&');\r\n\r\n // Step 4: Restore param capture groups\r\n let paramIdx = 0;\r\n pattern = pattern.replace(/\\u0000P\\u0000/g, () => `(?<${paramNames[paramIdx++]}>[^/]+)`);\r\n\r\n // Step 5: Restore wildcards as .*\r\n pattern = pattern.replace(/\\u0000W\\u0000/g, '.*');\r\n\r\n return new RegExp(`^${pattern}$`);\r\n};\r\n\r\n/**\r\n * Extracts param names from a route path.\r\n * @internal\r\n */\r\nconst extractParamNames = (path: string): string[] => {\r\n const matches = path.match(/:([a-zA-Z_][a-zA-Z0-9_]*)/g);\r\n return matches ? matches.map((m) => m.slice(1)) : [];\r\n};\r\n\r\n/**\r\n * Matches a path against route definitions and extracts params.\r\n * @internal\r\n */\r\nconst matchRoute = (\r\n path: string,\r\n routes: RouteDefinition[]\r\n): { matched: RouteDefinition; params: Record<string, string> } | null => {\r\n for (const route of routes) {\r\n const regex = pathToRegex(route.path);\r\n const match = path.match(regex);\r\n\r\n if (match) {\r\n const paramNames = extractParamNames(route.path);\r\n const params: Record<string, string> = {};\r\n\r\n // Extract named groups if available\r\n if (match.groups) {\r\n Object.assign(params, match.groups);\r\n } else {\r\n // Fallback for browsers without named groups\r\n paramNames.forEach((name, index) => {\r\n params[name] = match[index + 1] || '';\r\n });\r\n }\r\n\r\n return { matched: route, params };\r\n }\r\n }\r\n\r\n return null;\r\n};\r\n\r\n/**\r\n * Parses query string into an object.\r\n * Single values are stored as strings, duplicate keys become arrays.\r\n * @internal\r\n *\r\n * @example\r\n * parseQuery('?foo=1') // { foo: '1' }\r\n * parseQuery('?tag=a&tag=b') // { tag: ['a', 'b'] }\r\n * parseQuery('?x=1&y=2&x=3') // { x: ['1', '3'], y: '2' }\r\n */\r\nconst parseQuery = (search: string): Record<string, string | string[]> => {\r\n const query: Record<string, string | string[]> = {};\r\n const params = new URLSearchParams(search);\r\n\r\n params.forEach((value, key) => {\r\n const existing = query[key];\r\n if (existing === undefined) {\r\n // First occurrence: store as string\r\n query[key] = value;\r\n } else if (Array.isArray(existing)) {\r\n // Already an array: append\r\n existing.push(value);\r\n } else {\r\n // Second occurrence: convert to array\r\n query[key] = [existing, value];\r\n }\r\n });\r\n\r\n return query;\r\n};\r\n\r\n/**\r\n * Creates a Route object from the current URL.\r\n * @internal\r\n */\r\nconst createRoute = (\r\n pathname: string,\r\n search: string,\r\n hash: string,\r\n routes: RouteDefinition[]\r\n): Route => {\r\n const result = matchRoute(pathname, routes);\r\n\r\n return {\r\n path: pathname,\r\n params: result?.params ?? {},\r\n query: parseQuery(search),\r\n matched: result?.matched ?? null,\r\n hash: hash.replace(/^#/, ''),\r\n };\r\n};\r\n\r\n// ============================================================================\r\n// Navigation\r\n// ============================================================================\r\n\r\n/**\r\n * Navigates to a new path.\r\n *\r\n * @param path - The path to navigate to\r\n * @param options - Navigation options\r\n *\r\n * @example\r\n * ```ts\r\n * import { navigate } from 'bquery/router';\r\n *\r\n * // Push to history\r\n * await navigate('/dashboard');\r\n *\r\n * // Replace current entry\r\n * await navigate('/login', { replace: true });\r\n * ```\r\n */\r\nexport const navigate = async (\r\n path: string,\r\n options: { replace?: boolean } = {}\r\n): Promise<void> => {\r\n if (!activeRouter) {\r\n throw new Error('bQuery router: No router initialized. Call createRouter() first.');\r\n }\r\n\r\n await activeRouter[options.replace ? 'replace' : 'push'](path);\r\n};\r\n\r\n/**\r\n * Programmatically go back in history.\r\n *\r\n * @example\r\n * ```ts\r\n * import { back } from 'bquery/router';\r\n * back();\r\n * ```\r\n */\r\nexport const back = (): void => {\r\n if (activeRouter) {\r\n activeRouter.back();\r\n } else {\r\n history.back();\r\n }\r\n};\r\n\r\n/**\r\n * Programmatically go forward in history.\r\n *\r\n * @example\r\n * ```ts\r\n * import { forward } from 'bquery/router';\r\n * forward();\r\n * ```\r\n */\r\nexport const forward = (): void => {\r\n if (activeRouter) {\r\n activeRouter.forward();\r\n } else {\r\n history.forward();\r\n }\r\n};\r\n\r\n// ============================================================================\r\n// Router Creation\r\n// ============================================================================\r\n\r\n/**\r\n * Creates and initializes a router instance.\r\n *\r\n * @param options - Router configuration\r\n * @returns The router instance\r\n *\r\n * @example\r\n * ```ts\r\n * import { createRouter } from 'bquery/router';\r\n *\r\n * const router = createRouter({\r\n * routes: [\r\n * { path: '/', component: () => import('./pages/Home') },\r\n * { path: '/about', component: () => import('./pages/About') },\r\n * { path: '/user/:id', component: () => import('./pages/User') },\r\n * { path: '*', component: () => import('./pages/NotFound') },\r\n * ],\r\n * base: '/app',\r\n * });\r\n *\r\n * router.beforeEach((to, from) => {\r\n * if (to.path === '/admin' && !isAuthenticated()) {\r\n * return false; // Cancel navigation\r\n * }\r\n * });\r\n * ```\r\n */\r\nexport const createRouter = (options: RouterOptions): Router => {\r\n // Clean up any existing router to prevent guard leakage\r\n if (activeRouter) {\r\n activeRouter.destroy();\r\n }\r\n\r\n const { routes, base = '', hash: useHash = false } = options;\r\n\r\n // Instance-specific guards and hooks (not shared globally)\r\n const beforeGuards: NavigationGuard[] = [];\r\n const afterHooks: Array<(to: Route, from: Route) => void> = [];\r\n\r\n // Flatten nested routes\r\n const flatRoutes = flattenRoutes(routes, base);\r\n\r\n /**\r\n * Gets the current path from the URL.\r\n */\r\n const getCurrentPath = (): { pathname: string; search: string; hash: string } => {\r\n if (useHash) {\r\n const hashPath = window.location.hash.slice(1) || '/';\r\n const [pathname, rest = ''] = hashPath.split('?');\r\n const [search, hashPart = ''] = rest.split('#');\r\n return {\r\n pathname,\r\n search: search ? `?${search}` : '',\r\n hash: hashPart ? `#${hashPart}` : '',\r\n };\r\n }\r\n\r\n let pathname = window.location.pathname;\r\n if (base && pathname.startsWith(base)) {\r\n pathname = pathname.slice(base.length) || '/';\r\n }\r\n\r\n return {\r\n pathname,\r\n search: window.location.search,\r\n hash: window.location.hash,\r\n };\r\n };\r\n\r\n /**\r\n * Updates the route signal with current URL state.\r\n */\r\n const syncRoute = (): void => {\r\n const { pathname, search, hash } = getCurrentPath();\r\n const newRoute = createRoute(pathname, search, hash, flatRoutes);\r\n routeSignal.value = newRoute;\r\n };\r\n\r\n /**\r\n * Performs navigation with guards.\r\n */\r\n const performNavigation = async (\r\n path: string,\r\n method: 'pushState' | 'replaceState'\r\n ): Promise<void> => {\r\n const { pathname, search, hash } = getCurrentPath();\r\n const from = createRoute(pathname, search, hash, flatRoutes);\r\n\r\n // Parse the target path\r\n const url = new URL(path, window.location.origin);\r\n const toPath = useHash ? path : url.pathname;\r\n const to = createRoute(toPath, url.search, url.hash, flatRoutes);\r\n\r\n // Run beforeEach guards\r\n for (const guard of beforeGuards) {\r\n const result = await guard(to, from);\r\n if (result === false) {\r\n return; // Cancel navigation\r\n }\r\n }\r\n\r\n // Update browser history\r\n const fullPath = useHash ? `#${path}` : `${base}${path}`;\r\n history[method]({}, '', fullPath);\r\n\r\n // Update route signal\r\n syncRoute();\r\n\r\n // Run afterEach hooks\r\n for (const hook of afterHooks) {\r\n hook(routeSignal.value, from);\r\n }\r\n };\r\n\r\n /**\r\n * Handle popstate events (back/forward).\r\n */\r\n const handlePopState = async (): Promise<void> => {\r\n const { pathname, search, hash } = getCurrentPath();\r\n const from = routeSignal.value;\r\n const to = createRoute(pathname, search, hash, flatRoutes);\r\n\r\n // Run beforeEach guards (supports async guards)\r\n for (const guard of beforeGuards) {\r\n const result = await guard(to, from);\r\n if (result === false) {\r\n // Restore previous state\r\n const restorePath = useHash ? `#${from.path}` : `${base}${from.path}`;\r\n history.pushState({}, '', restorePath);\r\n return;\r\n }\r\n }\r\n\r\n syncRoute();\r\n\r\n for (const hook of afterHooks) {\r\n hook(routeSignal.value, from);\r\n }\r\n };\r\n\r\n // Attach popstate listener\r\n window.addEventListener('popstate', handlePopState);\r\n\r\n // Initialize route\r\n syncRoute();\r\n\r\n const router: Router = {\r\n push: (path: string) => performNavigation(path, 'pushState'),\r\n replace: (path: string) => performNavigation(path, 'replaceState'),\r\n back: () => history.back(),\r\n forward: () => history.forward(),\r\n go: (delta: number) => history.go(delta),\r\n\r\n beforeEach: (guard: NavigationGuard) => {\r\n beforeGuards.push(guard);\r\n return () => {\r\n const index = beforeGuards.indexOf(guard);\r\n if (index > -1) beforeGuards.splice(index, 1);\r\n };\r\n },\r\n\r\n afterEach: (hook: (to: Route, from: Route) => void) => {\r\n afterHooks.push(hook);\r\n return () => {\r\n const index = afterHooks.indexOf(hook);\r\n if (index > -1) afterHooks.splice(index, 1);\r\n };\r\n },\r\n\r\n currentRoute,\r\n routes: flatRoutes,\r\n\r\n destroy: () => {\r\n window.removeEventListener('popstate', handlePopState);\r\n beforeGuards.length = 0;\r\n afterHooks.length = 0;\r\n activeRouter = null;\r\n },\r\n };\r\n\r\n activeRouter = router;\r\n return router;\r\n};\r\n\r\n// ============================================================================\r\n// Utilities\r\n// ============================================================================\r\n\r\n/**\r\n * Flattens nested routes into a single array with full paths.\r\n * @internal\r\n */\r\nconst flattenRoutes = (routes: RouteDefinition[], base = ''): RouteDefinition[] => {\r\n const result: RouteDefinition[] = [];\r\n\r\n for (const route of routes) {\r\n const fullPath = route.path === '*' ? '*' : `${base}${route.path}`.replace(/\\/+/g, '/');\r\n\r\n result.push({\r\n ...route,\r\n path: fullPath,\r\n });\r\n\r\n if (route.children) {\r\n result.push(...flattenRoutes(route.children, fullPath));\r\n }\r\n }\r\n\r\n return result;\r\n};\r\n\r\n/**\r\n * Resolves a route by name and params.\r\n *\r\n * @param name - The route name\r\n * @param params - Route params to interpolate\r\n * @returns The resolved path\r\n *\r\n * @example\r\n * ```ts\r\n * import { resolve } from 'bquery/router';\r\n *\r\n * const path = resolve('user', { id: '42' });\r\n * // Returns '/user/42' if route is defined as { name: 'user', path: '/user/:id' }\r\n * ```\r\n */\r\nexport const resolve = (name: string, params: Record<string, string> = {}): string => {\r\n if (!activeRouter) {\r\n throw new Error('bQuery router: No router initialized.');\r\n }\r\n\r\n const route = activeRouter.routes.find((r) => r.name === name);\r\n if (!route) {\r\n throw new Error(`bQuery router: Route \"${name}\" not found.`);\r\n }\r\n\r\n let path = route.path;\r\n for (const [key, value] of Object.entries(params)) {\r\n path = path.replace(`:${key}`, encodeURIComponent(value));\r\n }\r\n\r\n return path;\r\n};\r\n\r\n/**\r\n * Checks if a path matches the current route.\r\n *\r\n * @param path - Path to check\r\n * @param exact - Whether to match exactly (default: false)\r\n * @returns True if the path matches\r\n *\r\n * @example\r\n * ```ts\r\n * import { isActive } from 'bquery/router';\r\n *\r\n * if (isActive('/dashboard')) {\r\n * // Highlight nav item\r\n * }\r\n * ```\r\n */\r\nexport const isActive = (path: string, exact = false): boolean => {\r\n const current = routeSignal.value.path;\r\n return exact ? current === path : current.startsWith(path);\r\n};\r\n\r\n/**\r\n * Creates a computed signal that checks if a path is active.\r\n *\r\n * @param path - Path to check\r\n * @param exact - Whether to match exactly\r\n * @returns A reactive signal\r\n *\r\n * @example\r\n * ```ts\r\n * import { isActiveSignal } from 'bquery/router';\r\n * import { effect } from 'bquery/reactive';\r\n *\r\n * const dashboardActive = isActiveSignal('/dashboard');\r\n * effect(() => {\r\n * navItem.classList.toggle('active', dashboardActive.value);\r\n * });\r\n * ```\r\n */\r\nexport const isActiveSignal = (path: string, exact = false): ReadonlySignal<boolean> => {\r\n return computed(() => {\r\n const current = routeSignal.value.path;\r\n return exact ? current === path : current.startsWith(path);\r\n });\r\n};\r\n\r\n// ============================================================================\r\n// Router Link Helper\r\n// ============================================================================\r\n\r\n/**\r\n * Creates click handler for router links.\r\n * Attach to anchor elements to enable client-side navigation.\r\n *\r\n * @param path - Target path\r\n * @param options - Navigation options\r\n * @returns Click event handler\r\n *\r\n * @example\r\n * ```ts\r\n * import { link } from 'bquery/router';\r\n * import { $ } from 'bquery/core';\r\n *\r\n * $('#nav-home').on('click', link('/'));\r\n * $('#nav-about').on('click', link('/about'));\r\n * ```\r\n */\r\nexport const link = (path: string, options: { replace?: boolean } = {}): ((e: Event) => void) => {\r\n return (e: Event) => {\r\n e.preventDefault();\r\n navigate(path, options);\r\n };\r\n};\r\n\r\n/**\r\n * Intercepts all link clicks within a container for client-side routing.\r\n * Only intercepts links with matching origins and no target attribute.\r\n *\r\n * @param container - The container element to intercept links in\r\n * @returns Cleanup function to remove the listener\r\n *\r\n * @example\r\n * ```ts\r\n * import { interceptLinks } from 'bquery/router';\r\n *\r\n * // Intercept all links in the app\r\n * const cleanup = interceptLinks(document.body);\r\n *\r\n * // Later, remove the interceptor\r\n * cleanup();\r\n * ```\r\n */\r\nexport const interceptLinks = (container: Element = document.body): (() => void) => {\r\n const handler = (e: Event) => {\r\n const target = e.target as HTMLElement;\r\n const anchor = target.closest('a');\r\n\r\n if (!anchor) return;\r\n if (anchor.target) return; // Has target attribute\r\n if (anchor.hasAttribute('download')) return;\r\n if (anchor.origin !== window.location.origin) return; // External link\r\n\r\n const path = anchor.pathname + anchor.search + anchor.hash;\r\n\r\n e.preventDefault();\r\n navigate(path);\r\n };\r\n\r\n container.addEventListener('click', handler);\r\n return () => container.removeEventListener('click', handler);\r\n};\r\n"],"names":["activeRouter","routeSignal","signal","currentRoute","computed","pathToRegex","path","PARAM_MARKER","WILDCARD_MARKER","paramNames","pattern","_","name","paramIdx","extractParamNames","matches","m","matchRoute","routes","route","regex","match","params","index","parseQuery","search","query","value","key","existing","createRoute","pathname","hash","result","navigate","options","back","forward","createRouter","base","useHash","beforeGuards","afterHooks","flatRoutes","flattenRoutes","getCurrentPath","hashPath","rest","hashPart","syncRoute","newRoute","performNavigation","method","from","url","toPath","to","guard","fullPath","hook","handlePopState","restorePath","router","delta","resolve","r","isActive","exact","current","isActiveSignal","link","interceptLinks","container","handler","anchor"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"sanitize.d.ts","sourceRoot":"","sources":["../../src/security/sanitize.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAMH;;GAEG;AACH,MAAM,WAAW,eAAe;IAC9B,kDAAkD;IAClD,SAAS,CAAC,EAAE,MAAM,EAAE,CAAC;IACrB,wDAAwD;IACxD,eAAe,CAAC,EAAE,MAAM,EAAE,CAAC;IAC3B,8CAA8C;IAC9C,mBAAmB,CAAC,EAAE,OAAO,CAAC;IAC9B,4DAA4D;IAC5D,YAAY,CAAC,EAAE,OAAO,CAAC;CACxB;AAsBD,qCAAqC;AACrC,UAAU,iBAAiB;IACzB,UAAU,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,WAAW,CAAC;CAC5C;AAED,2EAA2E;AAC3E,UAAU,WAAW;IACnB,QAAQ,IAAI,MAAM,CAAC;CACpB;AAKD;;;GAGG;AACH,eAAO,MAAM,uBAAuB,QAAO,OAE1C,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,qBAAqB,QAAO,iBAAiB,GAAG,IAgB5D,CAAC;AA2WF;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,YAAY,GAAI,MAAM,MAAM,EAAE,UAAS,eAAoB,KAAG,MAE1E,CAAC;AAEF;;;;;;GAMG;AACH,eAAO,MAAM,iBAAiB,GAAI,MAAM,MAAM,KAAG,WAAW,GAAG,MAM9D,CAAC;AAEF;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,UAAU,GAAI,MAAM,MAAM,KAAG,MAUzC,CAAC;AAEF;;;;;GAKG;AACH,eAAO,MAAM,SAAS,GAAI,MAAM,MAAM,KAAG,MAExC,CAAC;AAMF;;;;;;GAMG;AACH,eAAO,MAAM,aAAa,GAAI,SAAQ,MAAW,KAAG,MAOnD,CAAC;AAEF;;;;;;GAMG;AACH,eAAO,MAAM,eAAe,GAAI,WAAW,MAAM,KAAG,OAQnD,CAAC"}
1
+ {"version":3,"file":"sanitize.d.ts","sourceRoot":"","sources":["../../src/security/sanitize.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAMH;;GAEG;AACH,MAAM,WAAW,eAAe;IAC9B,kDAAkD;IAClD,SAAS,CAAC,EAAE,MAAM,EAAE,CAAC;IACrB,wDAAwD;IACxD,eAAe,CAAC,EAAE,MAAM,EAAE,CAAC;IAC3B,8CAA8C;IAC9C,mBAAmB,CAAC,EAAE,OAAO,CAAC;IAC9B,4DAA4D;IAC5D,YAAY,CAAC,EAAE,OAAO,CAAC;CACxB;AAsBD,qCAAqC;AACrC,UAAU,iBAAiB;IACzB,UAAU,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,WAAW,CAAC;CAC5C;AAED,2EAA2E;AAC3E,UAAU,WAAW;IACnB,QAAQ,IAAI,MAAM,CAAC;CACpB;AAKD;;;GAGG;AACH,eAAO,MAAM,uBAAuB,QAAO,OAE1C,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,qBAAqB,QAAO,iBAAiB,GAAG,IAgB5D,CAAC;AAmbF;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,YAAY,GAAI,MAAM,MAAM,EAAE,UAAS,eAAoB,KAAG,MAE1E,CAAC;AAEF;;;;;;GAMG;AACH,eAAO,MAAM,iBAAiB,GAAI,MAAM,MAAM,KAAG,WAAW,GAAG,MAM9D,CAAC;AAEF;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,UAAU,GAAI,MAAM,MAAM,KAAG,MAUzC,CAAC;AAEF;;;;;GAKG;AACH,eAAO,MAAM,SAAS,GAAI,MAAM,MAAM,KAAG,MAExC,CAAC;AAMF;;;;;;GAMG;AACH,eAAO,MAAM,aAAa,GAAI,SAAQ,MAAW,KAAG,MAOnD,CAAC;AAEF;;;;;;GAMG;AACH,eAAO,MAAM,eAAe,GAAI,WAAW,MAAM,KAAG,OAQnD,CAAC"}
@@ -1,17 +1,17 @@
1
- const h = "bquery-sanitizer";
2
- let l = null;
3
- const x = () => typeof window.trustedTypes < "u", y = () => {
4
- if (l) return l;
5
- const e = window;
6
- if (!e.trustedTypes) return null;
1
+ const w = "bquery-sanitizer";
2
+ let u = null;
3
+ const F = () => typeof window.trustedTypes < "u", C = () => {
4
+ if (u) return u;
5
+ const t = window;
6
+ if (!t.trustedTypes) return null;
7
7
  try {
8
- return l = e.trustedTypes.createPolicy(h, {
9
- createHTML: (t) => p(t)
10
- }), l;
8
+ return u = t.trustedTypes.createPolicy(w, {
9
+ createHTML: (e) => m(e)
10
+ }), u;
11
11
  } catch {
12
- return console.warn(`bQuery: Could not create Trusted Types policy "${h}"`), null;
12
+ return console.warn(`bQuery: Could not create Trusted Types policy "${w}"`), null;
13
13
  }
14
- }, A = /* @__PURE__ */ new Set([
14
+ }, E = /* @__PURE__ */ new Set([
15
15
  "a",
16
16
  "abbr",
17
17
  "address",
@@ -96,7 +96,7 @@ const x = () => typeof window.trustedTypes < "u", y = () => {
96
96
  "ul",
97
97
  "var",
98
98
  "wbr"
99
- ]), g = /* @__PURE__ */ new Set([
99
+ ]), b = /* @__PURE__ */ new Set([
100
100
  "script",
101
101
  "iframe",
102
102
  "frame",
@@ -114,7 +114,7 @@ const x = () => typeof window.trustedTypes < "u", y = () => {
114
114
  "svg",
115
115
  "foreignobject",
116
116
  "noscript"
117
- ]), S = /* @__PURE__ */ new Set([
117
+ ]), v = /* @__PURE__ */ new Set([
118
118
  // Global objects
119
119
  "document",
120
120
  "window",
@@ -151,7 +151,7 @@ const x = () => typeof window.trustedTypes < "u", y = () => {
151
151
  "innerHTML",
152
152
  "outerHTML",
153
153
  "textContent"
154
- ]), L = /* @__PURE__ */ new Set([
154
+ ]), N = /* @__PURE__ */ new Set([
155
155
  "alt",
156
156
  "class",
157
157
  "dir",
@@ -162,79 +162,100 @@ const x = () => typeof window.trustedTypes < "u", y = () => {
162
162
  "lang",
163
163
  "loading",
164
164
  "name",
165
+ "rel",
165
166
  "role",
166
167
  "src",
167
168
  "srcset",
168
169
  "style",
169
170
  "tabindex",
171
+ "target",
170
172
  "title",
171
173
  "type",
172
174
  "width",
173
175
  "aria-*"
174
- ]), C = ["on", "formaction", "xlink:", "xmlns:"], E = ["javascript:", "data:", "vbscript:", "file:"], v = (e, t, o) => {
175
- const a = e.toLowerCase();
176
- for (const u of C)
177
- if (a.startsWith(u)) return !1;
178
- return o && a.startsWith("data-") || a.startsWith("aria-") ? !0 : t.has(a);
179
- }, N = (e) => {
180
- const t = e.toLowerCase().trim();
181
- return !S.has(t);
182
- }, D = (e) => e.replace(/[\u0000-\u001F\u007F]+/g, "").replace(/[\u200B-\u200D\uFEFF\u2028\u2029]+/g, "").replace(/\\u[\da-fA-F]{4}/g, "").replace(/\s+/g, "").toLowerCase(), R = (e) => {
183
- const t = D(e);
184
- for (const o of E)
185
- if (t.startsWith(o)) return !1;
176
+ ]), x = ["on", "formaction", "xlink:", "xmlns:"], R = ["javascript:", "data:", "vbscript:", "file:"], O = (t, e, o) => {
177
+ const s = t.toLowerCase();
178
+ for (const i of x)
179
+ if (s.startsWith(i)) return !1;
180
+ return o && s.startsWith("data-") || s.startsWith("aria-") ? !0 : e.has(s);
181
+ }, U = (t) => {
182
+ const e = t.toLowerCase().trim();
183
+ return !v.has(e);
184
+ }, W = (t) => t.replace(/[\u0000-\u001F\u007F]+/g, "").replace(/[\u200B-\u200D\uFEFF\u2028\u2029]+/g, "").replace(/\\u[\da-fA-F]{4}/g, "").replace(/\s+/g, "").toLowerCase(), _ = (t) => {
185
+ const e = W(t);
186
+ for (const o of R)
187
+ if (e.startsWith(o)) return !1;
186
188
  return !0;
187
- }, p = (e, t = {}) => {
189
+ }, D = (t) => {
190
+ try {
191
+ const e = t.trim();
192
+ if (e.startsWith("//"))
193
+ return !0;
194
+ const o = e.toLowerCase();
195
+ return /^[a-z][a-z0-9+.-]*:/i.test(e) && !o.startsWith("http://") && !o.startsWith("https://") ? !0 : !o.startsWith("http://") && !o.startsWith("https://") ? !1 : typeof window > "u" || !window.location ? !0 : new URL(e, window.location.href).origin !== window.location.origin;
196
+ } catch {
197
+ return !0;
198
+ }
199
+ }, m = (t, e = {}) => {
188
200
  const {
189
201
  allowTags: o = [],
190
- allowAttributes: a = [],
191
- allowDataAttributes: u = !0,
192
- stripAllTags: w = !1
193
- } = t, T = new Set(
194
- [...A, ...o.map((r) => r.toLowerCase())].filter(
195
- (r) => !g.has(r)
202
+ allowAttributes: s = [],
203
+ allowDataAttributes: i = !0,
204
+ stripAllTags: T = !1
205
+ } = e, y = new Set(
206
+ [...E, ...o.map((r) => r.toLowerCase())].filter(
207
+ (r) => !b.has(r)
196
208
  )
197
- ), b = /* @__PURE__ */ new Set([
198
- ...L,
199
- ...a.map((r) => r.toLowerCase())
200
- ]), i = document.createElement("template");
201
- if (i.innerHTML = e, w)
202
- return i.content.textContent ?? "";
203
- const m = document.createTreeWalker(i.content, NodeFilter.SHOW_ELEMENT), d = [];
204
- for (; m.nextNode(); ) {
205
- const r = m.currentNode, f = r.tagName.toLowerCase();
206
- if (g.has(f)) {
209
+ ), A = /* @__PURE__ */ new Set([
210
+ ...N,
211
+ ...s.map((r) => r.toLowerCase())
212
+ ]), c = document.createElement("template");
213
+ if (c.innerHTML = t, T)
214
+ return c.content.textContent ?? "";
215
+ const h = document.createTreeWalker(c.content, NodeFilter.SHOW_ELEMENT), d = [];
216
+ for (; h.nextNode(); ) {
217
+ const r = h.currentNode, p = r.tagName.toLowerCase();
218
+ if (b.has(p)) {
207
219
  d.push(r);
208
220
  continue;
209
221
  }
210
- if (!T.has(f)) {
222
+ if (!y.has(p)) {
211
223
  d.push(r);
212
224
  continue;
213
225
  }
214
- const c = [];
226
+ const l = [];
215
227
  for (const n of Array.from(r.attributes)) {
216
- const s = n.name.toLowerCase();
217
- if (!v(s, b, u)) {
218
- c.push(n.name);
228
+ const a = n.name.toLowerCase();
229
+ if (!O(a, A, i)) {
230
+ l.push(n.name);
219
231
  continue;
220
232
  }
221
- if ((s === "id" || s === "name") && !N(n.value)) {
222
- c.push(n.name);
233
+ if ((a === "id" || a === "name") && !U(n.value)) {
234
+ l.push(n.name);
223
235
  continue;
224
236
  }
225
- (s === "href" || s === "src" || s === "srcset") && !R(n.value) && c.push(n.name);
237
+ (a === "href" || a === "src" || a === "srcset") && !_(n.value) && l.push(n.name);
226
238
  }
227
- for (const n of c)
239
+ for (const n of l)
228
240
  r.removeAttribute(n);
241
+ if (p === "a") {
242
+ const n = r.getAttribute("href"), L = r.getAttribute("target")?.toLowerCase() === "_blank", S = n && D(n);
243
+ if (L || S) {
244
+ const g = r.getAttribute("rel"), f = new Set(
245
+ g ? g.split(/\s+/).filter(Boolean) : []
246
+ );
247
+ f.add("noopener"), f.add("noreferrer"), r.setAttribute("rel", Array.from(f).join(" "));
248
+ }
249
+ }
229
250
  }
230
251
  for (const r of d)
231
252
  r.remove();
232
- return i.innerHTML;
233
- }, _ = (e, t = {}) => p(e, t), F = (e) => {
234
- const t = y();
235
- return t ? t.createHTML(e) : _(e);
236
- }, H = (e) => {
237
- const t = {
253
+ return c.innerHTML;
254
+ }, k = (t, e = {}) => m(t, e), H = (t) => {
255
+ const e = C();
256
+ return e ? e.createHTML(t) : k(t);
257
+ }, P = (t) => {
258
+ const e = {
238
259
  "&": "&amp;",
239
260
  "<": "&lt;",
240
261
  ">": "&gt;",
@@ -242,23 +263,23 @@ const x = () => typeof window.trustedTypes < "u", y = () => {
242
263
  "'": "&#x27;",
243
264
  "`": "&#x60;"
244
265
  };
245
- return e.replace(/[&<>"'`]/g, (o) => t[o]);
246
- }, O = (e) => p(e, { stripAllTags: !0 }), k = (e = 16) => {
247
- const t = new Uint8Array(e);
248
- return crypto.getRandomValues(t), btoa(String.fromCharCode(...t)).replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
249
- }, M = (e) => {
250
- const t = document.querySelector('meta[http-equiv="Content-Security-Policy"]');
251
- return t ? (t.getAttribute("content") ?? "").includes(e) : !1;
266
+ return t.replace(/[&<>"'`]/g, (o) => e[o]);
267
+ }, M = (t) => m(t, { stripAllTags: !0 }), z = (t = 16) => {
268
+ const e = new Uint8Array(t);
269
+ return crypto.getRandomValues(e), btoa(String.fromCharCode(...e)).replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
270
+ }, j = (t) => {
271
+ const e = document.querySelector('meta[http-equiv="Content-Security-Policy"]');
272
+ return e ? (e.getAttribute("content") ?? "").includes(t) : !1;
252
273
  };
253
274
  export {
254
- F as createTrustedHtml,
255
- H as escapeHtml,
256
- k as generateNonce,
257
- y as getTrustedTypesPolicy,
258
- M as hasCSPDirective,
259
- x as isTrustedTypesSupported,
260
- _ as sanitize,
261
- _ as sanitizeHtml,
262
- O as stripTags
275
+ H as createTrustedHtml,
276
+ P as escapeHtml,
277
+ z as generateNonce,
278
+ C as getTrustedTypesPolicy,
279
+ j as hasCSPDirective,
280
+ F as isTrustedTypesSupported,
281
+ k as sanitize,
282
+ k as sanitizeHtml,
283
+ M as stripTags
263
284
  };
264
285
  //# sourceMappingURL=security.es.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"security.es.mjs","sources":["../src/security/sanitize.ts"],"sourcesContent":["/**\r\n * Security utilities for HTML sanitization, CSP compatibility, and Trusted Types.\r\n * All DOM writes are sanitized by default to prevent XSS attacks.\r\n *\r\n * @module bquery/security\r\n */\r\n\r\n// ============================================================================\r\n// Types\r\n// ============================================================================\r\n\r\n/**\r\n * Sanitizer configuration options.\r\n */\r\nexport interface SanitizeOptions {\r\n /** Allow these additional tags (default: none) */\r\n allowTags?: string[];\r\n /** Allow these additional attributes (default: none) */\r\n allowAttributes?: string[];\r\n /** Allow data-* attributes (default: true) */\r\n allowDataAttributes?: boolean;\r\n /** Strip all tags and return plain text (default: false) */\r\n stripAllTags?: boolean;\r\n}\r\n\r\n/**\r\n * Trusted Types policy name.\r\n */\r\nconst POLICY_NAME = 'bquery-sanitizer';\r\n\r\n// ============================================================================\r\n// Trusted Types Support\r\n// ============================================================================\r\n\r\n/** Window interface extended with Trusted Types */\r\ninterface TrustedTypesWindow extends Window {\r\n trustedTypes?: {\r\n createPolicy: (\r\n name: string,\r\n rules: { createHTML?: (input: string) => string }\r\n ) => TrustedTypePolicy;\r\n isHTML?: (value: unknown) => boolean;\r\n };\r\n}\r\n\r\n/** Trusted Types policy interface */\r\ninterface TrustedTypePolicy {\r\n createHTML: (input: string) => TrustedHTML;\r\n}\r\n\r\n/** Trusted HTML type placeholder for environments without Trusted Types */\r\ninterface TrustedHTML {\r\n toString(): string;\r\n}\r\n\r\n/** Cached Trusted Types policy */\r\nlet cachedPolicy: TrustedTypePolicy | null = null;\r\n\r\n/**\r\n * Check if Trusted Types API is available.\r\n * @returns True if Trusted Types are supported\r\n */\r\nexport const isTrustedTypesSupported = (): boolean => {\r\n return typeof (window as TrustedTypesWindow).trustedTypes !== 'undefined';\r\n};\r\n\r\n/**\r\n * Get or create the bQuery Trusted Types policy.\r\n * @returns The Trusted Types policy or null if unsupported\r\n */\r\nexport const getTrustedTypesPolicy = (): TrustedTypePolicy | null => {\r\n if (cachedPolicy) return cachedPolicy;\r\n\r\n const win = window as TrustedTypesWindow;\r\n if (!win.trustedTypes) return null;\r\n\r\n try {\r\n cachedPolicy = win.trustedTypes.createPolicy(POLICY_NAME, {\r\n createHTML: (input: string) => sanitizeHtmlCore(input),\r\n });\r\n return cachedPolicy;\r\n } catch {\r\n // Policy may already exist or be blocked by CSP\r\n console.warn(`bQuery: Could not create Trusted Types policy \"${POLICY_NAME}\"`);\r\n return null;\r\n }\r\n};\r\n\r\n// ============================================================================\r\n// Default Safe Lists\r\n// ============================================================================\r\n\r\n/**\r\n * Default allowed HTML tags considered safe.\r\n */\r\nconst DEFAULT_ALLOWED_TAGS = new Set([\r\n 'a',\r\n 'abbr',\r\n 'address',\r\n 'article',\r\n 'aside',\r\n 'b',\r\n 'bdi',\r\n 'bdo',\r\n 'blockquote',\r\n 'br',\r\n 'button',\r\n 'caption',\r\n 'cite',\r\n 'code',\r\n 'col',\r\n 'colgroup',\r\n 'data',\r\n 'dd',\r\n 'del',\r\n 'details',\r\n 'dfn',\r\n 'div',\r\n 'dl',\r\n 'dt',\r\n 'em',\r\n 'figcaption',\r\n 'figure',\r\n 'footer',\r\n 'form',\r\n 'h1',\r\n 'h2',\r\n 'h3',\r\n 'h4',\r\n 'h5',\r\n 'h6',\r\n 'header',\r\n 'hgroup',\r\n 'hr',\r\n 'i',\r\n 'img',\r\n 'input',\r\n 'ins',\r\n 'kbd',\r\n 'label',\r\n 'legend',\r\n 'li',\r\n 'main',\r\n 'mark',\r\n 'nav',\r\n 'ol',\r\n 'optgroup',\r\n 'option',\r\n 'p',\r\n 'picture',\r\n 'pre',\r\n 'progress',\r\n 'q',\r\n 'rp',\r\n 'rt',\r\n 'ruby',\r\n 's',\r\n 'samp',\r\n 'section',\r\n 'select',\r\n 'small',\r\n 'source',\r\n 'span',\r\n 'strong',\r\n 'sub',\r\n 'summary',\r\n 'sup',\r\n 'table',\r\n 'tbody',\r\n 'td',\r\n 'textarea',\r\n 'tfoot',\r\n 'th',\r\n 'thead',\r\n 'time',\r\n 'tr',\r\n 'u',\r\n 'ul',\r\n 'var',\r\n 'wbr',\r\n]);\r\n\r\n/**\r\n * Explicitly dangerous tags that should never be allowed.\r\n * These are checked even if somehow added to allowTags.\r\n */\r\nconst DANGEROUS_TAGS = new Set([\r\n 'script',\r\n 'iframe',\r\n 'frame',\r\n 'frameset',\r\n 'object',\r\n 'embed',\r\n 'applet',\r\n 'link',\r\n 'meta',\r\n 'style',\r\n 'base',\r\n 'template',\r\n 'slot',\r\n 'math',\r\n 'svg',\r\n 'foreignobject',\r\n 'noscript',\r\n]);\r\n\r\n/**\r\n * Reserved IDs that could cause DOM clobbering attacks.\r\n * These are prevented to avoid overwriting global browser objects.\r\n */\r\nconst RESERVED_IDS = new Set([\r\n // Global objects\r\n 'document',\r\n 'window',\r\n 'location',\r\n 'top',\r\n 'self',\r\n 'parent',\r\n 'frames',\r\n 'history',\r\n 'navigator',\r\n 'screen',\r\n // Dangerous functions\r\n 'alert',\r\n 'confirm',\r\n 'prompt',\r\n 'eval',\r\n 'Function',\r\n // Document properties\r\n 'cookie',\r\n 'domain',\r\n 'referrer',\r\n 'body',\r\n 'head',\r\n 'forms',\r\n 'images',\r\n 'links',\r\n 'scripts',\r\n // DOM traversal properties\r\n 'children',\r\n 'parentNode',\r\n 'firstChild',\r\n 'lastChild',\r\n // Content manipulation\r\n 'innerHTML',\r\n 'outerHTML',\r\n 'textContent',\r\n]);\r\n\r\n/**\r\n * Default allowed attributes considered safe.\r\n */\r\nconst DEFAULT_ALLOWED_ATTRIBUTES = new Set([\r\n 'alt',\r\n 'class',\r\n 'dir',\r\n 'height',\r\n 'hidden',\r\n 'href',\r\n 'id',\r\n 'lang',\r\n 'loading',\r\n 'name',\r\n 'role',\r\n 'src',\r\n 'srcset',\r\n 'style',\r\n 'tabindex',\r\n 'title',\r\n 'type',\r\n 'width',\r\n 'aria-*',\r\n]);\r\n\r\n/**\r\n * Dangerous attribute prefixes to always remove.\r\n */\r\nconst DANGEROUS_ATTR_PREFIXES = ['on', 'formaction', 'xlink:', 'xmlns:'];\r\n\r\n/**\r\n * Dangerous URL protocols to block.\r\n */\r\nconst DANGEROUS_PROTOCOLS = ['javascript:', 'data:', 'vbscript:', 'file:'];\r\n\r\n// ============================================================================\r\n// Core Sanitization\r\n// ============================================================================\r\n\r\n/**\r\n * Check if an attribute name is allowed.\r\n * @internal\r\n */\r\nconst isAllowedAttribute = (\r\n name: string,\r\n allowedSet: Set<string>,\r\n allowDataAttrs: boolean\r\n): boolean => {\r\n const lowerName = name.toLowerCase();\r\n\r\n // Check dangerous prefixes\r\n for (const prefix of DANGEROUS_ATTR_PREFIXES) {\r\n if (lowerName.startsWith(prefix)) return false;\r\n }\r\n\r\n // Check data attributes\r\n if (allowDataAttrs && lowerName.startsWith('data-')) return true;\r\n\r\n // Check aria attributes (allowed by default)\r\n if (lowerName.startsWith('aria-')) return true;\r\n\r\n // Check explicit allow list\r\n return allowedSet.has(lowerName);\r\n};\r\n\r\n/**\r\n * Check if an ID/name value could cause DOM clobbering.\r\n * @internal\r\n */\r\nconst isSafeIdOrName = (value: string): boolean => {\r\n const lowerValue = value.toLowerCase().trim();\r\n return !RESERVED_IDS.has(lowerValue);\r\n};\r\n\r\n/**\r\n * Normalize URL by removing control characters, whitespace, and Unicode tricks.\r\n * Enhanced to prevent various bypass techniques.\r\n * @internal\r\n */\r\nconst normalizeUrl = (value: string): string =>\r\n value\r\n // Remove null bytes and control characters\r\n .replace(/[\\u0000-\\u001F\\u007F]+/g, '')\r\n // Remove zero-width characters that could hide malicious content\r\n .replace(/[\\u200B-\\u200D\\uFEFF\\u2028\\u2029]+/g, '')\r\n // Remove escaped Unicode sequences\r\n .replace(/\\\\u[\\da-fA-F]{4}/g, '')\r\n // Remove whitespace\r\n .replace(/\\s+/g, '')\r\n // Normalize case\r\n .toLowerCase();\r\n\r\n/**\r\n * Check if a URL value is safe.\r\n * @internal\r\n */\r\nconst isSafeUrl = (value: string): boolean => {\r\n const normalized = normalizeUrl(value);\r\n for (const protocol of DANGEROUS_PROTOCOLS) {\r\n if (normalized.startsWith(protocol)) return false;\r\n }\r\n return true;\r\n};\r\n\r\n/**\r\n * Core sanitization logic (without Trusted Types wrapper).\r\n * @internal\r\n */\r\nconst sanitizeHtmlCore = (html: string, options: SanitizeOptions = {}): string => {\r\n const {\r\n allowTags = [],\r\n allowAttributes = [],\r\n allowDataAttributes = true,\r\n stripAllTags = false,\r\n } = options;\r\n\r\n // Build combined allow sets (excluding dangerous tags even if specified)\r\n const allowedTags = new Set(\r\n [...DEFAULT_ALLOWED_TAGS, ...allowTags.map((t) => t.toLowerCase())].filter(\r\n (tag) => !DANGEROUS_TAGS.has(tag)\r\n )\r\n );\r\n const allowedAttrs = new Set([\r\n ...DEFAULT_ALLOWED_ATTRIBUTES,\r\n ...allowAttributes.map((a) => a.toLowerCase()),\r\n ]);\r\n\r\n // Use template for parsing\r\n const template = document.createElement('template');\r\n template.innerHTML = html;\r\n\r\n if (stripAllTags) {\r\n return template.content.textContent ?? '';\r\n }\r\n\r\n // Walk the DOM tree\r\n const walker = document.createTreeWalker(template.content, NodeFilter.SHOW_ELEMENT);\r\n\r\n const toRemove: Element[] = [];\r\n\r\n while (walker.nextNode()) {\r\n const el = walker.currentNode as Element;\r\n const tagName = el.tagName.toLowerCase();\r\n\r\n // Remove explicitly dangerous tags even if in allow list\r\n if (DANGEROUS_TAGS.has(tagName)) {\r\n toRemove.push(el);\r\n continue;\r\n }\r\n\r\n // Remove disallowed tags entirely\r\n if (!allowedTags.has(tagName)) {\r\n toRemove.push(el);\r\n continue;\r\n }\r\n\r\n // Process attributes\r\n const attrsToRemove: string[] = [];\r\n for (const attr of Array.from(el.attributes)) {\r\n const attrName = attr.name.toLowerCase();\r\n\r\n // Check if attribute is allowed\r\n if (!isAllowedAttribute(attrName, allowedAttrs, allowDataAttributes)) {\r\n attrsToRemove.push(attr.name);\r\n continue;\r\n }\r\n\r\n // Check for DOM clobbering on id and name attributes\r\n if ((attrName === 'id' || attrName === 'name') && !isSafeIdOrName(attr.value)) {\r\n attrsToRemove.push(attr.name);\r\n continue;\r\n }\r\n\r\n // Validate URL attributes\r\n if (\r\n (attrName === 'href' || attrName === 'src' || attrName === 'srcset') &&\r\n !isSafeUrl(attr.value)\r\n ) {\r\n attrsToRemove.push(attr.name);\r\n }\r\n }\r\n\r\n // Remove disallowed attributes\r\n for (const attrName of attrsToRemove) {\r\n el.removeAttribute(attrName);\r\n }\r\n }\r\n\r\n // Remove disallowed elements\r\n for (const el of toRemove) {\r\n el.remove();\r\n }\r\n\r\n return template.innerHTML;\r\n};\r\n\r\n// ============================================================================\r\n// Public API\r\n// ============================================================================\r\n\r\n/**\r\n * Sanitize HTML string, removing dangerous elements and attributes.\r\n * Uses Trusted Types when available for CSP compliance.\r\n *\r\n * @param html - The HTML string to sanitize\r\n * @param options - Sanitization options\r\n * @returns Sanitized HTML string\r\n *\r\n * @example\r\n * ```ts\r\n * const safe = sanitizeHtml('<div onclick=\"alert(1)\">Hello</div>');\r\n * // Returns: '<div>Hello</div>'\r\n * ```\r\n */\r\nexport const sanitizeHtml = (html: string, options: SanitizeOptions = {}): string => {\r\n return sanitizeHtmlCore(html, options);\r\n};\r\n\r\n/**\r\n * Create a Trusted HTML value for use with Trusted Types-enabled sites.\r\n * Falls back to regular string when Trusted Types are unavailable.\r\n *\r\n * @param html - The HTML string to wrap\r\n * @returns Trusted HTML value or sanitized string\r\n */\r\nexport const createTrustedHtml = (html: string): TrustedHTML | string => {\r\n const policy = getTrustedTypesPolicy();\r\n if (policy) {\r\n return policy.createHTML(html);\r\n }\r\n return sanitizeHtml(html);\r\n};\r\n\r\n/**\r\n * Escape HTML entities to prevent XSS.\r\n * Use this for displaying user content as text.\r\n *\r\n * @param text - The text to escape\r\n * @returns Escaped HTML string\r\n *\r\n * @example\r\n * ```ts\r\n * escapeHtml('<script>alert(1)</script>');\r\n * // Returns: '&lt;script&gt;alert(1)&lt;/script&gt;'\r\n * ```\r\n */\r\nexport const escapeHtml = (text: string): string => {\r\n const escapeMap: Record<string, string> = {\r\n '&': '&amp;',\r\n '<': '&lt;',\r\n '>': '&gt;',\r\n '\"': '&quot;',\r\n \"'\": '&#x27;',\r\n '`': '&#x60;',\r\n };\r\n return text.replace(/[&<>\"'`]/g, (char) => escapeMap[char]);\r\n};\r\n\r\n/**\r\n * Strip all HTML tags and return plain text.\r\n *\r\n * @param html - The HTML string to strip\r\n * @returns Plain text content\r\n */\r\nexport const stripTags = (html: string): string => {\r\n return sanitizeHtmlCore(html, { stripAllTags: true });\r\n};\r\n\r\n// ============================================================================\r\n// CSP Helpers\r\n// ============================================================================\r\n\r\n/**\r\n * Generate a nonce for inline scripts/styles.\r\n * Use with Content-Security-Policy nonce directives.\r\n *\r\n * @param length - Nonce length (default: 16)\r\n * @returns Cryptographically random nonce string\r\n */\r\nexport const generateNonce = (length: number = 16): string => {\r\n const array = new Uint8Array(length);\r\n crypto.getRandomValues(array);\r\n return btoa(String.fromCharCode(...array))\r\n .replace(/\\+/g, '-')\r\n .replace(/\\//g, '_')\r\n .replace(/=/g, '');\r\n};\r\n\r\n/**\r\n * Check if a CSP header is present with specific directive.\r\n * Useful for feature detection and fallback strategies.\r\n *\r\n * @param directive - The CSP directive to check (e.g., 'script-src')\r\n * @returns True if the directive appears to be enforced\r\n */\r\nexport const hasCSPDirective = (directive: string): boolean => {\r\n // Check meta tag\r\n const meta = document.querySelector('meta[http-equiv=\"Content-Security-Policy\"]');\r\n if (meta) {\r\n const content = meta.getAttribute('content') ?? '';\r\n return content.includes(directive);\r\n }\r\n return false;\r\n};\r\n"],"names":["POLICY_NAME","cachedPolicy","isTrustedTypesSupported","getTrustedTypesPolicy","win","input","sanitizeHtmlCore","DEFAULT_ALLOWED_TAGS","DANGEROUS_TAGS","RESERVED_IDS","DEFAULT_ALLOWED_ATTRIBUTES","DANGEROUS_ATTR_PREFIXES","DANGEROUS_PROTOCOLS","isAllowedAttribute","name","allowedSet","allowDataAttrs","lowerName","prefix","isSafeIdOrName","value","lowerValue","normalizeUrl","isSafeUrl","normalized","protocol","html","options","allowTags","allowAttributes","allowDataAttributes","stripAllTags","allowedTags","t","tag","allowedAttrs","a","template","walker","toRemove","el","tagName","attrsToRemove","attr","attrName","sanitizeHtml","createTrustedHtml","policy","escapeHtml","text","escapeMap","char","stripTags","generateNonce","length","array","hasCSPDirective","directive","meta"],"mappings":"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"}
1
+ {"version":3,"file":"security.es.mjs","sources":["../src/security/sanitize.ts"],"sourcesContent":["/**\r\n * Security utilities for HTML sanitization, CSP compatibility, and Trusted Types.\r\n * All DOM writes are sanitized by default to prevent XSS attacks.\r\n *\r\n * @module bquery/security\r\n */\r\n\r\n// ============================================================================\r\n// Types\r\n// ============================================================================\r\n\r\n/**\r\n * Sanitizer configuration options.\r\n */\r\nexport interface SanitizeOptions {\r\n /** Allow these additional tags (default: none) */\r\n allowTags?: string[];\r\n /** Allow these additional attributes (default: none) */\r\n allowAttributes?: string[];\r\n /** Allow data-* attributes (default: true) */\r\n allowDataAttributes?: boolean;\r\n /** Strip all tags and return plain text (default: false) */\r\n stripAllTags?: boolean;\r\n}\r\n\r\n/**\r\n * Trusted Types policy name.\r\n */\r\nconst POLICY_NAME = 'bquery-sanitizer';\r\n\r\n// ============================================================================\r\n// Trusted Types Support\r\n// ============================================================================\r\n\r\n/** Window interface extended with Trusted Types */\r\ninterface TrustedTypesWindow extends Window {\r\n trustedTypes?: {\r\n createPolicy: (\r\n name: string,\r\n rules: { createHTML?: (input: string) => string }\r\n ) => TrustedTypePolicy;\r\n isHTML?: (value: unknown) => boolean;\r\n };\r\n}\r\n\r\n/** Trusted Types policy interface */\r\ninterface TrustedTypePolicy {\r\n createHTML: (input: string) => TrustedHTML;\r\n}\r\n\r\n/** Trusted HTML type placeholder for environments without Trusted Types */\r\ninterface TrustedHTML {\r\n toString(): string;\r\n}\r\n\r\n/** Cached Trusted Types policy */\r\nlet cachedPolicy: TrustedTypePolicy | null = null;\r\n\r\n/**\r\n * Check if Trusted Types API is available.\r\n * @returns True if Trusted Types are supported\r\n */\r\nexport const isTrustedTypesSupported = (): boolean => {\r\n return typeof (window as TrustedTypesWindow).trustedTypes !== 'undefined';\r\n};\r\n\r\n/**\r\n * Get or create the bQuery Trusted Types policy.\r\n * @returns The Trusted Types policy or null if unsupported\r\n */\r\nexport const getTrustedTypesPolicy = (): TrustedTypePolicy | null => {\r\n if (cachedPolicy) return cachedPolicy;\r\n\r\n const win = window as TrustedTypesWindow;\r\n if (!win.trustedTypes) return null;\r\n\r\n try {\r\n cachedPolicy = win.trustedTypes.createPolicy(POLICY_NAME, {\r\n createHTML: (input: string) => sanitizeHtmlCore(input),\r\n });\r\n return cachedPolicy;\r\n } catch {\r\n // Policy may already exist or be blocked by CSP\r\n console.warn(`bQuery: Could not create Trusted Types policy \"${POLICY_NAME}\"`);\r\n return null;\r\n }\r\n};\r\n\r\n// ============================================================================\r\n// Default Safe Lists\r\n// ============================================================================\r\n\r\n/**\r\n * Default allowed HTML tags considered safe.\r\n */\r\nconst DEFAULT_ALLOWED_TAGS = new Set([\r\n 'a',\r\n 'abbr',\r\n 'address',\r\n 'article',\r\n 'aside',\r\n 'b',\r\n 'bdi',\r\n 'bdo',\r\n 'blockquote',\r\n 'br',\r\n 'button',\r\n 'caption',\r\n 'cite',\r\n 'code',\r\n 'col',\r\n 'colgroup',\r\n 'data',\r\n 'dd',\r\n 'del',\r\n 'details',\r\n 'dfn',\r\n 'div',\r\n 'dl',\r\n 'dt',\r\n 'em',\r\n 'figcaption',\r\n 'figure',\r\n 'footer',\r\n 'form',\r\n 'h1',\r\n 'h2',\r\n 'h3',\r\n 'h4',\r\n 'h5',\r\n 'h6',\r\n 'header',\r\n 'hgroup',\r\n 'hr',\r\n 'i',\r\n 'img',\r\n 'input',\r\n 'ins',\r\n 'kbd',\r\n 'label',\r\n 'legend',\r\n 'li',\r\n 'main',\r\n 'mark',\r\n 'nav',\r\n 'ol',\r\n 'optgroup',\r\n 'option',\r\n 'p',\r\n 'picture',\r\n 'pre',\r\n 'progress',\r\n 'q',\r\n 'rp',\r\n 'rt',\r\n 'ruby',\r\n 's',\r\n 'samp',\r\n 'section',\r\n 'select',\r\n 'small',\r\n 'source',\r\n 'span',\r\n 'strong',\r\n 'sub',\r\n 'summary',\r\n 'sup',\r\n 'table',\r\n 'tbody',\r\n 'td',\r\n 'textarea',\r\n 'tfoot',\r\n 'th',\r\n 'thead',\r\n 'time',\r\n 'tr',\r\n 'u',\r\n 'ul',\r\n 'var',\r\n 'wbr',\r\n]);\r\n\r\n/**\r\n * Explicitly dangerous tags that should never be allowed.\r\n * These are checked even if somehow added to allowTags.\r\n */\r\nconst DANGEROUS_TAGS = new Set([\r\n 'script',\r\n 'iframe',\r\n 'frame',\r\n 'frameset',\r\n 'object',\r\n 'embed',\r\n 'applet',\r\n 'link',\r\n 'meta',\r\n 'style',\r\n 'base',\r\n 'template',\r\n 'slot',\r\n 'math',\r\n 'svg',\r\n 'foreignobject',\r\n 'noscript',\r\n]);\r\n\r\n/**\r\n * Reserved IDs that could cause DOM clobbering attacks.\r\n * These are prevented to avoid overwriting global browser objects.\r\n */\r\nconst RESERVED_IDS = new Set([\r\n // Global objects\r\n 'document',\r\n 'window',\r\n 'location',\r\n 'top',\r\n 'self',\r\n 'parent',\r\n 'frames',\r\n 'history',\r\n 'navigator',\r\n 'screen',\r\n // Dangerous functions\r\n 'alert',\r\n 'confirm',\r\n 'prompt',\r\n 'eval',\r\n 'Function',\r\n // Document properties\r\n 'cookie',\r\n 'domain',\r\n 'referrer',\r\n 'body',\r\n 'head',\r\n 'forms',\r\n 'images',\r\n 'links',\r\n 'scripts',\r\n // DOM traversal properties\r\n 'children',\r\n 'parentNode',\r\n 'firstChild',\r\n 'lastChild',\r\n // Content manipulation\r\n 'innerHTML',\r\n 'outerHTML',\r\n 'textContent',\r\n]);\r\n\r\n/**\r\n * Default allowed attributes considered safe.\r\n */\r\nconst DEFAULT_ALLOWED_ATTRIBUTES = new Set([\r\n 'alt',\r\n 'class',\r\n 'dir',\r\n 'height',\r\n 'hidden',\r\n 'href',\r\n 'id',\r\n 'lang',\r\n 'loading',\r\n 'name',\r\n 'rel',\r\n 'role',\r\n 'src',\r\n 'srcset',\r\n 'style',\r\n 'tabindex',\r\n 'target',\r\n 'title',\r\n 'type',\r\n 'width',\r\n 'aria-*',\r\n]);\r\n\r\n/**\r\n * Dangerous attribute prefixes to always remove.\r\n */\r\nconst DANGEROUS_ATTR_PREFIXES = ['on', 'formaction', 'xlink:', 'xmlns:'];\r\n\r\n/**\r\n * Dangerous URL protocols to block.\r\n */\r\nconst DANGEROUS_PROTOCOLS = ['javascript:', 'data:', 'vbscript:', 'file:'];\r\n\r\n// ============================================================================\r\n// Core Sanitization\r\n// ============================================================================\r\n\r\n/**\r\n * Check if an attribute name is allowed.\r\n * @internal\r\n */\r\nconst isAllowedAttribute = (\r\n name: string,\r\n allowedSet: Set<string>,\r\n allowDataAttrs: boolean\r\n): boolean => {\r\n const lowerName = name.toLowerCase();\r\n\r\n // Check dangerous prefixes\r\n for (const prefix of DANGEROUS_ATTR_PREFIXES) {\r\n if (lowerName.startsWith(prefix)) return false;\r\n }\r\n\r\n // Check data attributes\r\n if (allowDataAttrs && lowerName.startsWith('data-')) return true;\r\n\r\n // Check aria attributes (allowed by default)\r\n if (lowerName.startsWith('aria-')) return true;\r\n\r\n // Check explicit allow list\r\n return allowedSet.has(lowerName);\r\n};\r\n\r\n/**\r\n * Check if an ID/name value could cause DOM clobbering.\r\n * @internal\r\n */\r\nconst isSafeIdOrName = (value: string): boolean => {\r\n const lowerValue = value.toLowerCase().trim();\r\n return !RESERVED_IDS.has(lowerValue);\r\n};\r\n\r\n/**\r\n * Normalize URL by removing control characters, whitespace, and Unicode tricks.\r\n * Enhanced to prevent various bypass techniques.\r\n * @internal\r\n */\r\nconst normalizeUrl = (value: string): string =>\r\n value\r\n // Remove null bytes and control characters\r\n .replace(/[\\u0000-\\u001F\\u007F]+/g, '')\r\n // Remove zero-width characters that could hide malicious content\r\n .replace(/[\\u200B-\\u200D\\uFEFF\\u2028\\u2029]+/g, '')\r\n // Remove escaped Unicode sequences\r\n .replace(/\\\\u[\\da-fA-F]{4}/g, '')\r\n // Remove whitespace\r\n .replace(/\\s+/g, '')\r\n // Normalize case\r\n .toLowerCase();\r\n\r\n/**\r\n * Check if a URL value is safe.\r\n * @internal\r\n */\r\nconst isSafeUrl = (value: string): boolean => {\r\n const normalized = normalizeUrl(value);\r\n for (const protocol of DANGEROUS_PROTOCOLS) {\r\n if (normalized.startsWith(protocol)) return false;\r\n }\r\n return true;\r\n};\r\n\r\n/**\r\n * Check if a URL is external (different origin).\r\n * @internal\r\n */\r\nconst isExternalUrl = (url: string): boolean => {\r\n try {\r\n // Normalize URL by trimming whitespace\r\n const trimmedUrl = url.trim();\r\n \r\n // Protocol-relative URLs (//example.com) are always external.\r\n // CRITICAL: This check must run before the relative-URL check below;\r\n // otherwise, a protocol-relative URL like \"//evil.com\" would be treated\r\n // as a non-http(s) relative URL and incorrectly classified as same-origin.\r\n // Handling them up front guarantees correct security classification.\r\n if (trimmedUrl.startsWith('//')) {\r\n return true;\r\n }\r\n \r\n // Normalize URL for case-insensitive protocol checks\r\n const lowerUrl = trimmedUrl.toLowerCase();\r\n \r\n // Check for non-http(s) protocols which are considered external/special\r\n // (mailto:, tel:, ftp:, etc.)\r\n const hasProtocol = /^[a-z][a-z0-9+.-]*:/i.test(trimmedUrl);\r\n if (hasProtocol && !lowerUrl.startsWith('http://') && !lowerUrl.startsWith('https://')) {\r\n // These are special protocols, not traditional \"external\" links\r\n // but we treat them as external for security consistency\r\n return true;\r\n }\r\n \r\n // Relative URLs are not external\r\n if (!lowerUrl.startsWith('http://') && !lowerUrl.startsWith('https://')) {\r\n return false;\r\n }\r\n \r\n // In non-browser environments (e.g., Node.js), treat all absolute URLs as external\r\n if (typeof window === 'undefined' || !window.location) {\r\n return true;\r\n }\r\n \r\n const urlObj = new URL(trimmedUrl, window.location.href);\r\n return urlObj.origin !== window.location.origin;\r\n } catch {\r\n // If URL parsing fails, treat as potentially external for safety\r\n return true;\r\n }\r\n};\r\n\r\n/**\r\n * Core sanitization logic (without Trusted Types wrapper).\r\n * @internal\r\n */\r\nconst sanitizeHtmlCore = (html: string, options: SanitizeOptions = {}): string => {\r\n const {\r\n allowTags = [],\r\n allowAttributes = [],\r\n allowDataAttributes = true,\r\n stripAllTags = false,\r\n } = options;\r\n\r\n // Build combined allow sets (excluding dangerous tags even if specified)\r\n const allowedTags = new Set(\r\n [...DEFAULT_ALLOWED_TAGS, ...allowTags.map((t) => t.toLowerCase())].filter(\r\n (tag) => !DANGEROUS_TAGS.has(tag)\r\n )\r\n );\r\n const allowedAttrs = new Set([\r\n ...DEFAULT_ALLOWED_ATTRIBUTES,\r\n ...allowAttributes.map((a) => a.toLowerCase()),\r\n ]);\r\n\r\n // Use template for parsing\r\n const template = document.createElement('template');\r\n template.innerHTML = html;\r\n\r\n if (stripAllTags) {\r\n return template.content.textContent ?? '';\r\n }\r\n\r\n // Walk the DOM tree\r\n const walker = document.createTreeWalker(template.content, NodeFilter.SHOW_ELEMENT);\r\n\r\n const toRemove: Element[] = [];\r\n\r\n while (walker.nextNode()) {\r\n const el = walker.currentNode as Element;\r\n const tagName = el.tagName.toLowerCase();\r\n\r\n // Remove explicitly dangerous tags even if in allow list\r\n if (DANGEROUS_TAGS.has(tagName)) {\r\n toRemove.push(el);\r\n continue;\r\n }\r\n\r\n // Remove disallowed tags entirely\r\n if (!allowedTags.has(tagName)) {\r\n toRemove.push(el);\r\n continue;\r\n }\r\n\r\n // Process attributes\r\n const attrsToRemove: string[] = [];\r\n for (const attr of Array.from(el.attributes)) {\r\n const attrName = attr.name.toLowerCase();\r\n\r\n // Check if attribute is allowed\r\n if (!isAllowedAttribute(attrName, allowedAttrs, allowDataAttributes)) {\r\n attrsToRemove.push(attr.name);\r\n continue;\r\n }\r\n\r\n // Check for DOM clobbering on id and name attributes\r\n if ((attrName === 'id' || attrName === 'name') && !isSafeIdOrName(attr.value)) {\r\n attrsToRemove.push(attr.name);\r\n continue;\r\n }\r\n\r\n // Validate URL attributes\r\n if (\r\n (attrName === 'href' || attrName === 'src' || attrName === 'srcset') &&\r\n !isSafeUrl(attr.value)\r\n ) {\r\n attrsToRemove.push(attr.name);\r\n }\r\n }\r\n\r\n // Remove disallowed attributes\r\n for (const attrName of attrsToRemove) {\r\n el.removeAttribute(attrName);\r\n }\r\n\r\n // Add rel=\"noopener noreferrer\" to external links for security\r\n if (tagName === 'a') {\r\n const href = el.getAttribute('href');\r\n const target = el.getAttribute('target');\r\n const hasTargetBlank = target?.toLowerCase() === '_blank';\r\n const isExternal = href && isExternalUrl(href);\r\n\r\n // Add security attributes to links opening in new window or external links\r\n if (hasTargetBlank || isExternal) {\r\n const existingRel = el.getAttribute('rel');\r\n const relValues = new Set(\r\n existingRel ? existingRel.split(/\\s+/).filter(Boolean) : []\r\n );\r\n \r\n // Add noopener and noreferrer\r\n relValues.add('noopener');\r\n relValues.add('noreferrer');\r\n \r\n el.setAttribute('rel', Array.from(relValues).join(' '));\r\n }\r\n }\r\n }\r\n\r\n // Remove disallowed elements\r\n for (const el of toRemove) {\r\n el.remove();\r\n }\r\n\r\n return template.innerHTML;\r\n};\r\n\r\n// ============================================================================\r\n// Public API\r\n// ============================================================================\r\n\r\n/**\r\n * Sanitize HTML string, removing dangerous elements and attributes.\r\n * Uses Trusted Types when available for CSP compliance.\r\n *\r\n * @param html - The HTML string to sanitize\r\n * @param options - Sanitization options\r\n * @returns Sanitized HTML string\r\n *\r\n * @example\r\n * ```ts\r\n * const safe = sanitizeHtml('<div onclick=\"alert(1)\">Hello</div>');\r\n * // Returns: '<div>Hello</div>'\r\n * ```\r\n */\r\nexport const sanitizeHtml = (html: string, options: SanitizeOptions = {}): string => {\r\n return sanitizeHtmlCore(html, options);\r\n};\r\n\r\n/**\r\n * Create a Trusted HTML value for use with Trusted Types-enabled sites.\r\n * Falls back to regular string when Trusted Types are unavailable.\r\n *\r\n * @param html - The HTML string to wrap\r\n * @returns Trusted HTML value or sanitized string\r\n */\r\nexport const createTrustedHtml = (html: string): TrustedHTML | string => {\r\n const policy = getTrustedTypesPolicy();\r\n if (policy) {\r\n return policy.createHTML(html);\r\n }\r\n return sanitizeHtml(html);\r\n};\r\n\r\n/**\r\n * Escape HTML entities to prevent XSS.\r\n * Use this for displaying user content as text.\r\n *\r\n * @param text - The text to escape\r\n * @returns Escaped HTML string\r\n *\r\n * @example\r\n * ```ts\r\n * escapeHtml('<script>alert(1)</script>');\r\n * // Returns: '&lt;script&gt;alert(1)&lt;/script&gt;'\r\n * ```\r\n */\r\nexport const escapeHtml = (text: string): string => {\r\n const escapeMap: Record<string, string> = {\r\n '&': '&amp;',\r\n '<': '&lt;',\r\n '>': '&gt;',\r\n '\"': '&quot;',\r\n \"'\": '&#x27;',\r\n '`': '&#x60;',\r\n };\r\n return text.replace(/[&<>\"'`]/g, (char) => escapeMap[char]);\r\n};\r\n\r\n/**\r\n * Strip all HTML tags and return plain text.\r\n *\r\n * @param html - The HTML string to strip\r\n * @returns Plain text content\r\n */\r\nexport const stripTags = (html: string): string => {\r\n return sanitizeHtmlCore(html, { stripAllTags: true });\r\n};\r\n\r\n// ============================================================================\r\n// CSP Helpers\r\n// ============================================================================\r\n\r\n/**\r\n * Generate a nonce for inline scripts/styles.\r\n * Use with Content-Security-Policy nonce directives.\r\n *\r\n * @param length - Nonce length (default: 16)\r\n * @returns Cryptographically random nonce string\r\n */\r\nexport const generateNonce = (length: number = 16): string => {\r\n const array = new Uint8Array(length);\r\n crypto.getRandomValues(array);\r\n return btoa(String.fromCharCode(...array))\r\n .replace(/\\+/g, '-')\r\n .replace(/\\//g, '_')\r\n .replace(/=/g, '');\r\n};\r\n\r\n/**\r\n * Check if a CSP header is present with specific directive.\r\n * Useful for feature detection and fallback strategies.\r\n *\r\n * @param directive - The CSP directive to check (e.g., 'script-src')\r\n * @returns True if the directive appears to be enforced\r\n */\r\nexport const hasCSPDirective = (directive: string): boolean => {\r\n // Check meta tag\r\n const meta = document.querySelector('meta[http-equiv=\"Content-Security-Policy\"]');\r\n if (meta) {\r\n const content = meta.getAttribute('content') ?? '';\r\n return content.includes(directive);\r\n }\r\n return false;\r\n};\r\n"],"names":["POLICY_NAME","cachedPolicy","isTrustedTypesSupported","getTrustedTypesPolicy","win","input","sanitizeHtmlCore","DEFAULT_ALLOWED_TAGS","DANGEROUS_TAGS","RESERVED_IDS","DEFAULT_ALLOWED_ATTRIBUTES","DANGEROUS_ATTR_PREFIXES","DANGEROUS_PROTOCOLS","isAllowedAttribute","name","allowedSet","allowDataAttrs","lowerName","prefix","isSafeIdOrName","value","lowerValue","normalizeUrl","isSafeUrl","normalized","protocol","isExternalUrl","url","trimmedUrl","lowerUrl","html","options","allowTags","allowAttributes","allowDataAttributes","stripAllTags","allowedTags","t","tag","allowedAttrs","a","template","walker","toRemove","el","tagName","attrsToRemove","attr","attrName","href","hasTargetBlank","isExternal","existingRel","relValues","sanitizeHtml","createTrustedHtml","policy","escapeHtml","text","escapeMap","char","stripTags","generateNonce","length","array","hasCSPDirective","directive","meta"],"mappings":"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"}