Bohaska commited on
Commit
5f880f3
·
1 Parent(s): 423bf50

overhaul TRLParser

Browse files
Files changed (1) hide show
  1. telegram_engine.py +135 -53
telegram_engine.py CHANGED
@@ -34,20 +34,120 @@ def ns_api_request(params, user_agent_override=USER_AGENT):
34
 
35
 
36
  class TRLParser:
37
- """Parses and evaluates a TRL string."""
 
 
 
38
 
39
- # This method is now responsible for handling the new regex primitive
40
- def _fetch_nations_from_primitive(self, category, args_str, current_nations):
41
- # API-based primitives (do not need current_nations)
42
- if category == "nations":
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
43
  args = [arg.strip() for arg in args_str.split(',')]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
44
  return {arg.lower().replace(' ', '_') for arg in args}
45
  if category == "regions":
46
- args = [arg.strip() for arg in args_str.split(',')]
47
- xml = ns_api_request({"q": "nations", "region": ",".join(args)})
48
  return set(re.findall(r'<NATION id="([^"]+)">', xml))
49
  if category == "wa":
50
- args = [arg.strip() for arg in args_str.split(',')]
51
  if "members" in args:
52
  xml = ns_api_request({"q": "wamembers"})
53
  return set(xml.split(','))
@@ -55,61 +155,43 @@ class TRLParser:
55
  xml = ns_api_request({"q": "wadelegates"})
56
  return set(xml.split(','))
57
  if category in ("new", "refounded"):
58
- limit = int(args_str) if args_str and args_str.isdigit() else 50
59
  happenings_filter = "founding" if category == "new" else "refounding"
60
  xml = ns_api_request({"q": "happenings", "filter": happenings_filter, "limit": limit})
61
  return set(re.findall(r'@@([^@]+)@@', xml))
62
-
63
- # --- NEW PRIMITIVE LOGIC ---
64
- # This primitive filters the list of nations already gathered.
65
  if category == "name_regex":
66
  try:
67
- # The regex pattern is the only argument
68
- pattern = re.compile(args_str.strip())
69
- # Return a new set of nations from the current list that match the pattern
70
  return {nation for nation in current_nations if pattern.search(nation)}
71
  except re.error as e:
72
- # Raise an error that can be caught by the main engine
73
- raise ValueError(f"Invalid Regular Expression '{args_str}': {e}")
74
- # --- END OF NEW LOGIC ---
75
-
76
  return set()
77
 
78
- # This method is updated to pass the current nations set to the primitive fetcher
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
79
  def evaluate(self, trl_string):
80
- """Evaluates the TRL string and returns a set of nation names."""
81
- # First, strip all comments to clean up the TRL string.
82
- # A comment is a '#' character followed by anything until the end of the line.
83
- trl_string = re.sub(r'#.*$', '', trl_string, flags=re.MULTILINE)
84
-
85
- trl_string = re.sub(r'^\s*(?=[a-zA-Z(])', '+', trl_string, flags=re.MULTILINE)
86
- trl_string = trl_string.replace('\n', ' ').strip()
87
- token_regex = re.compile(r'([+\-/])\s*(\([^)]+\)|[a-zA-Z_]+\s*\[(.*)\])\s*;')
88
-
89
- def _process_group(group_str):
90
- nations = set()
91
- tokens = token_regex.findall(group_str)
92
- for action, body in tokens:
93
- target_nations = set()
94
- if body.startswith('('):
95
- target_nations = _process_group(body[1:-1])
96
- else:
97
- match = re.match(r'([a-zA-Z_]+)\s*\[([^\]]+)\]', body)
98
- if match:
99
- category, args_str = match.groups()
100
- # Pass the current 'nations' set to the primitive handler
101
- target_nations = self._fetch_nations_from_primitive(
102
- category.lower(), args_str, current_nations=nations
103
- )
104
- if action == '+':
105
- nations.update(target_nations)
106
- elif action == '-':
107
- nations.difference_update(target_nations)
108
- elif action == '/':
109
- nations.intersection_update(target_nations)
110
- return nations
111
-
112
- return _process_group(trl_string)
113
 
114
 
