@devaloop/devalang 0.0.1-alpha.10 → 0.0.1-alpha.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/.devalang +9 -4
  2. package/Cargo.toml +54 -49
  3. package/README.md +59 -142
  4. package/docs/CHANGELOG.md +42 -1
  5. package/docs/ROADMAP.md +1 -1
  6. package/docs/TODO.md +1 -1
  7. package/examples/bank.deva +9 -0
  8. package/examples/duration.deva +9 -0
  9. package/examples/index.deva +0 -2
  10. package/out-tsc/bin/devalang.exe +0 -0
  11. package/package.json +1 -1
  12. package/project-version.json +3 -3
  13. package/rust/cli/bank.rs +455 -0
  14. package/rust/cli/build.rs +1 -1
  15. package/rust/cli/check.rs +1 -1
  16. package/rust/cli/driver.rs +280 -0
  17. package/rust/cli/install.rs +17 -0
  18. package/rust/cli/mod.rs +5 -200
  19. package/rust/cli/play.rs +1 -1
  20. package/rust/cli/update.rs +4 -0
  21. package/rust/common/cdn.rs +11 -0
  22. package/rust/common/mod.rs +1 -0
  23. package/rust/config/driver.rs +76 -0
  24. package/rust/config/loader.rs +98 -1
  25. package/rust/config/mod.rs +1 -15
  26. package/rust/core/audio/engine.rs +31 -3
  27. package/rust/core/audio/interpreter/arrow_call.rs +17 -4
  28. package/rust/core/audio/interpreter/trigger.rs +34 -1
  29. package/rust/core/audio/loader/trigger.rs +12 -0
  30. package/rust/core/lexer/handler/driver.rs +12 -1
  31. package/rust/core/lexer/handler/mod.rs +1 -0
  32. package/rust/core/lexer/handler/slash.rs +21 -0
  33. package/rust/core/lexer/token.rs +1 -0
  34. package/rust/core/parser/driver.rs +8 -0
  35. package/rust/core/parser/handler/arrow_call.rs +29 -4
  36. package/rust/core/parser/handler/dot.rs +103 -37
  37. package/rust/core/preprocessor/loader.rs +93 -14
  38. package/rust/core/preprocessor/resolver/driver.rs +5 -0
  39. package/rust/core/shared/bank.rs +21 -0
  40. package/rust/core/shared/duration.rs +1 -0
  41. package/rust/core/shared/mod.rs +2 -1
  42. package/rust/core/shared/value.rs +1 -0
  43. package/rust/installer/bank.rs +55 -0
  44. package/rust/installer/mod.rs +1 -0
  45. package/rust/lib.rs +1 -0
  46. package/rust/main.rs +62 -5
  47. package/rust/utils/installer.rs +56 -0
  48. package/rust/utils/mod.rs +2 -1
  49. package/docs/COMMANDS.md +0 -85
  50. package/docs/CONFIG.md +0 -30
  51. package/docs/SYNTAX.md +0 -230
@@ -1,5 +1,6 @@
1
1
  use std::{ fs, path::Path };
2
- use crate::config::Config;
2
+ use std::collections::HashMap;
3
+ use crate::config::driver::{ BankEntry, Config };
3
4
 
4
5
  pub fn load_config(path: Option<&Path>) -> Option<Config> {
5
6
  let config_path = path.unwrap_or_else(|| Path::new(".devalang"));
@@ -11,3 +12,99 @@ pub fn load_config(path: Option<&Path>) -> Option<Config> {
11
12
  None
12
13
  }
13
14
  }
