robotcode-robot 0.64.1__py3-none-any.whl → 0.66.0__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.
- robotcode/robot/__version__.py +1 -1
- robotcode/robot/utils/markdownformatter.py +358 -0
- {robotcode_robot-0.64.1.dist-info → robotcode_robot-0.66.0.dist-info}/METADATA +2 -2
- {robotcode_robot-0.64.1.dist-info → robotcode_robot-0.66.0.dist-info}/RECORD +6 -5
- {robotcode_robot-0.64.1.dist-info → robotcode_robot-0.66.0.dist-info}/WHEEL +0 -0
- {robotcode_robot-0.64.1.dist-info → robotcode_robot-0.66.0.dist-info}/licenses/LICENSE.txt +0 -0
robotcode/robot/__version__.py
CHANGED
@@ -1 +1 @@
|
|
1
|
-
__version__ = "0.
|
1
|
+
__version__ = "0.66.0"
|
@@ -0,0 +1,358 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
import functools
|
4
|
+
import itertools
|
5
|
+
import re
|
6
|
+
from abc import ABC, abstractmethod
|
7
|
+
from typing import Any, Callable, Iterator, List, Optional, Tuple
|
8
|
+
|
9
|
+
|
10
|
+
class Formatter(ABC):
|
11
|
+
_strip_lines = True
|
12
|
+
|
13
|
+
def __init__(self) -> None:
|
14
|
+
self._lines: List[str] = []
|
15
|
+
|
16
|
+
def handles(self, line: str) -> bool:
|
17
|
+
return self._handles(line.strip() if self._strip_lines else line)
|
18
|
+
|
19
|
+
@abstractmethod
|
20
|
+
def _handles(self, line: str) -> bool:
|
21
|
+
...
|
22
|
+
|
23
|
+
def add(self, line: str) -> None:
|
24
|
+
self._lines.append(line.strip() if self._strip_lines else line)
|
25
|
+
|
26
|
+
def end(self) -> str:
|
27
|
+
result = self.format(self._lines)
|
28
|
+
self._lines = []
|
29
|
+
return result
|
30
|
+
|
31
|
+
@abstractmethod
|
32
|
+
def format(self, lines: List[str]) -> str:
|
33
|
+
...
|
34
|
+
|
35
|
+
|
36
|
+
class MarkDownFormatter:
|
37
|
+
def __init__(self) -> None:
|
38
|
+
self._results: List[str] = []
|
39
|
+
self._formatters: List[Formatter] = [
|
40
|
+
TableFormatter(),
|
41
|
+
PreformattedFormatter(),
|
42
|
+
ListFormatter(),
|
43
|
+
HeaderFormatter(),
|
44
|
+
RulerFormatter(),
|
45
|
+
]
|
46
|
+
self._formatters.append(ParagraphFormatter(self._formatters[:]))
|
47
|
+
self._current: Optional[Formatter] = None
|
48
|
+
|
49
|
+
def format(self, text: str) -> str:
|
50
|
+
for line in text.splitlines():
|
51
|
+
self._process_line(line)
|
52
|
+
self._end_current()
|
53
|
+
return "\n".join(self._results)
|
54
|
+
|
55
|
+
def _process_line(self, line: str) -> None:
|
56
|
+
if not line.strip():
|
57
|
+
self._end_current()
|
58
|
+
elif self._current and self._current.handles(line):
|
59
|
+
self._current.add(line)
|
60
|
+
else:
|
61
|
+
self._end_current()
|
62
|
+
self._current = self._find_formatter(line)
|
63
|
+
if self._current is not None:
|
64
|
+
self._current.add(line)
|
65
|
+
|
66
|
+
def _end_current(self) -> None:
|
67
|
+
if self._current:
|
68
|
+
self._results.append(self._current.end())
|
69
|
+
self._current = None
|
70
|
+
|
71
|
+
def _find_formatter(self, line: str) -> Optional[Formatter]:
|
72
|
+
for formatter in self._formatters:
|
73
|
+
if formatter.handles(line):
|
74
|
+
return formatter
|
75
|
+
return None
|
76
|
+
|
77
|
+
|
78
|
+
class SingleLineFormatter(Formatter):
|
79
|
+
def _handles(self, line: str) -> bool:
|
80
|
+
return bool(not self._lines and self.match(line))
|
81
|
+
|
82
|
+
@abstractmethod
|
83
|
+
def match(self, line: str) -> Optional[re.Match[str]]:
|
84
|
+
...
|
85
|
+
|
86
|
+
def format(self, lines: List[str]) -> str:
|
87
|
+
return self.format_line(lines[0])
|
88
|
+
|
89
|
+
@abstractmethod
|
90
|
+
def format_line(self, line: str) -> str:
|
91
|
+
...
|
92
|
+
|
93
|
+
|
94
|
+
class HeaderFormatter(SingleLineFormatter):
|
95
|
+
_regex = re.compile(r"^(={1,5})\s+(\S.*?)\s+\1$")
|
96
|
+
|
97
|
+
def match(self, line: str) -> Optional[re.Match[str]]:
|
98
|
+
return self._regex.match(line)
|
99
|
+
|
100
|
+
def format_line(self, line: str) -> str:
|
101
|
+
m = self.match(line)
|
102
|
+
if m is not None:
|
103
|
+
level, text = m.groups()
|
104
|
+
|
105
|
+
return "%s %s\n" % ("#" * (len(level) + 1), text)
|
106
|
+
return ""
|
107
|
+
|
108
|
+
|
109
|
+
class LinkFormatter:
|
110
|
+
_image_exts = (".jpg", ".jpeg", ".png", ".gif", ".bmp", ".svg")
|
111
|
+
_link = re.compile(r"\[(.+?\|.*?)\]")
|
112
|
+
_url = re.compile(
|
113
|
+
r"""
|
114
|
+
((^|\ ) ["'(\[{]*) # begin of line or space and opt. any char "'([{
|
115
|
+
([a-z][\w+-.]*://[^\s|]+?) # url
|
116
|
+
(?=[)\]}"'.,!?:;|]* ($|\ )) # opt. any char )]}"'.,!?:;| and eol or space
|
117
|
+
""",
|
118
|
+
re.VERBOSE | re.MULTILINE | re.IGNORECASE,
|
119
|
+
)
|
120
|
+
|
121
|
+
def format_url(self, text: str) -> str:
|
122
|
+
return self._format_url(text, format_as_image=False)
|
123
|
+
|
124
|
+
def _format_url(self, text: str, format_as_image: bool = True) -> str:
|
125
|
+
if "://" not in text:
|
126
|
+
return text
|
127
|
+
return self._url.sub(functools.partial(self._replace_url, format_as_image), text)
|
128
|
+
|
129
|
+
def _replace_url(self, format_as_image: bool, match: re.Match[str]) -> str:
|
130
|
+
pre = match.group(1)
|
131
|
+
url = match.group(3)
|
132
|
+
if format_as_image and self._is_image(url):
|
133
|
+
return pre + self._get_image(url)
|
134
|
+
return pre + self._get_link(url)
|
135
|
+
|
136
|
+
def _get_image(self, src: str, title: Optional[str] = None) -> str:
|
137
|
+
return f""
|
138
|
+
|
139
|
+
def _get_link(self, href: str, content: Optional[str] = None) -> str:
|
140
|
+
return f"[{content or href}]({href})"
|
141
|
+
|
142
|
+
def _quot(self, attr: str) -> str:
|
143
|
+
return attr if '"' not in attr else attr.replace('"', """)
|
144
|
+
|
145
|
+
def format_link(self, text: str) -> str:
|
146
|
+
# 2nd, 4th, etc. token contains link, others surrounding content
|
147
|
+
tokens = self._link.split(text)
|
148
|
+
|
149
|
+
formatters: Iterator[Callable[[str], Any]] = itertools.cycle((self._format_url, self._format_link))
|
150
|
+
return "".join(f(t) for f, t in zip(formatters, tokens))
|
151
|
+
|
152
|
+
def _format_link(self, text: str) -> str:
|
153
|
+
link, content = [t.strip() for t in text.split("|", 1)]
|
154
|
+
if self._is_image(content):
|
155
|
+
content = self._get_image(content, link)
|
156
|
+
elif self._is_image(link):
|
157
|
+
return self._get_image(link, content)
|
158
|
+
if link.startswith("\\#"):
|
159
|
+
link = link.lower()
|
160
|
+
return self._get_link(link, content)
|
161
|
+
|
162
|
+
def remove_link(self, text: str) -> str:
|
163
|
+
# 2nd, 4th, etc. token contains link, others surrounding content
|
164
|
+
tokens = self._link.split(text)
|
165
|
+
if len(tokens) > 1:
|
166
|
+
formatters: Iterator[Callable[[str], Any]] = itertools.cycle([self._remove_link])
|
167
|
+
return "".join(f(t) for f, t in zip(formatters, tokens))
|
168
|
+
return text
|
169
|
+
|
170
|
+
def _remove_link(self, text: str) -> str:
|
171
|
+
if "|" not in text:
|
172
|
+
return text
|
173
|
+
|
174
|
+
link, content = [t.strip() for t in text.split("|", 1)]
|
175
|
+
if self._is_image(content):
|
176
|
+
return self._get_image(content, link)
|
177
|
+
|
178
|
+
return content
|
179
|
+
|
180
|
+
def _is_image(self, text: str) -> bool:
|
181
|
+
return text.startswith("data:image/") or text.lower().endswith(self._image_exts)
|
182
|
+
|
183
|
+
|
184
|
+
class LineFormatter:
|
185
|
+
_bold = re.compile(
|
186
|
+
r"""
|
187
|
+
( # prefix (group 1)
|
188
|
+
(^|\ ) # begin of line or space
|
189
|
+
["'(]* _? # optionally any char "'( and optional begin of italic
|
190
|
+
) #
|
191
|
+
\* # start of bold
|
192
|
+
([^\ ].*?) # no space and then anything (group 3)
|
193
|
+
\* # end of bold
|
194
|
+
(?= # start of postfix (non-capturing group)
|
195
|
+
_? ["').,!?:;]* # optional end of italic and any char "').,!?:;
|
196
|
+
($|\ ) # end of line or space
|
197
|
+
)
|
198
|
+
""",
|
199
|
+
re.VERBOSE,
|
200
|
+
)
|
201
|
+
_italic = re.compile(
|
202
|
+
r"""
|
203
|
+
( (^|\ ) ["'(]* ) # begin of line or space and opt. any char "'(
|
204
|
+
_ # start of italic
|
205
|
+
([^\ _].*?) # no space or underline and then anything
|
206
|
+
_ # end of italic
|
207
|
+
(?= ["').,!?:;]* ($|\ ) ) # opt. any char "').,!?:; and end of line or space
|
208
|
+
""",
|
209
|
+
re.VERBOSE,
|
210
|
+
)
|
211
|
+
_code = re.compile(
|
212
|
+
r"""
|
213
|
+
( (^|\ ) ["'(]* ) # same as above with _ changed to ``
|
214
|
+
``
|
215
|
+
([^\ `].*?)
|
216
|
+
``
|
217
|
+
(?= ["').,!?:;]* ($|\ ) )
|
218
|
+
""",
|
219
|
+
re.VERBOSE,
|
220
|
+
)
|
221
|
+
|
222
|
+
def __init__(self) -> None:
|
223
|
+
super().__init__()
|
224
|
+
|
225
|
+
self._formatters: List[Tuple[str, Callable[[str], str]]] = [
|
226
|
+
("<", self._quote_lower_then),
|
227
|
+
("#", self._quote_hash),
|
228
|
+
("*", self._format_bold),
|
229
|
+
("_", self._format_italic),
|
230
|
+
("``", self._format_code),
|
231
|
+
("", functools.partial(LinkFormatter().format_link)),
|
232
|
+
]
|
233
|
+
|
234
|
+
def format(self, line: str) -> str:
|
235
|
+
for marker, formatter in self._formatters:
|
236
|
+
if marker in line:
|
237
|
+
line = formatter(line)
|
238
|
+
return line
|
239
|
+
|
240
|
+
def _quote_lower_then(self, line: str) -> str:
|
241
|
+
return line.replace("<", "\\<")
|
242
|
+
|
243
|
+
def _quote_hash(self, line: str) -> str:
|
244
|
+
return line.replace("#", "\\#")
|
245
|
+
|
246
|
+
def _format_bold(self, line: str) -> str:
|
247
|
+
return self._bold.sub("\\1**\\3**", line)
|
248
|
+
|
249
|
+
def _format_italic(self, line: str) -> str:
|
250
|
+
return self._italic.sub("\\1*\\3*", line)
|
251
|
+
|
252
|
+
def _format_code(self, line: str) -> str:
|
253
|
+
return self._code.sub("\\1`\\3`", line)
|
254
|
+
|
255
|
+
|
256
|
+
class PreformattedFormatter(Formatter):
|
257
|
+
_format_line = functools.partial(LineFormatter().format)
|
258
|
+
|
259
|
+
def _handles(self, line: str) -> bool:
|
260
|
+
return line.startswith("| ") or line == "|"
|
261
|
+
|
262
|
+
def format(self, lines: List[str]) -> str:
|
263
|
+
lines = [LinkFormatter().remove_link(line[2:]) for line in lines]
|
264
|
+
return "```text\n" + "\n".join(lines) + "\n```\n"
|
265
|
+
|
266
|
+
|
267
|
+
class ParagraphFormatter(Formatter):
|
268
|
+
_format_line = functools.partial(LineFormatter().format)
|
269
|
+
|
270
|
+
def __init__(self, other_formatters: List[Formatter]) -> None:
|
271
|
+
super().__init__()
|
272
|
+
self._other_formatters = other_formatters
|
273
|
+
|
274
|
+
def _handles(self, line: str) -> bool:
|
275
|
+
return not any(other.handles(line) for other in self._other_formatters)
|
276
|
+
|
277
|
+
def format(self, lines: List[str]) -> str:
|
278
|
+
return self._format_line(" ".join(lines)) + "\n\n"
|
279
|
+
|
280
|
+
|
281
|
+
class ListFormatter(Formatter):
|
282
|
+
_strip_lines = False
|
283
|
+
_format_item = functools.partial(LineFormatter().format)
|
284
|
+
|
285
|
+
def _handles(self, line: str) -> bool:
|
286
|
+
return bool(line.strip().startswith("- ") or line.startswith(" ") and self._lines)
|
287
|
+
|
288
|
+
def format(self, lines: List[str]) -> str:
|
289
|
+
items = ["- %s" % self._format_item(line) for line in self._combine_lines(lines)]
|
290
|
+
return "\n".join(items) + "\n\n"
|
291
|
+
|
292
|
+
def _combine_lines(self, lines: List[str]) -> Iterator[str]:
|
293
|
+
current = []
|
294
|
+
for line in lines:
|
295
|
+
line = line.strip()
|
296
|
+
if not line.startswith("- "):
|
297
|
+
current.append(line)
|
298
|
+
continue
|
299
|
+
if current:
|
300
|
+
yield " ".join(current)
|
301
|
+
current = [line[2:].strip()]
|
302
|
+
yield " ".join(current)
|
303
|
+
|
304
|
+
|
305
|
+
class RulerFormatter(SingleLineFormatter):
|
306
|
+
regex = re.compile("^-{3,}$")
|
307
|
+
|
308
|
+
def match(self, line: str) -> Optional[re.Match[str]]:
|
309
|
+
return self.regex.match(line)
|
310
|
+
|
311
|
+
def format_line(self, line: str) -> str:
|
312
|
+
return "---"
|
313
|
+
|
314
|
+
|
315
|
+
class TableFormatter(Formatter):
|
316
|
+
_table_line = re.compile(r"^\| (.* |)\|$")
|
317
|
+
_line_splitter = re.compile(r" \|(?= )")
|
318
|
+
_format_cell_content = functools.partial(LineFormatter().format)
|
319
|
+
|
320
|
+
def _handles(self, line: str) -> bool:
|
321
|
+
return self._table_line.match(line) is not None
|
322
|
+
|
323
|
+
def format(self, lines: List[str]) -> str:
|
324
|
+
return self._format_table([self._split_to_cells(line) for line in lines])
|
325
|
+
|
326
|
+
def _split_to_cells(self, line: str) -> List[str]:
|
327
|
+
return [cell.strip() for cell in self._line_splitter.split(line[1:-1])]
|
328
|
+
|
329
|
+
def _format_table(self, rows: List[List[str]]) -> str:
|
330
|
+
table = []
|
331
|
+
|
332
|
+
max_columns = max(len(row) for row in rows)
|
333
|
+
|
334
|
+
try:
|
335
|
+
header_rows = [list(next(row for row in rows if any(cell for cell in row if cell.startswith("="))))]
|
336
|
+
except StopIteration:
|
337
|
+
header_rows = [[]]
|
338
|
+
|
339
|
+
body_rows = [row for row in rows if row not in header_rows]
|
340
|
+
|
341
|
+
for row in header_rows or [[]]:
|
342
|
+
row += [""] * (max_columns - len(row))
|
343
|
+
table.append(f'|{"|".join(self._format_cell(cell) for cell in row)}|')
|
344
|
+
|
345
|
+
row_ = [" :--- "] * max_columns
|
346
|
+
table.append(f'|{"|".join(row_)}|')
|
347
|
+
|
348
|
+
for row in body_rows:
|
349
|
+
row += [""] * (max_columns - len(row))
|
350
|
+
table.append(f'|{"|".join(self._format_cell(cell) for cell in row)}|')
|
351
|
+
|
352
|
+
return "\n".join(table) + "\n\n"
|
353
|
+
|
354
|
+
def _format_cell(self, content: str) -> str:
|
355
|
+
if content.startswith("=") and content.endswith("="):
|
356
|
+
content = content[1:-1]
|
357
|
+
|
358
|
+
return f" {self._format_cell_content(content).strip()} "
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: robotcode-robot
|
3
|
-
Version: 0.
|
3
|
+
Version: 0.66.0
|
4
4
|
Summary: Support classes for RobotCode for handling Robot Framework projects.
|
5
5
|
Project-URL: Homepage, https://robotcode.io
|
6
6
|
Project-URL: Donate, https://github.com/sponsors/d-biehl
|
@@ -26,7 +26,7 @@ Classifier: Topic :: Utilities
|
|
26
26
|
Classifier: Typing :: Typed
|
27
27
|
Requires-Python: >=3.8
|
28
28
|
Requires-Dist: platformdirs<3.12.0,>=3.2.0
|
29
|
-
Requires-Dist: robotcode-core==0.
|
29
|
+
Requires-Dist: robotcode-core==0.66.0
|
30
30
|
Requires-Dist: robotframework>=4.1.0
|
31
31
|
Requires-Dist: tomli>=1.1.0; python_version < '3.11'
|
32
32
|
Description-Content-Type: text/markdown
|
@@ -1,5 +1,5 @@
|
|
1
1
|
robotcode/robot/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
2
|
-
robotcode/robot/__version__.py,sha256=
|
2
|
+
robotcode/robot/__version__.py,sha256=rdBd9Kj9DDCtQNzVZEbaE1YatB7HZXL9qX4Ge-gXzaM,23
|
3
3
|
robotcode/robot/py.typed,sha256=bWew9mHgMy8LqMu7RuqQXFXLBxh2CRx0dUbSx-3wE48,27
|
4
4
|
robotcode/robot/config/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
5
5
|
robotcode/robot/config/loader.py,sha256=VcODWZzHT5GO7qS2od7MdeMVNtSchP6d4gp8ICEhrg4,5655
|
@@ -7,8 +7,9 @@ robotcode/robot/config/model.py,sha256=TaRUFWdb9j-f_ovkVhZPzbDgWzbmIRUaA0_NgoB-P
|
|
7
7
|
robotcode/robot/config/utils.py,sha256=hX-bMn9CFqxrcwQo-WPnRXh1PK434HYbLQgZp89ZHeE,2447
|
8
8
|
robotcode/robot/utils/__init__.py,sha256=aW_jxCqt1PR1LYu2LLWsm8lIqIqAi2ZBinACGdiLelE,448
|
9
9
|
robotcode/robot/utils/ast.py,sha256=qeTq6Em8bjE6urAZR5HlLDuqz-XNP6yG1eq-lVeay90,10098
|
10
|
+
robotcode/robot/utils/markdownformatter.py,sha256=3RHkP8JCg0IzeIY6iIp533V6ZQTaonpTqKdYaAjscpM,11653
|
10
11
|
robotcode/robot/utils/visitors.py,sha256=YuQuW228cxhUAdsI1TEXz0vT4QNubD037RDEftQ1FDg,3898
|
11
|
-
robotcode_robot-0.
|
12
|
-
robotcode_robot-0.
|
13
|
-
robotcode_robot-0.
|
14
|
-
robotcode_robot-0.
|
12
|
+
robotcode_robot-0.66.0.dist-info/METADATA,sha256=Bv7xvHZUys9YN4NGRnE9BGS9a0MFh-HsC1_4DumUUgM,2221
|
13
|
+
robotcode_robot-0.66.0.dist-info/WHEEL,sha256=9QBuHhg6FNW7lppboF2vKVbCGTVzsFykgRQjjlajrhA,87
|
14
|
+
robotcode_robot-0.66.0.dist-info/licenses/LICENSE.txt,sha256=B05uMshqTA74s-0ltyHKI6yoPfJ3zYgQbvcXfDVGFf8,10280
|
15
|
+
robotcode_robot-0.66.0.dist-info/RECORD,,
|
File without changes
|
File without changes
|