use std::collections::HashMap;
#[derive(Debug, Clone)]
pub struct PropTypeInfo {
pub js_type: String,
pub ts_type: Option<String>,
pub optional: bool,
}
fn strip_ts_comments(input: &str) -> String {
let mut result = String::with_capacity(input.len());
let bytes = input.as_bytes();
let mut i = 0;
let mut in_string = false;
let mut string_char = b'"';
while i < bytes.len() {
if in_string {
if bytes[i] == string_char && (i == 0 || bytes[i - 1] != b'\\') {
in_string = false;
}
result.push(bytes[i] as char);
i += 1;
continue;
}
match bytes[i] {
b'\'' | b'"' | b'`' => {
in_string = true;
string_char = bytes[i];
result.push(bytes[i] as char);
i += 1;
}
b'/' if i + 1 < bytes.len() && bytes[i + 1] == b'/' => {
while i < bytes.len() && bytes[i] != b'\n' {
i += 1;
}
}
b'/' if i + 1 < bytes.len() && bytes[i + 1] == b'*' => {
i += 2;
while i + 1 < bytes.len() && !(bytes[i] == b'*' && bytes[i + 1] == b'/') {
i += 1;
}
if i + 1 < bytes.len() {
i += 2; }
}
_ => {
result.push(bytes[i] as char);
i += 1;
}
}
}
result
}
pub fn extract_prop_types_from_type(type_args: &str) -> Vec<(String, PropTypeInfo)> {
let mut props = Vec::new();
let stripped = strip_ts_comments(type_args);
let content = stripped.trim();
let content = if content.starts_with('{') && content.ends_with('}') {
&content[1..content.len() - 1]
} else {
content
};
let mut depth: i32 = 0;
let mut current = String::new();
let chars: Vec<char> = content.chars().collect();
let mut i = 0;
while i < chars.len() {
let c = chars[i];
match c {
'{' | '<' | '(' | '[' => {
depth += 1;
current.push(c);
}
'}' | ')' | ']' => {
if depth > 0 {
depth -= 1;
}
current.push(c);
}
'>' => {
if i > 0 && chars[i - 1] == '=' {
current.push(c);
} else {
if depth > 0 {
depth -= 1;
}
current.push(c);
}
}
',' | ';' | '\n' if depth <= 0 => {
extract_prop_type_info(¤t, &mut props);
current.clear();
depth = 0;
}
_ => current.push(c),
}
i += 1;
}
extract_prop_type_info(¤t, &mut props);
props
}
fn extract_prop_type_info(segment: &str, props: &mut Vec<(String, PropTypeInfo)>) {
let trimmed = segment.trim();
if trimmed.is_empty() {
return;
}
if let Some(colon_pos) = trimmed.find(':') {
let name_part = &trimmed[..colon_pos];
let type_part = &trimmed[colon_pos + 1..];
let optional = name_part.ends_with('?');
let name = name_part.trim().trim_end_matches('?').trim();
if !name.is_empty() && is_valid_identifier(name) {
let ts_type_str = type_part.trim().to_string();
let js_type = ts_type_to_js_type(&ts_type_str);
if !props.iter().any(|(n, _)| n == name) {
props.push((
name.to_string(),
PropTypeInfo {
js_type,
ts_type: Some(ts_type_str),
optional,
},
));
}
}
}
}
fn ts_type_to_js_type(ts_type: &str) -> String {
let ts_type = ts_type.trim();
if (ts_type.starts_with('"') && ts_type.ends_with('"'))
|| (ts_type.starts_with('\'') && ts_type.ends_with('\''))
{
return "String".to_string();
}
if ts_type.parse::<f64>().is_ok() {
return "Number".to_string();
}
if ts_type == "true" || ts_type == "false" {
return "Boolean".to_string();
}
if ts_type.contains('|') {
let parts: Vec<&str> = ts_type.split('|').collect();
for part in parts {
let part = part.trim();
if part != "undefined" && part != "null" {
return ts_type_to_js_type(part);
}
}
}
match ts_type.to_lowercase().as_str() {
"string" => "String".to_string(),
"number" => "Number".to_string(),
"boolean" => "Boolean".to_string(),
"object" => "Object".to_string(),
"function" => "Function".to_string(),
"symbol" => "Symbol".to_string(),
_ => {
if ts_type.ends_with("[]") || ts_type.starts_with("Array<") {
"Array".to_string()
} else if ts_type.starts_with('{') || ts_type.contains(':') {
"Object".to_string()
} else if ts_type.starts_with('(') && ts_type.contains("=>") {
"Function".to_string()
} else {
let type_name = ts_type.split('<').next().unwrap_or(ts_type).trim();
match type_name {
"Date" | "RegExp" | "Error" | "Map" | "Set" | "WeakMap" | "WeakSet"
| "Promise" | "ArrayBuffer" | "DataView" | "Int8Array" | "Uint8Array"
| "Int16Array" | "Uint16Array" | "Int32Array" | "Uint32Array"
| "Float32Array" | "Float64Array" | "BigInt64Array" | "BigUint64Array"
| "URL" | "URLSearchParams" | "FormData" | "Blob" | "File" => {
type_name.to_string()
}
_ => "null".to_string(),
}
}
}
}
}
pub fn extract_emit_names_from_type(type_args: &str) -> Vec<String> {
let mut emits = Vec::new();
let mut in_string = false;
let mut quote_char = ' ';
let mut current_string = String::new();
for c in type_args.chars() {
if !in_string && (c == '\'' || c == '"') {
in_string = true;
quote_char = c;
current_string.clear();
} else if in_string && c == quote_char {
in_string = false;
if !current_string.is_empty() {
emits.push(current_string.clone());
}
} else if in_string {
current_string.push(c);
}
}
emits
}
pub fn extract_with_defaults_defaults(with_defaults_args: &str) -> HashMap<String, String> {
let mut defaults = HashMap::new();
let content = with_defaults_args.trim();
let chars: Vec<char> = content.chars().collect();
let define_props_pos = content.find("defineProps");
if define_props_pos.is_none() {
return defaults;
}
let start_search = define_props_pos.unwrap();
let mut paren_depth = 0;
let mut in_define_props_call = false;
let mut found_define_props_end = false;
let mut defaults_start = None;
let mut i = start_search;
while i < chars.len() {
let c = chars[i];
if !in_define_props_call {
if c == '(' {
in_define_props_call = true;
paren_depth = 1;
}
} else if !found_define_props_end {
match c {
'(' => paren_depth += 1,
')' => {
paren_depth -= 1;
if paren_depth == 0 {
found_define_props_end = true;
}
}
_ => {}
}
} else {
if c == '{' {
defaults_start = Some(i);
break;
}
}
i += 1;
}
if let Some(start) = defaults_start {
let mut brace_depth = 0;
let mut end = start;
for (j, &c) in chars.iter().enumerate().skip(start) {
match c {
'{' => brace_depth += 1,
'}' => {
brace_depth -= 1;
if brace_depth == 0 {
end = j;
break;
}
}
_ => {}
}
}
let defaults_content: String = chars[start + 1..end].iter().collect();
parse_defaults_object(&defaults_content, &mut defaults);
}
defaults
}
fn parse_defaults_object(content: &str, defaults: &mut HashMap<String, String>) {
let content = content.trim();
if content.is_empty() {
return;
}
let mut depth = 0;
let mut current = String::new();
for c in content.chars() {
match c {
'{' | '(' | '[' => {
depth += 1;
current.push(c);
}
'}' | ')' | ']' => {
depth -= 1;
current.push(c);
}
',' if depth == 0 => {
extract_default_pair(¤t, defaults);
current.clear();
}
_ => current.push(c),
}
}
extract_default_pair(¤t, defaults);
}
fn extract_default_pair(pair: &str, defaults: &mut HashMap<String, String>) {
let trimmed = pair.trim();
if trimmed.is_empty() {
return;
}
let mut depth = 0;
let mut colon_pos = None;
for (i, c) in trimmed.chars().enumerate() {
match c {
'{' | '(' | '[' | '<' => depth += 1,
'}' | ')' | ']' | '>' => depth -= 1,
':' if depth == 0 => {
colon_pos = Some(i);
break;
}
_ => {}
}
}
if let Some(pos) = colon_pos {
let key = trimmed[..pos].trim();
let value = trimmed[pos + 1..].trim();
if !key.is_empty() && !value.is_empty() {
defaults.insert(key.to_string(), value.to_string());
}
}
}
pub fn is_valid_identifier(s: &str) -> bool {
if s.is_empty() {
return false;
}
let mut chars = s.chars();
match chars.next() {
Some(c) if c.is_alphabetic() || c == '_' || c == '$' => {}
_ => return false,
}
chars.all(|c| c.is_alphanumeric() || c == '_' || c == '$')
}