15
+
16
+ pub fn update_bank_version_in_config(config: &mut Config, dependency: &str, new_version: &str) {
17
+ // Si le vecteur banks n'existe pas, on ne fait rien
18
+ if config.banks.is_none() {
19
+ println!("No banks configured.");
20
+ return;
21
+ }
22
+
23
+ let banks = config.banks.as_mut().unwrap();
24
+
25
+ if let Some(bank) = banks.iter_mut().find(|b| b.path.contains(dependency)) {
26
+ bank.version = Some(new_version.to_string());
27
+
28
+ if let Err(e) = config.write(config) {
29
+ eprintln!("❌ Failed to write config: {}", e);
30
+ } else {
31
+ println!("✅ Bank '{}' updated to version '{}'", dependency, new_version);
32
+ }
33
+ } else {
34
+ println!("Bank '{}' not found in config", dependency);
35
+ }
36
+ }
37
+
38
+ pub fn remove_bank_from_config(config: &mut Config, dependency: &str) {
39
+ if config.banks.is_none() {
40
+ println!("No banks configured.");
41
+ return;
42
+ }
43
+
44
+ let banks = config.banks.as_mut().unwrap();
45
+
46
+ if let Some(index) = banks.iter().position(|b| b.path.contains(dependency)) {
47
+ banks.remove(index);
48
+
49
+ if let Err(e) = config.write(config) {
50
+ eprintln!("❌ Failed to write config: {}", e);
51
+ } else {
52
+ println!("✅ Bank '{}' removed from config", dependency);
53
+ }
54
+ } else {
55
+ println!("Bank '{}' not found in config", dependency);
56
+ }
57
+ }
58
+
59
+ pub fn add_bank_to_config(config: &mut Config, real_path: &Path, dependency: &str) {
60
+ if config.banks.is_none() {
61
+ config.banks = Some(Vec::new());
62
+ }
63
+
64
+ let banks = config.banks.as_mut().unwrap();
65
+
66
+ let exists = banks.iter().any(|b| b.path == dependency);
67
+ if exists {
68
+ println!("Bank '{}' already in config", dependency);
69
+ return;
70
+ }
71
+
72
+ let metadata_path = Path::new(real_path).join("bank.toml");
73
+
74
+ if !metadata_path.exists() {
75
+ eprintln!("❌ Bank metadata file '{}' does not exist", metadata_path.display());
76
+ return;
77
+ }
78
+
79
+ let metadata_content = fs
80
+ ::read_to_string(&metadata_path)
81
+ .expect("Failed to read bank metadata file");
82
+
83
+ let metadata: HashMap<String, String> = toml
84
+ ::from_str(&metadata_content)
85
+ .expect("Failed to parse bank metadata file");
86
+
87
+ let bank_to_insert = BankEntry {
88
+ path: dependency.to_string(),
89
+ version: Some(
90
+ metadata
91
+ .get("version")
92
+ .cloned()
93
+ .unwrap_or_else(|| "0.0.1".to_string())
94
+ ),
95
+ author: Some(
96
+ metadata
97
+ .get("author")
98
+ .cloned()
99
+ .unwrap_or_else(|| "unknown".to_string())
100
+ ),
101
+ };
102
+
103
+ banks.push(bank_to_insert);
104
+
105
+ if let Err(e) = config.write(config) {
106
+ eprintln!("❌ Failed to write config: {}", e);
107
+ } else {
108
+ println!("✅ Bank '{}' added to config", dependency);
109
+ }
110
+ }
@@ -1,16 +1,2 @@
1
1
  pub mod loader;
2
-
3
- use serde::Deserialize;
4
-
5
- #[derive(Debug, Deserialize, Clone)]
6
- pub struct Config {
7
- pub defaults: ConfigDefaults,
8
- }
9
-
10
- #[derive(Debug, Deserialize, Clone)]
11
- pub struct ConfigDefaults {
12
- pub entry: Option<String>,
13
- pub output: Option<String>,
14
- pub watch: Option<bool>,
15
- pub repeat: Option<bool>,
16
- }
2
+ pub mod driver;
@@ -1,4 +1,4 @@
1
- use std::{ collections::HashMap, fs::File, io::BufReader };
1
+ use std::{ collections::HashMap, fs::File, io::BufReader, path::Path };
2
2
  use hound::{ SampleFormat, WavSpec, WavWriter };
3
3
  use rodio::{ Decoder, Source };
4
4
 
