on master help sign in
@data always takes a name
01778ceiko
DESIGN.md+2−1
@@ -389,7 +389,7 @@ A range counts down when its start is greater than its end.
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` |
391| `@include lib.hl as ui` | Take only its definitions, named `ui.name` |
392−| `@data file.json` / `@data $name file.json` | Load JSON values as variables (`$name.key`) |
392+| `@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 |
393| `@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 |
394| `@data $name env:NAME [default]` | Read an environment variable |
395| `@markdown` / `@markdown file.md` | Markdown, converted to HTML |
395| `@markdown` / `@markdown file.md` | Markdown, converted to HTML |
@@ -486,6 +486,7 @@ removed form with its replacement.
486| `$a ~ " " ~ $b` | `"$a $b"` |
486| `$a ~ " " ~ $b` | `"$a $b"` |
487| `@each $x in LIST [page N]` | Split the list, or filter it with `@if` |
487| `@each $x in LIST [page N]` | Split the list, or filter it with `@if` |
488| `...$bundle` | `$bundle` |
488| `...$bundle` | `$bundle` |
489+| `@data file.json` (keys as `$key`) | `@data $name file.json` (keys as `$name.key`) |
489| `[attrs]` alone on a line | `@el [attrs]` |
490| `[attrs]` alone on a line | `@el [attrs]` |
490| `animate`, `inset-area` | `animation`, `position-area` |
491| `animate`, `inset-area` | `animation`, `position-area` |
491| `$x\|uppercase`, `$c\|darken:10` (filters) | `${uppercase($x)}`, `${darken($c, 10)}` |
492| `$x\|uppercase`, `$c\|darken:10` (filters) | `${uppercase($x)}`, `${darken($c, 10)}` |
crates/htmlang-core/src/parser.rs+16−51
@@ -715,25 +715,24 @@ impl Evaluator {
715
715
716 // --- @data (load JSON file into variables) ---
716 // --- @data (load JSON file into variables) ---
717
717
718− // @data file.json top-level keys become variables
719 // @data $name file.json values as $name.key
718 // @data $name file.json values as $name.key
720 // @data $name dir/*.json each file as $name.STEM.key; $name
719 // @data $name dir/*.json each file as $name.STEM.key; $name
721 // lists the stems, $name._count counts them
720 // lists the stems, $name._count counts them
722 // @data $name env:NAME [DEFAULT] an environment variable
721 // @data $name env:NAME [DEFAULT] an environment variable
723 if let Some(rest) = content.strip_prefix("@data ") {
722 if let Some(rest) = content.strip_prefix("@data ") {
724 let rest = rest.trim();
723 let rest = rest.trim();
725− let (prefix, filename) = match rest.strip_prefix('$') {
724+ let Some((prefix, filename)) = rest
725+ .strip_prefix('$')
726− Some(named) => match named.split_once(char::is_whitespace) {
726+ .and_then(|named| named.split_once(char::is_whitespace))
727− Some((name, source)) => (name.to_string(), source.trim().to_string()),
727+ .map(|(name, source)| (name.to_string(), source.trim().to_string()))
728− None => {
728+ else {
729− return Err(ParseError {
729+ return Err(ParseError {
730− line: line_num,
730+ line: line_num,
731− message: "@data requires: @data $name SOURCE or @data file.json"
731+ message: format!(
732− .to_string(),
732+ "@data needs a name: write `@data $name {}` and use `$name.key`",
733+ rest.trim_start_matches('$')
734+ ),
733− });
735+ });
734− }
735− },
736− None => (String::new(), rest.to_string()),
737 };
736 };
738
737
739 if let Some(env) = filename.strip_prefix("env:") {
738 if let Some(env) = filename.strip_prefix("env:") {
@@ -741,12 +740,6 @@ impl Evaluator {
741 Some((var, default)) => (var, Some(default.trim())),
740 Some((var, default)) => (var, Some(default.trim())),
742 None => (env, None),
741 None => (env, None),
743 };
742 };
744− if prefix.is_empty() {
745− return Err(ParseError {
746− line: line_num,
747− message: format!("@data env:{} needs a name: @data $name env:{}", var, var),
748− });
749− }
750 let value = std::env::var(var)
743 let value = std::env::var(var)
751 .ok()
744 .ok()
752 .or_else(|| default.map(|d| substitute_vars(d, &ctx.variables)));
745 .or_else(|| default.map(|d| substitute_vars(d, &ctx.variables)));
@@ -768,12 +761,6 @@ impl Evaluator {
768
761
769 let filename = substitute_vars(&filename, &ctx.variables);
762 let filename = substitute_vars(&filename, &ctx.variables);
770 if filename.contains('*') {
763 if filename.contains('*') {
771− if prefix.is_empty() {
772− return Err(ParseError {
773− line: line_num,
774− message: format!("@data {} needs a name: @data $name {}", filename, filename),
775− });
776− }
777 self.load_data_glob(&prefix, &filename, line_num, &content, ctx);
764 self.load_data_glob(&prefix, &filename, line_num, &content, ctx);
778 return Ok(None);
765 return Ok(None);
779 }
766 }
@@ -800,32 +787,10 @@ impl Evaluator {
800
787
801 match parse_json_with_error(&json_text) {
788 match parse_json_with_error(&json_text) {
802 Ok(json) => {
789 Ok(json) => {
803− if prefix.is_empty() {
804− // No prefix: top-level object keys become variables directly
805− if let JsonValue::Object(pairs) = &json {
806− for (key, val) in pairs {
807− let mut sub = HashMap::new();
790+ let mut sub = HashMap::new();
808− flatten_json(key, val, &mut sub);
809− for (k, v) in sub {
810− ctx.variables.insert(k, v);
811− }
812− }
813− } else {
814− ctx.diagnostics.push(Diagnostic {
815− line: line_num,
816− column: None,
817− message: "@data without prefix requires a JSON object at top level"
818− .to_string(),
819− severity: Severity::Error,
820− source_line: Some(content.clone()),
821− });
822− }
823− } else {
824− let mut sub = HashMap::new();
825− flatten_json(&prefix, &json, &mut sub);
791+ flatten_json(&prefix, &json, &mut sub);
826− for (k, v) in sub {
792+ for (k, v) in sub {
827− ctx.variables.insert(k, v);
793+ ctx.variables.insert(k, v);
828− }
829 }
794 }
830 }
795 }
831 Err(detail) => {
796 Err(detail) => {
src/bin/htmlang_lsp/docs.rs+1−1
@@ -108,7 +108,7 @@ pub(crate) const DIRECTIVES: &[Doc] = &[
108 "Repeats its body for each item of a list or range; `$_index` counts from 0.",
108 "Repeats its body for each item of a list or range; `$_index` counts from 0.",
109 "@each $item in apple, banana\n @text $item\n@each $i in 1..10 step 2\n @text $i",
109 "@each $item in apple, banana\n @text $item\n@each $i in 1..10 step 2\n @text $i",
110 ),
110 ),
111− doc("data", "Loads a JSON file as variables.", "@data site.json"),
111+ doc("data", "Loads data as variables: a JSON file, a glob of them, or `env:NAME`.", "@data $site site.json"),
112];
112];
113
113
114/// htmlang's own attributes, plus CSS properties htmlang treats specially.
114/// htmlang's own attributes, plus CSS properties htmlang treats specially.
src/upgrade.rs+17
@@ -785,6 +785,16 @@ fn rewrite_directive_line(
785 ));
785 ));
786 return line.to_string();
786 return line.to_string();
787 }
787 }
788+ if let Some(rest) = trimmed.strip_prefix("@data ")
789+ && !rest.trim_start().starts_with('$')
790+ {
791+ manual.push((
792+ idx + 1,
793+ "@data needs a name now: write `@data $name FILE` and use `$name.key` for `$key`"
794+ .to_string(),
795+ ));
796+ return line.to_string();
797+ }
788 if trimmed.starts_with("@fetch ") {
798 if trimmed.starts_with("@fetch ") {
789 manual.push((
799 manual.push((
790 idx + 1,
800 idx + 1,
@@ -1579,6 +1589,13 @@ mod tests {
1579 );
1589 );
1580 }
1590 }
1581
1591
1592+ #[test]
1593+ fn unnamed_data_needs_a_manual_name() {
1594+ let result = super::upgrade("@data site.json\n@data $ok ok.json\n");
1595+ assert_eq!(result.manual.len(), 1);
1596+ assert_eq!(result.manual[0].0, 1);
1597+ }
1598+
1582 #[test]
1599 #[test]
1583 fn bare_attribute_lines_become_el() {
1600 fn bare_attribute_lines_become_el() {
1584 assert_eq!(up("[padding 4]\n x\n"), "@el [padding 4]\n x\n");
1601 assert_eq!(up("[padding 4]\n x\n"), "@el [padding 4]\n x\n");
tests/regressions.rs+3−3
@@ -52,9 +52,9 @@ fn non_ascii_hex_color_does_not_panic() {
52fn truncated_json_data_does_not_panic() {
52fn truncated_json_data_does_not_panic() {
53 let dir = scratch_dir("json_trunc");
53 let dir = scratch_dir("json_trunc");
54 std::fs::write(dir.join("d.json"), "{\"a\": 1,").unwrap();
54 std::fs::write(dir.join("d.json"), "{\"a\": 1,").unwrap();
55− compile_in(&dir, "@data d.json\n@text ok");
55+ compile_in(&dir, "@data $d d.json\n@text ok");
56 std::fs::write(dir.join("d.json"), "{").unwrap();
56 std::fs::write(dir.join("d.json"), "{").unwrap();
57− compile_in(&dir, "@data d.json\n@text ok");
57+ compile_in(&dir, "@data $d d.json\n@text ok");
58}
58}
59
59
60#[test]
60#[test]
@@ -65,7 +65,7 @@ fn json_unicode_escapes_are_decoded() {
65 r#"{"name": "Caf\u00e9", "emoji": "\ud83d\ude00"}"#,
65 r#"{"name": "Caf\u00e9", "emoji": "\ud83d\ude00"}"#,
66 )
66 )
67 .unwrap();
67 .unwrap();
68− let out = compile_in(&dir, "@data d.json\n@text $name $emoji");
68+ let out = compile_in(&dir, "@data $d d.json\n@text $d.name $d.emoji");
69 assert!(out.contains("Café 😀"), "{}", out);
69 assert!(out.contains("Café 😀"), "{}", out);
70}
70}
71
71
tests/snapshots.rs+1−1
@@ -4886,7 +4886,7 @@ fn error_invalid_json_in_data_directive() {
4886 let data_path = dir.join("bad.json");
4886 let data_path = dir.join("bad.json");
4887 std::fs::write(&data_path, "{ not: valid json }").unwrap();
4887 std::fs::write(&data_path, "{ not: valid json }").unwrap();
4888
4888
4889− let input = "@data bad.json\n";
4889+ let input = "@data $bad bad.json\n";
4890 let result = htmlang::parser::parse_with_base(input, Some(&dir));
4890 let result = htmlang::parser::parse_with_base(input, Some(&dir));
4891 let has_err = result.diagnostics.iter().any(|d| {
4891 let has_err = result.diagnostics.iter().any(|d| {
4892 d.severity == htmlang::parser::Severity::Error && d.message.contains("invalid JSON")
4892 d.severity == htmlang::parser::Severity::Error && d.message.contains("invalid JSON")
tests/snapshots/data_directive.hl+5−5
@@ -1,11 +1,11 @@
1@page Data Test
1@page Data Test
2−@data test_data.json
2+@data $site test_data.json
3
3
4−@text [bold, size 24] $title
4+@text [bold, size 24] $site.title
5−@text Version: $version
5+@text Version: $site.version
6
6
7−@each $tag in $tags
7+@each $tag in $site.tags
8 @text [color blue] $tag
8 @text [color blue] $tag
9
9
10−@each $label, $url in $links
10+@each $label, $url in $site.links
11 @link $url $label
11 @link $url $label