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.
@@ -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,,