@@ -169,9 +169,37 @@ impl AudioEngine {
169
169
  dur_sec: f32,
170
170
  effects: Option<HashMap<String, f32>>
171
171
  ) {
172
- let resolved = resolve_relative_path(&self.module_name.clone(), filepath);
172
+ if filepath.is_empty() {
173
+ eprintln!("❌ Empty file path provided for audio sample.");
174
+ return;
175
+ }
176
+
177
+ let mut resolved_path = String::new();
178
+
179
+ if filepath.starts_with("devalang://") {
180
+ let root = Path::new(env!("CARGO_MANIFEST_DIR"));
181
+ let parts = filepath.split("devalang://").collect::<Vec<&str>>();
182
+ let object_parts = parts.get(1).unwrap_or(&"").split("/").collect::<Vec<&str>>();
183
+ let object_type = object_parts.get(0).unwrap_or(&"").to_lowercase();
184
+ let object_dir = object_parts.get(1).unwrap_or(&"").to_string();
185
+ let object_name = object_parts.get(2).unwrap_or(&"").to_string();
186
+
187
+ if object_type.contains("bank") {
188
+ resolved_path = root
189
+ .join(".deva")
190
+ .join("bank")
191
+ .join(object_dir)
192
+ .join(format!("{}.wav", object_name))
193
+ .to_str()
194
+ .unwrap_or("")
195
+ .to_string();
196
+ } else {
197
+ eprintln!("❌ Unsupported devalang:// object type: {}", object_type);
198
+ return;
199
+ }
200
+ }
173
201
 
174
- let file = BufReader::new(File::open(resolved).expect("Failed to open audio file"));
202
+ let file = BufReader::new(File::open(resolved_path).expect("Failed to open audio file"));
175
203
  let decoder = Decoder::new(file).expect("Failed to decode audio file");
176
204
 
177
205
  // Mono or stereo reading possible here, we will duplicate in L/R
@@ -53,8 +53,8 @@ pub fn interprete_call_arrow_statement(
53
53
  return (*max_end_time, cursor_copy);
54
54
  };
55
55
 
56
- let freq = extract_f32(params, "freq").unwrap_or(440.0);
57
- let amp = extract_f32(params, "amp").unwrap_or(1.0);
56
+ let freq = extract_f32(params, "freq", base_bpm).unwrap_or(440.0);
57
+ let amp = extract_f32(params, "amp", base_bpm).unwrap_or(1.0);
58
58
 
59
59
  if method == "note" {
60
60
  let Some(Value::Identifier(note_name)) = args.get(0) else {
@@ -69,7 +69,9 @@ pub fn interprete_call_arrow_statement(
69
69
  }
70
70
  }
71
71
 
72
- let duration_ms = extract_f32(&final_note_params, "duration").unwrap_or(base_duration);
72
+ let duration_ms = extract_f32(&final_note_params, "duration", base_bpm).unwrap_or(
73
+ base_duration
74
+ );
73
75
  let duration_secs = duration_ms / 1000.0;
74
76
 
75
77
  let final_freq = note_to_freq(note_name);
@@ -101,10 +103,21 @@ pub fn interprete_call_arrow_statement(
101
103
  (*max_end_time, cursor_copy)
102
104
  }
103
105
 
104
- fn extract_f32(map: &HashMap<String, Value>, key: &str) -> Option<f32> {
106
+ fn extract_f32(map: &HashMap<String, Value>, key: &str, base_bpm: f32) -> Option<f32> {
105
107
  map.get(key).and_then(|v| {
106
108
  match v {
107
109
  Value::Number(n) => Some(*n),
110
+ Value::Beat(beat_str) => {
111
+ let parts: Vec<&str> = beat_str.split('/').collect();
112
+ if parts.len() == 2 {
113
+ let numerator = parts[0].parse::<f32>().ok()?;
114
+ let denominator = parts[1].parse::<f32>().ok()?;
115
+
116
+ Some((numerator / denominator) * ((60.0 / base_bpm) * 1000.0))
117
+ } else {
118
+ None
119
+ }
120
+ }
108
121
  _ => None,
109
122
  }
110
123
  })
@@ -14,7 +14,7 @@ pub fn interprete_trigger_statement(
14
14
  max_end_time: f32
15
15
  ) -> Option<(f32, f32, AudioEngine)> {
16
16
  if let StatementKind::Trigger { entity, duration } = &stmt.kind {
17
- if let Some(trigger_val) = variable_table.get(entity) {
17
+ if let Some(trigger_val) = resolve_namespaced_variable(entity, variable_table) {
18
18
  let duration_secs = match duration {
19
19
  Duration::Number(n) => *n,
20
20
 
@@ -41,6 +41,20 @@ pub fn interprete_trigger_statement(
41
41
  }
42
42
  }
43
43
 
44
+ Duration::Beat(beat_str) => {
45
+ // Assuming beat_str is in the format "numerator/denominator"
46
+ let parts: Vec<&str> = beat_str.split('/').collect();
47
+
48
+ if parts.len() != 2 {
49
+ eprintln!("❌ Invalid beat duration format: {}", beat_str);
50
+ return None;
51
+ }
52
+
53
+ let numerator: f32 = parts[0].parse().unwrap_or(1.0);
54
+ let denominator: f32 = parts[1].parse().unwrap_or(1.0);
55
+ numerator / denominator
56
+ }
57
+
44
58
  Duration::Auto => 1.0,
45
59
  };
46
60
 
@@ -67,3 +81,22 @@ pub fn interprete_trigger_statement(
67
81
 
68
82
  None
69
83
  }
84
+
85
+ fn resolve_namespaced_variable<'a>(path: &str, variables: &'a VariableTable) -> Option<&'a Value> {
86
+ let mut current: Option<&Value> = None;
87
+
88
+ for (i, part) in path.split('.').enumerate() {
89
+ if i == 0 {
90
+ current = variables.get(part);
91
+ } else {
92
+ current = match current {
93
+ Some(Value::Map(map)) => map.get(part),
94
+ _ => {
95
+ return None;
96
+ }
97
+ };
98
+ }
99
+ }
100
+
101
+ current
102
+ }
@@ -43,6 +43,18 @@ pub fn load_trigger(
43
43
  duration_as_secs = base_duration;
44
44
  }
45
45
 
46
+ Duration::Beat(beat_str) => {
47
+ let parts: Vec<&str> = beat_str.split('/').collect();
48
+
49
+ if parts.len() == 2 {
50
+ let numerator: f32 = parts[0].parse().unwrap_or(1.0);
51
+ let denominator: f32 = parts[1].parse().unwrap_or(1.0);
52
+ duration_as_secs = numerator / denominator * base_duration;
53
+ } else {
54
+ eprintln!("❌ Invalid beat duration format: {}", beat_str);
55
+ }
56
+ }
57
+
46
58
  _ => {
47
59
  eprintln!("❌ Invalid duration type. Expected an identifier.");
48
60
  }
@@ -1,6 +1,6 @@
1
1
  use crate::core::lexer::{
2
2
  handler::{
3
- arrow::handle_arrow_lexer, at::handle_at_lexer, brace::{ handle_lbrace_lexer, handle_rbrace_lexer }, colon::handle_colon_lexer, comment::handle_comment_lexer, dot::handle_dot_lexer, identifier::handle_identifier_lexer, indent::handle_indent_lexer, newline::handle_newline_lexer, number::handle_number_lexer, operator::handle_operator_lexer, string::handle_string_lexer
3
+ arrow::handle_arrow_lexer, at::handle_at_lexer, brace::{ handle_lbrace_lexer, handle_rbrace_lexer }, colon::handle_colon_lexer, comment::handle_comment_lexer, dot::handle_dot_lexer, identifier::handle_identifier_lexer, indent::handle_indent_lexer, newline::handle_newline_lexer, number::handle_number_lexer, operator::handle_operator_lexer, slash::handle_slash_lexer, string::handle_string_lexer
4
4
  },
5
5
  token::{ Token, TokenKind },
6
6
  };
@@ -101,6 +101,17 @@ pub fn handle_content_lexing(content: String) -> Result<Vec<Token>, String> {
101
101
  &mut column
102
102
  );
103
103
  }
104
+ '/' => {
105
+ handle_slash_lexer(
106
+ ch,
107
+ &mut chars,
108
+ &mut current_indent,
109
+ &mut indent_stack,
110
+ &mut tokens,
111
+ &mut line,
112
+ &mut column
113
+ );
114
+ }
104
115
  '-' => {
105
116
  handle_arrow_lexer(
106
117
  ch,
@@ -12,3 +12,4 @@ pub mod number;
12
12
  pub mod indent;
13
13
  pub mod string;
14
14
  pub mod arrow;
15
+ pub mod slash;
@@ -0,0 +1,21 @@
1
+ use crate::core::lexer::token::{ Token, TokenKind };
2
+
3
+ pub fn handle_slash_lexer(
4
+ char: char,
5
+ chars: &mut std::iter::Peekable<std::str::Chars>,
6
+ current_indent: &mut usize,
7
+ indent_stack: &mut Vec<usize>,
8
+ tokens: &mut Vec<Token>,
9
+ line: &mut usize,
10
+ column: &mut usize
11
+ ) {
12
+ let mut slash = char.to_string();
13
+
14
+ tokens.push(Token {
15
+ kind: TokenKind::Slash,
16
+ lexeme: slash,
17
+ line: *line,
18
+ column: *column,
19
+ indent: *current_indent,
20
+ });
21
+ }
@@ -49,6 +49,7 @@ pub enum TokenKind {
49
49
  Comma,
50
50
  Equals,
51
51
  Dot,
52
+ Slash,
52
53
 
53
54
  // ───── Operators ─────
54
55
  DoubleEquals,
@@ -64,6 +64,10 @@ impl Parser {
64
64
  }
65
65
  }
66
66
 
67
+ pub fn peek_nth_kind(&self, n: usize) -> Option<TokenKind> {
68
+ self.peek_nth(n).map(|t| t.kind.clone())
69
+ }
70
+
67
71
  pub fn advance_if(&mut self, kind: TokenKind) -> bool {
68
72
  if self.match_token(kind) { true } else { false }
69
73
  }
@@ -172,6 +176,10 @@ impl Parser {
172
176
  self.peek().map_or(false, |t| t.kind == kind)
173
177
  }
174
178
 
179
+ pub fn peek_kind(&self) -> Option<TokenKind> {
180
+ self.peek().map(|t| t.kind.clone())
181
+ }
182
+
175
183
  pub fn parse_map_value(&mut self) -> Option<Value> {
176
184
  if !self.match_token(TokenKind::LBrace) {
177
185
  return None;
@@ -89,14 +89,39 @@ pub fn parse_arrow_call(parser: &mut Parser, global_store: &mut GlobalStore) ->
89
89
  TokenKind::Identifier =>
90
90
  Value::Identifier(value_token.lexeme.clone()),
91
91
  TokenKind::String => Value::String(value_token.lexeme.clone()),
92
- TokenKind::Number =>
93
- Value::Number(
94
- value_token.lexeme.parse::<f32>().unwrap_or(0.0)
95
- ),
92
+ TokenKind::Number => {
93
+ if let Some(TokenKind::Slash) = parser.peek_kind() {
94
+ parser.advance(); // consume slash
95
+ if let Some(denominator_token) = parser.peek_clone() {
96
+ if denominator_token.kind == TokenKind::Number {
97
+ parser.advance(); // consume denominator
98
+ let denominator =
99
+ denominator_token.lexeme.clone();
100
+ Value::Beat(
101
+ format!(
102
+ "{}/{}",
103
+ value_token.lexeme,
104
+ denominator
105
+ )
106
+ )
107
+ } else {
108
+ Value::Unknown
109
+ }
110
+ } else {
111
+ Value::Unknown
112
+ }
113
+ } else {
114
+ // Regular number without slash
115
+ Value::Number(
116
+ value_token.lexeme.parse::<f32>().unwrap_or(0.0)
117
+ )
118
+ }
119
+ }
96
120
  TokenKind::Boolean =>
97
121
  Value::Boolean(
98
122
  value_token.lexeme.parse::<bool>().unwrap_or(false)
99
123
  ),
124
+
100
125
  _ => Value::Unknown,
101
126
  };
102
127
  map.insert(key, value);
@@ -6,14 +6,55 @@ use crate::core::{
6
6
  };
7
7
 
8
8
  pub fn parse_dot_token(parser: &mut Parser, _global_store: &mut GlobalStore) -> Statement {
9
- parser.advance(); // consume the dot token
9
+ parser.advance(); // consume the first dot
10
10
 
11
11
  let Some(dot_token) = parser.previous_clone() else {
12
12
  return Statement::unknown();
13
13
  };
14
14
 
15
- // .kick
16
- let Some(entity_token) = parser.peek_clone() else {
15
+ // Parse namespaced identifier: .808.kick.snare
16
+ let mut parts = Vec::new();
17
+
18
+ loop {
19
+ let Some(token) = parser.peek_clone() else {
20
+ break;
21
+ };
22
+
23
+ match token.kind {
24
+ // Stop if we encounter a likely duration keyword
25
+ TokenKind::Number => {
26
+ // If there's a slash after the number, it's probably a fraction (1/4)
27
+ if let Some(TokenKind::Slash) = parser.peek_nth_kind(1) {
28
+ break;
29
+ }
30
+
31
+ parts.push(token.lexeme.clone());
32
+ parser.advance();
33
+ }
34
+
35
+ TokenKind::Identifier => {
36
+ // Stop if it's the duration keyword "auto"
37
+ if token.lexeme == "auto" {
38
+ break;
39
+ }
40
+
41
+ parts.push(token.lexeme.clone());
42
+ parser.advance();
43
+ }
44
+
45
+ TokenKind::Dot => {
46
+ parser.advance(); // consume dot
47
+ }
48
+
49
+ _ => {
50
+ break;
51
+ }
52
+ }
53
+ }
54
+
55
+ let entity = parts.join(".");
56
+
57
+ if entity.is_empty() {
17
58
  return Statement {
18
59
  kind: StatementKind::Trigger {
19
60
  entity: String::new(),
@@ -24,68 +65,93 @@ pub fn parse_dot_token(parser: &mut Parser, _global_store: &mut GlobalStore) ->
24
65
  line: dot_token.line,
25
66
  column: dot_token.column,
26
67
  };
27
- };
28
-
29
- parser.advance(); // consume entity
30
- let entity = entity_token.lexeme.clone();
68
+ }
31
69
 
32
70
  // Check if there's a duration
33
71
  let next = parser.peek_clone();
34
72
 
35
73
  let (duration, value) = match next {
36
- // If no more tokens, it's just `.kick`
37
74
  None => (Duration::Auto, Value::Null),
38
75
 
39
76
  Some(token) =>
40
77
  match token.kind {
41
- TokenKind::Newline | TokenKind::EOF => { (Duration::Auto, Value::Null) }
78
+ TokenKind::Newline | TokenKind::EOF => (Duration::Auto, Value::Null),
42
79
 
43
80
  TokenKind::Number => {
44
- let duration_lexeme = token.lexeme.clone();
45
- parser.advance(); // consume duration
81
+ let numerator = token.lexeme.clone();
82
+ parser.advance(); // consume numerator
83
+
84
+ if let Some(TokenKind::Slash) = parser.peek_kind() {
85
+ parser.advance(); // consume slash
86
+
87
+ if let Some(denominator_token) = parser.peek_clone() {
88
+ if denominator_token.kind == TokenKind::Number {
89
+ let denominator = denominator_token.lexeme.clone();
90
+ parser.advance(); // consume denominator
91
+
92
+ let beat_str = format!("{}/{}", numerator, denominator);
93
+ let beat_duration = Duration::Beat(beat_str);
94
+
95
+ let val = match parser.peek_clone() {
96
+ Some(param_token) if
97
+ param_token.kind == TokenKind::Identifier
98
+ => {
99
+ parser.advance();
100
+ Value::Identifier(param_token.lexeme.clone())
101
+ }
102
+ Some(param_token) if param_token.kind == TokenKind::LBrace => {
103
+ parser.parse_map_value().unwrap_or(Value::Null)
104
+ }
105
+ _ => Value::Null,
106
+ };
107
+
108
+ return Statement {
109
+ kind: StatementKind::Trigger {
110
+ entity,
111
+ duration: beat_duration,
112
+ },
113
+ value: val,
114
+ indent: dot_token.indent,
115
+ line: dot_token.line,
116
+ column: dot_token.column,
117
+ };
118
+ }
119
+ }
120
+ }
46
121
 
47
- // Try to parse optional value (ex: .kick 250 params)
48
- match parser.peek_clone() {
122
+ // fallback: simple numeric duration
123
+ let duration = parse_duration(numerator);
124
+
125
+ let val = match parser.peek_clone() {
49
126
  Some(param_token) if param_token.kind == TokenKind::Identifier => {
50
127
  parser.advance();
51
- (
52
- parse_duration(duration_lexeme),
53
- Value::Identifier(param_token.lexeme.clone()),
54
- )
128
+ Value::Identifier(param_token.lexeme.clone())
55
129
  }
56
-
57
130
  Some(param_token) if param_token.kind == TokenKind::LBrace => {
58
- // Handle value as Map
59
- let map = parser.parse_map_value(); // Assumes you have a helper for map
60
- (parse_duration(duration_lexeme), map.unwrap_or(Value::Null))
131
+ parser.parse_map_value().unwrap_or(Value::Null)
61
132
  }
133
+ _ => Value::Null,
134
+ };
62
135
 
63
- _ => (parse_duration(duration_lexeme), Value::Null),
64
- }
136
+ (duration, val)
65
137
  }
66
138
 
67
139
  TokenKind::Identifier => {
68
140
  let duration_lexeme = token.lexeme.clone();
69
141
  parser.advance(); // consume duration
70
142
 
71
- // Try to parse optional value (ex: .kick auto params)
72
- match parser.peek_clone() {
143
+ let val = match parser.peek_clone() {
73
144
  Some(param_token) if param_token.kind == TokenKind::Identifier => {
74
145
  parser.advance();
75
- (
76
- parse_duration(duration_lexeme),
77
- Value::Identifier(param_token.lexeme.clone()),
78
- )
146
+ Value::Identifier(param_token.lexeme.clone())
79
147
  }
80
-
81
148
  Some(param_token) if param_token.kind == TokenKind::LBrace => {
82
- // Handle value as Map
83
- let map = parser.parse_map_value(); // Assumes you have a helper for map
84
- (parse_duration(duration_lexeme), map.unwrap_or(Value::Null))
149
+ parser.parse_map_value().unwrap_or(Value::Null)
85
150
  }
151
+ _ => Value::Null,
152
+ };
86
153
 
87
- _ => (parse_duration(duration_lexeme), Value::Null),
88
- }
154
+ (parse_duration(duration_lexeme), val)
89
155
  }
90
156
 
91
157
  _ => (Duration::Auto, Value::Null),
@@ -104,8 +170,8 @@ pub fn parse_dot_token(parser: &mut Parser, _global_store: &mut GlobalStore) ->
104
170
  fn parse_duration(s: String) -> Duration {
105
171
  if s == "auto" {
106
172
  Duration::Auto
107
- } else if s.parse::<f32>().is_ok() {
108
- Duration::Number(s.parse().unwrap())
173
+ } else if let Ok(num) = s.parse::<f32>() {
174
+ Duration::Number(num)
109
175
  } else {
110
176
  Duration::Identifier(s)
111
177
  }