1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51 |
import re
from dataclasses import dataclass
from html import unescape
from .actions.registry import REGISTRY
# Command vocabulary comes from the action registry, so parsing and execution
# never drift apart.
_ALIAS_TO_COMMAND = REGISTRY.alias_map()
PHRASE_ALIASES = REGISTRY.phrase_map()
_MENTION = re.compile(r"@[\w.\-]+(?:@[\w.\-]+)?")
_WHITESPACE = re.compile(r"\s+")
@dataclass
class Parsed:
name: str | None
arg: str = ""
def strip_html(content):
text = re.sub(r"(?i)<br\s*/?>", " ", content)
text = re.sub(r"(?i)</p>", " ", text)
text = re.sub(r"<[^>]+>", "", text)
return unescape(text)
def _after(raw, keyword):
match = re.search(r"\b" + re.escape(keyword) + r"\b", raw, re.IGNORECASE)
return raw[match.end():].strip() if match else ""
def parse(content):
raw = _WHITESPACE.sub(" ", strip_html(content)).strip()
detect = _WHITESPACE.sub(" ", _MENTION.sub(" ", raw)).strip().lower()
for phrase, command in PHRASE_ALIASES.items():
if detect.startswith(phrase):
return Parsed(command, _after(raw, phrase))
for token in detect.split():
command = _ALIAS_TO_COMMAND.get(token)
if command:
return Parsed(command, _after(raw, token))
return Parsed(None)
def parse_command(content):
return parse(content).name |