on master help sign in
@include does one thing: drop 'as' namespacing
030614eiko
DESIGN.md+1−1
@@ -388,7 +388,6 @@ A range counts down when its start is greater than its end.
388| Directive | Effect |
388| Directive | Effect |
389|---|---|
389|---|---|
390| `@include file.hl` | Insert another file: its content and definitions (a library of `@let`s emits nothing) |
390| `@include file.hl` | Insert another file: its content and definitions (a library of `@let`s emits nothing) |
391−| `@include lib.hl as ui` | Take only its definitions, named `ui.name` |
392| `@data $name file.json` | Load JSON values as variables (`$name.key`) |
391| `@data $name file.json` | Load JSON values as variables (`$name.key`) |
393| `@data $name dir/*.json` | Load each file as `$name.STEM.key`; `$name` lists the stems |
392| `@data $name dir/*.json` | Load each file as `$name.STEM.key`; `$name` lists the stems |
394| `@data $name env:NAME [default]` | Read an environment variable |
393| `@data $name env:NAME [default]` | Read an environment variable |
@@ -486,6 +485,7 @@ removed form with its replacement.
486| `$a ~ " " ~ $b` | `"$a $b"` |
485| `$a ~ " " ~ $b` | `"$a $b"` |
487| `@each $x in LIST [page N]` | Split the list, or filter it with `@if` |
486| `@each $x in LIST [page N]` | Split the list, or filter it with `@if` |
488| `...$bundle` | `$bundle` |
487| `...$bundle` | `$bundle` |
488+| `@include lib.hl as ui` | `@include lib.hl`, without the `ui.` prefix |
489| `@data file.json` (keys as `$key`) | `@data $name file.json` (keys as `$name.key`) |
489| `@data file.json` (keys as `$key`) | `@data $name file.json` (keys as `$name.key`) |
490| `[attrs]` alone on a line | `@el [attrs]` |
490| `[attrs]` alone on a line | `@el [attrs]` |
491| `animate`, `inset-area` | `animation`, `position-area` |
491| `animate`, `inset-area` | `animation`, `position-area` |
crates/htmlang-core/src/parser.rs+21−90
@@ -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>,
100}
98}
101
99
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(),
181 };
178 };
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 {
499
496
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()
507+ ),
510− };
508+ });
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);
516− filename
517− };
518− let filename = substitute_vars(&filename, &ctx.variables);
514+ let filename = substitute_vars(filename, &ctx.variables);
519
515
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 {
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),
578+ let import_line = format!("@include \"{}\"", rel_name);
593− };
594 matched_nodes.extend(self.eval_block(&syntax::parse(&import_line), ctx));
579 matched_nodes.extend(self.eval_block(&syntax::parse(&import_line), ctx));
595 }
580 }
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),
602 };
587 };
603
588
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 {
639
624
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());
645
629
646 let diag_count_before = ctx.diagnostics.len();
630 let diag_count_before = ctx.diagnostics.len();
647− let mut included_nodes = Vec::new();
631+
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);
632+ let included_nodes = self.eval_block(&syntax::parse(&imported_text), ctx);
700− }
701
633
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 {
707
639
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));
712 }
643 }
713
644
src/upgrade.rs+31−2
@@ -238,13 +238,42 @@ pub fn upgrade(input: &str) -> Upgrade {
238 if input.ends_with('\n') {
238 if input.ends_with('\n') {
239 output.push('\n');
239 output.push('\n');
240 }
240 }
241+ let (output, unaliased) = drop_include_aliases(&output);
241 Upgrade {
242 Upgrade {
242 output,
243 output,
243− changes,
244+ changes: changes + unaliased,
244 manual,
245 manual,
245 }
246 }
246}
247}
247
248
249+/// `@include lib.hl as ui` → `@include lib.hl`, and `@ui.card` / `$ui.x`
250+/// lose their `ui.` prefix.
251+fn drop_include_aliases(text: &str) -> (String, usize) {
252+ let mut aliases = Vec::new();
253+ let mut lines: Vec<String> = Vec::new();
254+ for line in text.split('\n') {
255+ let trimmed = line.trim_start();
256+ if let Some(rest) = trimmed.strip_prefix("@include ")
257+ && let Some((file, alias)) = rest.rsplit_once(" as ")
258+ {
259+ aliases.push(alias.trim().to_string());
260+ lines.push(format!("{}@include {}", &line[..line.len() - trimmed.len()], file.trim()));
261+ } else {
262+ lines.push(line.to_string());
263+ }
264+ }
265+ if aliases.is_empty() {
266+ return (text.to_string(), 0);
267+ }
268+ let mut out = lines.join("\n");
269+ for alias in &aliases {
270+ out = out
271+ .replace(&format!("@{}.", alias), "@")
272+ .replace(&format!("${}.", alias), "$");
273+ }
274+ (out, aliases.len())
275+}
276+
248struct Block {
277struct Block {
249 lines: Vec<String>,
278 lines: Vec<String>,
250 end: usize,
279 end: usize,
@@ -1403,7 +1432,7 @@ mod tests {
1403 assert_eq!(up("@define c [bold]\n@mixin m [italic]"), "@let c [bold]\n@let m [italic]");
1432 assert_eq!(up("@define c [bold]\n@mixin m [italic]"), "@let c [bold]\n@let m [italic]");
1404 assert_eq!(up("@for $i in 1..3\n $i"), "@each $i in 1..3\n $i");
1433 assert_eq!(up("@for $i in 1..3\n $i"), "@each $i in 1..3\n $i");
1405 assert_eq!(up("@use \"lib.hl\" a, b"), "@include lib.hl");
1434 assert_eq!(up("@use \"lib.hl\" a, b"), "@include lib.hl");
1406− assert_eq!(up("@import theme.hl\n@import \"ui.hl\" as ui"), "@include theme.hl\n@include \"ui.hl\" as ui");
1435+ assert_eq!(up("@import theme.hl\n@import \"ui.hl\" as ui\n@ui.card [x $ui.gap]"), "@include theme.hl\n@include \"ui.hl\"\n@card [x $gap]");
1407 assert_eq!(
1436 assert_eq!(
1408 up("@repeat 2\n @text $_count"),
1437 up("@repeat 2\n @text $_count"),
1409 "@each $_ in 1..2\n @text 2"
1438 "@each $_ in 1..2\n @text 2"
tests/snapshots.rs−29
@@ -4849,35 +4849,6 @@ fn error_circular_include_filesystem() {
4849 );
4849 );
4850}
4850}
4851
4851
4852−#[test]
4853−fn import_with_alias_prefixes_definitions() {
4854− // Verify that @include with alias registers imported fns under `alias.name`.
4855− let dir = std::env::temp_dir().join("htmlang_test_import_alias");
4856− let _ = std::fs::remove_dir_all(&dir);
4857− std::fs::create_dir_all(&dir).unwrap();
4858− let lib_path = dir.join("lib.hl");
4859− std::fs::write(&lib_path, "@let card\n @el\n @text card-body\n").unwrap();
4860−
4861− let input = "@include lib.hl as ui\n@ui.card\n";
4862− let result = htmlang::parser::parse_with_base(input, Some(&dir));
4863− let html = htmlang::codegen::generate(&result.document);
4864− let _ = std::fs::remove_dir_all(&dir);
4865−
4866− assert!(
4867− result
4868− .diagnostics
4869− .iter()
4870− .all(|d| d.severity != htmlang::parser::Severity::Error),
4871− "no errors expected, got: {:?}",
4872− result.diagnostics
4873− );
4874− assert!(
4875− html.contains("card-body"),
4876− "alias prefixed call should expand, got: {}",
4877− html
4878− );
4879−}
4880−
4881#[test]
4852#[test]
4882fn error_invalid_json_in_data_directive() {
4853fn error_invalid_json_in_data_directive() {
4883 let dir = std::env::temp_dir().join("htmlang_test_bad_json");
4854 let dir = std::env::temp_dir().join("htmlang_test_bad_json");