@@ -803,39 +803,48 @@ def _parse_bbox_setting(self) -> None:
803803 if i > 0 :
804804 self .expect (TokenType .COMMA )
805805
806- if not self .match (TokenType .NUMBER ):
806+ if not self .match (TokenType .NUMBER , TokenType . TEMPLATE_PLACEHOLDER ):
807807 self .error (f"Expected coordinate { i + 1 } in bbox" )
808808 else :
809809 coord = self .advance ()
810- try :
811- coord_val = float (coord .value )
812- if i in [0 , 2 ]: # latitude values
813- if not - 90 <= coord_val <= 90 :
814- self .error (
815- f"Latitude must be between -90 and 90: " f"{ coord_val } "
816- )
817- else : # longitude values
818- if not - 180 <= coord_val <= 180 :
819- self .error (
820- f"Longitude must be between -180 and 180: "
821- f"{ coord_val } "
822- )
823- except ValueError :
824- self .error (f"Invalid coordinate: { coord .value } " )
810+ # Only validate if it's a number (skip template placeholders)
811+ if coord .type == TokenType .NUMBER :
812+ self ._validate_bbox_coordinate (coord , i )
813+
814+ def _validate_bbox_coordinate (self , coord : Token , index : int ) -> None :
815+ """Validate a single bbox coordinate."""
816+ try :
817+ coord_val = float (coord .value )
818+ if index in [0 , 2 ]: # latitude values
819+ if not - 90 <= coord_val <= 90 :
820+ self .error (f"Latitude must be between -90 and 90: { coord_val } " )
821+ else : # longitude values
822+ if not - 180 <= coord_val <= 180 :
823+ self .error (f"Longitude must be between -180 and 180: { coord_val } " )
824+ except ValueError :
825+ self .error (f"Invalid coordinate: { coord .value } " )
825826
826827 def _parse_date_setting (self , setting_token : Token ) -> None :
827828 """Parse date, diff, or adiff settings."""
828829 self .expect (TokenType .COLON )
829830
830- if not self .match (TokenType .STRING ):
831- self .error (f"Expected date string after { setting_token .value } :" )
831+ if not self .match (TokenType .STRING , TokenType .TEMPLATE_PLACEHOLDER ):
832+ error_msg = (
833+ f"Expected date string or template placeholder after "
834+ f"{ setting_token .value } :"
835+ )
836+ self .error (error_msg )
832837 else :
833838 date_str = self .advance ()
834- # Basic ISO 8601 date format validation - also accept template placeholders
835- # Accept both colons and hyphens in time part (common variation)
836- iso_pattern = r"^\d{4}-\d{2}-\d{2}T\d{2}[-:]\d{2}[-:]\d{2}Z$"
837- if not (re .match (iso_pattern , date_str .value ) or "{{" in date_str .value ):
838- self .error ("Invalid date format. Expected YYYY-MM-DDTHH:MM:SSZ" )
839+ # Only validate if it's a string (skip template placeholders)
840+ if date_str .type == TokenType .STRING :
841+ # Basic ISO 8601 date format validation
842+ # Accept both colons and hyphens in time part (common variation)
843+ iso_pattern = r"^\d{4}-\d{2}-\d{2}T\d{2}[-:]\d{2}[-:]\d{2}Z$"
844+ if not (
845+ re .match (iso_pattern , date_str .value ) or "{{" in date_str .value
846+ ):
847+ self .error ("Invalid date format. Expected YYYY-MM-DDTHH:MM:SSZ" )
839848
840849 def _parse_unknown_setting (self , setting_token : Token ) -> None :
841850 """Parse unknown settings with warning."""
@@ -874,22 +883,57 @@ def _parse_csv_parameters(self) -> None:
874883 self .advance () # Skip (
875884
876885 # Parse CSV parameters - field lists, options, separators, etc.
877- paren_depth = 1
878- while paren_depth > 0 and not self .match (TokenType .EOF ):
879- token = self .current_token ()
880- if token .type == TokenType .LPAREN :
881- paren_depth += 1
882- elif token .type == TokenType .RPAREN :
883- paren_depth -= 1
884-
885- if paren_depth > 0 : # Don't advance past the final closing paren
886- self .advance ()
886+ # CSV format can be: csv(field1,field2,...; options; separator)
887+ self ._parse_csv_field_list ()
888+
889+ # Handle optional sections separated by semicolons
890+ while self .match (TokenType .SEMICOLON ):
891+ self .advance () # Skip ;
892+ # Parse options or separators
893+ while not self .match (TokenType .SEMICOLON , TokenType .RPAREN , TokenType .EOF ):
894+ if self .match (TokenType .STRING , TokenType .IDENTIFIER , TokenType .NUMBER ):
895+ self .advance ()
896+ else :
897+ break
887898
888899 if self .match (TokenType .RPAREN ):
889900 self .advance () # Skip final )
890901 else :
891902 self .error ("Expected ')' after CSV parameters" )
892903
904+ def _parse_csv_field_list (self ) -> None :
905+ """Parse CSV field list (first part of CSV parameters)."""
906+ # Handle first field
907+ if self .match (TokenType .STRING , TokenType .IDENTIFIER , TokenType .COLON ):
908+ self ._parse_csv_field ()
909+
910+ # Handle additional fields separated by commas
911+ while self .match (TokenType .COMMA ):
912+ self .advance () # Skip comma
913+ if not self .match (
914+ TokenType .SEMICOLON , TokenType .RPAREN
915+ ): # Not end of field list
916+ self ._parse_csv_field ()
917+
918+ def _parse_csv_field (self ) -> None :
919+ """Parse a single CSV field specification."""
920+ # Handle special field types like ::id, ::type, etc.
921+ if self .match (TokenType .COLON ):
922+ self .advance () # Skip first :
923+ if self .match (TokenType .COLON ):
924+ self .advance () # Skip second :
925+ # Parse field name after ::
926+ if self .match (TokenType .IDENTIFIER ):
927+ self .advance ()
928+ # Handle quoted field names or regular identifiers
929+ elif self .match (TokenType .STRING , TokenType .IDENTIFIER ):
930+ self .advance ()
931+ else :
932+ # Allow any tokens in CSV field specifications for now
933+ # since CSV field syntax can be complex
934+ if not self .match (TokenType .COMMA , TokenType .SEMICOLON , TokenType .RPAREN ):
935+ self .advance ()
936+
893937 def parse_settings (self ) -> bool :
894938 """Parse settings statement."""
895939 if not self .match (TokenType .LBRACKET ):
@@ -1373,16 +1417,18 @@ def _parse_id_list_filter(self) -> None:
13731417
13741418 def _parse_user_list_filter (self ) -> None :
13751419 """Parse user list filter."""
1376- if not self .match (TokenType .STRING ):
1377- self .error ("Expected username string after 'user:'" )
1420+ if not self .match (TokenType .STRING , TokenType . TEMPLATE_PLACEHOLDER ):
1421+ self .error ("Expected username string or template placeholder after 'user:'" )
13781422 else :
13791423 self .advance () # First username
13801424
13811425 # Parse additional usernames
13821426 while self .match (TokenType .COMMA ):
13831427 self .advance ()
1384- if not self .match (TokenType .STRING ):
1385- self .error ("Expected username string in user list" )
1428+ if not self .match (TokenType .STRING , TokenType .TEMPLATE_PLACEHOLDER ):
1429+ self .error (
1430+ "Expected username string or template placeholder in user list"
1431+ )
13861432 else :
13871433 self .advance ()
13881434
@@ -1900,40 +1946,65 @@ def _parse_block_parameters(self, block_type: Token) -> None:
19001946 self .expect (TokenType .RPAREN )
19011947
19021948 def _parse_for_evaluator (self ) -> None :
1903- """Parse FOR loop evaluator like t["key"] or user()."""
1949+ """Parse FOR loop evaluator like t["key"], user(), keys(), etc ."""
19041950 # Handle user() pattern
19051951 if (
19061952 self .match (TokenType .IDENTIFIER )
19071953 and self .current_token ().value .lower () == "user"
19081954 and self .peek_token ()
19091955 and self .peek_token ().type == TokenType .LPAREN
19101956 ):
1911-
19121957 self .advance () # Skip 'user'
19131958 self .advance () # Skip '('
19141959 self .expect (TokenType .RPAREN )
19151960 return
19161961
1917- # Handle t["key"] pattern
1918- if not self .match (TokenType .IDENTIFIER ):
1919- self .error ("Expected evaluator identifier in for loop" )
1962+ # Handle keys() pattern
1963+ if (
1964+ self .match (TokenType .IDENTIFIER )
1965+ and self .current_token ().value .lower () == "keys"
1966+ and self .peek_token ()
1967+ and self .peek_token ().type == TokenType .LPAREN
1968+ ):
1969+ self .advance () # Skip 'keys'
1970+ self .advance () # Skip '('
1971+ self .expect (TokenType .RPAREN )
19201972 return
1921- self .advance ()
19221973
1923- # Parse tag filter part like ["key"]
1924- if self .match (TokenType .LBRACKET ):
1974+ # Handle t ["key"] pattern and other complex expressions
1975+ if self .match (TokenType .IDENTIFIER ):
19251976 self .advance ()
1926- if self .match (TokenType .STRING , TokenType .IDENTIFIER ):
1977+
1978+ # Parse tag filter part like ["key"] or function calls
1979+ if self .match (TokenType .LBRACKET ):
19271980 self .advance ()
1928- else :
1929- self .error ("Expected key name in for evaluator" )
1930- self .expect (TokenType .RBRACKET )
1981+ if self .match (TokenType .STRING , TokenType .IDENTIFIER ):
1982+ self .advance ()
1983+ else :
1984+ self .error ("Expected key name in for evaluator" )
1985+ self .expect (TokenType .RBRACKET )
1986+ elif self .match (TokenType .LPAREN ):
1987+ # Handle function calls in for evaluator
1988+ self .advance () # Skip (
1989+ # Parse function arguments
1990+ while not self .match (TokenType .RPAREN , TokenType .EOF ):
1991+ if self .match (
1992+ TokenType .STRING , TokenType .IDENTIFIER , TokenType .NUMBER
1993+ ):
1994+ self .advance ()
1995+ elif self .match (TokenType .COMMA ):
1996+ self .advance ()
1997+ else :
1998+ break
1999+ self .expect (TokenType .RPAREN )
19312000 else :
1932- self .error ("Expected tag filter in for evaluator " )
2001+ self .error ("Expected evaluator identifier in for loop " )
19332002
19342003 def _parse_block_body (self ) -> bool :
19352004 """Parse block body and return whether braces were used."""
19362005 has_braces = False
2006+
2007+ # Handle both braces { } and parentheses ( ) for block bodies
19372008 if self .match (TokenType .LBRACE ):
19382009 has_braces = True
19392010 self .advance ()
@@ -1943,6 +2014,15 @@ def _parse_block_body(self) -> bool:
19432014 break
19442015
19452016 self .expect (TokenType .RBRACE )
2017+ elif self .match (TokenType .LPAREN ):
2018+ has_braces = True # Treat parentheses like braces for semicolon handling
2019+ self .advance ()
2020+
2021+ while not self .match (TokenType .RPAREN , TokenType .EOF ):
2022+ if not self .parse_statement ():
2023+ break
2024+
2025+ self .expect (TokenType .RPAREN )
19462026 return has_braces
19472027
19482028 def _parse_else_clause (self ) -> bool :
@@ -1986,8 +2066,13 @@ def parse_block_statement(self):
19862066 if block_type .type == TokenType .IF :
19872067 has_braces = self ._parse_else_clause () or has_braces
19882068
1989- # Only expect semicolon if we didn't use braces
1990- if not has_braces :
2069+ # Semicolon handling: optional for braced statements, required for non-braced
2070+ if has_braces :
2071+ # For braced statements, semicolon is completely optional
2072+ if self .match (TokenType .SEMICOLON ):
2073+ self .advance ()
2074+ else :
2075+ # For non-braced statements, semicolon is required
19912076 self .expect (TokenType .SEMICOLON )
19922077
19932078 return True
@@ -2263,8 +2348,37 @@ def _parse_make_statement(self) -> None:
22632348
22642349 def _parse_make_value_expression (self ) -> None :
22652350 """Parse value expression in make statement."""
2351+ self ._parse_make_ternary_expression ()
2352+
2353+ def _parse_make_ternary_expression (self ) -> None :
2354+ """Parse ternary expressions (condition ? true_value : false_value)."""
2355+ self ._parse_make_comparison_expression ()
2356+
2357+ # Handle ternary operator
2358+ if self .match (TokenType .QUESTION ):
2359+ self .advance () # Skip ?
2360+ self ._parse_make_comparison_expression () # true value
2361+ if self .match (TokenType .COLON ):
2362+ self .advance () # Skip :
2363+ self ._parse_make_comparison_expression () # false value
2364+ else :
2365+ self .error ("Expected ':' in ternary expression" )
2366+
2367+ def _parse_make_comparison_expression (self ) -> None :
2368+ """Parse comparison expressions (==, !=, <, >, <=, >=)."""
22662369 self ._parse_make_additive_expression ()
22672370
2371+ while self .match (
2372+ TokenType .EQUAL_EQUAL ,
2373+ TokenType .NOT_EQUALS ,
2374+ TokenType .LESS_THAN ,
2375+ TokenType .GREATER_THAN ,
2376+ TokenType .LESS_EQUAL ,
2377+ TokenType .GREATER_EQUAL ,
2378+ ):
2379+ self .advance () # Skip operator
2380+ self ._parse_make_additive_expression ()
2381+
22682382 def _parse_make_additive_expression (self ) -> None :
22692383 """Parse additive expressions (+ and -)."""
22702384 self ._parse_make_multiplicative_expression ()
@@ -2377,10 +2491,20 @@ def _parse_convert_assignments(self) -> None:
23772491
23782492 def _parse_convert_single_assignment (self ) -> None :
23792493 """Parse a single assignment in a convert statement."""
2380- # Handle ::id syntax
2494+ # Handle ::id syntax or ::=id() syntax
23812495 if self .match (TokenType .COLON ):
23822496 self .advance () # Skip first :
2383- self .expect (TokenType .COLON ) # Skip second :
2497+ if self .match (TokenType .COLON ):
2498+ self .advance () # Skip second :
2499+ # Handle ::= syntax (double colon equals)
2500+ if self .match (TokenType .EQUALS ):
2501+ self .advance () # Skip =
2502+ # Parse value expression after ::=
2503+ self ._parse_convert_assignment_value ()
2504+ return
2505+ else :
2506+ # Single colon, rewind
2507+ self .pos -= 1
23842508
23852509 # Parse the identifier
23862510 if not self .match (TokenType .IDENTIFIER ):
@@ -2399,36 +2523,8 @@ def _parse_convert_single_assignment(self) -> None:
23992523
24002524 def _parse_convert_assignment_value (self ) -> None :
24012525 """Parse the value part of a convert assignment."""
2402- # Handle function calls like id(), type()
2403- if (
2404- self .match (TokenType .IDENTIFIER )
2405- and self .peek_ahead (1 )
2406- and self .peek_ahead (1 ).type == TokenType .LPAREN
2407- ):
2408- self .advance () # Function name
2409- self .expect (TokenType .LPAREN )
2410-
2411- # Parse function arguments (can be nested)
2412- if not self .match (TokenType .RPAREN ):
2413- self ._parse_make_function_args () # Reuse from make statement
2414-
2415- self .expect (TokenType .RPAREN )
2416- # Handle tag access like t["key"]
2417- elif self .match (TokenType .IDENTIFIER ):
2418- self .advance ()
2419- # Handle tag access like t["key"]
2420- if self .match (TokenType .LBRACKET ):
2421- self .advance () # Skip [
2422- if self .match (TokenType .STRING ):
2423- self .advance () # Skip string key
2424- else :
2425- self .error ("Expected string key in tag access" )
2426- self .expect (TokenType .RBRACKET )
2427- # Handle simple values
2428- elif self .match (TokenType .NUMBER , TokenType .STRING ):
2429- self .advance ()
2430- else :
2431- self .error ("Expected value expression in convert assignment" )
2526+ # Reuse the sophisticated expression parsing from make statements
2527+ self ._parse_make_value_expression ()
24322528
24332529 def _parse_parentheses_content (self ) -> None :
24342530 """Parse content inside parentheses, handling nested parentheses."""
0 commit comments