on master help sign in
Merge @import into @include
5fa0bb4iko
crates/htmlang-core/src/parser.rs+19−87
@@ -931,79 +931,7 @@ impl Parser {
931 }
931 }
932
932
933 if let Some(rest) = content.strip_prefix("@include ") {
933 if let Some(rest) = content.strip_prefix("@include ") {
934− let filename = substitute_vars(rest.trim(), &ctx.variables);
935− let resolved = match &ctx.base_path {
936− Some(base) => base.join(&filename),
937− None => PathBuf::from(&filename),
938− };
939−
940− // Circular include check
941− if ctx.include_stack.contains(&resolved) {
942− let cycle_chain = format_include_chain(&ctx.include_stack);
943− ctx.diagnostics.push(Diagnostic {
944− line: line_num,
945− column: None,
946− message: format!(
947− "circular include '{}' (cycle: {} → {})",
948− filename, cycle_chain, filename
949− ),
950− severity: Severity::Error,
951− source_line: Some(content.clone()),
952− });
953− return Ok(None);
954− }
955−
956− // Use file cache to avoid redundant reads
957− let included_text = if let Some(cached) = ctx.file_cache.get(&resolved) {
958− cached.clone()
959− } else {
960− match std::fs::read_to_string(&resolved) {
961− Ok(text) => {
962− ctx.file_cache.insert(resolved.clone(), text.clone());
963− text
964− }
965− Err(e) => {
966− ctx.diagnostics.push(Diagnostic {
967− line: line_num,
968− column: None,
969− message: format!("cannot include '{}': {}", filename, e),
970− severity: Severity::Error,
971− source_line: Some(content.clone()),
972− });
973− return Ok(None);
974− }
975− }
976− };
977−
978− ctx.included_files.push(resolved.clone());
979− ctx.include_stack.push(resolved.clone());
980− let saved_base = ctx.base_path.clone();
981− ctx.base_path = resolved.parent().map(|p| p.to_path_buf());
982−
983− let diag_count_before = ctx.diagnostics.len();
984− let included_lines = preprocess(&included_text);
985− let mut included_parser = Parser {
986− lines: included_lines,
987− pos: 0,
988− };
989− let nodes = included_parser.parse_children(0, ctx);
990−
991− // Annotate new diagnostics with include chain
992− let include_chain = format_include_chain(&ctx.include_stack);
993− for d in &mut ctx.diagnostics[diag_count_before..] {
994− d.message = format!("{}\n in {}", d.message, include_chain);
995− }
996−
997− ctx.base_path = saved_base;
998− ctx.include_stack.pop();
999− return Ok(Some(nodes));
1000− }
1001−
1002− // --- @import (definitions only, no DOM nodes) ---
1003−
1004− if let Some(rest) = content.strip_prefix("@import ") {
1005 let rest = rest.trim();
934 let rest = rest.trim();
1006− // Support @import "file.hl" as prefix — namespace imported definitions
1007 let (filename, alias) = if let Some((file_part, alias_part)) = rest.rsplit_once(" as ")
935 let (filename, alias) = if let Some((file_part, alias_part)) = rest.rsplit_once(" as ")
1008 {
936 {
1009 (
937 (
@@ -1077,13 +1005,14 @@ impl Parser {
1077 });
1005 });
1078 return Ok(None);
1006 return Ok(None);
1079 }
1007 }
1008+ let mut matched_nodes = Vec::new();
1080 for file_path in matched_files {
1009 for file_path in matched_files {
1081 let rel_name = file_path
1010 let rel_name = file_path
1082 .strip_prefix(&base_dir)
1011 .strip_prefix(&base_dir)
1083 .unwrap_or(&file_path)
1012 .unwrap_or(&file_path)
1084 .to_string_lossy()
1013 .to_string_lossy()
1085 .to_string();
1014 .to_string();
1086− // Synthesize an @import line for each matched file
1015+ // Synthesize an @include line for each matched file
1087 let import_line = match &alias {
1016 let import_line = match &alias {
1088 Some(pfx) => {
1017 Some(pfx) => {
1089 let stem = file_path
1018 let stem = file_path
@@ -1091,18 +1020,18 @@ impl Parser {
1091 .unwrap_or_default()
1020 .unwrap_or_default()
1092 .to_string_lossy()
1021 .to_string_lossy()
1093 .to_string();
1022 .to_string();
1094− format!("@import \"{}\" as {}.{}", rel_name, pfx, stem)
1023+ format!("@include \"{}\" as {}.{}", rel_name, pfx, stem)
1095 }
1024 }
1096− None => format!("@import \"{}\"", rel_name),
1025+ None => format!("@include \"{}\"", rel_name),
1097 };
1026 };
1098 let synth_lines = preprocess(&import_line);
1027 let synth_lines = preprocess(&import_line);
1099 let mut synth_parser = Parser {
1028 let mut synth_parser = Parser {
1100 lines: synth_lines,
1029 lines: synth_lines,
1101 pos: 0,
1030 pos: 0,
1102 };
1031 };
1103− let _ = synth_parser.parse_children(0, ctx);
1032+ matched_nodes.extend(synth_parser.parse_children(0, ctx));
1104 }
1033 }
1105− return Ok(None);
1034+ return Ok(Some(matched_nodes));
1106 }
1035 }
1107
1036
1108 let resolved = match &ctx.base_path {
1037 let resolved = match &ctx.base_path {
@@ -1116,7 +1045,7 @@ impl Parser {
1116 line: line_num,
1045 line: line_num,
1117 column: None,
1046 column: None,
1118 message: format!(
1047 message: format!(
1119− "circular import '{}' (cycle: {} → {})",
1048+ "circular include '{}' (cycle: {} → {})",
1120 filename, cycle_chain, filename
1049 filename, cycle_chain, filename
1121 ),
1050 ),
1122 severity: Severity::Error,
1051 severity: Severity::Error,
@@ -1137,7 +1066,7 @@ impl Parser {
1137 ctx.diagnostics.push(Diagnostic {
1066 ctx.diagnostics.push(Diagnostic {
1138 line: line_num,
1067 line: line_num,
1139 column: None,
1068 column: None,
1140− message: format!("cannot import '{}': {}", filename, e),
1069+ message: format!("cannot include '{}': {}", filename, e),
1141 severity: Severity::Error,
1070 severity: Severity::Error,
1142 source_line: Some(content.clone()),
1071 source_line: Some(content.clone()),
1143 });
1072 });
@@ -1153,6 +1082,7 @@ impl Parser {
1153 ctx.base_path = resolved.parent().map(|p| p.to_path_buf());
1082 ctx.base_path = resolved.parent().map(|p| p.to_path_buf());
1154
1083
1155 let diag_count_before = ctx.diagnostics.len();
1084 let diag_count_before = ctx.diagnostics.len();
1085+ let mut included_nodes = Vec::new();
1156
1086
1157 if let Some(ref prefix) = alias {
1087 if let Some(ref prefix) = alias {
1158 // Snapshot just the *key sets* before parsing — much cheaper
1088 // Snapshot just the *key sets* before parsing — much cheaper
@@ -1209,13 +1139,12 @@ impl Parser {
1209 }
1139 }
1210 }
1140 }
1211 } else {
1141 } else {
1212− // Parse the file but discard DOM nodes — only keep definitions
1213− let imported_lines = preprocess(&imported_text);
1142+ let included_lines = preprocess(&imported_text);
1214− let mut imported_parser = Parser {
1143+ let mut included_parser = Parser {
1215− lines: imported_lines,
1144+ lines: included_lines,
1216 pos: 0,
1145 pos: 0,
1217 };
1146 };
1218− let _discarded_nodes = imported_parser.parse_children(0, ctx);
1147+ included_nodes = included_parser.parse_children(0, ctx);
1219 }
1148 }
1220
1149
1221 // Annotate new diagnostics with import chain
1150 // Annotate new diagnostics with import chain
@@ -1227,7 +1156,7 @@ impl Parser {
1227 ctx.base_path = saved_base;
1156 ctx.base_path = saved_base;
1228 ctx.include_stack.pop();
1157 ctx.include_stack.pop();
1229 ctx.import_stack.pop();
1158 ctx.import_stack.pop();
1230− return Ok(None); // No nodes emitted
1159+ return Ok(Some(included_nodes));
1231 }
1160 }
1232
1161
1233 // --- @env (compile-time environment variables) ---
1162 // --- @env (compile-time environment variables) ---
@@ -2714,7 +2643,6 @@ const KNOWN_DIRECTIVES: &[&str] = &[
2714 "page",
2643 "page",
2715 "let",
2644 "let",
2716 "include",
2645 "include",
2717− "import",
2718 "raw",
2646 "raw",
2719 "keyframes",
2647 "keyframes",
2720 "if",
2648 "if",
@@ -2776,7 +2704,11 @@ const REMOVED_SYNTAX: &[(&str, &str)] = &[
2776 "@switch",
2704 "@switch",
2777 "use `@match`, with `@let __switch [...]` inside a case for its attributes",
2705 "use `@match`, with `@let __switch [...]` inside a case for its attributes",
2778 ),
2706 ),
2707+ ("@use", "use `@include`"),
2708+ (
2779− ("@use", "use `@import`"),
2709+ "@import",
2710+ "use `@include` (a file with only definitions emits no content)",
2711+ ),
2780 ("@with", "use `@let alias $source`"),
2712 ("@with", "use `@let alias $source`"),
2781 (
2713 (
2782 "@layout",
2714 "@layout",
src/upgrade.rs+6−2
@@ -447,6 +447,9 @@ fn rewrite_directive_line(
447 {
447 {
448 return format!("{pad}@let {} = {}", name, value.trim());
448 return format!("{pad}@let {} = {}", name, value.trim());
449 }
449 }
450+ if let Some(rest) = trimmed.strip_prefix("@import ") {
451+ return format!("{pad}@include {}", rest);
452+ }
450 if let Some(rest) = trimmed.strip_prefix("@unless ") {
453 if let Some(rest) = trimmed.strip_prefix("@unless ") {
451 return format!("{pad}@if not {}", rest);
454 return format!("{pad}@if not {}", rest);
452 }
455 }
@@ -466,7 +469,7 @@ fn rewrite_directive_line(
466 manual.push((idx + 1, "could not read the file name in @use".to_string()));
469 manual.push((idx + 1, "could not read the file name in @use".to_string()));
467 return line.to_string();
470 return line.to_string();
468 }
471 }
469− return format!("{pad}@import {}", file);
472+ return format!("{pad}@include {}", file);
470 }
473 }
471 line.to_string()
474 line.to_string()
472}
475}
@@ -869,7 +872,8 @@ mod tests {
869 assert_eq!(up("@fn card $t\n @text $t"), "@let card $t\n @text $t");
872 assert_eq!(up("@fn card $t\n @text $t"), "@let card $t\n @text $t");
870 assert_eq!(up("@define c [bold]\n@mixin m [italic]"), "@let c [bold]\n@let m [italic]");
873 assert_eq!(up("@define c [bold]\n@mixin m [italic]"), "@let c [bold]\n@let m [italic]");
871 assert_eq!(up("@for $i in 1..3\n $i"), "@each $i in 1..3\n $i");
874 assert_eq!(up("@for $i in 1..3\n $i"), "@each $i in 1..3\n $i");
872− assert_eq!(up("@use \"lib.hl\" a, b"), "@import lib.hl");
875+ assert_eq!(up("@use \"lib.hl\" a, b"), "@include lib.hl");
876+ assert_eq!(up("@import theme.hl\n@import \"ui.hl\" as ui"), "@include theme.hl\n@include \"ui.hl\" as ui");
873 assert_eq!(
877 assert_eq!(
874 up("@repeat 2\n @text $_count"),
878 up("@repeat 2\n @text $_count"),
875 "@each $_ in 1..2\n @text 2"
879 "@each $_ in 1..2\n @text 2"
tests/snapshots.rs+7−6
@@ -758,6 +758,7 @@ fn removed_directives_point_to_upgrade() {
758 "@repeat 2\n hi",
758 "@repeat 2\n hi",
759 "@switch $v\n @case a\n hi",
759 "@switch $v\n @case a\n hi",
760 "@use \"a.hl\" x",
760 "@use \"a.hl\" x",
761+ "@import a.hl",
761 "@with $a as b\n hi",
762 "@with $a as b\n hi",
762 "@layout base.hl\n hi",
763 "@layout base.hl\n hi",
763 "@scope .c\n .t {}",
764 "@scope .c\n .t {}",
@@ -3153,13 +3154,13 @@ fn test_carousel_children_snap() {
3153fn test_import_missing_file() {
3154fn test_import_missing_file() {
3154 // We can't test @use with actual files in unit tests easily, but we can verify
3155 // We can't test @use with actual files in unit tests easily, but we can verify
3155 // the parser recognizes the directive without errors when it can't find the file
3156 // the parser recognizes the directive without errors when it can't find the file
3156− let result = htmlang::parser::parse("@import nonexistent.hl");
3157+ let result = htmlang::parser::parse("@include nonexistent.hl");
3157 let has_use_error = result.diagnostics.iter().any(|d| {
3158 let has_use_error = result.diagnostics.iter().any(|d| {
3158− d.message.contains("cannot import") && d.severity == htmlang::parser::Severity::Error
3159+ d.message.contains("cannot include") && d.severity == htmlang::parser::Severity::Error
3159 });
3160 });
3160 assert!(
3161 assert!(
3161 has_use_error,
3162 has_use_error,
3162− "@import should report error for missing file, got: {:?}",
3163+ "@include should report error for missing file, got: {:?}",
3163 result.diagnostics
3164 result.diagnostics
3164 );
3165 );
3165}
3166}
@@ -5155,7 +5156,7 @@ fn perf_large_document() {
5155}
5156}
5156
5157
5157// ---------------------------------------------------------------------------
5158// ---------------------------------------------------------------------------
5158−// Filesystem-based error tests for @include / @import / @data
5159+// Filesystem-based error tests for @include / @data
5159// ---------------------------------------------------------------------------
5160// ---------------------------------------------------------------------------
5160
5161
5161#[test]
5162#[test]
@@ -5185,14 +5186,14 @@ fn error_circular_include_filesystem() {
5185
5186
5186#[test]
5187#[test]
5187fn import_with_alias_prefixes_definitions() {
5188fn import_with_alias_prefixes_definitions() {
5188− // Verify that @import with alias registers imported fns under `alias.name`.
5189+ // Verify that @include with alias registers imported fns under `alias.name`.
5189 let dir = std::env::temp_dir().join("htmlang_test_import_alias");
5190 let dir = std::env::temp_dir().join("htmlang_test_import_alias");
5190 let _ = std::fs::remove_dir_all(&dir);
5191 let _ = std::fs::remove_dir_all(&dir);
5191 std::fs::create_dir_all(&dir).unwrap();
5192 std::fs::create_dir_all(&dir).unwrap();
5192 let lib_path = dir.join("lib.hl");
5193 let lib_path = dir.join("lib.hl");
5193 std::fs::write(&lib_path, "@let card\n @el\n @text card-body\n").unwrap();
5194 std::fs::write(&lib_path, "@let card\n @el\n @text card-body\n").unwrap();
5194
5195
5195− let input = "@import lib.hl as ui\n@ui.card\n";
5196+ let input = "@include lib.hl as ui\n@ui.card\n";
5196 let result = htmlang::parser::parse_with_base(input, Some(&dir));
5197 let result = htmlang::parser::parse_with_base(input, Some(&dir));
5197 let html = htmlang::codegen::generate(&result.document);
5198 let html = htmlang::codegen::generate(&result.document);
5198 let _ = std::fs::remove_dir_all(&dir);
5199 let _ = std::fs::remove_dir_all(&dir);