avrae-ls 0.6.0__py3-none-any.whl → 0.6.2__py3-none-any.whl

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.
avrae_ls/dice.py ADDED
@@ -0,0 +1,33 @@
1
+ from __future__ import annotations
2
+
3
+ import d20
4
+
5
+
6
+ class RerollableStringifier(d20.SimpleStringifier):
7
+ """Stringifier that emits rerollable expressions and skips dropped dice."""
8
+
9
+ def _stringify(self, node):
10
+ if not node.kept:
11
+ return None
12
+ return super()._stringify(node)
13
+
14
+ def _str_expression(self, node):
15
+ return self._stringify(node.roll)
16
+
17
+ def _str_literal(self, node):
18
+ return str(node.total)
19
+
20
+ def _str_parenthetical(self, node):
21
+ return f"({self._stringify(node.value)})"
22
+
23
+ def _str_set(self, node):
24
+ out = f"{', '.join([self._stringify(v) for v in node.values if v.kept])}"
25
+ if len(node.values) == 1:
26
+ return f"({out},)"
27
+ return f"({out})"
28
+
29
+ def _str_dice(self, node):
30
+ return self._str_set(node)
31
+
32
+ def _str_die(self, node):
33
+ return str(node.total)
avrae_ls/parser.py ADDED
@@ -0,0 +1,68 @@
1
+ from __future__ import annotations
2
+
3
+ import re
4
+ from dataclasses import dataclass
5
+ from typing import List
6
+
7
+
8
+ @dataclass
9
+ class DraconicBlock:
10
+ code: str
11
+ line_offset: int
12
+ char_offset: int = 0
13
+ line_count: int = 0
14
+ inline: bool = False
15
+
16
+
17
+ DRACONIC_RE = re.compile(r"<drac2>([\s\S]*?)</drac2>", re.IGNORECASE)
18
+ INLINE_DRACONIC_RE = re.compile(r"\{\{([\s\S]*?)\}\}", re.DOTALL)
19
+ INLINE_ROLL_RE = re.compile(r"(?<!\{)\{(?!\{)([\s\S]*?)(?<!\})\}(?!\})", re.DOTALL)
20
+
21
+
22
+ def find_draconic_blocks(source: str) -> List[DraconicBlock]:
23
+ matches: list[tuple[int, DraconicBlock]] = []
24
+
25
+ def _block_from_match(match: re.Match[str], inline: bool = False) -> tuple[int, int, DraconicBlock]:
26
+ raw = match.group(1)
27
+ prefix = source[: match.start(1)]
28
+ line_offset = prefix.count("\n")
29
+ # Column where draconic content starts on its first line
30
+ last_nl = prefix.rfind("\n")
31
+ start_col = match.start(1) - (last_nl + 1 if last_nl != -1 else 0)
32
+ char_offset = start_col
33
+ # Trim leading blank lines inside the block while tracking the line shift
34
+ while raw.startswith("\n"):
35
+ raw = raw[1:]
36
+ line_offset += 1
37
+ char_offset = 0
38
+ line_count = raw.count("\n") + 1 if raw else 1
39
+ return match.start(), match.end(), DraconicBlock(
40
+ code=raw,
41
+ line_offset=line_offset,
42
+ char_offset=char_offset,
43
+ line_count=line_count,
44
+ inline=inline,
45
+ )
46
+
47
+ blocks: list[DraconicBlock] = []
48
+ for match in DRACONIC_RE.finditer(source):
49
+ matches.append(_block_from_match(match))
50
+ for match in INLINE_DRACONIC_RE.finditer(source):
51
+ matches.append(_block_from_match(match, inline=True))
52
+
53
+ matches.sort(key=lambda item: item[0])
54
+ last_end = -1
55
+ for start, end, block in matches:
56
+ if start < last_end:
57
+ continue
58
+ blocks.append(block)
59
+ last_end = end
60
+ return blocks
61
+
62
+
63
+ def primary_block_or_source(source: str) -> tuple[str, int, int]:
64
+ blocks = find_draconic_blocks(source)
65
+ if not blocks:
66
+ return source, 0, 0
67
+ block = blocks[0]
68
+ return block.code, block.line_offset, block.char_offset