use std::path::{Component, Path};
pub(super) fn collapse_whitespace(text: &str) -> String {
text.split_whitespace().collect::<Vec<_>>().join(" ")
}
pub(super) fn extract_js_module_specifier(text: &str) -> Option<String> {
if let Some((_, after_from)) = text.rsplit_once(" from ") {
return extract_quoted_string(after_from);
}
let rest = text.strip_prefix("import ")?;
extract_quoted_string(rest)
}
pub(super) fn extract_js_import_clause(text: &str) -> Option<&str> {
let rest = text.strip_prefix("import ")?;
let (clause, _) = rest.rsplit_once(" from ")?;
Some(clause)
}
pub(super) fn extract_quoted_string(text: &str) -> Option<String> {
let quote = text.find(['"', '\'', '`'])?;
let quote_char = text[quote..].chars().next()?;
let after_quote = &text[quote + quote_char.len_utf8()..];
let mut escaped = false;
let mut idx = 0;
while idx < after_quote.len() {
let ch = after_quote[idx..].chars().next()?;
if escaped {
escaped = false;
idx += ch.len_utf8();
continue;
}
if ch == '\\' {
escaped = true;
idx += ch.len_utf8();
continue;
}
if quote_char == '`' && ch == '$' && after_quote[idx + ch.len_utf8()..].starts_with('{') {
idx = skip_template_interpolation(after_quote, idx + ch.len_utf8() + 1)?;
continue;
}
if ch == quote_char {
return Some(after_quote[..idx].to_string());
}
idx += ch.len_utf8();
}
None
}
fn skip_template_interpolation(text: &str, mut idx: usize) -> Option<usize> {
let mut brace_depth = 1usize;
let mut in_single = false;
let mut in_double = false;
let mut in_backtick = false;
let mut escaped = false;
while idx < text.len() {
let ch = text[idx..].chars().next()?;
if escaped {
escaped = false;
idx += ch.len_utf8();
continue;
}
if (in_single || in_double || in_backtick) && ch == '\\' {
escaped = true;
idx += ch.len_utf8();
continue;
}
match ch {
'\'' if !in_double && !in_backtick => in_single = !in_single,
'"' if !in_single && !in_backtick => in_double = !in_double,
'`' if !in_single && !in_double => in_backtick = !in_backtick,
'{' if !in_single && !in_double && !in_backtick => brace_depth += 1,
'}' if !in_single && !in_double && !in_backtick => {
brace_depth -= 1;
idx += ch.len_utf8();
if brace_depth == 0 {
return Some(idx);
}
continue;
}
_ => {}
}
idx += ch.len_utf8();
}
None
}
pub(super) fn go_default_package_alias(module: &str) -> String {
let module = module.trim_end_matches('/');
let last_segment = module.rsplit('/').next().unwrap_or(module);
let without_version = last_segment
.rsplit_once(".v")
.filter(|(_, version)| !version.is_empty() && version.chars().all(|ch| ch.is_ascii_digit()))
.map(|(name, _)| name)
.unwrap_or(last_segment);
without_version.replace('-', "_")
}
pub(super) fn split_alias(text: &str) -> (&str, Option<&str>) {
if let Some((name, alias)) = text.split_once(" as ") {
(name.trim(), Some(alias.trim()))
} else {
(text.trim(), None)
}
}
pub(super) fn split_rust_use_group(text: &str) -> Option<(&str, &str)> {
let mut depth = 0usize;
let mut start = None;
for (idx, ch) in text.char_indices() {
match ch {
'{' => {
if depth == 0 {
start = Some(idx);
}
depth += 1;
}
'}' if depth > 0 => {
depth -= 1;
if depth == 0 {
let start = start?;
if text[idx + ch.len_utf8()..].trim().is_empty() {
return Some((text[..start].trim(), text[start + 1..idx].trim()));
}
return None;
}
}
_ => {}
}
}
None
}
pub(super) fn rust_join_use_path(prefix: &str, item: &str) -> Option<String> {
let prefix = prefix.trim().trim_end_matches("::").trim();
let item = item.trim();
if item.is_empty() {
return None;
}
let (item_path, alias) = split_alias(item);
let item_path = item_path.trim();
if item_path.is_empty() {
return None;
}
let path = if item_path == "self" {
if prefix.is_empty() {
return None;
}
prefix.to_string()
} else if prefix.is_empty() {
item_path.to_string()
} else {
format!("{prefix}::{item_path}")
};
Some(match alias {
Some(alias) if !alias.is_empty() => format!("{path} as {alias}"),
_ => path,
})
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(super) struct SplitTopLevelError {
delimiter: char,
position: usize,
kind: &'static str,
context: String,
}
impl std::fmt::Display for SplitTopLevelError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{} while splitting on `{}` at byte {} near `{}`",
self.kind, self.delimiter, self.position, self.context
)
}
}
impl std::error::Error for SplitTopLevelError {}
impl SplitTopLevelError {
fn new(text: &str, delimiter: char, position: usize, kind: &'static str) -> Self {
Self {
delimiter,
position,
kind,
context: split_error_context(text, position),
}
}
}
fn split_error_context(text: &str, position: usize) -> String {
const CONTEXT_CHARS: usize = 24;
let position = position.min(text.len());
let start = text[..position]
.char_indices()
.rev()
.nth(CONTEXT_CHARS)
.map(|(idx, _)| idx)
.unwrap_or(0);
let end = text[position..]
.char_indices()
.nth(CONTEXT_CHARS)
.map(|(idx, _)| position + idx)
.unwrap_or(text.len());
text[start..end].replace('\n', "\\n")
}
pub(super) fn split_top_level(
text: &str,
delimiter: char,
) -> Result<Vec<&str>, SplitTopLevelError> {
let mut parts = Vec::new();
let mut start = 0;
let mut paren_depth = 0usize;
let mut brace_depth = 0usize;
let mut bracket_depth = 0usize;
let mut in_single = false;
let mut in_double = false;
let mut escaped = false;
for (idx, ch) in text.char_indices() {
if escaped {
escaped = false;
continue;
}
if (in_single || in_double) && ch == '\\' {
escaped = true;
continue;
}
match ch {
'\'' if !in_double => in_single = !in_single,
'"' if !in_single => in_double = !in_double,
'(' if !in_single && !in_double => paren_depth += 1,
')' if !in_single && !in_double && paren_depth > 0 => paren_depth -= 1,
')' if !in_single && !in_double => {
return Err(SplitTopLevelError::new(
text,
delimiter,
idx,
"unbalanced closing parenthesis",
));
}
'{' if !in_single && !in_double => brace_depth += 1,
'}' if !in_single && !in_double && brace_depth > 0 => brace_depth -= 1,
'}' if !in_single && !in_double => {
return Err(SplitTopLevelError::new(
text,
delimiter,
idx,
"unbalanced closing brace",
));
}
'[' if !in_single && !in_double => bracket_depth += 1,
']' if !in_single && !in_double && bracket_depth > 0 => bracket_depth -= 1,
']' if !in_single && !in_double => {
return Err(SplitTopLevelError::new(
text,
delimiter,
idx,
"unbalanced closing bracket",
));
}
ch if ch == delimiter
&& !in_single
&& !in_double
&& paren_depth == 0
&& brace_depth == 0
&& bracket_depth == 0 =>
{
parts.push(text[start..idx].trim());
start = idx + ch.len_utf8();
}
_ => {}
}
}
parts.push(text[start..].trim());
if in_single || in_double {
return Err(SplitTopLevelError::new(
text,
delimiter,
text.len(),
"unterminated string literal",
));
}
if paren_depth != 0 || brace_depth != 0 || bracket_depth != 0 {
return Err(SplitTopLevelError::new(
text,
delimiter,
text.len(),
"unbalanced opening delimiter",
));
}
Ok(parts)
}
pub(super) fn is_ruby_constant_name(name: &str) -> bool {
is_uppercase_ascii_alnum_underscore_name(name)
}
fn is_uppercase_ascii_alnum_underscore_name(name: &str) -> bool {
name.chars()
.next()
.is_some_and(|ch| ch.is_ascii_uppercase())
&& name
.chars()
.all(|ch| ch.is_ascii_alphanumeric() || ch == '_')
}
pub(super) fn dart_import_alias(text: &str) -> Option<String> {
let after_as = text.split_once(" as ")?.1;
let alias = after_as
.split_whitespace()
.next()
.unwrap_or_default()
.trim_end_matches(';');
if alias.is_empty() {
None
} else {
Some(alias.to_string())
}
}
pub(super) fn dart_local_import_target(
uri: &str,
rel_path: &str,
self_package: Option<&str>,
) -> Option<String> {
if let Some(rest) = uri.strip_prefix("package:") {
let (package, within) = rest.split_once('/')?;
if within.is_empty() || Some(package) != self_package {
return None;
}
return Some(normalize_relative_dart_path(&Path::new("lib").join(within)));
}
if uri.contains(':') {
return None;
}
let dir = Path::new(rel_path)
.parent()
.unwrap_or_else(|| Path::new(""));
let resolved = normalize_relative_dart_path(&dir.join(uri));
(!resolved.is_empty()).then_some(resolved)
}
fn normalize_relative_dart_path(path: &Path) -> String {
let mut parts: Vec<String> = Vec::new();
for component in path.components() {
match component {
Component::CurDir => {}
Component::ParentDir => {
parts.pop();
}
Component::Normal(part) => parts.push(part.to_string_lossy().into_owned()),
Component::RootDir | Component::Prefix(_) => {}
}
}
parts.join("/")
}
pub(super) fn is_elixir_alias(name: &str) -> bool {
is_uppercase_ascii_alnum_underscore_name(name)
}
pub(super) fn is_elixir_alias_path(path: &str) -> bool {
path.split('.').all(is_elixir_alias)
}
pub(super) fn elixir_alias_as(text: &str) -> Option<String> {
let after = text.split_once(" as: ")?.1;
let alias = after
.split([',', ' ', ')', ']'])
.next()
.unwrap_or_default()
.trim();
if is_elixir_alias(alias) {
Some(alias.to_string())
} else {
None
}
}