//#region src/redirects/patterns.ts
/**
* URL pattern matching for redirects.
*
* Uses Astro's route syntax: [param] for named segments, [...rest] for catch-all.
* Compiles patterns to safe regexes -- no user-supplied regex, no ReDoS risk.
*
* @example
* ```ts
* const compiled = compilePattern("/old-blog/[...path]");
* const match = matchPattern(compiled, "/old-blog/2024/01/post");
* // match = { path: "2024/01/post" }
*
* interpolateDestination("/blog/[...path]", match);
* // "/blog/2024/01/post"
* ```
*/
/** Matches [paramName] placeholders */
const PARAM_PATTERN = /\[(\w+)\]/g;
/** Matches [...splatName] placeholders */
const SPLAT_PATTERN = /\[\.\.\.(\w+)\]/g;
/** Combined pattern for validation: matches both [param] and [...splat] */
const ANY_PLACEHOLDER = /\[(?:\.\.\.)?(\w+)\]/g;
/** Nested brackets check: [foo[ */
const NESTED_BRACKETS = /\[[^\]]*\[/;
/** Empty brackets: [] */
const EMPTY_BRACKETS = /\[\]/;
/** Count open brackets */
const OPEN_BRACKET = /\[/g;
/** Count close brackets */
const CLOSE_BRACKET = /\]/g;
/** Split on capture groups in compiled regex string */
const CAPTURE_GROUP_SPLIT = /(\([^)]+\))/;
/** Escape regex-special characters in literal parts */
const REGEX_SPECIAL_CHARS = /[.*+?^${}|\\]/g;
/**
* Returns true if a source string contains [param] or [...splat] placeholders.
*/
function isPattern(source) {
	return source.match(ANY_PLACEHOLDER) !== null;
}
/**
* Validate that a pattern string is well-formed.
* Returns null if valid, or an error message if invalid.
*/
function validatePattern(source) {
	if (!source.startsWith("/")) return "Pattern must start with /";
	if (NESTED_BRACKETS.test(source)) return "Nested brackets are not allowed";
	if (EMPTY_BRACKETS.test(source)) return "Empty brackets are not allowed";
	if ((source.match(OPEN_BRACKET) ?? []).length !== (source.match(CLOSE_BRACKET) ?? []).length) return "Unmatched brackets";
	const segments = source.split("/").filter(Boolean);
	for (let i = 0; i < segments.length; i++) {
		const segment = segments[i];
		if (SPLAT_PATTERN.test(segment) && i !== segments.length - 1) {
			SPLAT_PATTERN.lastIndex = 0;
			return "Catch-all [...param] must be in the last segment";
		}
		SPLAT_PATTERN.lastIndex = 0;
	}
	for (const segment of segments) {
		const placeholders = segment.match(ANY_PLACEHOLDER);
		if (placeholders && placeholders.length > 1) return "Each segment can contain at most one placeholder";
		if (placeholders && placeholders[0] !== segment) return "A placeholder must be the entire segment, not mixed with literal text";
	}
	const names = [];
	for (const m of source.matchAll(ANY_PLACEHOLDER)) {
		const name = m[1];
		if (names.includes(name)) return `Duplicate parameter name: ${name}`;
		names.push(name);
	}
	return null;
}
/**
* Validate that all placeholders in a destination exist in the source.
* Returns null if valid, or an error message if invalid.
*/
function validateDestinationParams(source, destination) {
	const sourceNames = /* @__PURE__ */ new Set();
	for (const m of source.matchAll(ANY_PLACEHOLDER)) sourceNames.add(m[1]);
	for (const m of destination.matchAll(ANY_PLACEHOLDER)) {
		const name = m[1];
		if (!sourceNames.has(name)) return `Destination references [${name}] which is not captured in the source pattern`;
	}
	return null;
}
/**
* Compile a URL pattern into a regex for matching.
*
* - `[param]` matches a single path segment (`[^/]+`)
* - `[...rest]` matches one or more remaining segments (`.+`)
*/
function compilePattern(source) {
	const paramNames = [];
	let regexStr = source.replace(SPLAT_PATTERN, (_match, name) => {
		paramNames.push(name);
		return "(.+)";
	});
	regexStr = regexStr.replace(PARAM_PATTERN, (_match, name) => {
		paramNames.push(name);
		return "([^/]+)";
	});
	const escaped = regexStr.split(CAPTURE_GROUP_SPLIT).map((part, i) => {
		if (i % 2 === 1) return part;
		return part.replace(REGEX_SPECIAL_CHARS, "\\$&");
	}).join("");
	return {
		regex: new RegExp(`^${escaped}$`),
		paramNames,
		source
	};
}
/**
* Match a path against a compiled pattern.
* Returns captured params or null if no match.
*/
function matchPattern(compiled, path) {
	const match = path.match(compiled.regex);
	if (!match) return null;
	const params = {};
	for (let i = 0; i < compiled.paramNames.length; i++) {
		const value = match[i + 1];
		if (value !== void 0) params[compiled.paramNames[i]] = value;
	}
	return params;
}
/**
* Interpolate captured params into a destination pattern.
*
* @example
* interpolateDestination("/blog/[...path]", { path: "2024/01/post" })
* // "/blog/2024/01/post"
*/
function interpolateDestination(destination, params) {
	let result = destination.replace(SPLAT_PATTERN, (_match, name) => {
		return params[name] ?? "";
	});
	result = result.replace(PARAM_PATTERN, (_match, name) => {
		return params[name] ?? "";
	});
	return result;
}

//#endregion
export { validateDestinationParams as a, matchPattern as i, interpolateDestination as n, validatePattern as o, isPattern as r, compilePattern as t };
//# sourceMappingURL=patterns-BKmjvM7K.mjs.map