use vize_atelier_sfc::SfcScriptBlock;
use super::{
MappingFeatures, SourceMap, SourceMapping, SourceRange, VirtualDocument, VirtualLanguage,
};
pub struct ScriptCodeGenerator {
output: String,
mappings: Vec<SourceMapping>,
gen_offset: u32,
block_offset: u32,
}
impl ScriptCodeGenerator {
pub fn new() -> Self {
Self {
output: String::new(),
mappings: Vec::new(),
gen_offset: 0,
block_offset: 0,
}
}
pub fn generate(&mut self, script: &SfcScriptBlock, is_setup: bool) -> VirtualDocument {
self.output.clear();
self.mappings.clear();
self.gen_offset = 0;
self.block_offset = script.loc.start as u32;
if is_setup {
self.write_line("// Virtual TypeScript for <script setup>");
} else {
self.write_line("// Virtual TypeScript for <script>");
}
self.write_line("// Generated by vize_maestro");
self.write_line("");
let content_gen_start = self.gen_offset;
let content = script.content.as_ref();
self.write(content);
let content_len = content.len() as u32;
if content_len > 0 {
self.mappings.push(SourceMapping::with_features(
SourceRange::new(0, content_len),
SourceRange::new(content_gen_start, content_gen_start + content_len),
MappingFeatures::all(),
));
}
if !content.ends_with('\n') {
self.write_line("");
}
let mut source_map = SourceMap::from_mappings(self.mappings.clone());
source_map.set_block_offset(self.block_offset);
VirtualDocument {
uri: String::new(), content: self.output.clone(),
language: if is_setup {
VirtualLanguage::ScriptSetup
} else {
VirtualLanguage::Script
},
source_map,
}
}
pub fn generate_with_exports(
&mut self,
script: &SfcScriptBlock,
is_setup: bool,
bindings: &[String],
) -> VirtualDocument {
self.output.clear();
self.mappings.clear();
self.gen_offset = 0;
self.block_offset = script.loc.start as u32;
if is_setup {
self.write_line("// Virtual TypeScript for <script setup> with exports");
} else {
self.write_line("// Virtual TypeScript for <script> with exports");
}
self.write_line("// Generated by vize_maestro");
self.write_line("");
let content_gen_start = self.gen_offset;
let content = script.content.as_ref();
self.write(content);
let content_len = content.len() as u32;
if content_len > 0 {
self.mappings.push(SourceMapping::with_features(
SourceRange::new(0, content_len),
SourceRange::new(content_gen_start, content_gen_start + content_len),
MappingFeatures::all(),
));
}
if !content.ends_with('\n') {
self.write_line("");
}
if !bindings.is_empty() {
self.write_line("");
self.write_line("// Exports for template");
self.write("export { ");
self.write(&bindings.join(", "));
self.write_line(" };");
}
let mut source_map = SourceMap::from_mappings(self.mappings.clone());
source_map.set_block_offset(self.block_offset);
VirtualDocument {
uri: String::new(),
content: self.output.clone(),
language: if is_setup {
VirtualLanguage::ScriptSetup
} else {
VirtualLanguage::Script
},
source_map,
}
}
fn write(&mut self, s: &str) {
self.output.push_str(s);
self.gen_offset += s.len() as u32;
}
fn write_line(&mut self, s: &str) {
self.output.push_str(s);
self.output.push('\n');
self.gen_offset += s.len() as u32 + 1;
}
}
impl Default for ScriptCodeGenerator {
fn default() -> Self {
Self::new()
}
}
pub fn extract_simple_bindings(content: &str, is_setup: bool) -> Vec<String> {
let mut bindings = Vec::new();
if is_setup {
for line in content.lines() {
let trimmed = line.trim();
if trimmed.starts_with("const ") || trimmed.starts_with("let ") {
if let Some(rest) = trimmed
.strip_prefix("const ")
.or_else(|| trimmed.strip_prefix("let "))
{
if rest.starts_with('{') {
if let Some(end) = rest.find('}') {
let inner = &rest[1..end];
for part in inner.split(',') {
let name = part.split(':').next().unwrap_or("").trim();
if !name.is_empty() && is_valid_identifier(name) {
bindings.push(name.to_string());
}
}
}
}
else if rest.starts_with('[') {
if let Some(end) = rest.find(']') {
let inner = &rest[1..end];
for part in inner.split(',') {
let name = part.trim();
if !name.is_empty() && is_valid_identifier(name) {
bindings.push(name.to_string());
}
}
}
}
else if let Some(name) = rest.split('=').next() {
let name = name.trim();
if is_valid_identifier(name) {
bindings.push(name.to_string());
}
}
}
}
else if trimmed.starts_with("function ") {
if let Some(rest) = trimmed.strip_prefix("function ") {
if let Some(name) = rest.split('(').next() {
let name = name.trim();
if is_valid_identifier(name) {
bindings.push(name.to_string());
}
}
}
}
}
}
bindings
}
fn is_valid_identifier(s: &str) -> bool {
if s.is_empty() {
return false;
}
let mut chars = s.chars();
let first = chars.next().unwrap();
if !first.is_alphabetic() && first != '_' && first != '$' {
return false;
}
chars.all(|c| c.is_alphanumeric() || c == '_' || c == '$')
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_extract_simple_bindings() {
let content = r#"
const message = ref('hello')
const count = ref(0)
let mutable = 'test'
function handleClick() {}
const { a, b } = useData()
"#;
let bindings = extract_simple_bindings(content, true);
assert!(bindings.contains(&"message".to_string()));
assert!(bindings.contains(&"count".to_string()));
assert!(bindings.contains(&"mutable".to_string()));
assert!(bindings.contains(&"handleClick".to_string()));
assert!(bindings.contains(&"a".to_string()));
assert!(bindings.contains(&"b".to_string()));
}
#[test]
fn test_is_valid_identifier() {
assert!(is_valid_identifier("foo"));
assert!(is_valid_identifier("_foo"));
assert!(is_valid_identifier("$foo"));
assert!(is_valid_identifier("foo123"));
assert!(!is_valid_identifier("123foo"));
assert!(!is_valid_identifier(""));
assert!(!is_valid_identifier("foo-bar"));
}
}