@@ -95,8 +95,6 @@ struct ParseContext {
9595 canonical: Option<String>,
9696 /// Base URL for relative links
9797 base_url: Option<String>,
98− /// Track @import paths for circular dependency detection
99− import_stack: Vec<PathBuf>,
10098}
10199
102100/// Evaluates a syntax tree (see `syntax.rs`) into the document's nodes.
@@ -177,7 +175,6 @@ pub fn parse_with_base(input: &str, base_path: Option<&Path>) -> ParseResult {
177175 define_lines: HashMap::new(),
178176 canonical: None,
179177 base_url: None,
180− import_stack: Vec::new(),
181178 };
182179 load_prelude(&mut ctx);
183180 let nodes = Evaluator.eval_block(&tree, &mut ctx);
@@ -499,23 +496,22 @@ impl Evaluator {
499496
500497 if let Some(rest) = content.strip_prefix("@include ") {
501498 let rest = rest.trim();
502− let (filename, alias) = if let Some((file_part, alias_part)) = rest.rsplit_once(" as ")
503− {
504− (
505− file_part.trim().to_string(),
506− Some(alias_part.trim().to_string()),
507− )
508− } else {
509− (rest.to_string(), None)
510− };
511− // Strip quotes from filename
512− let filename =
513− if filename.starts_with('"') && filename.ends_with('"') && filename.len() >= 2 {
514− filename[1..filename.len() - 1].to_string()
515− } else {
516− filename
517− };
518− let filename = substitute_vars(&filename, &ctx.variables);
499+ if let Some((file, alias)) = rest.rsplit_once(" as ") {
500+ return Err(ParseError {
501+ line: line_num,
502+ message: format!(
503+ "`@include ... as` was removed: write `@include {}` and drop the `{}.` \
504+ prefix (run `htmlang upgrade`)",
505+ file.trim(),
506+ alias.trim()
507+ ),
508+ });
509+ }
510+ let filename = rest
511+ .strip_prefix('"')
512+ .and_then(|f| f.strip_suffix('"'))
513+ .unwrap_or(rest);
514+ let filename = substitute_vars(filename, &ctx.variables);
519515
520516 // Glob support: if filename contains *, expand to multiple imports
521517 if filename.contains('*') {
@@ -579,18 +575,7 @@ impl Evaluator {
579575 .unwrap_or(&file_path)
580576 .to_string_lossy()
581577 .to_string();
582− // Synthesize an @include line for each matched file
583− let import_line = match &alias {
584− Some(pfx) => {
585− let stem = file_path
586− .file_stem()
587− .unwrap_or_default()
588− .to_string_lossy()
589− .to_string();
590− format!("@include \"{}\" as {}.{}", rel_name, pfx, stem)
591− }
592− None => format!("@include \"{}\"", rel_name),
593− };
578+ let import_line = format!("@include \"{}\"", rel_name);
594579 matched_nodes.extend(self.eval_block(&syntax::parse(&import_line), ctx));
595580 }
596581 return Ok(Some(matched_nodes));
@@ -601,8 +586,8 @@ impl Evaluator {
601586 None => PathBuf::from(&filename),
602587 };
603588
604− if ctx.include_stack.contains(&resolved) || ctx.import_stack.contains(&resolved) {
605− let cycle_chain = format_include_chain(&ctx.import_stack);
589+ if ctx.include_stack.contains(&resolved) {
590+ let cycle_chain = format_include_chain(&ctx.include_stack);
606591 ctx.diagnostics.push(Diagnostic {
607592 line: line_num,
608593 column: None,
@@ -639,65 +624,12 @@ impl Evaluator {
639624
640625 ctx.included_files.push(resolved.clone());
641626 ctx.include_stack.push(resolved.clone());
642− ctx.import_stack.push(resolved.clone());
643627 let saved_base = ctx.base_path.clone();
644628 ctx.base_path = resolved.parent().map(|p| p.to_path_buf());
645629
646630 let diag_count_before = ctx.diagnostics.len();
647− let mut included_nodes = Vec::new();
648−
649− if let Some(ref prefix) = alias {
650− // Snapshot just the *key sets* before parsing — much cheaper
651− // than cloning the full HashMaps, since we only need to know
652− // which definitions are new afterwards.
653− let fn_keys_before: std::collections::HashSet<String> =
654− ctx.functions.keys().cloned().collect();
655− let define_keys_before: std::collections::HashSet<String> =
656− ctx.defines.keys().cloned().collect();
657− let var_keys_before: std::collections::HashSet<String> =
658− ctx.variables.keys().cloned().collect();
659−
660− let _discarded_nodes = self.eval_block(&syntax::parse(&imported_text), ctx);
661−
662− // Remove newly added entries and re-insert them under the prefix.
663− let new_fn_keys: Vec<String> = ctx
664− .functions
665− .keys()
666− .filter(|k| !fn_keys_before.contains(*k))
667− .cloned()
668− .collect();
669− for name in new_fn_keys {
670− if let Some(def) = ctx.functions.remove(&name) {
671− ctx.functions.insert(format!("{}.{}", prefix, name), def);
672− }
673− }
674− let new_define_keys: Vec<String> = ctx
675− .defines
676− .keys()
677− .filter(|k| !define_keys_before.contains(*k))
678− .cloned()
679− .collect();
680− for name in new_define_keys {
681− if let Some(attrs) = ctx.defines.remove(&name) {
682− ctx.defines.insert(format!("{}.{}", prefix, name), attrs);
683− }
684− }
685− let new_var_keys: Vec<String> = ctx
686− .variables
687− .keys()
688− .filter(|k| !var_keys_before.contains(*k))
689− .cloned()
690− .collect();
691− for name in new_var_keys {
692− if let Some(val) = ctx.variables.remove(&name)
693− && !name.starts_with("__")
694− {
695− ctx.variables.insert(format!("{}.{}", prefix, name), val);
696− }
697− }
698− } else {
699− included_nodes = self.eval_block(&syntax::parse(&imported_text), ctx);
700− }
631+
632+ let included_nodes = self.eval_block(&syntax::parse(&imported_text), ctx);
701633
702634 // Annotate new diagnostics with import chain
703635 let import_chain = format_include_chain(&ctx.include_stack);
@@ -707,7 +639,6 @@ impl Evaluator {
707639
708640 ctx.base_path = saved_base;
709641 ctx.include_stack.pop();
710− ctx.import_stack.pop();
711642 return Ok(Some(included_nodes));
712643 }
713644