#[derive(Debug, Clone)]
pub struct ParsedIdent {
pub path: String,
pub layer: Option<String>,
#[expect(
dead_code,
reason = "parsed from ident but not yet used in node ID construction"
)]
pub module_type: Option<String>,
pub fragment: Option<String>,
}
pub fn parse_ident(ident: &str) -> ParsedIdent {
let mut remaining = ident.trim();
let fragment = peel_suffix(remaining, '<', '>');
if let Some((rest, _)) = &fragment {
remaining = rest.trim();
}
let module_type = peel_suffix(remaining, '(', ')');
if let Some((rest, _)) = &module_type {
remaining = rest.trim();
}
let layer = peel_suffix(remaining, '[', ']');
if let Some((rest, _)) = &layer {
remaining = rest.trim();
}
let template_args = peel_suffix(remaining, '{', '}');
if let Some((rest, _)) = &template_args {
remaining = rest.trim();
}
ParsedIdent {
path: remaining.to_string(),
layer: layer.map(|(_, v)| v),
module_type: module_type.map(|(_, v)| v),
fragment: fragment.map(|(_, v)| v),
}
}
pub fn node_id(parsed: &ParsedIdent, collapse_fragments: bool) -> String {
let mut id = parsed.path.clone();
if let Some(layer) = &parsed.layer {
id = format!("{id} [{layer}]");
}
if !collapse_fragments && let Some(fragment) = &parsed.fragment {
id = format!("{id} <{fragment}>");
}
id
}
fn peel_suffix(s: &str, open: char, close: char) -> Option<(&str, String)> {
let s = s.trim_end();
if !s.ends_with(close) {
return None;
}
let bytes = s.as_bytes();
let mut depth = 0i32;
for i in (0..bytes.len()).rev() {
let ch = bytes[i] as char;
if ch == close {
depth += 1;
} else if ch == open {
depth -= 1;
if depth == 0 {
let content = &s[i + open.len_utf8()..s.len() - close.len_utf8()];
let before = &s[..i];
return Some((before, content.to_string()));
}
}
}
None
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_simple_ident() {
let p = parse_ident("[project]/src/app/page.tsx [app-rsc] (ecmascript)");
assert_eq!(p.path, "[project]/src/app/page.tsx");
assert_eq!(p.layer.as_deref(), Some("app-rsc"));
assert_eq!(p.module_type.as_deref(), Some("ecmascript"));
assert!(p.fragment.is_none());
}
#[test]
fn test_ident_with_fragment() {
let p = parse_ident("[project]/src/utils.ts [app-rsc] (ecmascript) <exports>");
assert_eq!(p.path, "[project]/src/utils.ts");
assert_eq!(p.layer.as_deref(), Some("app-rsc"));
assert_eq!(p.fragment.as_deref(), Some("exports"));
}
#[test]
fn test_ident_no_layer() {
let p = parse_ident("[project]/src/global.css (css)");
assert_eq!(p.path, "[project]/src/global.css");
assert!(p.layer.is_none());
assert_eq!(p.module_type.as_deref(), Some("css"));
}
#[test]
fn test_node_id_with_layer() {
let p = parse_ident("[project]/src/app/page.tsx [app-rsc] (ecmascript)");
assert_eq!(node_id(&p, false), "[project]/src/app/page.tsx [app-rsc]");
}
#[test]
fn test_node_id_fragment_expanded() {
let p = parse_ident("[project]/src/utils.ts [app-rsc] (ecmascript) <exports>");
assert_eq!(
node_id(&p, false),
"[project]/src/utils.ts [app-rsc] <exports>"
);
}
#[test]
fn test_node_id_fragment_collapsed() {
let p = parse_ident("[project]/src/utils.ts [app-rsc] (ecmascript) <exports>");
assert_eq!(node_id(&p, true), "[project]/src/utils.ts [app-rsc]");
}
}