@ape-egg/vibe 1.3.2 → 1.6.1
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 +195 -0
- package/README.md +214 -16
- package/boot.js +0 -1
- package/compiler/native/vibe-compiler-darwin-arm64 +0 -0
- package/compiler/src/Cargo.lock +720 -41
- package/compiler/src/Cargo.toml +12 -3
- package/compiler/src/compiler/PRE-RENDERING-IMPLEMENTATION.md +241 -0
- package/compiler/src/compiler/compile.rs +562 -180
- 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 +47 -31
- 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} +219 -133
- 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,20 @@ impl Compiler {
|
|
|
423
502
|
}
|
|
424
503
|
}
|
|
425
504
|
|
|
505
|
+
// Canonicalize both output and source paths for reliable comparison
|
|
506
|
+
let canonical_output = self.config.output.canonicalize()
|
|
507
|
+
.unwrap_or_else(|_| self.config.output.clone());
|
|
508
|
+
let canonical_source = self.config.source.canonicalize()
|
|
509
|
+
.unwrap_or_else(|_| self.config.source.clone());
|
|
426
510
|
|
|
427
511
|
// Track compile and copy times separately
|
|
428
512
|
let compile_start = Instant::now();
|
|
429
513
|
|
|
430
514
|
// Process all HTML files in source (excluding components directory)
|
|
431
|
-
self.process_directory_html_only(&
|
|
515
|
+
self.process_directory_html_only(&canonical_source, &parser, "", &canonical_output, &canonical_source, &mut stats)?;
|
|
432
516
|
|
|
433
517
|
// Process source directory for assets (CSS, JS, images, etc.)
|
|
434
|
-
self.process_directory_assets_only(&
|
|
518
|
+
self.process_directory_assets_only(&canonical_source, "", &canonical_output, &canonical_source, &mut stats)?;
|
|
435
519
|
|
|
436
520
|
let process_time = compile_start.elapsed();
|
|
437
521
|
|
|
@@ -505,6 +589,190 @@ impl Compiler {
|
|
|
505
589
|
Ok(stats)
|
|
506
590
|
}
|
|
507
591
|
|
|
592
|
+
/// Compile specific HTML files (incremental compilation for watch mode)
|
|
593
|
+
pub fn compile_specific_html_files(
|
|
594
|
+
&mut self,
|
|
595
|
+
files: &[PathBuf],
|
|
596
|
+
parser: &HtmlParser,
|
|
597
|
+
) -> Result<CompileStats, CompileError> {
|
|
598
|
+
let mut stats = CompileStats {
|
|
599
|
+
files_compiled: 0,
|
|
600
|
+
files_copied: 0,
|
|
601
|
+
internal_components_unique: 0,
|
|
602
|
+
external_components_unique: 0,
|
|
603
|
+
internal_components_total: 0,
|
|
604
|
+
external_components_total: 0,
|
|
605
|
+
compile_time_ms: 0.0,
|
|
606
|
+
copy_time_ms: 0.0,
|
|
607
|
+
components_time_ms: 0.0,
|
|
608
|
+
validation_time_ms: None,
|
|
609
|
+
node_modules_time_ms: None,
|
|
610
|
+
package_manager: None,
|
|
611
|
+
node_modules_copied_as_is: false,
|
|
612
|
+
components_as_is: self.config.components_as_is,
|
|
613
|
+
};
|
|
614
|
+
|
|
615
|
+
let start = Instant::now();
|
|
616
|
+
|
|
617
|
+
// Pre-fetch components needed by these specific files (only if not components_as_is)
|
|
618
|
+
if !self.config.components_as_is {
|
|
619
|
+
let fetch_start = Instant::now();
|
|
620
|
+
self.fetch_components_for_files(files, parser)?;
|
|
621
|
+
stats.components_time_ms = fetch_start.elapsed().as_secs_f64() * 1000.0;
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
for file_path in files {
|
|
625
|
+
// Calculate relative path
|
|
626
|
+
let relative_path = file_path
|
|
627
|
+
.strip_prefix(&self.config.source)
|
|
628
|
+
.map(|p| p.parent().unwrap_or(Path::new("")))
|
|
629
|
+
.unwrap_or(Path::new(""))
|
|
630
|
+
.to_str()
|
|
631
|
+
.unwrap_or("");
|
|
632
|
+
|
|
633
|
+
let (internal, external, component_srcs) = self.compile_html_file(file_path, parser, relative_path)?;
|
|
634
|
+
stats.files_compiled += 1;
|
|
635
|
+
stats.internal_components_total += internal;
|
|
636
|
+
stats.external_components_total += external;
|
|
637
|
+
|
|
638
|
+
// Track unique components
|
|
639
|
+
for src in &component_srcs {
|
|
640
|
+
self.unique_components.insert(src.clone());
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
if let Some(ref mut logger) = self.logger {
|
|
644
|
+
logger.log(file_path, FileOperation::Compiled, &self.config.source);
|
|
645
|
+
for src in component_srcs {
|
|
646
|
+
logger.log_component_occurrence(src);
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
// Calculate unique components
|
|
652
|
+
let internal_unique = self.unique_components.iter()
|
|
653
|
+
.filter(|src| !src.starts_with("http://") && !src.starts_with("https://"))
|
|
654
|
+
.count();
|
|
655
|
+
let external_unique = self.unique_components.iter()
|
|
656
|
+
.filter(|src| src.starts_with("http://") || src.starts_with("https://"))
|
|
657
|
+
.count();
|
|
658
|
+
|
|
659
|
+
stats.internal_components_unique = internal_unique;
|
|
660
|
+
stats.external_components_unique = external_unique;
|
|
661
|
+
|
|
662
|
+
// Calculate compile time (excluding component fetch time which is already tracked)
|
|
663
|
+
let elapsed = start.elapsed().as_secs_f64() * 1000.0;
|
|
664
|
+
stats.compile_time_ms = elapsed - stats.components_time_ms;
|
|
665
|
+
|
|
666
|
+
Ok(stats)
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
/// Copy specific asset files (incremental compilation for watch mode)
|
|
670
|
+
pub fn copy_specific_asset_files(
|
|
671
|
+
&mut self,
|
|
672
|
+
files: &[PathBuf],
|
|
673
|
+
) -> Result<CompileStats, CompileError> {
|
|
674
|
+
let mut stats = CompileStats {
|
|
675
|
+
files_compiled: 0,
|
|
676
|
+
files_copied: 0,
|
|
677
|
+
internal_components_unique: 0,
|
|
678
|
+
external_components_unique: 0,
|
|
679
|
+
internal_components_total: 0,
|
|
680
|
+
external_components_total: 0,
|
|
681
|
+
compile_time_ms: 0.0,
|
|
682
|
+
copy_time_ms: 0.0,
|
|
683
|
+
components_time_ms: 0.0,
|
|
684
|
+
validation_time_ms: None,
|
|
685
|
+
node_modules_time_ms: None,
|
|
686
|
+
package_manager: None,
|
|
687
|
+
node_modules_copied_as_is: false,
|
|
688
|
+
components_as_is: self.config.components_as_is,
|
|
689
|
+
};
|
|
690
|
+
|
|
691
|
+
let start = Instant::now();
|
|
692
|
+
|
|
693
|
+
for file_path in files {
|
|
694
|
+
// Calculate relative path for preserving directory structure
|
|
695
|
+
let relative_path = file_path
|
|
696
|
+
.strip_prefix(&self.config.source)
|
|
697
|
+
.map(|p| p.parent().unwrap_or(Path::new("")))
|
|
698
|
+
.unwrap_or(Path::new(""))
|
|
699
|
+
.to_str()
|
|
700
|
+
.unwrap_or("");
|
|
701
|
+
|
|
702
|
+
self.copy_file(file_path, relative_path)?;
|
|
703
|
+
stats.files_copied += 1;
|
|
704
|
+
|
|
705
|
+
if let Some(ref mut logger) = self.logger {
|
|
706
|
+
logger.log(file_path, FileOperation::Copied, &self.config.source);
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
let elapsed = start.elapsed().as_secs_f64() * 1000.0;
|
|
711
|
+
stats.copy_time_ms = elapsed;
|
|
712
|
+
|
|
713
|
+
Ok(stats)
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
/// Generate manifests for specific HTML files (incremental compilation for watch mode)
|
|
717
|
+
pub fn generate_manifests_for_files(&self, files: &[PathBuf]) -> Result<ManifestStats, CompileError> {
|
|
718
|
+
let start = Instant::now();
|
|
719
|
+
let mut pages_processed = 0;
|
|
720
|
+
let mut pages_skipped = 0;
|
|
721
|
+
|
|
722
|
+
for file_path in files {
|
|
723
|
+
// Convert source path to output path
|
|
724
|
+
let relative_path = file_path
|
|
725
|
+
.strip_prefix(&self.config.source)
|
|
726
|
+
.unwrap_or(file_path)
|
|
727
|
+
.to_str()
|
|
728
|
+
.unwrap();
|
|
729
|
+
|
|
730
|
+
let output_path = self.config.output.join(relative_path);
|
|
731
|
+
|
|
732
|
+
if !output_path.exists() {
|
|
733
|
+
pages_skipped += 1;
|
|
734
|
+
continue;
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
// Read compiled HTML from output
|
|
738
|
+
let html = fs::read_to_string(&output_path)
|
|
739
|
+
.map_err(|e| CompileError::ReadError {
|
|
740
|
+
path: output_path.display().to_string(),
|
|
741
|
+
source: e,
|
|
742
|
+
})?;
|
|
743
|
+
|
|
744
|
+
// Try to generate manifest for this file (skip on error)
|
|
745
|
+
match Self::generate_file_manifest(
|
|
746
|
+
&html,
|
|
747
|
+
&output_path,
|
|
748
|
+
&self.config.output,
|
|
749
|
+
relative_path,
|
|
750
|
+
self.verbose,
|
|
751
|
+
self.config.iterations_as_is,
|
|
752
|
+
self.config.components_as_is,
|
|
753
|
+
&self.config.source,
|
|
754
|
+
) {
|
|
755
|
+
Ok(()) => {
|
|
756
|
+
pages_processed += 1;
|
|
757
|
+
}
|
|
758
|
+
Err(e) => {
|
|
759
|
+
pages_skipped += 1;
|
|
760
|
+
if self.verbose {
|
|
761
|
+
println!(" Skipped ({}): {}", e, relative_path);
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
let total_time_ms = start.elapsed().as_millis() as f64;
|
|
768
|
+
|
|
769
|
+
Ok(ManifestStats {
|
|
770
|
+
pages_processed,
|
|
771
|
+
pages_skipped,
|
|
772
|
+
total_time_ms,
|
|
773
|
+
})
|
|
774
|
+
}
|
|
775
|
+
|
|
508
776
|
/// Generate manifest for a single file
|
|
509
777
|
fn generate_file_manifest(
|
|
510
778
|
html: &str,
|
|
@@ -512,21 +780,30 @@ impl Compiler {
|
|
|
512
780
|
output_dir: &Path,
|
|
513
781
|
relative_path: &str,
|
|
514
782
|
_verbose: bool,
|
|
783
|
+
iterations_as_is: bool,
|
|
784
|
+
components_as_is: bool,
|
|
785
|
+
source_root: &Path,
|
|
515
786
|
) -> Result<(), String> {
|
|
516
|
-
use crate::compiler::state_extractor::StateExtractor;
|
|
517
787
|
use crate::compiler::manifest_builder::ManifestBuilder;
|
|
518
788
|
use crate::compiler::value_stamper::ValueStamper;
|
|
789
|
+
use crate::compiler::component_tagger::ComponentTagger;
|
|
519
790
|
|
|
520
|
-
//
|
|
521
|
-
let
|
|
791
|
+
// Tag components with deterministic IDs and structure state
|
|
792
|
+
let tagged = ComponentTagger::tag_components(html, &source_root.to_path_buf())?;
|
|
793
|
+
let html = &tagged.html; // Use modified HTML with data-vibe-component-id attributes
|
|
794
|
+
let state = tagged.state;
|
|
522
795
|
|
|
523
|
-
// Build manifest
|
|
796
|
+
// Build manifest from ORIGINAL HTML (before stamping, so templates have @[...] markers)
|
|
524
797
|
let manifest_builder = ManifestBuilder::new();
|
|
525
|
-
let manifest = manifest_builder.build_from_html(html, &state)?;
|
|
798
|
+
let manifest = manifest_builder.build_from_html(html, &state, iterations_as_is)?;
|
|
526
799
|
|
|
527
|
-
// Stamp values (pre-render)
|
|
528
|
-
|
|
529
|
-
let
|
|
800
|
+
// Stamp values (pre-render) AFTER building manifest
|
|
801
|
+
// When components_as_is is true, skip stamping inside component elements (runtime will handle them)
|
|
802
|
+
let stamper = ValueStamper::new(&state, components_as_is)?;
|
|
803
|
+
let mut pre_rendered = stamper.stamp_html(html.to_string())?;
|
|
804
|
+
|
|
805
|
+
// Remove FOUC prevention since HTML is pre-rendered
|
|
806
|
+
pre_rendered = remove_fouc_prevention(pre_rendered);
|
|
530
807
|
|
|
531
808
|
// Write pre-rendered HTML
|
|
532
809
|
fs::write(html_path, pre_rendered)
|
|
@@ -572,31 +849,46 @@ impl Compiler {
|
|
|
572
849
|
// Find all HTML files in compiled output (excluding components directory)
|
|
573
850
|
let html_files = self.find_all_html_files(&self.config.output)?;
|
|
574
851
|
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
852
|
+
// Process manifests in parallel
|
|
853
|
+
let output_dir = self.config.output.clone();
|
|
854
|
+
let source_root = self.config.source.clone();
|
|
855
|
+
let verbose = self.verbose;
|
|
856
|
+
let iterations_as_is = self.config.iterations_as_is;
|
|
857
|
+
let components_as_is = self.config.components_as_is;
|
|
858
|
+
|
|
859
|
+
let results: Vec<_> = html_files
|
|
860
|
+
.par_iter()
|
|
861
|
+
.map(|html_path| {
|
|
862
|
+
let relative_path = html_path.strip_prefix(&output_dir)
|
|
863
|
+
.unwrap()
|
|
864
|
+
.to_str()
|
|
865
|
+
.unwrap();
|
|
866
|
+
|
|
867
|
+
// Read compiled HTML
|
|
868
|
+
let html = match fs::read_to_string(html_path) {
|
|
869
|
+
Ok(h) => h,
|
|
870
|
+
Err(_) => return (false, Some(relative_path.to_string())),
|
|
871
|
+
};
|
|
587
872
|
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
println!(" Skipped ({}): {}", e, relative_path);
|
|
873
|
+
// Try to generate manifest for this file (skip on error)
|
|
874
|
+
match Self::generate_file_manifest(&html, html_path, &output_dir, relative_path, verbose, iterations_as_is, components_as_is, &source_root) {
|
|
875
|
+
Ok(()) => (true, None),
|
|
876
|
+
Err(e) => {
|
|
877
|
+
if verbose {
|
|
878
|
+
eprintln!(" Skipped ({}): {}", e, relative_path);
|
|
879
|
+
}
|
|
880
|
+
(false, Some(relative_path.to_string()))
|
|
597
881
|
}
|
|
598
|
-
// Don't fail compilation, just skip this file's manifest
|
|
599
882
|
}
|
|
883
|
+
})
|
|
884
|
+
.collect();
|
|
885
|
+
|
|
886
|
+
// Count results
|
|
887
|
+
for (success, _) in results {
|
|
888
|
+
if success {
|
|
889
|
+
pages_processed += 1;
|
|
890
|
+
} else {
|
|
891
|
+
pages_skipped += 1;
|
|
600
892
|
}
|
|
601
893
|
}
|
|
602
894
|
|
|
@@ -615,6 +907,8 @@ impl Compiler {
|
|
|
615
907
|
dir: &Path,
|
|
616
908
|
parser: &HtmlParser,
|
|
617
909
|
relative_path: &str,
|
|
910
|
+
canonical_output: &Path,
|
|
911
|
+
canonical_source: &Path,
|
|
618
912
|
stats: &mut CompileStats,
|
|
619
913
|
) -> Result<(), CompileError> {
|
|
620
914
|
let entries = fs::read_dir(dir).map_err(|e| CompileError::ReadError {
|
|
@@ -627,8 +921,14 @@ impl Compiler {
|
|
|
627
921
|
let file_name = path.file_name().unwrap().to_str().unwrap();
|
|
628
922
|
|
|
629
923
|
if path.is_dir() {
|
|
924
|
+
// Skip output directory (dynamically check, not hardcoded "compiled")
|
|
925
|
+
let canonical_path = path.canonicalize().unwrap_or_else(|_| path.clone());
|
|
926
|
+
if canonical_path == *canonical_output || canonical_path.starts_with(canonical_output) {
|
|
927
|
+
continue;
|
|
928
|
+
}
|
|
929
|
+
|
|
630
930
|
// Skip special directories
|
|
631
|
-
if
|
|
931
|
+
if should_skip_path(&path, file_name) {
|
|
632
932
|
continue;
|
|
633
933
|
}
|
|
634
934
|
|
|
@@ -644,7 +944,7 @@ impl Compiler {
|
|
|
644
944
|
format!("{}/{}", relative_path, file_name)
|
|
645
945
|
};
|
|
646
946
|
|
|
647
|
-
self.process_directory_html_only(&path, parser, &new_relative, stats)?;
|
|
947
|
+
self.process_directory_html_only(&path, parser, &new_relative, canonical_output, canonical_source, stats)?;
|
|
648
948
|
} else if path.extension().and_then(|e| e.to_str()) == Some("html") {
|
|
649
949
|
let (internal, external, component_srcs) = self.compile_html_file(&path, parser, relative_path)?;
|
|
650
950
|
stats.files_compiled += 1;
|
|
@@ -665,7 +965,7 @@ impl Compiler {
|
|
|
665
965
|
};
|
|
666
966
|
|
|
667
967
|
if let Some(ref mut logger) = self.logger {
|
|
668
|
-
logger.log(&path, FileOperation::Compiled,
|
|
968
|
+
logger.log(&path, FileOperation::Compiled, canonical_source);
|
|
669
969
|
|
|
670
970
|
// Log each component occurrence (for counting)
|
|
671
971
|
if let Some(all_srcs) = all_srcs {
|
|
@@ -692,6 +992,8 @@ impl Compiler {
|
|
|
692
992
|
&mut self,
|
|
693
993
|
dir: &Path,
|
|
694
994
|
relative_path: &str,
|
|
995
|
+
canonical_output: &Path,
|
|
996
|
+
canonical_source: &Path,
|
|
695
997
|
stats: &mut CompileStats,
|
|
696
998
|
) -> Result<(), CompileError> {
|
|
697
999
|
let entries = fs::read_dir(dir).map_err(|e| CompileError::ReadError {
|
|
@@ -704,8 +1006,14 @@ impl Compiler {
|
|
|
704
1006
|
let file_name = path.file_name().unwrap().to_str().unwrap();
|
|
705
1007
|
|
|
706
1008
|
if path.is_dir() {
|
|
1009
|
+
// Skip output directory (dynamically check, not hardcoded "compiled")
|
|
1010
|
+
let canonical_path = path.canonicalize().unwrap_or_else(|_| path.clone());
|
|
1011
|
+
if canonical_path == *canonical_output || canonical_path.starts_with(canonical_output) {
|
|
1012
|
+
continue;
|
|
1013
|
+
}
|
|
1014
|
+
|
|
707
1015
|
// Skip special directories
|
|
708
|
-
if
|
|
1016
|
+
if should_skip_path(&path, file_name) {
|
|
709
1017
|
continue;
|
|
710
1018
|
}
|
|
711
1019
|
|
|
@@ -723,7 +1031,7 @@ impl Compiler {
|
|
|
723
1031
|
} else {
|
|
724
1032
|
format!("{}/{}", relative_path, file_name)
|
|
725
1033
|
};
|
|
726
|
-
self.copy_directory(&path, &new_relative, stats)?;
|
|
1034
|
+
self.copy_directory(&path, &new_relative, canonical_source, stats)?;
|
|
727
1035
|
}
|
|
728
1036
|
// Skip further processing (don't recurse into components)
|
|
729
1037
|
continue;
|
|
@@ -736,14 +1044,19 @@ impl Compiler {
|
|
|
736
1044
|
format!("{}/{}", relative_path, file_name)
|
|
737
1045
|
};
|
|
738
1046
|
|
|
739
|
-
self.process_directory_assets_only(&path, &new_relative, stats)?;
|
|
1047
|
+
self.process_directory_assets_only(&path, &new_relative, canonical_output, canonical_source, stats)?;
|
|
740
1048
|
} else if let Some(ext) = path.extension().and_then(|e| e.to_str()) {
|
|
1049
|
+
// Skip files matching skip patterns
|
|
1050
|
+
if should_skip_path(&path, file_name) {
|
|
1051
|
+
continue;
|
|
1052
|
+
}
|
|
1053
|
+
|
|
741
1054
|
// Only copy non-HTML assets
|
|
742
1055
|
if ext != "html" && MIRROR_EXTENSIONS.contains(&ext) {
|
|
743
1056
|
self.copy_file(&path, relative_path)?;
|
|
744
1057
|
stats.files_copied += 1;
|
|
745
1058
|
if let Some(ref mut logger) = self.logger {
|
|
746
|
-
logger.log(&path, FileOperation::Copied,
|
|
1059
|
+
logger.log(&path, FileOperation::Copied, canonical_source);
|
|
747
1060
|
}
|
|
748
1061
|
}
|
|
749
1062
|
}
|
|
@@ -775,14 +1088,14 @@ impl Compiler {
|
|
|
775
1088
|
(0, 0, Vec::new())
|
|
776
1089
|
};
|
|
777
1090
|
|
|
778
|
-
// Compile: transform custom tags to <component>, inline if needed,
|
|
1091
|
+
// Compile: transform custom tags to <component>, inline if needed, transform custom elements
|
|
779
1092
|
let processed = parser.process_html_with_cache(
|
|
780
1093
|
&content,
|
|
781
|
-
self.config.
|
|
782
|
-
&self.config.
|
|
1094
|
+
self.config.elements_as_is,
|
|
1095
|
+
&self.config.reserved_elements,
|
|
783
1096
|
self.config.components_as_is,
|
|
784
1097
|
&self.config.components,
|
|
785
|
-
&self.
|
|
1098
|
+
&self.component_cache,
|
|
786
1099
|
);
|
|
787
1100
|
|
|
788
1101
|
// Minify if requested
|
|
@@ -844,7 +1157,7 @@ impl Compiler {
|
|
|
844
1157
|
|
|
845
1158
|
// Read component content
|
|
846
1159
|
let content = if component_src.starts_with("http://") || component_src.starts_with("https://") {
|
|
847
|
-
match self.
|
|
1160
|
+
match self.fetch_external_component_raw(component_src) {
|
|
848
1161
|
Ok(c) => c,
|
|
849
1162
|
Err(_) => return,
|
|
850
1163
|
}
|
|
@@ -883,10 +1196,42 @@ impl Compiler {
|
|
|
883
1196
|
Ok(())
|
|
884
1197
|
}
|
|
885
1198
|
|
|
886
|
-
|
|
887
|
-
|
|
1199
|
+
/// Fetch components only for specific files (used in incremental compilation)
|
|
1200
|
+
fn fetch_components_for_files(&mut self, files: &[PathBuf], parser: &HtmlParser) -> Result<(), CompileError> {
|
|
1201
|
+
for html_file in files {
|
|
1202
|
+
let content = match fs::read_to_string(html_file) {
|
|
1203
|
+
Ok(c) => c,
|
|
1204
|
+
Err(e) => {
|
|
1205
|
+
if self.verbose {
|
|
1206
|
+
eprintln!(" Warning: Failed to read {}: {}", html_file.display(), e);
|
|
1207
|
+
}
|
|
1208
|
+
continue;
|
|
1209
|
+
}
|
|
1210
|
+
};
|
|
888
1211
|
|
|
889
|
-
|
|
1212
|
+
// Transform custom tags to <component> tags
|
|
1213
|
+
let transformed = parser.process_html(
|
|
1214
|
+
&content,
|
|
1215
|
+
false, // elements_as_is
|
|
1216
|
+
&[], // reserved_elements
|
|
1217
|
+
true, // components_as_is (don't inline, just transform)
|
|
1218
|
+
&self.config.components,
|
|
1219
|
+
);
|
|
1220
|
+
|
|
1221
|
+
// Extract and fetch components recursively
|
|
1222
|
+
let components = self.extract_component_srcs(&transformed);
|
|
1223
|
+
for component_src in components {
|
|
1224
|
+
self.fetch_component_recursive(&component_src, parser);
|
|
1225
|
+
}
|
|
1226
|
+
}
|
|
1227
|
+
|
|
1228
|
+
Ok(())
|
|
1229
|
+
}
|
|
1230
|
+
|
|
1231
|
+
/// Fetch and cache all components for inlining (separate from validation)
|
|
1232
|
+
/// This recursively fetches all components (external and internal) and populates component_cache
|
|
1233
|
+
fn fetch_all_components(&mut self, parser: &HtmlParser) -> Result<(), CompileError> {
|
|
1234
|
+
// Find all HTML files in source directory (need to scan pages to find external component references)
|
|
890
1235
|
let html_files = self.find_all_html_files(&self.config.source)?;
|
|
891
1236
|
|
|
892
1237
|
// Scan each HTML file for components
|
|
@@ -894,135 +1239,159 @@ impl Compiler {
|
|
|
894
1239
|
let content = match fs::read_to_string(&html_file) {
|
|
895
1240
|
Ok(c) => c,
|
|
896
1241
|
Err(e) => {
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
error_message: format!("Failed to read file: {}", e),
|
|
901
|
-
});
|
|
1242
|
+
if self.verbose {
|
|
1243
|
+
eprintln!(" Warning: Failed to read {}: {}", html_file.display(), e);
|
|
1244
|
+
}
|
|
902
1245
|
continue;
|
|
903
1246
|
}
|
|
904
1247
|
};
|
|
905
1248
|
|
|
906
|
-
// Transform custom tags to <component> tags
|
|
907
|
-
// Use process_html with elements_as_is=true to transform without inlining
|
|
1249
|
+
// Transform custom tags to <component> tags
|
|
908
1250
|
let transformed = parser.process_html(
|
|
909
1251
|
&content,
|
|
910
|
-
false, //
|
|
911
|
-
&[], //
|
|
912
|
-
true, //
|
|
1252
|
+
false, // elements_as_is
|
|
1253
|
+
&[], // reserved_elements
|
|
1254
|
+
true, // components_as_is (don't inline, just transform)
|
|
913
1255
|
&self.config.components,
|
|
914
1256
|
);
|
|
915
1257
|
|
|
916
|
-
// Extract
|
|
1258
|
+
// Extract and fetch components recursively
|
|
917
1259
|
let components = self.extract_component_srcs(&transformed);
|
|
918
1260
|
for component_src in components {
|
|
919
|
-
self.
|
|
920
|
-
&component_src,
|
|
921
|
-
&html_file.display().to_string(),
|
|
922
|
-
&mut errors,
|
|
923
|
-
parser,
|
|
924
|
-
);
|
|
1261
|
+
self.fetch_component_recursive(&component_src, parser);
|
|
925
1262
|
}
|
|
926
1263
|
}
|
|
927
1264
|
|
|
928
|
-
// If any errors, return them all
|
|
929
|
-
if !errors.is_empty() {
|
|
930
|
-
return Err(CompileError::ComponentValidationFailed(errors));
|
|
931
|
-
}
|
|
932
|
-
|
|
933
1265
|
Ok(())
|
|
934
1266
|
}
|
|
935
1267
|
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
1268
|
+
/// Recursively fetch a component and its nested components (for inlining)
|
|
1269
|
+
/// Returns the fully inlined content (all nested components resolved)
|
|
1270
|
+
fn fetch_component_recursive(&mut self, component_src: &str, parser: &HtmlParser) -> Option<String> {
|
|
1271
|
+
// Normalize path: ensure it starts with / (unless it's a URL)
|
|
1272
|
+
let normalized_src = if component_src.starts_with("http://") || component_src.starts_with("https://") {
|
|
1273
|
+
component_src.to_string()
|
|
1274
|
+
} else {
|
|
1275
|
+
let without_prefix = component_src.trim_start_matches("./");
|
|
1276
|
+
if without_prefix.starts_with('/') {
|
|
1277
|
+
without_prefix.to_string()
|
|
1278
|
+
} else {
|
|
1279
|
+
format!("/{}", without_prefix)
|
|
1280
|
+
}
|
|
1281
|
+
};
|
|
1282
|
+
|
|
1283
|
+
// Return cached if already fetched and fully resolved
|
|
1284
|
+
if let Some(cached) = self.component_cache.get(&normalized_src) {
|
|
1285
|
+
return Some(cached.clone());
|
|
946
1286
|
}
|
|
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
|
-
}
|
|
1287
|
+
|
|
1288
|
+
// Fetch external or read internal component (raw content)
|
|
1289
|
+
let content = if normalized_src.starts_with("http://") || normalized_src.starts_with("https://") {
|
|
1290
|
+
match self.fetch_external_component_raw(&normalized_src) {
|
|
1291
|
+
Ok(c) => c,
|
|
969
1292
|
Err(e) => {
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
});
|
|
1293
|
+
if self.verbose {
|
|
1294
|
+
eprintln!(" Warning: Failed to fetch {}: {}", normalized_src, e);
|
|
1295
|
+
}
|
|
1296
|
+
return None;
|
|
975
1297
|
}
|
|
976
1298
|
}
|
|
977
1299
|
} 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
|
-
});
|
|
1300
|
+
match self.resolve_component_path(&normalized_src) {
|
|
1301
|
+
Ok(path) => match fs::read_to_string(&path) {
|
|
1302
|
+
Ok(c) => c,
|
|
1303
|
+
Err(e) => {
|
|
1304
|
+
if self.verbose {
|
|
1305
|
+
eprintln!(" Warning: Failed to read component {}: {}", normalized_src, e);
|
|
1006
1306
|
}
|
|
1307
|
+
return None;
|
|
1007
1308
|
}
|
|
1008
|
-
}
|
|
1309
|
+
},
|
|
1009
1310
|
Err(e) => {
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
});
|
|
1311
|
+
if self.verbose {
|
|
1312
|
+
eprintln!(" Warning: {}", e);
|
|
1313
|
+
}
|
|
1314
|
+
return None;
|
|
1015
1315
|
}
|
|
1016
1316
|
}
|
|
1317
|
+
};
|
|
1318
|
+
|
|
1319
|
+
// Transform custom tags in component (but keep component tags as-is for now)
|
|
1320
|
+
let mut transformed = parser.process_html(
|
|
1321
|
+
&content,
|
|
1322
|
+
self.config.elements_as_is, // respect global config
|
|
1323
|
+
&self.config.reserved_elements.clone(), // respect global config
|
|
1324
|
+
true, // components_as_is = true (keep <component> tags for now so we can recursively resolve them)
|
|
1325
|
+
&self.config.components,
|
|
1326
|
+
);
|
|
1327
|
+
|
|
1328
|
+
// Recursively fetch and inline all nested components
|
|
1329
|
+
let nested = self.extract_component_srcs(&transformed);
|
|
1330
|
+
for nested_src in nested {
|
|
1331
|
+
if let Some(nested_content) = self.fetch_component_recursive(&nested_src, parser) {
|
|
1332
|
+
// Inline this nested component into the current component
|
|
1333
|
+
transformed = parser.inline_single_component(&transformed, &nested_src, &nested_content);
|
|
1334
|
+
}
|
|
1017
1335
|
}
|
|
1336
|
+
|
|
1337
|
+
// Cache the fully resolved content
|
|
1338
|
+
self.component_cache.insert(normalized_src.clone(), transformed.clone());
|
|
1339
|
+
|
|
1340
|
+
Some(transformed)
|
|
1018
1341
|
}
|
|
1019
1342
|
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1343
|
+
/// Validate that component filenames don't match reserved elements
|
|
1344
|
+
fn validate_component_names(&self) -> Result<(), CompileError> {
|
|
1345
|
+
let components_dir = self.config.components_path();
|
|
1346
|
+
|
|
1347
|
+
if !components_dir.exists() {
|
|
1348
|
+
return Ok(()); // No components directory
|
|
1024
1349
|
}
|
|
1025
1350
|
|
|
1351
|
+
let component_files = self.find_all_html_files(&components_dir)?;
|
|
1352
|
+
|
|
1353
|
+
for component_file in component_files {
|
|
1354
|
+
let file_name = component_file
|
|
1355
|
+
.file_stem()
|
|
1356
|
+
.and_then(|s| s.to_str())
|
|
1357
|
+
.unwrap_or("");
|
|
1358
|
+
|
|
1359
|
+
// Check if filename (case-sensitive) matches any reserved element
|
|
1360
|
+
if self.config.reserved_elements.contains(&file_name.to_string()) {
|
|
1361
|
+
let relative_path = component_file
|
|
1362
|
+
.strip_prefix(&self.config.source)
|
|
1363
|
+
.unwrap_or(&component_file);
|
|
1364
|
+
|
|
1365
|
+
return Err(CompileError::ReservedComponentName {
|
|
1366
|
+
component_name: file_name.to_string(),
|
|
1367
|
+
file_path: relative_path.display().to_string(),
|
|
1368
|
+
});
|
|
1369
|
+
}
|
|
1370
|
+
}
|
|
1371
|
+
|
|
1372
|
+
Ok(())
|
|
1373
|
+
}
|
|
1374
|
+
|
|
1375
|
+
/// Validate HTML syntax for all files (controlled by validate flag)
|
|
1376
|
+
fn validate_html_syntax(&self) -> Result<(), CompileError> {
|
|
1377
|
+
// Find all HTML files in source directory
|
|
1378
|
+
let html_files = self.find_all_html_files(&self.config.source)?;
|
|
1379
|
+
|
|
1380
|
+
for html_file in html_files {
|
|
1381
|
+
let content = fs::read_to_string(&html_file).map_err(|e| CompileError::ReadError {
|
|
1382
|
+
path: html_file.display().to_string(),
|
|
1383
|
+
source: e,
|
|
1384
|
+
})?;
|
|
1385
|
+
|
|
1386
|
+
// Validate HTML syntax
|
|
1387
|
+
self.validate_html(&content, &html_file)?;
|
|
1388
|
+
}
|
|
1389
|
+
|
|
1390
|
+
Ok(())
|
|
1391
|
+
}
|
|
1392
|
+
|
|
1393
|
+
/// Fetch external component without caching (returns raw content)
|
|
1394
|
+
fn fetch_external_component_raw(&self, url: &str) -> Result<String, String> {
|
|
1026
1395
|
// Fetch from URL
|
|
1027
1396
|
match reqwest::blocking::get(url) {
|
|
1028
1397
|
Ok(response) => {
|
|
@@ -1030,11 +1399,7 @@ impl Compiler {
|
|
|
1030
1399
|
return Err(format!("HTTP {} - {}", response.status().as_u16(), response.status().canonical_reason().unwrap_or("Unknown")));
|
|
1031
1400
|
}
|
|
1032
1401
|
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
|
-
}
|
|
1402
|
+
Ok(content) => Ok(content),
|
|
1038
1403
|
Err(e) => Err(format!("Failed to read response body: {}", e)),
|
|
1039
1404
|
}
|
|
1040
1405
|
}
|
|
@@ -1057,7 +1422,9 @@ impl Compiler {
|
|
|
1057
1422
|
|
|
1058
1423
|
fn extract_component_srcs(&self, content: &str) -> Vec<String> {
|
|
1059
1424
|
let mut srcs = Vec::new();
|
|
1060
|
-
|
|
1425
|
+
// Match both <component src="..."> and <div class="component" src="...">
|
|
1426
|
+
// After elements_as_is=false transform, <component> becomes <div class="component">
|
|
1427
|
+
let re = Regex::new(r#"(?:<component|<div\s+class="component")[^>]+src\s*=\s*["']([^"']+)["']"#).unwrap();
|
|
1061
1428
|
for cap in re.captures_iter(content) {
|
|
1062
1429
|
if let Some(src) = cap.get(1) {
|
|
1063
1430
|
srcs.push(src.as_str().to_string());
|
|
@@ -1068,11 +1435,15 @@ impl Compiler {
|
|
|
1068
1435
|
|
|
1069
1436
|
fn find_all_html_files(&self, dir: &Path) -> Result<Vec<PathBuf>, CompileError> {
|
|
1070
1437
|
let mut html_files = Vec::new();
|
|
1071
|
-
|
|
1438
|
+
// Get output directory basename to skip nested directories with same name
|
|
1439
|
+
let output_dir_name = self.config.output.file_name()
|
|
1440
|
+
.and_then(|n| n.to_str())
|
|
1441
|
+
.unwrap_or("");
|
|
1442
|
+
self.find_html_files_recursive(dir, &mut html_files, output_dir_name)?;
|
|
1072
1443
|
Ok(html_files)
|
|
1073
1444
|
}
|
|
1074
1445
|
|
|
1075
|
-
fn find_html_files_recursive(&self, dir: &Path, files: &mut Vec<PathBuf
|
|
1446
|
+
fn find_html_files_recursive(&self, dir: &Path, files: &mut Vec<PathBuf>, output_dir_name: &str) -> Result<(), CompileError> {
|
|
1076
1447
|
if !dir.is_dir() {
|
|
1077
1448
|
return Ok(());
|
|
1078
1449
|
}
|
|
@@ -1092,8 +1463,8 @@ impl Compiler {
|
|
|
1092
1463
|
let file_name = entry.file_name();
|
|
1093
1464
|
let file_name_str = file_name.to_string_lossy();
|
|
1094
1465
|
|
|
1095
|
-
// Skip hidden files and specific directories
|
|
1096
|
-
if file_name_str.starts_with('.') ||
|
|
1466
|
+
// Skip hidden files and specific directories/patterns
|
|
1467
|
+
if file_name_str.starts_with('.') || should_skip_path(&path, &file_name_str) {
|
|
1097
1468
|
continue;
|
|
1098
1469
|
}
|
|
1099
1470
|
|
|
@@ -1102,8 +1473,13 @@ impl Compiler {
|
|
|
1102
1473
|
continue;
|
|
1103
1474
|
}
|
|
1104
1475
|
|
|
1476
|
+
// Skip nested directories with same name as output dir (from previous bad compilations)
|
|
1477
|
+
if !output_dir_name.is_empty() && file_name_str == output_dir_name {
|
|
1478
|
+
continue;
|
|
1479
|
+
}
|
|
1480
|
+
|
|
1105
1481
|
if path.is_dir() {
|
|
1106
|
-
self.find_html_files_recursive(&path, files)?;
|
|
1482
|
+
self.find_html_files_recursive(&path, files, output_dir_name)?;
|
|
1107
1483
|
} else if path.extension().and_then(|e| e.to_str()) == Some("html") {
|
|
1108
1484
|
files.push(path);
|
|
1109
1485
|
}
|
|
@@ -1151,6 +1527,7 @@ impl Compiler {
|
|
|
1151
1527
|
&mut self,
|
|
1152
1528
|
dir: &Path,
|
|
1153
1529
|
relative_path: &str,
|
|
1530
|
+
canonical_source: &Path,
|
|
1154
1531
|
stats: &mut CompileStats,
|
|
1155
1532
|
) -> Result<(), CompileError> {
|
|
1156
1533
|
let entries = fs::read_dir(dir).map_err(|e| CompileError::ReadError {
|
|
@@ -1162,14 +1539,19 @@ impl Compiler {
|
|
|
1162
1539
|
let path = entry.path();
|
|
1163
1540
|
let file_name = path.file_name().unwrap().to_str().unwrap();
|
|
1164
1541
|
|
|
1542
|
+
// Skip files/directories matching skip patterns
|
|
1543
|
+
if should_skip_path(&path, file_name) {
|
|
1544
|
+
continue;
|
|
1545
|
+
}
|
|
1546
|
+
|
|
1165
1547
|
if path.is_dir() {
|
|
1166
1548
|
let new_relative = format!("{}/{}", relative_path, file_name);
|
|
1167
|
-
self.copy_directory(&path, &new_relative, stats)?;
|
|
1549
|
+
self.copy_directory(&path, &new_relative, canonical_source, stats)?;
|
|
1168
1550
|
} else {
|
|
1169
1551
|
self.copy_file(&path, relative_path)?;
|
|
1170
1552
|
stats.files_copied += 1;
|
|
1171
1553
|
if let Some(ref mut logger) = self.logger {
|
|
1172
|
-
logger.log(&path, FileOperation::Copied,
|
|
1554
|
+
logger.log(&path, FileOperation::Copied, canonical_source);
|
|
1173
1555
|
}
|
|
1174
1556
|
}
|
|
1175
1557
|
}
|