@ape-egg/vibe 1.3.2 → 1.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +177 -0
- package/README.md +97 -0
- package/boot.js +0 -1
- package/compiler/native/vibe-compiler-darwin-arm64 +0 -0
- package/compiler/src/Cargo.lock +719 -40
- package/compiler/src/Cargo.toml +11 -2
- package/compiler/src/compiler/PRE-RENDERING-IMPLEMENTATION.md +241 -0
- package/compiler/src/compiler/compile.rs +552 -175
- package/compiler/src/compiler/component_tagger.rs +234 -0
- package/compiler/src/compiler/iteration_optimizer.rs +351 -0
- package/compiler/src/compiler/js_analyzer.rs +572 -0
- package/compiler/src/compiler/manifest_builder.rs +251 -26
- package/compiler/src/compiler/mod.rs +5 -1
- package/compiler/src/compiler/state_extractor.rs +140 -25
- package/compiler/src/compiler/value_stamper.rs +579 -88
- package/compiler/src/compiler/watcher.rs +579 -0
- package/compiler/src/config.rs +51 -8
- package/compiler/src/main.rs +41 -28
- package/compiler/src/parser/html.rs +229 -118
- package/component.js +23 -11
- package/package.json +1 -1
- package/runtime/cleanup.js +4 -4
- package/runtime/component.js +98 -21
- package/runtime/conditionals.js +2 -2
- package/runtime/constants.js +2 -1
- package/runtime/index.js +152 -30
- package/runtime/iterate.js +27 -5
- package/runtime/parse.js +2 -1
- package/runtime/pre-compiled-iterations.js +153 -0
- package/runtime/{hyperspeed.js → pre-compiled-manifest.js} +204 -132
- package/runtime/utils.js +2 -1
- package/test-results/.last-run.json +4 -0
- package/vibe.css +19 -0
- package/runtime/component-state.js +0 -63
|
@@ -6,6 +6,8 @@ use std::collections::{BTreeMap, HashSet, HashMap};
|
|
|
6
6
|
use thiserror::Error;
|
|
7
7
|
use colored::Colorize;
|
|
8
8
|
use regex::Regex;
|
|
9
|
+
use glob::Pattern;
|
|
10
|
+
use rayon::prelude::*;
|
|
9
11
|
|
|
10
12
|
use crate::config::Config;
|
|
11
13
|
use crate::parser::HtmlParser;
|
|
@@ -18,6 +20,10 @@ use crate::parser::HtmlParser;
|
|
|
18
20
|
// Future refactoring: Replace with configurable include/exclude patterns,
|
|
19
21
|
// or a more sophisticated asset pipeline.
|
|
20
22
|
// =============================================================================
|
|
23
|
+
// FOUC prevention class/attribute name (matches runtime/constants.js)
|
|
24
|
+
// Can be used as class (.vibe-fouc) or attribute ([vibe-fouc])
|
|
25
|
+
const FOUC_CLASS_OR_ATTR: &str = "vibe-fouc";
|
|
26
|
+
|
|
21
27
|
const MIRROR_EXTENSIONS: &[&str] = &[
|
|
22
28
|
"css", "js",
|
|
23
29
|
// Fonts
|
|
@@ -32,16 +38,78 @@ const MIRROR_EXTENSIONS: &[&str] = &[
|
|
|
32
38
|
"json", "xml", "csv",
|
|
33
39
|
];
|
|
34
40
|
|
|
35
|
-
//
|
|
36
|
-
|
|
41
|
+
// Files and directories to skip when walking source (supports glob patterns)
|
|
42
|
+
// Note: Output directory is checked dynamically (not hardcoded here)
|
|
43
|
+
pub const SKIP_FILES: &[&str] = &[
|
|
37
44
|
".git",
|
|
38
45
|
".claude",
|
|
39
|
-
"
|
|
40
|
-
"
|
|
46
|
+
"target", // Rust build artifacts
|
|
47
|
+
"tests", // Test files
|
|
41
48
|
"test-results",
|
|
42
49
|
"playwright-report",
|
|
50
|
+
"node_modules", // Skip node_modules during scanning
|
|
51
|
+
"**/*.test.js", // Test files
|
|
52
|
+
"**/*.config.js", // Config files
|
|
53
|
+
"**/*.test.ts", // Test files
|
|
54
|
+
"**/*.config.ts", // Config files
|
|
43
55
|
];
|
|
44
56
|
|
|
57
|
+
/// Check if a path should be skipped based on SKIP_FILES patterns
|
|
58
|
+
pub fn should_skip_path(path: &Path, name: &str) -> bool {
|
|
59
|
+
// Check exact name match (for directories and simple filenames)
|
|
60
|
+
if SKIP_FILES.contains(&name) {
|
|
61
|
+
return true;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Check glob patterns (e.g., **/*.test.js)
|
|
65
|
+
for pattern_str in SKIP_FILES {
|
|
66
|
+
if pattern_str.contains('*') {
|
|
67
|
+
if let Ok(pattern) = Pattern::new(pattern_str) {
|
|
68
|
+
// Try matching against just the filename
|
|
69
|
+
if pattern.matches(name) {
|
|
70
|
+
return true;
|
|
71
|
+
}
|
|
72
|
+
// Try matching against the full path
|
|
73
|
+
if let Some(path_str) = path.to_str() {
|
|
74
|
+
if pattern.matches(path_str) {
|
|
75
|
+
return true;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
false
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/// Remove FOUC prevention class and/or attribute from compiled HTML
|
|
86
|
+
/// Since HTML is pre-rendered, there's no need for FOUC prevention
|
|
87
|
+
fn remove_fouc_prevention(html: String) -> String {
|
|
88
|
+
let mut result = html;
|
|
89
|
+
|
|
90
|
+
// Remove as class: class="vibe-fouc" or class="vibe-fouc other-class" or class="other-class vibe-fouc"
|
|
91
|
+
result = Regex::new(&format!(r#"\s+class="{}""#, FOUC_CLASS_OR_ATTR))
|
|
92
|
+
.unwrap()
|
|
93
|
+
.replace_all(&result, "")
|
|
94
|
+
.to_string();
|
|
95
|
+
result = Regex::new(&format!(r#"class="{}\s+"#, FOUC_CLASS_OR_ATTR))
|
|
96
|
+
.unwrap()
|
|
97
|
+
.replace_all(&result, r#"class=""#)
|
|
98
|
+
.to_string();
|
|
99
|
+
result = Regex::new(&format!(r#"class="([^"]*\s+){}(\s+[^"]*)""#, FOUC_CLASS_OR_ATTR))
|
|
100
|
+
.unwrap()
|
|
101
|
+
.replace_all(&result, r#"class="$1$2""#)
|
|
102
|
+
.to_string();
|
|
103
|
+
|
|
104
|
+
// Remove as attribute: vibe-fouc or vibe-fouc=""
|
|
105
|
+
result = Regex::new(&format!(r#"\s+{}(?:="[^"]*")?"#, FOUC_CLASS_OR_ATTR))
|
|
106
|
+
.unwrap()
|
|
107
|
+
.replace_all(&result, "")
|
|
108
|
+
.to_string();
|
|
109
|
+
|
|
110
|
+
result
|
|
111
|
+
}
|
|
112
|
+
|
|
45
113
|
#[derive(Error, Debug)]
|
|
46
114
|
pub enum CompileError {
|
|
47
115
|
#[error("Source directory not found: {0}")]
|
|
@@ -72,15 +140,11 @@ pub enum CompileError {
|
|
|
72
140
|
CommandError(String),
|
|
73
141
|
#[error("Package manager not found")]
|
|
74
142
|
PackageManagerNotFound,
|
|
75
|
-
#[error("Component
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
pub struct ComponentError {
|
|
81
|
-
pub component_src: String,
|
|
82
|
-
pub referenced_in: String,
|
|
83
|
-
pub error_message: String,
|
|
143
|
+
#[error("Component name '{component_name}' in '{file_path}' conflicts with reserved element. Component filenames cannot match standard HTML elements (case-insensitive). Use PascalCase (e.g., 'Nav.html' instead of 'nav.html') to avoid conflicts.")]
|
|
144
|
+
ReservedComponentName {
|
|
145
|
+
component_name: String,
|
|
146
|
+
file_path: String,
|
|
147
|
+
},
|
|
84
148
|
}
|
|
85
149
|
|
|
86
150
|
pub struct CompileStats {
|
|
@@ -353,8 +417,8 @@ pub struct Compiler {
|
|
|
353
417
|
verbose: bool,
|
|
354
418
|
logger: Option<VerboseLogger>,
|
|
355
419
|
unique_components: HashSet<String>,
|
|
356
|
-
|
|
357
|
-
|
|
420
|
+
/// Cache for all components (both internal paths and external URLs)
|
|
421
|
+
component_cache: std::collections::HashMap<String, String>,
|
|
358
422
|
}
|
|
359
423
|
|
|
360
424
|
impl Compiler {
|
|
@@ -365,8 +429,7 @@ impl Compiler {
|
|
|
365
429
|
verbose,
|
|
366
430
|
logger,
|
|
367
431
|
unique_components: HashSet::new(),
|
|
368
|
-
|
|
369
|
-
external_component_cache: HashMap::new(),
|
|
432
|
+
component_cache: HashMap::new(),
|
|
370
433
|
}
|
|
371
434
|
}
|
|
372
435
|
|
|
@@ -395,21 +458,37 @@ impl Compiler {
|
|
|
395
458
|
));
|
|
396
459
|
}
|
|
397
460
|
|
|
398
|
-
// Load components for HTML compilation
|
|
461
|
+
// Load components for HTML compilation
|
|
399
462
|
let mut parser = HtmlParser::new(self.config.components_path());
|
|
400
463
|
parser.load_elements()?;
|
|
401
464
|
|
|
402
|
-
// Validate
|
|
403
|
-
|
|
465
|
+
// Validate component names don't conflict with reserved elements
|
|
466
|
+
self.validate_component_names()?;
|
|
467
|
+
|
|
468
|
+
// Fetch and cache all components for inlining (when not components_as_is)
|
|
469
|
+
// This is separate from validation - we need component content for inlining regardless of validate flag
|
|
404
470
|
if !self.config.components_as_is {
|
|
471
|
+
let fetch_start = Instant::now();
|
|
472
|
+
if self.verbose {
|
|
473
|
+
println!("\nFetching components for inlining...");
|
|
474
|
+
}
|
|
475
|
+
self.fetch_all_components(&parser)?;
|
|
476
|
+
stats.components_time_ms = fetch_start.elapsed().as_secs_f64() * 1000.0;
|
|
477
|
+
if self.verbose {
|
|
478
|
+
println!(" All components fetched successfully");
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
// Validate HTML syntax if requested (optional, controlled by validate flag)
|
|
483
|
+
if self.config.validate {
|
|
405
484
|
let validation_start = Instant::now();
|
|
406
485
|
if self.verbose {
|
|
407
|
-
println!("\nValidating
|
|
486
|
+
println!("\nValidating HTML syntax...");
|
|
408
487
|
}
|
|
409
|
-
self.
|
|
488
|
+
self.validate_html_syntax()?;
|
|
410
489
|
stats.validation_time_ms = Some(validation_start.elapsed().as_secs_f64() * 1000.0);
|
|
411
490
|
if self.verbose {
|
|
412
|
-
println!("
|
|
491
|
+
println!(" HTML validation completed successfully");
|
|
413
492
|
}
|
|
414
493
|
}
|
|
415
494
|
|
|
@@ -423,15 +502,18 @@ impl Compiler {
|
|
|
423
502
|
}
|
|
424
503
|
}
|
|
425
504
|
|
|
505
|
+
// Canonicalize output path once for reliable comparison
|
|
506
|
+
let canonical_output = self.config.output.canonicalize()
|
|
507
|
+
.unwrap_or_else(|_| self.config.output.clone());
|
|
426
508
|
|
|
427
509
|
// Track compile and copy times separately
|
|
428
510
|
let compile_start = Instant::now();
|
|
429
511
|
|
|
430
512
|
// Process all HTML files in source (excluding components directory)
|
|
431
|
-
self.process_directory_html_only(&self.config.source.clone(), &parser, "", &mut stats)?;
|
|
513
|
+
self.process_directory_html_only(&self.config.source.clone(), &parser, "", &canonical_output, &mut stats)?;
|
|
432
514
|
|
|
433
515
|
// Process source directory for assets (CSS, JS, images, etc.)
|
|
434
|
-
self.process_directory_assets_only(&self.config.source.clone(), "", &mut stats)?;
|
|
516
|
+
self.process_directory_assets_only(&self.config.source.clone(), "", &canonical_output, &mut stats)?;
|
|
435
517
|
|
|
436
518
|
let process_time = compile_start.elapsed();
|
|
437
519
|
|
|
@@ -505,6 +587,190 @@ impl Compiler {
|
|
|
505
587
|
Ok(stats)
|
|
506
588
|
}
|
|
507
589
|
|
|
590
|
+
/// Compile specific HTML files (incremental compilation for watch mode)
|
|
591
|
+
pub fn compile_specific_html_files(
|
|
592
|
+
&mut self,
|
|
593
|
+
files: &[PathBuf],
|
|
594
|
+
parser: &HtmlParser,
|
|
595
|
+
) -> Result<CompileStats, CompileError> {
|
|
596
|
+
let mut stats = CompileStats {
|
|
597
|
+
files_compiled: 0,
|
|
598
|
+
files_copied: 0,
|
|
599
|
+
internal_components_unique: 0,
|
|
600
|
+
external_components_unique: 0,
|
|
601
|
+
internal_components_total: 0,
|
|
602
|
+
external_components_total: 0,
|
|
603
|
+
compile_time_ms: 0.0,
|
|
604
|
+
copy_time_ms: 0.0,
|
|
605
|
+
components_time_ms: 0.0,
|
|
606
|
+
validation_time_ms: None,
|
|
607
|
+
node_modules_time_ms: None,
|
|
608
|
+
package_manager: None,
|
|
609
|
+
node_modules_copied_as_is: false,
|
|
610
|
+
components_as_is: self.config.components_as_is,
|
|
611
|
+
};
|
|
612
|
+
|
|
613
|
+
let start = Instant::now();
|
|
614
|
+
|
|
615
|
+
// Pre-fetch components needed by these specific files (only if not components_as_is)
|
|
616
|
+
if !self.config.components_as_is {
|
|
617
|
+
let fetch_start = Instant::now();
|
|
618
|
+
self.fetch_components_for_files(files, parser)?;
|
|
619
|
+
stats.components_time_ms = fetch_start.elapsed().as_secs_f64() * 1000.0;
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
for file_path in files {
|
|
623
|
+
// Calculate relative path
|
|
624
|
+
let relative_path = file_path
|
|
625
|
+
.strip_prefix(&self.config.source)
|
|
626
|
+
.map(|p| p.parent().unwrap_or(Path::new("")))
|
|
627
|
+
.unwrap_or(Path::new(""))
|
|
628
|
+
.to_str()
|
|
629
|
+
.unwrap_or("");
|
|
630
|
+
|
|
631
|
+
let (internal, external, component_srcs) = self.compile_html_file(file_path, parser, relative_path)?;
|
|
632
|
+
stats.files_compiled += 1;
|
|
633
|
+
stats.internal_components_total += internal;
|
|
634
|
+
stats.external_components_total += external;
|
|
635
|
+
|
|
636
|
+
// Track unique components
|
|
637
|
+
for src in &component_srcs {
|
|
638
|
+
self.unique_components.insert(src.clone());
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
if let Some(ref mut logger) = self.logger {
|
|
642
|
+
logger.log(file_path, FileOperation::Compiled, &self.config.source);
|
|
643
|
+
for src in component_srcs {
|
|
644
|
+
logger.log_component_occurrence(src);
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
// Calculate unique components
|
|
650
|
+
let internal_unique = self.unique_components.iter()
|
|
651
|
+
.filter(|src| !src.starts_with("http://") && !src.starts_with("https://"))
|
|
652
|
+
.count();
|
|
653
|
+
let external_unique = self.unique_components.iter()
|
|
654
|
+
.filter(|src| src.starts_with("http://") || src.starts_with("https://"))
|
|
655
|
+
.count();
|
|
656
|
+
|
|
657
|
+
stats.internal_components_unique = internal_unique;
|
|
658
|
+
stats.external_components_unique = external_unique;
|
|
659
|
+
|
|
660
|
+
// Calculate compile time (excluding component fetch time which is already tracked)
|
|
661
|
+
let elapsed = start.elapsed().as_secs_f64() * 1000.0;
|
|
662
|
+
stats.compile_time_ms = elapsed - stats.components_time_ms;
|
|
663
|
+
|
|
664
|
+
Ok(stats)
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
/// Copy specific asset files (incremental compilation for watch mode)
|
|
668
|
+
pub fn copy_specific_asset_files(
|
|
669
|
+
&mut self,
|
|
670
|
+
files: &[PathBuf],
|
|
671
|
+
) -> Result<CompileStats, CompileError> {
|
|
672
|
+
let mut stats = CompileStats {
|
|
673
|
+
files_compiled: 0,
|
|
674
|
+
files_copied: 0,
|
|
675
|
+
internal_components_unique: 0,
|
|
676
|
+
external_components_unique: 0,
|
|
677
|
+
internal_components_total: 0,
|
|
678
|
+
external_components_total: 0,
|
|
679
|
+
compile_time_ms: 0.0,
|
|
680
|
+
copy_time_ms: 0.0,
|
|
681
|
+
components_time_ms: 0.0,
|
|
682
|
+
validation_time_ms: None,
|
|
683
|
+
node_modules_time_ms: None,
|
|
684
|
+
package_manager: None,
|
|
685
|
+
node_modules_copied_as_is: false,
|
|
686
|
+
components_as_is: self.config.components_as_is,
|
|
687
|
+
};
|
|
688
|
+
|
|
689
|
+
let start = Instant::now();
|
|
690
|
+
|
|
691
|
+
for file_path in files {
|
|
692
|
+
// Calculate relative path for preserving directory structure
|
|
693
|
+
let relative_path = file_path
|
|
694
|
+
.strip_prefix(&self.config.source)
|
|
695
|
+
.map(|p| p.parent().unwrap_or(Path::new("")))
|
|
696
|
+
.unwrap_or(Path::new(""))
|
|
697
|
+
.to_str()
|
|
698
|
+
.unwrap_or("");
|
|
699
|
+
|
|
700
|
+
self.copy_file(file_path, relative_path)?;
|
|
701
|
+
stats.files_copied += 1;
|
|
702
|
+
|
|
703
|
+
if let Some(ref mut logger) = self.logger {
|
|
704
|
+
logger.log(file_path, FileOperation::Copied, &self.config.source);
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
let elapsed = start.elapsed().as_secs_f64() * 1000.0;
|
|
709
|
+
stats.copy_time_ms = elapsed;
|
|
710
|
+
|
|
711
|
+
Ok(stats)
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
/// Generate manifests for specific HTML files (incremental compilation for watch mode)
|
|
715
|
+
pub fn generate_manifests_for_files(&self, files: &[PathBuf]) -> Result<ManifestStats, CompileError> {
|
|
716
|
+
let start = Instant::now();
|
|
717
|
+
let mut pages_processed = 0;
|
|
718
|
+
let mut pages_skipped = 0;
|
|
719
|
+
|
|
720
|
+
for file_path in files {
|
|
721
|
+
// Convert source path to output path
|
|
722
|
+
let relative_path = file_path
|
|
723
|
+
.strip_prefix(&self.config.source)
|
|
724
|
+
.unwrap_or(file_path)
|
|
725
|
+
.to_str()
|
|
726
|
+
.unwrap();
|
|
727
|
+
|
|
728
|
+
let output_path = self.config.output.join(relative_path);
|
|
729
|
+
|
|
730
|
+
if !output_path.exists() {
|
|
731
|
+
pages_skipped += 1;
|
|
732
|
+
continue;
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
// Read compiled HTML from output
|
|
736
|
+
let html = fs::read_to_string(&output_path)
|
|
737
|
+
.map_err(|e| CompileError::ReadError {
|
|
738
|
+
path: output_path.display().to_string(),
|
|
739
|
+
source: e,
|
|
740
|
+
})?;
|
|
741
|
+
|
|
742
|
+
// Try to generate manifest for this file (skip on error)
|
|
743
|
+
match Self::generate_file_manifest(
|
|
744
|
+
&html,
|
|
745
|
+
&output_path,
|
|
746
|
+
&self.config.output,
|
|
747
|
+
relative_path,
|
|
748
|
+
self.verbose,
|
|
749
|
+
self.config.iterations_as_is,
|
|
750
|
+
self.config.components_as_is,
|
|
751
|
+
&self.config.source,
|
|
752
|
+
) {
|
|
753
|
+
Ok(()) => {
|
|
754
|
+
pages_processed += 1;
|
|
755
|
+
}
|
|
756
|
+
Err(e) => {
|
|
757
|
+
pages_skipped += 1;
|
|
758
|
+
if self.verbose {
|
|
759
|
+
println!(" Skipped ({}): {}", e, relative_path);
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
let total_time_ms = start.elapsed().as_millis() as f64;
|
|
766
|
+
|
|
767
|
+
Ok(ManifestStats {
|
|
768
|
+
pages_processed,
|
|
769
|
+
pages_skipped,
|
|
770
|
+
total_time_ms,
|
|
771
|
+
})
|
|
772
|
+
}
|
|
773
|
+
|
|
508
774
|
/// Generate manifest for a single file
|
|
509
775
|
fn generate_file_manifest(
|
|
510
776
|
html: &str,
|
|
@@ -512,21 +778,30 @@ impl Compiler {
|
|
|
512
778
|
output_dir: &Path,
|
|
513
779
|
relative_path: &str,
|
|
514
780
|
_verbose: bool,
|
|
781
|
+
iterations_as_is: bool,
|
|
782
|
+
components_as_is: bool,
|
|
783
|
+
source_root: &Path,
|
|
515
784
|
) -> Result<(), String> {
|
|
516
|
-
use crate::compiler::state_extractor::StateExtractor;
|
|
517
785
|
use crate::compiler::manifest_builder::ManifestBuilder;
|
|
518
786
|
use crate::compiler::value_stamper::ValueStamper;
|
|
787
|
+
use crate::compiler::component_tagger::ComponentTagger;
|
|
519
788
|
|
|
520
|
-
//
|
|
521
|
-
let
|
|
789
|
+
// Tag components with deterministic IDs and structure state
|
|
790
|
+
let tagged = ComponentTagger::tag_components(html, &source_root.to_path_buf())?;
|
|
791
|
+
let html = &tagged.html; // Use modified HTML with data-vibe-component-id attributes
|
|
792
|
+
let state = tagged.state;
|
|
522
793
|
|
|
523
|
-
// Build manifest
|
|
794
|
+
// Build manifest from ORIGINAL HTML (before stamping, so templates have @[...] markers)
|
|
524
795
|
let manifest_builder = ManifestBuilder::new();
|
|
525
|
-
let manifest = manifest_builder.build_from_html(html, &state)?;
|
|
796
|
+
let manifest = manifest_builder.build_from_html(html, &state, iterations_as_is)?;
|
|
526
797
|
|
|
527
|
-
// Stamp values (pre-render)
|
|
528
|
-
|
|
529
|
-
let
|
|
798
|
+
// Stamp values (pre-render) AFTER building manifest
|
|
799
|
+
// When components_as_is is true, skip stamping inside component elements (runtime will handle them)
|
|
800
|
+
let stamper = ValueStamper::new(&state, components_as_is)?;
|
|
801
|
+
let mut pre_rendered = stamper.stamp_html(html.to_string())?;
|
|
802
|
+
|
|
803
|
+
// Remove FOUC prevention since HTML is pre-rendered
|
|
804
|
+
pre_rendered = remove_fouc_prevention(pre_rendered);
|
|
530
805
|
|
|
531
806
|
// Write pre-rendered HTML
|
|
532
807
|
fs::write(html_path, pre_rendered)
|
|
@@ -572,31 +847,46 @@ impl Compiler {
|
|
|
572
847
|
// Find all HTML files in compiled output (excluding components directory)
|
|
573
848
|
let html_files = self.find_all_html_files(&self.config.output)?;
|
|
574
849
|
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
850
|
+
// Process manifests in parallel
|
|
851
|
+
let output_dir = self.config.output.clone();
|
|
852
|
+
let source_root = self.config.source.clone();
|
|
853
|
+
let verbose = self.verbose;
|
|
854
|
+
let iterations_as_is = self.config.iterations_as_is;
|
|
855
|
+
let components_as_is = self.config.components_as_is;
|
|
856
|
+
|
|
857
|
+
let results: Vec<_> = html_files
|
|
858
|
+
.par_iter()
|
|
859
|
+
.map(|html_path| {
|
|
860
|
+
let relative_path = html_path.strip_prefix(&output_dir)
|
|
861
|
+
.unwrap()
|
|
862
|
+
.to_str()
|
|
863
|
+
.unwrap();
|
|
864
|
+
|
|
865
|
+
// Read compiled HTML
|
|
866
|
+
let html = match fs::read_to_string(html_path) {
|
|
867
|
+
Ok(h) => h,
|
|
868
|
+
Err(_) => return (false, Some(relative_path.to_string())),
|
|
869
|
+
};
|
|
587
870
|
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
println!(" Skipped ({}): {}", e, relative_path);
|
|
871
|
+
// Try to generate manifest for this file (skip on error)
|
|
872
|
+
match Self::generate_file_manifest(&html, html_path, &output_dir, relative_path, verbose, iterations_as_is, components_as_is, &source_root) {
|
|
873
|
+
Ok(()) => (true, None),
|
|
874
|
+
Err(e) => {
|
|
875
|
+
if verbose {
|
|
876
|
+
eprintln!(" Skipped ({}): {}", e, relative_path);
|
|
877
|
+
}
|
|
878
|
+
(false, Some(relative_path.to_string()))
|
|
597
879
|
}
|
|
598
|
-
// Don't fail compilation, just skip this file's manifest
|
|
599
880
|
}
|
|
881
|
+
})
|
|
882
|
+
.collect();
|
|
883
|
+
|
|
884
|
+
// Count results
|
|
885
|
+
for (success, _) in results {
|
|
886
|
+
if success {
|
|
887
|
+
pages_processed += 1;
|
|
888
|
+
} else {
|
|
889
|
+
pages_skipped += 1;
|
|
600
890
|
}
|
|
601
891
|
}
|
|
602
892
|
|
|
@@ -615,6 +905,7 @@ impl Compiler {
|
|
|
615
905
|
dir: &Path,
|
|
616
906
|
parser: &HtmlParser,
|
|
617
907
|
relative_path: &str,
|
|
908
|
+
canonical_output: &Path,
|
|
618
909
|
stats: &mut CompileStats,
|
|
619
910
|
) -> Result<(), CompileError> {
|
|
620
911
|
let entries = fs::read_dir(dir).map_err(|e| CompileError::ReadError {
|
|
@@ -627,8 +918,14 @@ impl Compiler {
|
|
|
627
918
|
let file_name = path.file_name().unwrap().to_str().unwrap();
|
|
628
919
|
|
|
629
920
|
if path.is_dir() {
|
|
921
|
+
// Skip output directory (dynamically check, not hardcoded "compiled")
|
|
922
|
+
let canonical_path = path.canonicalize().unwrap_or_else(|_| path.clone());
|
|
923
|
+
if canonical_path == *canonical_output || canonical_path.starts_with(canonical_output) {
|
|
924
|
+
continue;
|
|
925
|
+
}
|
|
926
|
+
|
|
630
927
|
// Skip special directories
|
|
631
|
-
if
|
|
928
|
+
if should_skip_path(&path, file_name) {
|
|
632
929
|
continue;
|
|
633
930
|
}
|
|
634
931
|
|
|
@@ -644,7 +941,7 @@ impl Compiler {
|
|
|
644
941
|
format!("{}/{}", relative_path, file_name)
|
|
645
942
|
};
|
|
646
943
|
|
|
647
|
-
self.process_directory_html_only(&path, parser, &new_relative, stats)?;
|
|
944
|
+
self.process_directory_html_only(&path, parser, &new_relative, canonical_output, stats)?;
|
|
648
945
|
} else if path.extension().and_then(|e| e.to_str()) == Some("html") {
|
|
649
946
|
let (internal, external, component_srcs) = self.compile_html_file(&path, parser, relative_path)?;
|
|
650
947
|
stats.files_compiled += 1;
|
|
@@ -692,6 +989,7 @@ impl Compiler {
|
|
|
692
989
|
&mut self,
|
|
693
990
|
dir: &Path,
|
|
694
991
|
relative_path: &str,
|
|
992
|
+
canonical_output: &Path,
|
|
695
993
|
stats: &mut CompileStats,
|
|
696
994
|
) -> Result<(), CompileError> {
|
|
697
995
|
let entries = fs::read_dir(dir).map_err(|e| CompileError::ReadError {
|
|
@@ -704,8 +1002,14 @@ impl Compiler {
|
|
|
704
1002
|
let file_name = path.file_name().unwrap().to_str().unwrap();
|
|
705
1003
|
|
|
706
1004
|
if path.is_dir() {
|
|
1005
|
+
// Skip output directory (dynamically check, not hardcoded "compiled")
|
|
1006
|
+
let canonical_path = path.canonicalize().unwrap_or_else(|_| path.clone());
|
|
1007
|
+
if canonical_path == *canonical_output || canonical_path.starts_with(canonical_output) {
|
|
1008
|
+
continue;
|
|
1009
|
+
}
|
|
1010
|
+
|
|
707
1011
|
// Skip special directories
|
|
708
|
-
if
|
|
1012
|
+
if should_skip_path(&path, file_name) {
|
|
709
1013
|
continue;
|
|
710
1014
|
}
|
|
711
1015
|
|
|
@@ -736,8 +1040,13 @@ impl Compiler {
|
|
|
736
1040
|
format!("{}/{}", relative_path, file_name)
|
|
737
1041
|
};
|
|
738
1042
|
|
|
739
|
-
self.process_directory_assets_only(&path, &new_relative, stats)?;
|
|
1043
|
+
self.process_directory_assets_only(&path, &new_relative, canonical_output, stats)?;
|
|
740
1044
|
} else if let Some(ext) = path.extension().and_then(|e| e.to_str()) {
|
|
1045
|
+
// Skip files matching skip patterns
|
|
1046
|
+
if should_skip_path(&path, file_name) {
|
|
1047
|
+
continue;
|
|
1048
|
+
}
|
|
1049
|
+
|
|
741
1050
|
// Only copy non-HTML assets
|
|
742
1051
|
if ext != "html" && MIRROR_EXTENSIONS.contains(&ext) {
|
|
743
1052
|
self.copy_file(&path, relative_path)?;
|
|
@@ -775,14 +1084,14 @@ impl Compiler {
|
|
|
775
1084
|
(0, 0, Vec::new())
|
|
776
1085
|
};
|
|
777
1086
|
|
|
778
|
-
// Compile: transform custom tags to <component>, inline if needed,
|
|
1087
|
+
// Compile: transform custom tags to <component>, inline if needed, transform custom elements
|
|
779
1088
|
let processed = parser.process_html_with_cache(
|
|
780
1089
|
&content,
|
|
781
|
-
self.config.
|
|
782
|
-
&self.config.
|
|
1090
|
+
self.config.elements_as_is,
|
|
1091
|
+
&self.config.reserved_elements,
|
|
783
1092
|
self.config.components_as_is,
|
|
784
1093
|
&self.config.components,
|
|
785
|
-
&self.
|
|
1094
|
+
&self.component_cache,
|
|
786
1095
|
);
|
|
787
1096
|
|
|
788
1097
|
// Minify if requested
|
|
@@ -844,7 +1153,7 @@ impl Compiler {
|
|
|
844
1153
|
|
|
845
1154
|
// Read component content
|
|
846
1155
|
let content = if component_src.starts_with("http://") || component_src.starts_with("https://") {
|
|
847
|
-
match self.
|
|
1156
|
+
match self.fetch_external_component_raw(component_src) {
|
|
848
1157
|
Ok(c) => c,
|
|
849
1158
|
Err(_) => return,
|
|
850
1159
|
}
|
|
@@ -883,10 +1192,42 @@ impl Compiler {
|
|
|
883
1192
|
Ok(())
|
|
884
1193
|
}
|
|
885
1194
|
|
|
886
|
-
|
|
887
|
-
|
|
1195
|
+
/// Fetch components only for specific files (used in incremental compilation)
|
|
1196
|
+
fn fetch_components_for_files(&mut self, files: &[PathBuf], parser: &HtmlParser) -> Result<(), CompileError> {
|
|
1197
|
+
for html_file in files {
|
|
1198
|
+
let content = match fs::read_to_string(html_file) {
|
|
1199
|
+
Ok(c) => c,
|
|
1200
|
+
Err(e) => {
|
|
1201
|
+
if self.verbose {
|
|
1202
|
+
eprintln!(" Warning: Failed to read {}: {}", html_file.display(), e);
|
|
1203
|
+
}
|
|
1204
|
+
continue;
|
|
1205
|
+
}
|
|
1206
|
+
};
|
|
888
1207
|
|
|
889
|
-
|
|
1208
|
+
// Transform custom tags to <component> tags
|
|
1209
|
+
let transformed = parser.process_html(
|
|
1210
|
+
&content,
|
|
1211
|
+
false, // elements_as_is
|
|
1212
|
+
&[], // reserved_elements
|
|
1213
|
+
true, // components_as_is (don't inline, just transform)
|
|
1214
|
+
&self.config.components,
|
|
1215
|
+
);
|
|
1216
|
+
|
|
1217
|
+
// Extract and fetch components recursively
|
|
1218
|
+
let components = self.extract_component_srcs(&transformed);
|
|
1219
|
+
for component_src in components {
|
|
1220
|
+
self.fetch_component_recursive(&component_src, parser);
|
|
1221
|
+
}
|
|
1222
|
+
}
|
|
1223
|
+
|
|
1224
|
+
Ok(())
|
|
1225
|
+
}
|
|
1226
|
+
|
|
1227
|
+
/// Fetch and cache all components for inlining (separate from validation)
|
|
1228
|
+
/// This recursively fetches all components (external and internal) and populates component_cache
|
|
1229
|
+
fn fetch_all_components(&mut self, parser: &HtmlParser) -> Result<(), CompileError> {
|
|
1230
|
+
// Find all HTML files in source directory (need to scan pages to find external component references)
|
|
890
1231
|
let html_files = self.find_all_html_files(&self.config.source)?;
|
|
891
1232
|
|
|
892
1233
|
// Scan each HTML file for components
|
|
@@ -894,135 +1235,159 @@ impl Compiler {
|
|
|
894
1235
|
let content = match fs::read_to_string(&html_file) {
|
|
895
1236
|
Ok(c) => c,
|
|
896
1237
|
Err(e) => {
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
error_message: format!("Failed to read file: {}", e),
|
|
901
|
-
});
|
|
1238
|
+
if self.verbose {
|
|
1239
|
+
eprintln!(" Warning: Failed to read {}: {}", html_file.display(), e);
|
|
1240
|
+
}
|
|
902
1241
|
continue;
|
|
903
1242
|
}
|
|
904
1243
|
};
|
|
905
1244
|
|
|
906
|
-
// Transform custom tags to <component> tags
|
|
907
|
-
// Use process_html with elements_as_is=true to transform without inlining
|
|
1245
|
+
// Transform custom tags to <component> tags
|
|
908
1246
|
let transformed = parser.process_html(
|
|
909
1247
|
&content,
|
|
910
|
-
false, //
|
|
911
|
-
&[], //
|
|
912
|
-
true, //
|
|
1248
|
+
false, // elements_as_is
|
|
1249
|
+
&[], // reserved_elements
|
|
1250
|
+
true, // components_as_is (don't inline, just transform)
|
|
913
1251
|
&self.config.components,
|
|
914
1252
|
);
|
|
915
1253
|
|
|
916
|
-
// Extract
|
|
1254
|
+
// Extract and fetch components recursively
|
|
917
1255
|
let components = self.extract_component_srcs(&transformed);
|
|
918
1256
|
for component_src in components {
|
|
919
|
-
self.
|
|
920
|
-
&component_src,
|
|
921
|
-
&html_file.display().to_string(),
|
|
922
|
-
&mut errors,
|
|
923
|
-
parser,
|
|
924
|
-
);
|
|
1257
|
+
self.fetch_component_recursive(&component_src, parser);
|
|
925
1258
|
}
|
|
926
1259
|
}
|
|
927
1260
|
|
|
928
|
-
// If any errors, return them all
|
|
929
|
-
if !errors.is_empty() {
|
|
930
|
-
return Err(CompileError::ComponentValidationFailed(errors));
|
|
931
|
-
}
|
|
932
|
-
|
|
933
1261
|
Ok(())
|
|
934
1262
|
}
|
|
935
1263
|
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
1264
|
+
/// Recursively fetch a component and its nested components (for inlining)
|
|
1265
|
+
/// Returns the fully inlined content (all nested components resolved)
|
|
1266
|
+
fn fetch_component_recursive(&mut self, component_src: &str, parser: &HtmlParser) -> Option<String> {
|
|
1267
|
+
// Normalize path: ensure it starts with / (unless it's a URL)
|
|
1268
|
+
let normalized_src = if component_src.starts_with("http://") || component_src.starts_with("https://") {
|
|
1269
|
+
component_src.to_string()
|
|
1270
|
+
} else {
|
|
1271
|
+
let without_prefix = component_src.trim_start_matches("./");
|
|
1272
|
+
if without_prefix.starts_with('/') {
|
|
1273
|
+
without_prefix.to_string()
|
|
1274
|
+
} else {
|
|
1275
|
+
format!("/{}", without_prefix)
|
|
1276
|
+
}
|
|
1277
|
+
};
|
|
1278
|
+
|
|
1279
|
+
// Return cached if already fetched and fully resolved
|
|
1280
|
+
if let Some(cached) = self.component_cache.get(&normalized_src) {
|
|
1281
|
+
return Some(cached.clone());
|
|
946
1282
|
}
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
match self.fetch_external_component(component_src) {
|
|
953
|
-
Ok(content) => {
|
|
954
|
-
// Transform custom tags in fetched content
|
|
955
|
-
let transformed = parser.process_html(
|
|
956
|
-
&content,
|
|
957
|
-
false,
|
|
958
|
-
&[],
|
|
959
|
-
true,
|
|
960
|
-
&self.config.components,
|
|
961
|
-
);
|
|
962
|
-
|
|
963
|
-
// Recursively scan for nested components
|
|
964
|
-
let nested = self.extract_component_srcs(&transformed);
|
|
965
|
-
for nested_src in nested {
|
|
966
|
-
self.validate_component_recursive(&nested_src, component_src, errors, parser);
|
|
967
|
-
}
|
|
968
|
-
}
|
|
1283
|
+
|
|
1284
|
+
// Fetch external or read internal component (raw content)
|
|
1285
|
+
let content = if normalized_src.starts_with("http://") || normalized_src.starts_with("https://") {
|
|
1286
|
+
match self.fetch_external_component_raw(&normalized_src) {
|
|
1287
|
+
Ok(c) => c,
|
|
969
1288
|
Err(e) => {
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
});
|
|
1289
|
+
if self.verbose {
|
|
1290
|
+
eprintln!(" Warning: Failed to fetch {}: {}", normalized_src, e);
|
|
1291
|
+
}
|
|
1292
|
+
return None;
|
|
975
1293
|
}
|
|
976
1294
|
}
|
|
977
1295
|
} else {
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
Ok(content) => {
|
|
985
|
-
// Transform custom tags in component content
|
|
986
|
-
let transformed = parser.process_html(
|
|
987
|
-
&content,
|
|
988
|
-
false,
|
|
989
|
-
&[],
|
|
990
|
-
true,
|
|
991
|
-
&self.config.components,
|
|
992
|
-
);
|
|
993
|
-
|
|
994
|
-
// Recursively scan for nested components
|
|
995
|
-
let nested = self.extract_component_srcs(&transformed);
|
|
996
|
-
for nested_src in nested {
|
|
997
|
-
self.validate_component_recursive(&nested_src, component_src, errors, parser);
|
|
998
|
-
}
|
|
999
|
-
}
|
|
1000
|
-
Err(e) => {
|
|
1001
|
-
errors.push(ComponentError {
|
|
1002
|
-
component_src: component_src.to_string(),
|
|
1003
|
-
referenced_in: referenced_in.to_string(),
|
|
1004
|
-
error_message: format!("Failed to read component file: {}", e),
|
|
1005
|
-
});
|
|
1296
|
+
match self.resolve_component_path(&normalized_src) {
|
|
1297
|
+
Ok(path) => match fs::read_to_string(&path) {
|
|
1298
|
+
Ok(c) => c,
|
|
1299
|
+
Err(e) => {
|
|
1300
|
+
if self.verbose {
|
|
1301
|
+
eprintln!(" Warning: Failed to read component {}: {}", normalized_src, e);
|
|
1006
1302
|
}
|
|
1303
|
+
return None;
|
|
1007
1304
|
}
|
|
1008
|
-
}
|
|
1305
|
+
},
|
|
1009
1306
|
Err(e) => {
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
});
|
|
1307
|
+
if self.verbose {
|
|
1308
|
+
eprintln!(" Warning: {}", e);
|
|
1309
|
+
}
|
|
1310
|
+
return None;
|
|
1015
1311
|
}
|
|
1016
1312
|
}
|
|
1313
|
+
};
|
|
1314
|
+
|
|
1315
|
+
// Transform custom tags in component (but keep component tags as-is for now)
|
|
1316
|
+
let mut transformed = parser.process_html(
|
|
1317
|
+
&content,
|
|
1318
|
+
self.config.elements_as_is, // respect global config
|
|
1319
|
+
&self.config.reserved_elements.clone(), // respect global config
|
|
1320
|
+
true, // components_as_is = true (keep <component> tags for now so we can recursively resolve them)
|
|
1321
|
+
&self.config.components,
|
|
1322
|
+
);
|
|
1323
|
+
|
|
1324
|
+
// Recursively fetch and inline all nested components
|
|
1325
|
+
let nested = self.extract_component_srcs(&transformed);
|
|
1326
|
+
for nested_src in nested {
|
|
1327
|
+
if let Some(nested_content) = self.fetch_component_recursive(&nested_src, parser) {
|
|
1328
|
+
// Inline this nested component into the current component
|
|
1329
|
+
transformed = parser.inline_single_component(&transformed, &nested_src, &nested_content);
|
|
1330
|
+
}
|
|
1017
1331
|
}
|
|
1332
|
+
|
|
1333
|
+
// Cache the fully resolved content
|
|
1334
|
+
self.component_cache.insert(normalized_src.clone(), transformed.clone());
|
|
1335
|
+
|
|
1336
|
+
Some(transformed)
|
|
1018
1337
|
}
|
|
1019
1338
|
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1339
|
+
/// Validate that component filenames don't match reserved elements
|
|
1340
|
+
fn validate_component_names(&self) -> Result<(), CompileError> {
|
|
1341
|
+
let components_dir = self.config.components_path();
|
|
1342
|
+
|
|
1343
|
+
if !components_dir.exists() {
|
|
1344
|
+
return Ok(()); // No components directory
|
|
1024
1345
|
}
|
|
1025
1346
|
|
|
1347
|
+
let component_files = self.find_all_html_files(&components_dir)?;
|
|
1348
|
+
|
|
1349
|
+
for component_file in component_files {
|
|
1350
|
+
let file_name = component_file
|
|
1351
|
+
.file_stem()
|
|
1352
|
+
.and_then(|s| s.to_str())
|
|
1353
|
+
.unwrap_or("");
|
|
1354
|
+
|
|
1355
|
+
// Check if filename (case-sensitive) matches any reserved element
|
|
1356
|
+
if self.config.reserved_elements.contains(&file_name.to_string()) {
|
|
1357
|
+
let relative_path = component_file
|
|
1358
|
+
.strip_prefix(&self.config.source)
|
|
1359
|
+
.unwrap_or(&component_file);
|
|
1360
|
+
|
|
1361
|
+
return Err(CompileError::ReservedComponentName {
|
|
1362
|
+
component_name: file_name.to_string(),
|
|
1363
|
+
file_path: relative_path.display().to_string(),
|
|
1364
|
+
});
|
|
1365
|
+
}
|
|
1366
|
+
}
|
|
1367
|
+
|
|
1368
|
+
Ok(())
|
|
1369
|
+
}
|
|
1370
|
+
|
|
1371
|
+
/// Validate HTML syntax for all files (controlled by validate flag)
|
|
1372
|
+
fn validate_html_syntax(&self) -> Result<(), CompileError> {
|
|
1373
|
+
// Find all HTML files in source directory
|
|
1374
|
+
let html_files = self.find_all_html_files(&self.config.source)?;
|
|
1375
|
+
|
|
1376
|
+
for html_file in html_files {
|
|
1377
|
+
let content = fs::read_to_string(&html_file).map_err(|e| CompileError::ReadError {
|
|
1378
|
+
path: html_file.display().to_string(),
|
|
1379
|
+
source: e,
|
|
1380
|
+
})?;
|
|
1381
|
+
|
|
1382
|
+
// Validate HTML syntax
|
|
1383
|
+
self.validate_html(&content, &html_file)?;
|
|
1384
|
+
}
|
|
1385
|
+
|
|
1386
|
+
Ok(())
|
|
1387
|
+
}
|
|
1388
|
+
|
|
1389
|
+
/// Fetch external component without caching (returns raw content)
|
|
1390
|
+
fn fetch_external_component_raw(&self, url: &str) -> Result<String, String> {
|
|
1026
1391
|
// Fetch from URL
|
|
1027
1392
|
match reqwest::blocking::get(url) {
|
|
1028
1393
|
Ok(response) => {
|
|
@@ -1030,11 +1395,7 @@ impl Compiler {
|
|
|
1030
1395
|
return Err(format!("HTTP {} - {}", response.status().as_u16(), response.status().canonical_reason().unwrap_or("Unknown")));
|
|
1031
1396
|
}
|
|
1032
1397
|
match response.text() {
|
|
1033
|
-
Ok(content) =>
|
|
1034
|
-
// Cache the result
|
|
1035
|
-
self.external_component_cache.insert(url.to_string(), content.clone());
|
|
1036
|
-
Ok(content)
|
|
1037
|
-
}
|
|
1398
|
+
Ok(content) => Ok(content),
|
|
1038
1399
|
Err(e) => Err(format!("Failed to read response body: {}", e)),
|
|
1039
1400
|
}
|
|
1040
1401
|
}
|
|
@@ -1057,7 +1418,9 @@ impl Compiler {
|
|
|
1057
1418
|
|
|
1058
1419
|
fn extract_component_srcs(&self, content: &str) -> Vec<String> {
|
|
1059
1420
|
let mut srcs = Vec::new();
|
|
1060
|
-
|
|
1421
|
+
// Match both <component src="..."> and <div class="component" src="...">
|
|
1422
|
+
// After elements_as_is=false transform, <component> becomes <div class="component">
|
|
1423
|
+
let re = Regex::new(r#"(?:<component|<div\s+class="component")[^>]+src\s*=\s*["']([^"']+)["']"#).unwrap();
|
|
1061
1424
|
for cap in re.captures_iter(content) {
|
|
1062
1425
|
if let Some(src) = cap.get(1) {
|
|
1063
1426
|
srcs.push(src.as_str().to_string());
|
|
@@ -1068,11 +1431,15 @@ impl Compiler {
|
|
|
1068
1431
|
|
|
1069
1432
|
fn find_all_html_files(&self, dir: &Path) -> Result<Vec<PathBuf>, CompileError> {
|
|
1070
1433
|
let mut html_files = Vec::new();
|
|
1071
|
-
|
|
1434
|
+
// Get output directory basename to skip nested directories with same name
|
|
1435
|
+
let output_dir_name = self.config.output.file_name()
|
|
1436
|
+
.and_then(|n| n.to_str())
|
|
1437
|
+
.unwrap_or("");
|
|
1438
|
+
self.find_html_files_recursive(dir, &mut html_files, output_dir_name)?;
|
|
1072
1439
|
Ok(html_files)
|
|
1073
1440
|
}
|
|
1074
1441
|
|
|
1075
|
-
fn find_html_files_recursive(&self, dir: &Path, files: &mut Vec<PathBuf
|
|
1442
|
+
fn find_html_files_recursive(&self, dir: &Path, files: &mut Vec<PathBuf>, output_dir_name: &str) -> Result<(), CompileError> {
|
|
1076
1443
|
if !dir.is_dir() {
|
|
1077
1444
|
return Ok(());
|
|
1078
1445
|
}
|
|
@@ -1092,8 +1459,8 @@ impl Compiler {
|
|
|
1092
1459
|
let file_name = entry.file_name();
|
|
1093
1460
|
let file_name_str = file_name.to_string_lossy();
|
|
1094
1461
|
|
|
1095
|
-
// Skip hidden files and specific directories
|
|
1096
|
-
if file_name_str.starts_with('.') ||
|
|
1462
|
+
// Skip hidden files and specific directories/patterns
|
|
1463
|
+
if file_name_str.starts_with('.') || should_skip_path(&path, &file_name_str) {
|
|
1097
1464
|
continue;
|
|
1098
1465
|
}
|
|
1099
1466
|
|
|
@@ -1102,8 +1469,13 @@ impl Compiler {
|
|
|
1102
1469
|
continue;
|
|
1103
1470
|
}
|
|
1104
1471
|
|
|
1472
|
+
// Skip nested directories with same name as output dir (from previous bad compilations)
|
|
1473
|
+
if !output_dir_name.is_empty() && file_name_str == output_dir_name {
|
|
1474
|
+
continue;
|
|
1475
|
+
}
|
|
1476
|
+
|
|
1105
1477
|
if path.is_dir() {
|
|
1106
|
-
self.find_html_files_recursive(&path, files)?;
|
|
1478
|
+
self.find_html_files_recursive(&path, files, output_dir_name)?;
|
|
1107
1479
|
} else if path.extension().and_then(|e| e.to_str()) == Some("html") {
|
|
1108
1480
|
files.push(path);
|
|
1109
1481
|
}
|
|
@@ -1162,6 +1534,11 @@ impl Compiler {
|
|
|
1162
1534
|
let path = entry.path();
|
|
1163
1535
|
let file_name = path.file_name().unwrap().to_str().unwrap();
|
|
1164
1536
|
|
|
1537
|
+
// Skip files/directories matching skip patterns
|
|
1538
|
+
if should_skip_path(&path, file_name) {
|
|
1539
|
+
continue;
|
|
1540
|
+
}
|
|
1541
|
+
|
|
1165
1542
|
if path.is_dir() {
|
|
1166
1543
|
let new_relative = format!("{}/{}", relative_path, file_name);
|
|
1167
1544
|
self.copy_directory(&path, &new_relative, stats)?;
|