final public static function path_parser(string $path) {
		# path must start with slash
		if ($path[0] != '/') {
			self::throw_error(
				"Router: path invalid in '$path'.");
		}
		# ignore trailing slash
		if ($path != '/')
			$path = rtrim($path, '/');

		# allowed characters in path
		$valid_chars = 'a-zA-Z0-9\_\.\-@%:';
		# param left delimiter
		$elf = '<\{';
		# param right delimiter
		$erg = '>\}';
		# param delimiter
		$delims = "\/${elf}${erg}";
		# non-delimiter
		$non_delims = "[^${delims}]";
		# valid param key
		$valid_key = "[a-z][a-z0-9\_]*";

		if (!preg_match("!^[${valid_chars}${delims}]+$!", $path)) {
			# invalid characters
			self::throw_error(
				"Router: invalid characters in path: '$path'.");
		}

		if (
			preg_match("!${non_delims}[${elf}]!", $path) ||
			preg_match("![${erg}]${non_delims}!", $path)
		) {
			# invalid dynamic path pattern
			self::throw_error(
				"Router: dynamic path not well-formed: '$path'.");
		}

		preg_match_all("!/([$elf][^$erg]+[$erg])!", $path, $tokens,
			PREG_OFFSET_CAPTURE);

		$keys = $symbols = [];
		foreach ($tokens[1] as $token) {

			$key = str_replace(['{','}','<','>'], '', $token[0]);
			if (!preg_match("!^${valid_key}\$!i", $key)) {
				# invalid param key
				self::throw_error(
					"Router: invalid param key: '$path'.");
			}

			$keys[] = $key;
			$replacement = $valid_chars;
			if (strpos($token[0], '{') !== false)
				$replacement .= '/';
			$replacement = '([' . $replacement . ']+)';
			$symbols[] = [$replacement, $token[1], strlen($token[0])];
		}

		if (count($keys) > count(array_unique($keys))) {
			# never allow key reuse to prevent unexpected overrides
			self::throw_error("Router: param key reused: '$path'.");
		}

		# construct regex pattern for all capturing keys
		$idx = 0;
		$pattern = '';
		while ($idx < strlen($path)) {
			$matched = false;
			foreach ($symbols as $symbol) {
				if ($idx < $symbol[1])
					continue;
				if ($idx == $symbol[1]) {
					$matched = true;
					$pattern .= $symbol[0];
					$idx++;
					$idx += $symbol[2] - 1;
				}
			}
			if (!$matched) {
				$pattern .= $path[$idx];
				$idx++;
			}
		}

		return [$pattern, $keys];
	}