// Routes Compiler - Compile route definitions to optimized format
//
///|
/// Normalize path (remove duplicate slashes, remove trailing slash)
fn normalize_path(path : String) -> String {
if path == "" || path == "/" {
return "/"
}
let chars = path.to_array()
let result : Array[Char] = []
let mut prev_slash = false
for c in chars {
if c == '/' {
if !prev_slash {
result.push(c)
}
prev_slash = true
} else {
result.push(c)
prev_slash = false
}
}
// Remove trailing slash (except root)
let len = result.length()
if len > 1 && result[len - 1] == '/' {
let _ = result.pop()
}
String::from_array(result)
}
///|
/// Extract parameter names from path pattern
/// Example: "/user/:id/posts/:postId" -> ["id", "postId"]
/// Does not include catch-all parameters ([...slug], [[...slug]])
fn extract_param_names(path : String) -> Array[String] {
let result : Array[String] = []
let segments = split_by(path, '/')
for segment in segments {
if segment.has_prefix(":") && segment.length() > 1 {
// ":id" -> "id"
let name = segment[1:].to_owned()
result.push(name)
} else if segment.has_prefix("[") &&
!segment.has_prefix("[...") &&
!segment.has_prefix("[[...") {
// "[id]" -> "id" (Next.js style single param)
if segment.has_suffix("]") && segment.length() > 2 {
let name = segment[1:segment.length() - 1].to_owned()
result.push(name)
}
}
// [...slug] and [[...slug]] are handled separately as catch-all
}
result
}
///|
/// Extract catch-all info from path pattern
/// Example: "/docs/[...slug]" -> Some({ name: "slug", optional: false })
/// "/docs/[[...slug]]" -> Some({ name: "slug", optional: true })
fn extract_catch_all(path : String) -> CatchAllInfo? {
let segments = split_by(path, '/')
if segments.is_empty() {
return None
}
let last = segments[segments.length() - 1]
// Optional catch-all: [[...name]]
if last.has_prefix("[[...") && last.has_suffix("]]") && last.length() > 7 {
let name = last[5:last.length() - 2].to_owned()
return Some({ name, optional: true, })
}
// Required catch-all: [...name]
if last.has_prefix("[...") && last.has_suffix("]") && last.length() > 5 {
let name = last[4:last.length() - 1].to_owned()
return Some({ name, optional: false, })
}
None
}
///|
/// Compile Routes to CompiledRoutes
pub fn compile(
routes : Array[Routes],
base? : String = "",
) -> Array[CompiledRoutes] {
let result : Array[CompiledRoutes] = []
compile_inner(routes, base, [], result)
result
}
///|
fn compile_inner(
routes : Array[Routes],
prefix : String,
inherited_layouts : Array[String],
result : Array[CompiledRoutes],
) -> Unit {
for route in routes {
match route {
Page(path~, component~, title~, meta~) => {
let full_path = normalize_path(prefix + path)
let param_names = extract_param_names(full_path)
let catch_all = extract_catch_all(full_path)
result.push({
pattern: full_path,
param_names,
component,
layouts: inherited_layouts.copy(),
kind: Page,
title,
meta,
catch_all,
})
}
Get(path~, handler~) => {
let full_path = normalize_path(prefix + path)
let param_names = extract_param_names(full_path)
let catch_all = extract_catch_all(full_path)
result.push({
pattern: full_path,
param_names,
component: handler,
layouts: [],
kind: GetApi,
title: "",
meta: [],
catch_all,
})
}
Post(path~, handler~) => {
let full_path = normalize_path(prefix + path)
let param_names = extract_param_names(full_path)
let catch_all = extract_catch_all(full_path)
result.push({
pattern: full_path,
param_names,
component: handler,
layouts: [],
kind: PostApi,
title: "",
meta: [],
catch_all,
})
}
Layout(segment~, children~, layout~) => {
let new_prefix = normalize_path(prefix + segment)
let new_layouts = inherited_layouts.copy()
new_layouts.push(layout)
compile_inner(children, new_prefix, new_layouts, result)
}
}
}
}