robotcode-robot 0.64.1__py3-none-any.whl → 0.66.0__py3-none-any.whl

Sign up to get free protection for your applications and to get access to all the features.
@@ -1 +1 @@
1
- __version__ = "0.64.1"
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"![{title or src}]({src})"
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.64.1
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.64.1
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=T4-vW_ByO6apa0V-uPVXwtgvQiE-ODq0knBtcK_morc,23
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.64.1.dist-info/METADATA,sha256=oNK4E4R8spREm_5o5af31VKQXUKjrmtu0eiDqFARSH8,2221
12
- robotcode_robot-0.64.1.dist-info/WHEEL,sha256=9QBuHhg6FNW7lppboF2vKVbCGTVzsFykgRQjjlajrhA,87
13
- robotcode_robot-0.64.1.dist-info/licenses/LICENSE.txt,sha256=B05uMshqTA74s-0ltyHKI6yoPfJ3zYgQbvcXfDVGFf8,10280
14
- robotcode_robot-0.64.1.dist-info/RECORD,,
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,,