tagify 0.0.1__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.
- tagify/__init__.py +4 -0
- tagify/parser.py +145 -0
- tagify-0.0.1.dist-info/LICENSE +21 -0
- tagify-0.0.1.dist-info/METADATA +34 -0
- tagify-0.0.1.dist-info/RECORD +7 -0
- tagify-0.0.1.dist-info/WHEEL +5 -0
- tagify-0.0.1.dist-info/top_level.txt +1 -0
tagify/__init__.py
ADDED
tagify/parser.py
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import re
|
|
2
|
+
|
|
3
|
+
from typing import Any, Callable
|
|
4
|
+
|
|
5
|
+
# RegEx patterns
|
|
6
|
+
_re_placeholder = re.compile(r"{\s*([\w\d\.]+|\w+\(.+?\))\s*}")
|
|
7
|
+
_re_blocks = re.compile(r"{% elif (.+?) %}|{% else %}")
|
|
8
|
+
_re_terms = re.compile(r"(\s&&\s|\s\|\|\s)")
|
|
9
|
+
_re_match = re.compile(r"([\w\(\),]+)\s*(==|!=)\s*(.+)")
|
|
10
|
+
_re_variables = re.compile(r"{% set ([\w\d]+)\s*=\s*(.*) %}")
|
|
11
|
+
_re_conditions = re.compile(r"{% elif (.+?) %}")
|
|
12
|
+
_re_conditional_pattern = re.compile(
|
|
13
|
+
r"{% if (.+?) %}(.+?){% endif %}",
|
|
14
|
+
flags=re.DOTALL
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
__all__ = (
|
|
18
|
+
"TemplateParser",
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class TemplateParser:
|
|
23
|
+
def __init__(
|
|
24
|
+
self,
|
|
25
|
+
context: dict | None = None,
|
|
26
|
+
functions: dict | None = None,
|
|
27
|
+
*,
|
|
28
|
+
conditionals: bool = True,
|
|
29
|
+
):
|
|
30
|
+
self.context: dict[str, Any] = context or {}
|
|
31
|
+
self.functions: dict[str, Callable] = functions or {}
|
|
32
|
+
|
|
33
|
+
self._conditionals = conditionals
|
|
34
|
+
|
|
35
|
+
def render(self, template: str) -> str:
|
|
36
|
+
"""Render the template with placeholders, conditionals, and function calls."""
|
|
37
|
+
template = self._process_variables(template) # Replace variables
|
|
38
|
+
|
|
39
|
+
if self._conditionals:
|
|
40
|
+
template = self._process_conditionals(template) # Process if/else/elif blocks
|
|
41
|
+
|
|
42
|
+
template = self._process_placeholders(template) # Replace placeholders and function calls
|
|
43
|
+
return template.strip() # Remove any trailing whitespace
|
|
44
|
+
|
|
45
|
+
def _process_variables(self, template: str) -> str:
|
|
46
|
+
"""Replace all variables in the template with their values."""
|
|
47
|
+
for match in _re_variables.finditer(template):
|
|
48
|
+
key, value = match.groups()
|
|
49
|
+
self.context[key.strip()] = value.strip()
|
|
50
|
+
|
|
51
|
+
return _re_variables.sub("", template)
|
|
52
|
+
|
|
53
|
+
def _parse_placeholder(self, key: str) -> str:
|
|
54
|
+
"""Evaluate placeholders or function calls."""
|
|
55
|
+
safe_unused = "{" + str(key) + "}"
|
|
56
|
+
# Check if the placeholder is a function call
|
|
57
|
+
if "(" in key and key.endswith(")"):
|
|
58
|
+
func_name, args = self._parse_function_call(key)
|
|
59
|
+
if func_name in self.functions:
|
|
60
|
+
try:
|
|
61
|
+
return str(self.functions[func_name](*args))
|
|
62
|
+
except Exception as e:
|
|
63
|
+
return f"[ FUNC_ERR:{func_name}: {e} ]"
|
|
64
|
+
return safe_unused # Return unmodified if function not found
|
|
65
|
+
|
|
66
|
+
# Split the key by dots and try to access each part as an attribute
|
|
67
|
+
parts = key.split(".")
|
|
68
|
+
value = self.context.get(parts[0], safe_unused) # Start with the base context
|
|
69
|
+
|
|
70
|
+
try:
|
|
71
|
+
for part in parts[1:]:
|
|
72
|
+
value = value[part] # Dig into each nested dict value
|
|
73
|
+
except KeyError:
|
|
74
|
+
return safe_unused # Return as-is if any part is inaccessible
|
|
75
|
+
|
|
76
|
+
return str(value)
|
|
77
|
+
|
|
78
|
+
def _process_placeholders(self, template: str) -> str:
|
|
79
|
+
"""Replace all placeholders in the template with their values."""
|
|
80
|
+
return _re_placeholder.sub(
|
|
81
|
+
lambda m: self._parse_placeholder(m.group(1)),
|
|
82
|
+
template
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
def _process_conditionals(self, template: str) -> str:
|
|
86
|
+
"""Handle if, elif, else conditionals in the template."""
|
|
87
|
+
return _re_conditional_pattern.sub(
|
|
88
|
+
self._evaluate_conditional_block,
|
|
89
|
+
template
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
def _evaluate_conditional_block(self, match: re.Match) -> str:
|
|
93
|
+
"""Evaluate if, elif, and else conditions and return the appropriate block."""
|
|
94
|
+
condition, content = match.groups()
|
|
95
|
+
blocks = _re_blocks.split(content)
|
|
96
|
+
conditions = [condition] + _re_conditions.findall(content)
|
|
97
|
+
|
|
98
|
+
for cond, block in zip(conditions, blocks):
|
|
99
|
+
if self._evaluate_condition(cond.strip()):
|
|
100
|
+
return block.strip()
|
|
101
|
+
|
|
102
|
+
return blocks[-1].strip() if "{% else %}" in content else ""
|
|
103
|
+
|
|
104
|
+
def _evaluate_condition(self, condition: str) -> bool:
|
|
105
|
+
"""Evaluate conditions safely without using eval."""
|
|
106
|
+
# Split by logical operators and evaluate each subcondition
|
|
107
|
+
terms = _re_terms.split(condition)
|
|
108
|
+
result = self._evaluate_comparison(terms[0].strip())
|
|
109
|
+
|
|
110
|
+
for i in range(1, len(terms), 2):
|
|
111
|
+
operator = terms[i].strip()
|
|
112
|
+
next_term = self._evaluate_comparison(terms[i + 1].strip())
|
|
113
|
+
|
|
114
|
+
if operator == "&&":
|
|
115
|
+
result = result and next_term
|
|
116
|
+
elif operator == "||":
|
|
117
|
+
result = result or next_term
|
|
118
|
+
|
|
119
|
+
return result
|
|
120
|
+
|
|
121
|
+
def _evaluate_comparison(self, term: str) -> bool:
|
|
122
|
+
"""Evaluate a single comparison expression like 'user == "Alice"'."""
|
|
123
|
+
match = _re_match.match(term)
|
|
124
|
+
if not match:
|
|
125
|
+
return False
|
|
126
|
+
|
|
127
|
+
left, operator, right = match.groups()
|
|
128
|
+
left_value = str(self.context.get(left, left))
|
|
129
|
+
right_value = right.strip('"').strip("'")
|
|
130
|
+
|
|
131
|
+
if left_value.isdigit() and right_value.isdigit():
|
|
132
|
+
left_value = int(left_value)
|
|
133
|
+
right_value = int(right_value)
|
|
134
|
+
|
|
135
|
+
if operator == "==":
|
|
136
|
+
return left_value == right_value
|
|
137
|
+
elif operator == "!=":
|
|
138
|
+
return left_value != right_value
|
|
139
|
+
|
|
140
|
+
def _parse_function_call(self, func_string: str) -> tuple[str, list[str]]:
|
|
141
|
+
"""Parse function calls like 'func_name(arg1, arg2)'."""
|
|
142
|
+
func_name = func_string.split("(", 1)[0]
|
|
143
|
+
args_string = func_string[len(func_name) + 1:-1] # Remove function name and parentheses
|
|
144
|
+
args = [arg.strip().strip('"').strip("'") for arg in args_string.split(",")]
|
|
145
|
+
return func_name, args
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 xelA
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: tagify
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: Parser that converts strings with variables and functions to more advance strings
|
|
5
|
+
Author-email: AlexFlipnote <root@alexflipnote.dev>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Repository, https://github.com/xelA/tagify
|
|
8
|
+
Keywords: discord,tag,parser,template
|
|
9
|
+
Requires-Python: >=3.11.0
|
|
10
|
+
Description-Content-Type: text/markdown
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Provides-Extra: dev
|
|
13
|
+
Requires-Dist: pyright; extra == "dev"
|
|
14
|
+
Requires-Dist: flake8; extra == "dev"
|
|
15
|
+
Requires-Dist: toml; extra == "dev"
|
|
16
|
+
Provides-Extra: maintainer
|
|
17
|
+
Requires-Dist: twine; extra == "maintainer"
|
|
18
|
+
Requires-Dist: wheel; extra == "maintainer"
|
|
19
|
+
Requires-Dist: build; extra == "maintainer"
|
|
20
|
+
|
|
21
|
+
# tagify
|
|
22
|
+
Parser that converts strings with variables and functions to more advance strings
|
|
23
|
+
|
|
24
|
+
Style is a bit inspired by jinja2 format, but not 100% the same.
|
|
25
|
+
|
|
26
|
+
This project is very early development, I might use it or not, but I'm not sure.
|
|
27
|
+
|
|
28
|
+
## Requirements
|
|
29
|
+
- Python >=3.11
|
|
30
|
+
|
|
31
|
+
## Install
|
|
32
|
+
```
|
|
33
|
+
pip install tagify
|
|
34
|
+
```
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
tagify/__init__.py,sha256=7EjfU5y2I9WRREjs0VbewSxcMwnOI3xTHCRukRoCopw,64
|
|
2
|
+
tagify/parser.py,sha256=70nPgtVuD-ivJPQ8VI9K3rleRm2199np5DbVqqLPBgk,5630
|
|
3
|
+
tagify-0.0.1.dist-info/LICENSE,sha256=XjgX6hq_173pwLEhPGC9IoSLGeHooNG0a3LHBj8tTps,1082
|
|
4
|
+
tagify-0.0.1.dist-info/METADATA,sha256=1ju_8UpIMsvFxa7BSGV1qkL10Wlu_G_zlF_QEBJiQYo,1025
|
|
5
|
+
tagify-0.0.1.dist-info/WHEEL,sha256=P9jw-gEje8ByB7_hXoICnHtVCrEwMQh-630tKvQWehc,91
|
|
6
|
+
tagify-0.0.1.dist-info/top_level.txt,sha256=4L8Fez5sONVYp7vZ5h-9ZXHxFwy8iPC-dOD_vDeI_2w,7
|
|
7
|
+
tagify-0.0.1.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
tagify
|