vize_maestro 0.0.1-alpha.26

Maestro - Language Server Protocol implementation for Vize Vue templates
Documentation
//! Script virtual code generation.
//!
//! Preserves script content and generates bindings export for template.

use vize_atelier_sfc::SfcScriptBlock;

use super::{
    MappingFeatures, SourceMap, SourceMapping, SourceRange, VirtualDocument, VirtualLanguage,
};

/// Script code generator.
pub struct ScriptCodeGenerator {
    /// Generated output
    output: String,
    /// Source mappings
    mappings: Vec<SourceMapping>,
    /// Current position in generated output
    gen_offset: u32,
    /// Block offset in original SFC
    block_offset: u32,
}

impl ScriptCodeGenerator {
    /// Create a new script code generator.
    pub fn new() -> Self {
        Self {
            output: String::new(),
            mappings: Vec::new(),
            gen_offset: 0,
            block_offset: 0,
        }
    }

    /// Generate virtual TypeScript from a script block.
    pub fn generate(&mut self, script: &SfcScriptBlock, is_setup: bool) -> VirtualDocument {
        // Reset state
        self.output.clear();
        self.mappings.clear();
        self.gen_offset = 0;
        self.block_offset = script.loc.start as u32;

        // Generate header comment
        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("");

        // Track the start of the actual content
        let content_gen_start = self.gen_offset;

        // Write the script content with 1:1 mapping
        let content = script.content.as_ref();
        self.write(content);

        // Create a mapping for the entire 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(),
            ));
        }

        // Add newline if needed
        if !content.ends_with('\n') {
            self.write_line("");
        }

        // Create source map
        let mut source_map = SourceMap::from_mappings(self.mappings.clone());
        source_map.set_block_offset(self.block_offset);

        VirtualDocument {
            uri: String::new(), // Will be set by generator
            content: self.output.clone(),
            language: if is_setup {
                VirtualLanguage::ScriptSetup
            } else {
                VirtualLanguage::Script
            },
            source_map,
        }
    }

    /// Generate with binding exports for template usage.
    pub fn generate_with_exports(
        &mut self,
        script: &SfcScriptBlock,
        is_setup: bool,
        bindings: &[String],
    ) -> VirtualDocument {
        // Reset state
        self.output.clear();
        self.mappings.clear();
        self.gen_offset = 0;
        self.block_offset = script.loc.start as u32;

        // Generate header
        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("");

        // Track content start
        let content_gen_start = self.gen_offset;

        // Write the script content
        let content = script.content.as_ref();
        self.write(content);

        // Create mapping for 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(),
            ));
        }

        // Add newline
        if !content.ends_with('\n') {
            self.write_line("");
        }

        // Generate export for template context
        if !bindings.is_empty() {
            self.write_line("");
            self.write_line("// Exports for template");
            self.write("export { ");
            self.write(&bindings.join(", "));
            self.write_line(" };");
        }

        // Create source map
        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()
    }
}

/// Extract binding names from script content.
///
/// This is a simple extraction that looks for common patterns.
/// For accurate binding extraction, use vize_atelier_sfc's binding analysis.
pub fn extract_simple_bindings(content: &str, is_setup: bool) -> Vec<String> {
    let mut bindings = Vec::new();

    if is_setup {
        // In script setup, top-level bindings are exposed
        // Look for: const x, let x, function x, import { x }
        for line in content.lines() {
            let trimmed = line.trim();

            // const/let declarations
            if trimmed.starts_with("const ") || trimmed.starts_with("let ") {
                if let Some(rest) = trimmed
                    .strip_prefix("const ")
                    .or_else(|| trimmed.strip_prefix("let "))
                {
                    // Handle destructuring: const { a, b } = ...
                    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());
                                }
                            }
                        }
                    }
                    // Handle array destructuring: const [a, b] = ...
                    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());
                                }
                            }
                        }
                    }
                    // Simple declaration: const x = ...
                    else if let Some(name) = rest.split('=').next() {
                        let name = name.trim();
                        if is_valid_identifier(name) {
                            bindings.push(name.to_string());
                        }
                    }
                }
            }
            // Function declarations
            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
}

/// Check if a string is a valid JavaScript identifier.
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"));
    }
}