scribe_analysis/heuristics/
template_detection.rs

1//! # Advanced Template Detection System
2//!
3//! Implements sophisticated template engine detection using multiple methods:
4//! 1. Extension-based detection for known template file types
5//! 2. AST-based content pattern analysis for template syntax detection
6//! 3. Tree-sitter parsing for HTML/XML files that might be templates
7//! 4. Directory context analysis for template-like structures
8//!
9//! This module uses tree-sitter AST parsing instead of regex patterns
10//! for accurate template detection and better performance.
11
12use std::collections::HashMap;
13use std::path::{Path, PathBuf};
14use std::fs;
15use std::io::{BufRead, BufReader};
16use once_cell::sync::Lazy;
17use scribe_core::Result;
18use tree_sitter::{Parser, Language as TsLanguage, Node, Tree};
19
20/// Template engine classification
21#[derive(Debug, Clone, PartialEq, Eq, Hash)]
22pub enum TemplateEngine {
23    // JavaScript template engines
24    Handlebars,
25    Mustache,
26    Ejs,
27    Pug,
28    Jade,
29    
30    // Python template engines  
31    Django,
32    Jinja2,
33    Mako,
34    
35    // PHP template engines
36    Twig,
37    Smarty,
38    
39    // Ruby template engines
40    Erb,
41    Haml,
42    
43    // Other template engines
44    Liquid,
45    Dust,
46    Eta,
47    
48    // Frontend frameworks with templates
49    Vue,
50    Svelte,
51    React, // JSX/TSX
52    Angular,
53    
54    // Generic/Unknown template
55    Generic,
56}
57
58impl TemplateEngine {
59    /// Get the typical file extensions for this template engine
60    pub fn extensions(&self) -> &'static [&'static str] {
61        match self {
62            TemplateEngine::Handlebars => &[".hbs", ".handlebars"],
63            TemplateEngine::Mustache => &[".mustache"],
64            TemplateEngine::Ejs => &[".ejs"],
65            TemplateEngine::Pug => &[".pug"],
66            TemplateEngine::Jade => &[".jade"],
67            TemplateEngine::Django => &[".html", ".htm"], // Context dependent
68            TemplateEngine::Jinja2 => &[".j2", ".jinja", ".jinja2"],
69            TemplateEngine::Mako => &[".mako"],
70            TemplateEngine::Twig => &[".twig"],
71            TemplateEngine::Smarty => &[".tpl"],
72            TemplateEngine::Erb => &[".erb", ".rhtml"],
73            TemplateEngine::Haml => &[".haml"],
74            TemplateEngine::Liquid => &[".liquid"],
75            TemplateEngine::Dust => &[".dust"],
76            TemplateEngine::Eta => &[".eta"],
77            TemplateEngine::Vue => &[".vue"],
78            TemplateEngine::Svelte => &[".svelte"],
79            TemplateEngine::React => &[".jsx", ".tsx"],
80            TemplateEngine::Angular => &[".html"], // Context dependent
81            TemplateEngine::Generic => &[],
82        }
83    }
84    
85    /// Get score boost factor for this template engine
86    pub fn score_boost(&self) -> f64 {
87        match self {
88            // High priority for dedicated template files
89            TemplateEngine::Handlebars | TemplateEngine::Mustache | 
90            TemplateEngine::Jinja2 | TemplateEngine::Twig | 
91            TemplateEngine::Liquid => 1.5,
92            
93            // Moderate priority for framework templates
94            TemplateEngine::Vue | TemplateEngine::Svelte | 
95            TemplateEngine::React => 1.3,
96            
97            // Standard boost for other template engines
98            TemplateEngine::Ejs | TemplateEngine::Pug | TemplateEngine::Erb |
99            TemplateEngine::Haml => 1.2,
100            
101            // Lower boost for generic HTML templates
102            TemplateEngine::Django | TemplateEngine::Angular => 1.0,
103            
104            // Minimal boost for unknown templates
105            TemplateEngine::Generic => 0.8,
106            
107            _ => 1.0,
108        }
109    }
110}
111
112/// Method used for template detection
113#[derive(Debug, Clone, PartialEq, Eq)]
114pub enum TemplateDetectionMethod {
115    /// Detected by file extension
116    Extension,
117    /// Detected by content pattern analysis
118    ContentPattern,
119    /// Detected by directory context
120    DirectoryContext,
121    /// Detected by language heuristics
122    LanguageHeuristic,
123}
124
125/// Result of template detection
126#[derive(Debug, Clone)]
127pub struct TemplateDetectionResult {
128    /// Whether the file is detected as a template
129    pub is_template: bool,
130    /// The detected template engine (if any)
131    pub engine: Option<TemplateEngine>,
132    /// Method used for detection
133    pub detection_method: TemplateDetectionMethod,
134    /// Confidence score (0.0 to 1.0)
135    pub confidence: f64,
136    /// Score boost to apply
137    pub score_boost: f64,
138}
139
140impl TemplateDetectionResult {
141    pub fn not_template() -> Self {
142        Self {
143            is_template: false,
144            engine: None,
145            detection_method: TemplateDetectionMethod::Extension,
146            confidence: 0.0,
147            score_boost: 0.0,
148        }
149    }
150    
151    pub fn template(engine: TemplateEngine, method: TemplateDetectionMethod, confidence: f64) -> Self {
152        let score_boost = engine.score_boost();
153        Self {
154            is_template: true,
155            engine: Some(engine),
156            detection_method: method,
157            confidence,
158            score_boost,
159        }
160    }
161}
162
163/// Template pattern definition for content analysis
164#[derive(Debug, Clone)]
165pub struct TemplatePattern {
166    pub open_tag: String,
167    pub close_tag: String,
168    pub engine: TemplateEngine,
169    pub min_occurrences: usize,
170}
171
172impl TemplatePattern {
173    pub fn new(open: &str, close: &str, engine: TemplateEngine, min_occurrences: usize) -> Self {
174        Self {
175            open_tag: open.to_string(),
176            close_tag: close.to_string(),
177            engine,
178            min_occurrences,
179        }
180    }
181}
182
183/// Static template patterns for content analysis
184static TEMPLATE_PATTERNS: Lazy<Vec<TemplatePattern>> = Lazy::new(|| {
185    vec![
186        // Handlebars, Mustache patterns
187        TemplatePattern::new("{{", "}}", TemplateEngine::Handlebars, 2),
188        TemplatePattern::new("{{{", "}}}", TemplateEngine::Handlebars, 1),
189        
190        // Jinja2, Django, Liquid patterns
191        TemplatePattern::new("{%", "%}", TemplateEngine::Jinja2, 2),
192        TemplatePattern::new("{{", "}}", TemplateEngine::Jinja2, 1), // Also Handlebars
193        
194        // EJS, ERB patterns
195        TemplatePattern::new("<%", "%>", TemplateEngine::Ejs, 2),
196        TemplatePattern::new("<%=", "%>", TemplateEngine::Ejs, 1),
197        TemplatePattern::new("<%#", "%>", TemplateEngine::Ejs, 1),
198        
199        // FreeMarker patterns
200        TemplatePattern::new("<#", "#>", TemplateEngine::Generic, 2),
201        
202        // Template literals and other patterns
203        TemplatePattern::new("${", "}", TemplateEngine::Generic, 3),
204        TemplatePattern::new("@{", "}", TemplateEngine::Generic, 2),
205        TemplatePattern::new("[[", "]]", TemplateEngine::Generic, 2),
206    ]
207});
208
209/// Extension to template engine mapping
210static EXTENSION_MAP: Lazy<HashMap<&'static str, TemplateEngine>> = Lazy::new(|| {
211    let mut map = HashMap::new();
212    
213    // Dedicated template extensions
214    map.insert(".njk", TemplateEngine::Jinja2);
215    map.insert(".nunjucks", TemplateEngine::Jinja2);
216    map.insert(".hbs", TemplateEngine::Handlebars);
217    map.insert(".handlebars", TemplateEngine::Handlebars);
218    map.insert(".j2", TemplateEngine::Jinja2);
219    map.insert(".jinja", TemplateEngine::Jinja2);
220    map.insert(".jinja2", TemplateEngine::Jinja2);
221    map.insert(".twig", TemplateEngine::Twig);
222    map.insert(".liquid", TemplateEngine::Liquid);
223    map.insert(".mustache", TemplateEngine::Mustache);
224    map.insert(".ejs", TemplateEngine::Ejs);
225    map.insert(".erb", TemplateEngine::Erb);
226    map.insert(".rhtml", TemplateEngine::Erb);
227    map.insert(".haml", TemplateEngine::Haml);
228    map.insert(".pug", TemplateEngine::Pug);
229    map.insert(".jade", TemplateEngine::Jade);
230    map.insert(".dust", TemplateEngine::Dust);
231    map.insert(".eta", TemplateEngine::Eta);
232    map.insert(".svelte", TemplateEngine::Svelte);
233    map.insert(".vue", TemplateEngine::Vue);
234    map.insert(".jsx", TemplateEngine::React);
235    map.insert(".tsx", TemplateEngine::React);
236    
237    map
238});
239
240/// Single-pattern indicators for template detection
241static SINGLE_PATTERNS: &[&str] = &[
242    "ng-",          // Angular directives
243    "v-",           // Vue directives  
244    ":",            // Vue shorthand or other template syntax
245    "data-bind",    // Knockout.js
246    "handlebars",   // Handlebars comments
247    "jinja",        // Jinja comments
248    "mustache",     // Mustache comments
249    "twig",         // Twig comments
250    "liquid",       // Liquid comments
251];
252
253/// Template directory indicators
254static TEMPLATE_DIRECTORIES: &[&str] = &[
255    "template",
256    "templates", 
257    "_includes",
258    "_layouts",
259    "layout",
260    "layouts",
261    "view",
262    "views",
263    "component",
264    "components",
265    "partial",
266    "partials",
267];
268
269/// Advanced template detection engine
270pub struct TemplateDetector {
271    /// AST parsers for different languages
272    parsers: HashMap<String, Parser>,
273    /// File content cache (for performance)
274    content_cache: HashMap<PathBuf, String>,
275    /// Cache size limit
276    max_cache_size: usize,
277}
278
279impl std::fmt::Debug for TemplateDetector {
280    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
281        f.debug_struct("TemplateDetector")
282            .field("parsers", &format!("[{} parsers]", self.parsers.len()))
283            .field("content_cache", &format!("[{} cached items]", self.content_cache.len()))
284            .field("max_cache_size", &self.max_cache_size)
285            .finish()
286    }
287}
288
289impl TemplateDetector {
290    /// Create a new template detector
291    pub fn new() -> Result<Self> {
292        let mut parsers = HashMap::new();
293        
294        // Initialize HTML parser for template detection
295        let mut html_parser = Parser::new();
296        html_parser.set_language(tree_sitter_html::language())
297            .map_err(|e| scribe_core::ScribeError::parse(format!("Failed to set HTML language: {}", e)))?;
298        parsers.insert("html".to_string(), html_parser);
299        
300        Ok(Self {
301            parsers,
302            content_cache: HashMap::new(),
303            max_cache_size: 100, // Cache up to 100 files
304        })
305    }
306    
307    /// Detect if a file is a template and get appropriate score boost
308    pub fn detect_template(&mut self, file_path: &str) -> Result<TemplateDetectionResult> {
309        let path = Path::new(file_path);
310        
311        // Method 1: Extension-based detection (fastest)
312        if let Some(result) = self.detect_by_extension(path) {
313            return Ok(result);
314        }
315        
316        // Method 2: Directory context analysis
317        if let Some(result) = self.detect_by_directory_context(path) {
318            return Ok(result);
319        }
320        
321        // Method 3: Content pattern analysis (slower, for ambiguous files)
322        if self.should_analyze_content(path) {
323            if let Some(result) = self.detect_by_content_patterns(path)? {
324                return Ok(result);
325            }
326        }
327        
328        // Method 4: Language-specific heuristics
329        if let Some(result) = self.detect_by_language_heuristics(path) {
330            return Ok(result);
331        }
332        
333        Ok(TemplateDetectionResult::not_template())
334    }
335    
336    /// Get template score boost for a file path
337    pub fn get_score_boost(&self, file_path: &str) -> Result<f64> {
338        // Use a simplified version that doesn't require mutable self
339        let path = Path::new(file_path);
340        
341        // Extension-based detection
342        if let Some(engine) = self.detect_engine_by_extension(path) {
343            return Ok(engine.score_boost());
344        }
345        
346        // Directory context check
347        if self.is_in_template_directory(path) {
348            return Ok(1.2); // Moderate boost for template directories
349        }
350        
351        Ok(0.0)
352    }
353    
354    /// Detect template engine by file extension
355    fn detect_by_extension(&self, path: &Path) -> Option<TemplateDetectionResult> {
356        if let Some(engine) = self.detect_engine_by_extension(path) {
357            return Some(TemplateDetectionResult::template(
358                engine, 
359                TemplateDetectionMethod::Extension,
360                0.95 // High confidence for extension-based detection
361            ));
362        }
363        None
364    }
365    
366    fn detect_engine_by_extension(&self, path: &Path) -> Option<TemplateEngine> {
367        let extension = path.extension()?.to_str()?.to_lowercase();
368        let ext_with_dot = format!(".{}", extension);
369        
370        EXTENSION_MAP.get(ext_with_dot.as_str()).cloned()
371    }
372    
373    /// Detect by directory context
374    fn detect_by_directory_context(&self, path: &Path) -> Option<TemplateDetectionResult> {
375        if self.is_in_template_directory(path) {
376            // Check if it's HTML/XML in a template directory
377            if let Some(ext) = path.extension() {
378                let ext_str = ext.to_str()?.to_lowercase();
379                if matches!(ext_str.as_str(), "html" | "htm" | "xml") {
380                    return Some(TemplateDetectionResult::template(
381                        TemplateEngine::Generic,
382                        TemplateDetectionMethod::DirectoryContext,
383                        0.7 // Moderate confidence for directory context
384                    ));
385                }
386            }
387        }
388        None
389    }
390    
391    fn is_in_template_directory(&self, path: &Path) -> bool {
392        let path_str = path.to_string_lossy().to_lowercase();
393        TEMPLATE_DIRECTORIES.iter().any(|dir| path_str.contains(dir))
394    }
395    
396    /// Check if file should be analyzed for content patterns
397    fn should_analyze_content(&self, path: &Path) -> bool {
398        // Only analyze potentially ambiguous files
399        if let Some(ext) = path.extension() {
400            let ext_str = ext.to_str().unwrap_or("").to_lowercase();
401            return matches!(ext_str.as_str(), "html" | "htm" | "xml" | "js" | "ts");
402        }
403        false
404    }
405    
406    /// Detect templates by AST-based content pattern analysis
407    fn detect_by_content_patterns(&mut self, path: &Path) -> Result<Option<TemplateDetectionResult>> {
408        let content = self.read_file_content(path)?;
409        
410        // First check for simple template patterns (lightweight check)
411        for pattern in TEMPLATE_PATTERNS.iter() {
412            let occurrences = self.count_pattern_occurrences(&content, &pattern.open_tag, &pattern.close_tag);
413            
414            if occurrences >= pattern.min_occurrences {
415                return Ok(Some(TemplateDetectionResult::template(
416                    pattern.engine.clone(),
417                    TemplateDetectionMethod::ContentPattern,
418                    0.8 // Good confidence for pattern-based detection
419                )));
420            }
421        }
422        
423        // If it's HTML/XML content, try AST-based analysis
424        if self.should_use_ast_analysis(path) {
425            if let Some(result) = self.analyze_with_ast(path, &content)? {
426                return Ok(Some(result));
427            }
428        }
429        
430        // Check for single-pattern indicators as fallback
431        let content_lower = content.to_lowercase();
432        for &pattern in SINGLE_PATTERNS {
433            if content_lower.contains(pattern) {
434                return Ok(Some(TemplateDetectionResult::template(
435                    TemplateEngine::Generic,
436                    TemplateDetectionMethod::ContentPattern,
437                    0.6 // Lower confidence for single patterns
438                )));
439            }
440        }
441        
442        Ok(None)
443    }
444    
445    /// Detect by language-specific heuristics
446    fn detect_by_language_heuristics(&self, path: &Path) -> Option<TemplateDetectionResult> {
447        // This is a placeholder for more sophisticated language analysis
448        // In a full implementation, this might use tree-sitter or similar
449        // for AST-based template detection
450        
451        if let Some(ext) = path.extension() {
452            let ext_str = ext.to_str()?.to_lowercase();
453            
454            // JSX/TSX are React templates
455            if matches!(ext_str.as_str(), "jsx" | "tsx") {
456                return Some(TemplateDetectionResult::template(
457                    TemplateEngine::React,
458                    TemplateDetectionMethod::LanguageHeuristic,
459                    0.9
460                ));
461            }
462        }
463        
464        None
465    }
466    
467    /// Read file content with caching
468    fn read_file_content(&mut self, path: &Path) -> Result<String> {
469        // Check cache first
470        if let Some(content) = self.content_cache.get(path) {
471            return Ok(content.clone());
472        }
473        
474        // Read file (limit to first 2KB for performance)
475        let file = fs::File::open(path)?;
476        let reader = BufReader::new(file);
477        let mut content = String::new();
478        let mut bytes_read = 0;
479        const MAX_READ_SIZE: usize = 2048;
480        
481        for line in reader.lines() {
482            let line = line?;
483            if bytes_read + line.len() > MAX_READ_SIZE {
484                break;
485            }
486            content.push_str(&line);
487            content.push('\n');
488            bytes_read += line.len() + 1;
489        }
490        
491        // Cache the content (with size limit)
492        if self.content_cache.len() < self.max_cache_size {
493            self.content_cache.insert(path.to_path_buf(), content.clone());
494        }
495        
496        Ok(content)
497    }
498    
499    /// Count occurrences of a pattern pair in content
500    fn count_pattern_occurrences(&self, content: &str, open_tag: &str, close_tag: &str) -> usize {
501        let open_count = content.matches(open_tag).count();
502        let close_count = content.matches(close_tag).count();
503        
504        // Return the minimum of open and close tags (pairs)
505        open_count.min(close_count)
506    }
507    
508    /// Check if file should use AST analysis
509    fn should_use_ast_analysis(&self, path: &Path) -> bool {
510        if let Some(ext) = path.extension() {
511            let ext_str = ext.to_str().unwrap_or("").to_lowercase();
512            return matches!(ext_str.as_str(), "html" | "htm" | "xml" | "vue" | "svelte");
513        }
514        false
515    }
516    
517    /// Analyze content using AST parsing
518    fn analyze_with_ast(&mut self, path: &Path, content: &str) -> Result<Option<TemplateDetectionResult>> {
519        if let Some(parser) = self.parsers.get_mut("html") {
520            if let Some(tree) = parser.parse(content, None) {
521                let root_node = tree.root_node();
522                
523                // Check for template-specific patterns in AST
524                if self.has_template_attributes(&root_node) {
525                    let engine = self.detect_template_engine_from_ast(&root_node, path);
526                    return Ok(Some(TemplateDetectionResult::template(
527                        engine,
528                        TemplateDetectionMethod::ContentPattern,
529                        0.85 // High confidence for AST-based detection
530                    )));
531                }
532            }
533        }
534        Ok(None)
535    }
536    
537    /// Check for template-specific attributes in HTML AST
538    fn has_template_attributes(&self, node: &Node) -> bool {
539        let template_indicators = [
540            "v-",        // Vue.js directives
541            "ng-",       // Angular directives  
542            "*ng",       // Angular structural directives
543            ":bind",     // Vue binding
544            "@click",    // Vue events
545            "{{{",       // Template expressions
546            "{{",        // Template expressions
547            "<%",        // EJS, ERB
548            "{%",        // Jinja2, Liquid
549        ];
550        
551        self.node_contains_patterns(node, &template_indicators)
552    }
553    
554    /// Recursively check if node or its children contain template patterns
555    fn node_contains_patterns(&self, node: &Node, patterns: &[&str]) -> bool {
556        // Check current node kind
557        if patterns.iter().any(|&pattern| node.kind().contains(pattern)) {
558            return true;
559        }
560        
561        // Check node text content if it's a text node
562        if node.kind() == "text" || node.kind() == "attribute_value" {
563            // For text nodes, we'd need the actual content, which requires the source text
564            // This is a simplified check - in practice you'd get the node's text from content
565            return true; // Assume potential template content for now
566        }
567        
568        // Recursively check children
569        for i in 0..node.child_count() {
570            if let Some(child) = node.child(i) {
571                if self.node_contains_patterns(&child, patterns) {
572                    return true;
573                }
574            }
575        }
576        
577        false
578    }
579    
580    /// Detect specific template engine from AST patterns
581    fn detect_template_engine_from_ast(&self, node: &Node, path: &Path) -> TemplateEngine {
582        // Check file extension first
583        if let Some(ext) = path.extension() {
584            let ext_str = ext.to_str().unwrap_or("").to_lowercase();
585            match ext_str.as_str() {
586                "vue" => return TemplateEngine::Vue,
587                "svelte" => return TemplateEngine::Svelte,
588                _ => {}
589            }
590        }
591        
592        // Analyze AST structure for engine-specific patterns
593        if self.has_vue_patterns(node) {
594            TemplateEngine::Vue
595        } else if self.has_angular_patterns(node) {
596            TemplateEngine::Angular
597        } else if self.has_react_patterns(node) {
598            TemplateEngine::React
599        } else {
600            TemplateEngine::Generic
601        }
602    }
603    
604    /// Check for Vue.js specific patterns
605    fn has_vue_patterns(&self, node: &Node) -> bool {
606        let vue_patterns = ["v-if", "v-for", "v-model", "v-bind", "@click", ":class"];
607        self.node_contains_patterns(node, &vue_patterns)
608    }
609    
610    /// Check for Angular specific patterns
611    fn has_angular_patterns(&self, node: &Node) -> bool {
612        let angular_patterns = ["*ngFor", "*ngIf", "(click)", "[class]", "[(ngModel)]"];
613        self.node_contains_patterns(node, &angular_patterns)
614    }
615    
616    /// Check for React JSX patterns (limited in HTML parser)
617    fn has_react_patterns(&self, node: &Node) -> bool {
618        let react_patterns = ["className", "onClick", "onChange"];
619        self.node_contains_patterns(node, &react_patterns)
620    }
621    
622    /// Clear content cache
623    pub fn clear_cache(&mut self) {
624        self.content_cache.clear();
625    }
626}
627
628impl Default for TemplateDetector {
629    fn default() -> Self {
630        Self::new().expect("Failed to create TemplateDetector")
631    }
632}
633
634/// Convenience function to check if a file is a template
635pub fn is_template_file(file_path: &str) -> Result<bool> {
636    let mut detector = TemplateDetector::new()?;
637    let result = detector.detect_template(file_path)?;
638    Ok(result.is_template)
639}
640
641/// Convenience function to get template score boost
642pub fn get_template_score_boost(file_path: &str) -> Result<f64> {
643    let detector = TemplateDetector::new()?;
644    detector.get_score_boost(file_path)
645}
646
647#[cfg(test)]
648mod tests {
649    use super::*;
650    use std::fs;
651    use tempfile::NamedTempFile;
652    use std::io::Write;
653    
654    fn create_test_file(content: &str, extension: &str) -> NamedTempFile {
655        let mut file = NamedTempFile::new().unwrap();
656        file.write_all(content.as_bytes()).unwrap();
657        
658        // Rename with proper extension (this is a bit hacky but works for tests)
659        let path = file.path().with_extension(extension);
660        std::fs::rename(file.path(), &path).unwrap();
661        
662        file
663    }
664    
665    #[test]
666    fn test_extension_based_detection() {
667        let detector = TemplateDetector::new().unwrap();
668        
669        // Test known template extensions
670        assert_eq!(
671            detector.detect_engine_by_extension(Path::new("template.hbs")),
672            Some(TemplateEngine::Handlebars)
673        );
674        
675        assert_eq!(
676            detector.detect_engine_by_extension(Path::new("view.j2")),
677            Some(TemplateEngine::Jinja2)
678        );
679        
680        assert_eq!(
681            detector.detect_engine_by_extension(Path::new("component.jsx")),
682            Some(TemplateEngine::React)
683        );
684        
685        // Test non-template extension
686        assert_eq!(
687            detector.detect_engine_by_extension(Path::new("script.js")),
688            None
689        );
690    }
691    
692    #[test]
693    fn test_directory_context_detection() {
694        let detector = TemplateDetector::new().unwrap();
695        
696        assert!(detector.is_in_template_directory(Path::new("templates/layout.html")));
697        assert!(detector.is_in_template_directory(Path::new("src/components/header.html")));
698        assert!(!detector.is_in_template_directory(Path::new("src/utils/helper.js")));
699    }
700    
701    #[test]
702    fn test_pattern_counting() {
703        let detector = TemplateDetector::new().unwrap();
704        let content = "Hello {{ name }}! Welcome to {{ site }}.";
705        
706        assert_eq!(detector.count_pattern_occurrences(content, "{{", "}}"), 2);
707        assert_eq!(detector.count_pattern_occurrences(content, "{%", "%}"), 0);
708    }
709    
710    #[test]
711    fn test_template_score_boost() {
712        let detector = TemplateDetector::new().unwrap();
713        
714        // High boost for dedicated template files
715        assert!(detector.get_score_boost("template.hbs").unwrap() > 1.0);
716        
717        // No boost for regular files
718        assert_eq!(detector.get_score_boost("script.js").unwrap(), 0.0);
719    }
720    
721    #[test]
722    fn test_engine_score_boost() {
723        assert!(TemplateEngine::Handlebars.score_boost() > 1.0);
724        assert!(TemplateEngine::React.score_boost() > 1.0);
725        assert!(TemplateEngine::Generic.score_boost() < 1.0);
726    }
727}