@@ -95,8 +95,6 @@ struct ParseContext {
95 canonical: Option<String>,
95 canonical: Option<String>,
96 /// Base URL for relative links
96 /// Base URL for relative links
97 base_url: Option<String>,
97 base_url: Option<String>,
98− /// Track @import paths for circular dependency detection
99− import_stack: Vec<PathBuf>,
102/// Evaluates a syntax tree (see `syntax.rs`) into the document's nodes.
100/// 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 {
177 define_lines: HashMap::new(),
175 define_lines: HashMap::new(),
178 canonical: None,
176 canonical: None,
179 base_url: None,
177 base_url: None,
180− import_stack: Vec::new(),
182 load_prelude(&mut ctx);
179 load_prelude(&mut ctx);
183 let nodes = Evaluator.eval_block(&tree, &mut ctx);
180 let nodes = Evaluator.eval_block(&tree, &mut ctx);
@@ -499,23 +496,22 @@ impl Evaluator {
500 if let Some(rest) = content.strip_prefix("@include ") {
497 if let Some(rest) = content.strip_prefix("@include ") {
501 let rest = rest.trim();
498 let rest = rest.trim();
502− let (filename, alias) = if let Some((file_part, alias_part)) = rest.rsplit_once(" as ")
499+ if let Some((file, alias)) = rest.rsplit_once(" as ") {
503− {
500+ return Err(ParseError {
504− (
501+ line: line_num,
505− file_part.trim().to_string(),
502+ message: format!(
506− Some(alias_part.trim().to_string()),
503+ "`@include ... as` was removed: write `@include {}` and drop the `{}.` \
507− )
504+ prefix (run `htmlang upgrade`)",
508− } else {
505+ file.trim(),
509− (rest.to_string(), None)
506+ alias.trim()
511− // Strip quotes from filename
509+ }
512− let filename =
510+ let filename = rest
513− if filename.starts_with('"') && filename.ends_with('"') && filename.len() >= 2 {
511+ .strip_prefix('"')
514− filename[1..filename.len() - 1].to_string()
512+ .and_then(|f| f.strip_suffix('"'))
515− } else {
513+ .unwrap_or(rest);
518− let filename = substitute_vars(&filename, &ctx.variables);
514+ let filename = substitute_vars(filename, &ctx.variables);
520 // Glob support: if filename contains *, expand to multiple imports
516 // Glob support: if filename contains *, expand to multiple imports
521 if filename.contains('*') {
517 if filename.contains('*') {
@@ -579,18 +575,7 @@ impl Evaluator {
579 .unwrap_or(&file_path)
575 .unwrap_or(&file_path)
580 .to_string_lossy()
576 .to_string_lossy()
581 .to_string();
577 .to_string();
582− // Synthesize an @include line for each matched file
583− let import_line = match &alias {
585− let stem = file_path
587− .unwrap_or_default()
590− format!("@include \"{}\" as {}.{}", rel_name, pfx, stem)
592− None => format!("@include \"{}\"", rel_name),
578+ let import_line = format!("@include \"{}\"", rel_name);
594 matched_nodes.extend(self.eval_block(&syntax::parse(&import_line), ctx));
579 matched_nodes.extend(self.eval_block(&syntax::parse(&import_line), ctx));
596 return Ok(Some(matched_nodes));
581 return Ok(Some(matched_nodes));
@@ -601,8 +586,8 @@ impl Evaluator {
601 None => PathBuf::from(&filename),
586 None => PathBuf::from(&filename),
604− if ctx.include_stack.contains(&resolved) || ctx.import_stack.contains(&resolved) {
589+ if ctx.include_stack.contains(&resolved) {
605− let cycle_chain = format_include_chain(&ctx.import_stack);
590+ let cycle_chain = format_include_chain(&ctx.include_stack);
606 ctx.diagnostics.push(Diagnostic {
591 ctx.diagnostics.push(Diagnostic {
607 line: line_num,
592 line: line_num,
608 column: None,
593 column: None,
@@ -639,65 +624,12 @@ impl Evaluator {
640 ctx.included_files.push(resolved.clone());
625 ctx.included_files.push(resolved.clone());
641 ctx.include_stack.push(resolved.clone());
626 ctx.include_stack.push(resolved.clone());
642− ctx.import_stack.push(resolved.clone());
643 let saved_base = ctx.base_path.clone();
627 let saved_base = ctx.base_path.clone();
644 ctx.base_path = resolved.parent().map(|p| p.to_path_buf());
628 ctx.base_path = resolved.parent().map(|p| p.to_path_buf());
646 let diag_count_before = ctx.diagnostics.len();
630 let diag_count_before = ctx.diagnostics.len();
647− let mut included_nodes = Vec::new();
631+
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();
660− let _discarded_nodes = self.eval_block(&syntax::parse(&imported_text), ctx);
662− // Remove newly added entries and re-insert them under the prefix.
663− let new_fn_keys: Vec<String> = ctx
666− .filter(|k| !fn_keys_before.contains(*k))
669− for name in new_fn_keys {
670− if let Some(def) = ctx.functions.remove(&name) {
671− ctx.functions.insert(format!("{}.{}", prefix, name), def);
674− let new_define_keys: Vec<String> = ctx
677− .filter(|k| !define_keys_before.contains(*k))
680− for name in new_define_keys {
681− if let Some(attrs) = ctx.defines.remove(&name) {
682− ctx.defines.insert(format!("{}.{}", prefix, name), attrs);
685− let new_var_keys: Vec<String> = ctx
688− .filter(|k| !var_keys_before.contains(*k))
691− for name in new_var_keys {
692− if let Some(val) = ctx.variables.remove(&name)
693− && !name.starts_with("__")
695− ctx.variables.insert(format!("{}.{}", prefix, name), val);
699− included_nodes = self.eval_block(&syntax::parse(&imported_text), ctx);
632+ let included_nodes = self.eval_block(&syntax::parse(&imported_text), ctx);
702 // Annotate new diagnostics with import chain
634 // Annotate new diagnostics with import chain
703 let import_chain = format_include_chain(&ctx.include_stack);
635 let import_chain = format_include_chain(&ctx.include_stack);
@@ -707,7 +639,6 @@ impl Evaluator {
708 ctx.base_path = saved_base;
640 ctx.base_path = saved_base;
709 ctx.include_stack.pop();
641 ctx.include_stack.pop();
710− ctx.import_stack.pop();
711 return Ok(Some(included_nodes));
642 return Ok(Some(included_nodes));