115
  def telegram_engine(
 
34
 
35
 
36
  class TRLParser:
37
+ """
38
+ A robust, recursive-descent parser for Template Recipient Language,
39
+ modeled after the official JavaScript implementation.
40
+ """
41
 
42
+ def _consume_whitespace(self, text):
43
+ return text.lstrip()
44
+
45
+ def _parse_primitive(self, text):
46
+ text = self._consume_whitespace(text)
47
+
48
+ # Match category
49
+ category_match = re.match(r'^[a-zA-Z_]+', text)
50
+ if not category_match:
51
+ raise ValueError("Expected a primitive category (e.g., 'nations', 'regions')")
52
+ category = category_match.group(0)
53
+ text = text[len(category):]
54
+ text = self._consume_whitespace(text)
55
+
56
+ # Match arguments within brackets
57
+ if not text.startswith('['):
58
+ raise ValueError("Expected '[' after primitive category")
59
+
60
+ # Find the first closing bracket. This matches the official implementation's logic.
61
+ try:
62
+ end_bracket_index = text.index(']')
63
+ except ValueError:
64
+ raise ValueError("Unmatched '[' in primitive arguments")
65
+
66
+ args_str = text[1:end_bracket_index]
67
+ text = text[end_bracket_index + 1:]
68
+
69
+ # For regex, we don't split by comma. For others, we do.
70
+ if category == "name_regex":
71
+ args = [args_str]
72
+ else:
73
  args = [arg.strip() for arg in args_str.split(',')]
74
+
75
+ primitive = {"type": "primitive", "category": category, "args": args}
76
+ return primitive, text
77
+
78
+ def _parse_command(self, text):
79
+ text = self._consume_whitespace(text)
80
+
81
+ # Match action
82
+ action_char = text[0]
83
+ if action_char in "+-/":
84
+ action = {"+": "add", "-": "remove", "/": "limit"}[action_char]
85
+ text = text[1:]
86
+ else:
87
+ action = "add" # Default action
88
+
89
+ text = self._consume_whitespace(text)
90
+
91
+ # Match body (either a group or a primitive)
92
+ if text.startswith('('):
93
+ body, text = self._parse_group(text)
94
+ else:
95
+ body, text = self._parse_primitive(text)
96
+
97
+ text = self._consume_whitespace(text)
98
+ if not text.startswith(';'):
99
+ raise ValueError("Expected ';' to terminate a command")
100
+ text = text[1:]
101
+
102
+ command = {"type": "command", "action": action, "body": body}
103
+ return command, text
104
+
105
+ def _parse_group(self, text):
106
+ text = self._consume_whitespace(text)
107
+ if not text.startswith('('):
108
+ raise ValueError("Expected '(' to start a group")
109
+ text = text[1:] # Consume '('
110
+
111
+ commands = []
112
+ while True:
113
+ text = self._consume_whitespace(text)
114
+ if not text:
115
+ raise ValueError("Unmatched '(' in group definition")
116
+ if text.startswith(')'):
117
+ break
118
+ command, text = self._parse_command(text)
119
+ commands.append(command)
120
+
121
+ text = text[1:] # Consume ')'
122
+ group = {"type": "group", "commands": commands}
123
+ return group, text
124
+
125
+ def parse(self, trl_string):
126
+ """Parses a raw TRL string into a structured command tree."""
127
+ # Pre-processing
128
+ cleaned_string = re.sub(r'#.*$', '', trl_string, flags=re.MULTILINE)
129
+ # Wrap the entire string in an implicit group for the parser
130
+ group_to_parse = f"({cleaned_string})"
131
+
132
+ parsed_group, remaining_text = self._parse_group(group_to_parse)
133
+
134
+ if self._consume_whitespace(remaining_text):
135
+ raise ValueError(f"Unexpected trailing characters in TRL string: {remaining_text}")
136
+
137
+ return parsed_group
138
+
139
+ # --- EVALUATION LOGIC ---
140
+ def _evaluate_primitive(self, primitive, current_nations):
141
+ category = primitive["category"]
142
+ args = primitive["args"]
143
+ args_str = ",".join(args) # Re-join for the old API fetcher
144
+
145
+ if category == "nations":
146
  return {arg.lower().replace(' ', '_') for arg in args}
147
  if category == "regions":
148
+ xml = ns_api_request({"q": "nations", "region": args_str})
 
149
  return set(re.findall(r'<NATION id="([^"]+)">', xml))
150
  if category == "wa":
 
151
  if "members" in args:
152
  xml = ns_api_request({"q": "wamembers"})
153
  return set(xml.split(','))
 
155
  xml = ns_api_request({"q": "wadelegates"})
156
  return set(xml.split(','))
157
  if category in ("new", "refounded"):
158
+ limit = int(args[0]) if args and args[0].isdigit() else 50
159
  happenings_filter = "founding" if category == "new" else "refounding"
160
  xml = ns_api_request({"q": "happenings", "filter": happenings_filter, "limit": limit})
161
  return set(re.findall(r'@@([^@]+)@@', xml))
 
 
 
162
  if category == "name_regex":
163
  try:
164
+ pattern = re.compile(args[0].strip())
 
 
165
  return {nation for nation in current_nations if pattern.search(nation)}
166
  except re.error as e:
167
+ raise ValueError(f"Invalid Regular Expression '{args[0]}': {e}")
 
 
 
168
  return set()
169
 
170
+ def _evaluate_group(self, group, initial_nations=None):
171
+ nations = initial_nations if initial_nations is not None else set()
172
+ for command in group["commands"]:
173
+ action = command["action"]
174
+ body = command["body"]
175
+
176
+ if body["type"] == "group":
177
+ target_nations = self._evaluate_group(body)
178
+ else: # Primitive
179
+ target_nations = self._evaluate_primitive(body, current_nations=nations)
180
+
181
+ if action == 'add':
182
+ nations.update(target_nations)
183
+ elif action == 'remove':
184
+ nations.difference_update(target_nations)
185
+ elif action == 'limit':
186
+ nations.intersection_update(target_nations)
187
+ return nations
188
+
189
  def evaluate(self, trl_string):
190
+ """Parses and then evaluates a TRL string."""
191
+ if not trl_string.strip():
192
+ return set()
193
+ parsed_structure = self.parse(trl_string)
194
+ return self._evaluate_group(parsed_structure)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
195
 
196
 
197
  def telegram_engine(