robotcode-robot 2.9.0.dev115__py3-none-any.whl → 2.9.0.dev124__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__ = "2.9.0-dev.115"
1
+ __version__ = "2.9.0-dev.124"
@@ -90,6 +90,7 @@ from ..utils.markdown_docs import (
90
90
  code_span,
91
91
  code_span_variables,
92
92
  escape_link_text,
93
+ escape_text,
93
94
  extract_reference_definitions,
94
95
  heading_anchors,
95
96
  iter_headings,
@@ -99,6 +100,7 @@ from ..utils.markdown_docs import (
99
100
  section_anchors,
100
101
  shift_headings,
101
102
  slugify,
103
+ unescape_text,
102
104
  unique_reference_labels,
103
105
  )
104
106
  from ..utils.markdownformatter import MarkDownFormatter, NameLinker
@@ -241,18 +243,17 @@ def _page_heading_shift(doc_format: str, levels: int) -> Optional[Callable[[str]
241
243
 
242
244
 
243
245
  # A run of `#` after a space at the end of a keyword name would close its ATX heading: `### Use C #` shows "Use C".
244
- _RE_CLOSING_HASHES = re.compile(r"(\s)(#+)$")
245
- _RE_ESCAPED_CLOSING_HASHES = re.compile(r"(\s)\\(#+)$")
246
246
 
247
247
 
248
248
  def _heading_name(name: str) -> str:
249
- """A keyword name as the title of its heading: a closing run of `#` escaped."""
250
- return _RE_CLOSING_HASHES.sub(r"\1\\\2", name)
249
+ """A keyword name as the title of its heading: what Markdown would read as markup escaped,
250
+ also a closing run of `#`."""
251
+ return escape_text(name)
251
252
 
252
253
 
253
254
  def _heading_title_name(title: str) -> str:
254
255
  """The keyword name of a heading title that `_heading_name` wrote."""
255
- return _RE_ESCAPED_CLOSING_HASHES.sub(r"\1\2", title)
256
+ return unescape_text(title)
256
257
 
257
258
 
258
259
  def _match_heading_anchors(headings: List[Tuple[str, str]], titles: List[Set[str]]) -> List[str]:
@@ -1348,7 +1349,8 @@ class KeywordDoc(SourceEntity):
1348
1349
  if add_type:
1349
1350
  result = (
1350
1351
  f"#{'#' * header_level} "
1351
- f"{(self.libtype or 'Library').capitalize() if self.is_initializer else 'Keyword'} *{self.name}*\n"
1352
+ f"{(self.libtype or 'Library').capitalize() if self.is_initializer else 'Keyword'} "
1353
+ f"*{_heading_name(self.name)}*\n"
1352
1354
  )
1353
1355
  else:
1354
1356
  if not self.is_initializer:
@@ -1993,9 +1995,9 @@ class LibraryDoc:
1993
1995
  for level, title, anchor in heading_anchors(page):
1994
1996
  if level == header_level + 2:
1995
1997
  in_introduction = title == "Introduction"
1996
- section_anchors_by_title.setdefault(normalize_reference(title), anchor)
1998
+ section_anchors_by_title.setdefault(normalize_reference(_heading_title_name(title)), anchor)
1997
1999
  elif level > header_level + 2 and in_introduction:
1998
- introduction.setdefault(normalize_reference(title), anchor)
2000
+ introduction.setdefault(normalize_reference(_heading_title_name(title)), anchor)
1999
2001
  section_anchors_by_title.update(introduction)
2000
2002
  _, type_anchors = self.get_page_anchors(page, header_level)
2001
2003
 
@@ -22,6 +22,7 @@ __all__ = [
22
22
  "code_span",
23
23
  "code_span_variables",
24
24
  "escape_link_text",
25
+ "escape_text",
25
26
  "extract_reference_definitions",
26
27
  "heading_anchors",
27
28
  "iter_headings",
@@ -35,6 +36,7 @@ __all__ = [
35
36
  "section_anchors",
36
37
  "shift_headings",
37
38
  "slugify",
39
+ "unescape_text",
38
40
  "unique_reference_labels",
39
41
  ]
40
42
 
@@ -148,6 +150,36 @@ def escape_link_text(text: str) -> str:
148
150
  return _RE_LINK_TEXT_SPECIAL.sub(r"\\\1", text)
149
151
 
150
152
 
153
+ _RE_TEXT_SPECIAL = re.compile(
154
+ r"""
155
+ \\(?=[!-/:-@\[-`{-~]|\Z) # a backslash before ASCII punctuation or at the end
156
+ | [<\#`] # always
157
+ | (?<!\s)\* | \*(?!\s) # a star, unless it has space on both sides
158
+ | (?<![^\W_])_ | _(?![^\W_]) # an underscore, unless it is inside a word
159
+ | &(?=\#?\w+;) # what reads as an entity
160
+ | \](?=\() # what reads as an inline link
161
+ | ~(?=~) | (?<=~)~ # what reads as strikethrough
162
+ """,
163
+ re.VERBOSE,
164
+ )
165
+ _RE_ESCAPED_VARIABLE = re.compile(r"(?<!\\)((?:\\\\)+)\$")
166
+ _RE_MARKDOWN_ESCAPE = re.compile(r"\\([!-/:-@\[-`{-~])")
167
+
168
+
169
+ def escape_text(text: str) -> str:
170
+ """Escape what Markdown would read as markup in text that is meant as text, only where
171
+ it would change the text: `snake_case`, `${list}[0]` and `2 * 3` stay as they are.
172
+
173
+ An odd number of backslashes before `$` is an escaped variable and stays one."""
174
+ text = _RE_TEXT_SPECIAL.sub(r"\\\g<0>", text)
175
+ return _RE_ESCAPED_VARIABLE.sub(lambda m: m.group(1) + ("\\$" if len(m.group(1)) % 4 else "$"), text)
176
+
177
+
178
+ def unescape_text(text: str) -> str:
179
+ """The text that Markdown shows for `text`, without its escapes."""
180
+ return _RE_MARKDOWN_ESCAPE.sub(r"\1", text)
181
+
182
+
151
183
  def anchor_link_resolver(kind: str, name: str) -> Optional[str]:
152
184
  """A `LinkResolver` for pages that show a whole library: keywords and
153
185
  sections have a heading there. The heading of a type can carry a number,
@@ -555,7 +587,8 @@ def _code_span_variables_in(text: str) -> str:
555
587
  # variables that follow each other directly share one code span: `${TEMPDIR}${/}`
556
588
  while (following := search_variable(text[end:], "$", ignore_errors=True)).start == 0:
557
589
  end += following.end
558
- result.append(text[: match.start] + code_span(text[match.start : end]))
590
+ # inside code an escape would show
591
+ result.append(text[: match.start] + code_span(unescape_text(text[match.start : end])))
559
592
  text = text[end:]
560
593
  result.append(text)
561
594
  return "".join(result)
@@ -5,7 +5,7 @@ import re
5
5
  from abc import ABC, abstractmethod
6
6
  from typing import Any, Callable, Final, Iterator, List, Optional, Tuple
7
7
 
8
- from .markdown_docs import code_span, escape_link_text
8
+ from .markdown_docs import code_span, escape_link_text, escape_text
9
9
 
10
10
  # Gets a name in single backticks of documentation in Robot Framework's format and
11
11
  # returns the target of a link to it, or `None` if it stays inline code.
@@ -19,10 +19,15 @@ _RE_CODE_SPAN: Final["re.Pattern[str]"] = re.compile(
19
19
  | (?<!`) (`+) (?!`) # a run of backticks (group 1)
20
20
  ([^\ue000]+?) # anything but code that is already held (group 2)
21
21
  (?<!`) \1 (?!`) # up to the next run of the same length
22
- | `+ # a run of backticks without a partner
23
22
  """,
24
23
  re.VERBOSE,
25
24
  )
25
+ # the documentation's own characters that the conversion uses
26
+ _RE_OWN_MARKERS: Final["re.Pattern[str]"] = re.compile("[\ue000\ue002\ue003]")
27
+ # what is no text: held code (group 1) and the syntax the formatter writes itself (group 2)
28
+ _RE_NOT_TEXT: Final["re.Pattern[str]"] = re.compile("(\ue000\\d+\ue001)|\ue002(.*?)\ue003")
29
+ # what starts a Markdown block: a quote, a list or a reference definition (group 1), a numbered list (groups 2 and 3)
30
+ _RE_BLOCK_START: Final["re.Pattern[str]"] = re.compile(r"^(?:(>|[+-](?=\s|$)|\[(?=[^\]]*\]:))|(\d{1,9})([.)])(?=\s|$))")
26
31
  _RE_THREE_BACKTICKS: Final["re.Pattern[str]"] = re.compile("(?<!`)```(?!`)")
27
32
  _RE_LONG_BACKTICKS: Final["re.Pattern[str]"] = re.compile("`{3,}")
28
33
 
@@ -123,7 +128,7 @@ class HeaderFormatter(SingleLineFormatter):
123
128
  if m is not None:
124
129
  level, text = m.groups()
125
130
 
126
- return "%s %s\n" % ("#" * (len(level) + 1), text)
131
+ return "%s %s\n" % ("#" * (len(level) + 1), _format_heading(text))
127
132
  return ""
128
133
 
129
134
 
@@ -155,15 +160,14 @@ class LinkFormatter:
155
160
  return pre + self._get_link(url)
156
161
 
157
162
  def _get_image(self, src: str, title: Optional[str] = None) -> str:
158
- return f"![{title or src}]({self._target(src)})"
163
+ return _syntax(f"![{escape_text(title or src)}]({self._target(src)})")
159
164
 
160
165
  def _get_link(self, href: str, content: Optional[str] = None) -> str:
161
- return f"[{content or href}]({self._target(href)})"
166
+ return _syntax("[") + (content or href) + _syntax(f"]({self._target(href)})")
162
167
 
163
168
  def _target(self, target: str) -> str:
164
- # `#` is escaped in the whole line before links are formatted, a link target needs it as it is
165
- target = target.replace("\\#", "#")
166
- # Markdown takes a target with spaces only in angle brackets; `<` is already escaped in the whole line
169
+ target = target.replace("<", "\\<")
170
+ # Markdown takes a target with spaces only in angle brackets
167
171
  if " " in target:
168
172
  return "<" + target.replace(">", "\\>") + ">"
169
173
  return target
@@ -184,7 +188,7 @@ class LinkFormatter:
184
188
  content = self._get_image(content, link)
185
189
  elif self._is_image(link):
186
190
  return self._get_image(link, content)
187
- if link.startswith("\\#"):
191
+ if link.startswith("#"):
188
192
  link = link.lower()
189
193
  return self._get_link(link, content)
190
194
 
@@ -214,6 +218,46 @@ def _hold(held: List[str], text: str) -> str:
214
218
  return f"\ue000{len(held) - 1}\ue001"
215
219
 
216
220
 
221
+ def _hold_own_markers(line: str, held: List[str]) -> str:
222
+ return _RE_OWN_MARKERS.sub(lambda m: _hold(held, m.group(0)), line)
223
+
224
+
225
+ def _restore(line: str, held: List[str]) -> str:
226
+ # backwards, the documentation's own characters can be part of held code
227
+ for index, text in reversed(list(enumerate(held))):
228
+ line = line.replace(f"\ue000{index}\ue001", text)
229
+ return line
230
+
231
+
232
+ def _syntax(text: str) -> str:
233
+ """Marks what the formatter writes itself, the escape of the text leaves it alone."""
234
+ return f"\ue002{text}\ue003"
235
+
236
+
237
+ def _escape_line(line: str) -> str:
238
+ result: List[str] = []
239
+ position = 0
240
+ for match in _RE_NOT_TEXT.finditer(line):
241
+ result.append(escape_text(line[position : match.start()]))
242
+ result.append(match.group(1) or match.group(2))
243
+ position = match.end()
244
+ result.append(escape_text(line[position:]))
245
+ return "".join(result)
246
+
247
+
248
+ def _escape_block_start(match: re.Match[str]) -> str:
249
+ return f"\\{match.group(1)}" if match.group(1) else f"{match.group(2)}\\{match.group(3)}"
250
+
251
+
252
+ def _format_heading(text: str) -> str:
253
+ """Robot Framework formats nothing in a heading, what Markdown would read as markup is text there."""
254
+ held: List[str] = []
255
+ text = _hold_own_markers(text, held)
256
+ if "`" in text:
257
+ text = _RE_CODE_SPAN.sub(lambda m: m.group(0) if m.group(1) is None else _hold(held, m.group(0)), text)
258
+ return _restore(_escape_line(text), held)
259
+
260
+
217
261
  class LineFormatter:
218
262
  _bold: Final["re.Pattern[str]"] = re.compile(
219
263
  r"""
@@ -257,8 +301,6 @@ _ # end of italic
257
301
 
258
302
  self._name_linker = name_linker
259
303
  self._formatters: List[Tuple[str, Callable[[str], str]]] = [
260
- ("<", self._quote_lower_then),
261
- ("#", self._quote_hash),
262
304
  ("*", self._format_bold),
263
305
  ("_", self._format_italic),
264
306
  ("", LinkFormatter().format_link),
@@ -266,9 +308,7 @@ _ # end of italic
266
308
 
267
309
  def format(self, line: str) -> str:
268
310
  held: List[str] = []
269
- if "\ue000" in line:
270
- # the documentation's own character must not read as a placeholder
271
- line = line.replace("\ue000", _hold(held, "\ue000"))
311
+ line = _hold_own_markers(line, held)
272
312
  if "`" in line:
273
313
  # what Robot Framework reads as code decides first, then what else Markdown reads as code
274
314
  line = self._code.sub(lambda m: m.group(1) + _hold(held, code_span(_code_text(m.group(3)))), line)
@@ -276,16 +316,14 @@ _ # end of italic
276
316
  for marker, formatter in self._formatters:
277
317
  if marker in line:
278
318
  line = formatter(line)
279
- # backwards, the documentation's own character can be part of held code
280
- for index, text in reversed(list(enumerate(held))):
281
- line = line.replace(f"\ue000{index}\ue001", text)
282
- return line
319
+ # last, so that all that is left of the line is text
320
+ line = _RE_BLOCK_START.sub(_escape_block_start, _escape_line(line))
321
+ return _restore(line, held)
283
322
 
284
323
  def _hold_span(self, match: re.Match[str], held: List[str]) -> str:
285
324
  text, fence, span = match.group(0, 1, 2)
286
325
  if fence is None:
287
- # Markdown must not pair a backtick left over with the code written here
288
- return text if text.startswith("\\") else text.replace("`", "\\`")
326
+ return text
289
327
  # for Robot Framework this is no code, but Markdown shows it as code
290
328
  if len(fence) > 1:
291
329
  return _hold(held, text)
@@ -294,17 +332,11 @@ _ # end of italic
294
332
  return _hold(held, f"[{escape_link_text(span)}]({target})")
295
333
  return _hold(held, code_span(_code_text(span)))
296
334
 
297
- def _quote_lower_then(self, line: str) -> str:
298
- return line.replace("<", "\\<")
299
-
300
- def _quote_hash(self, line: str) -> str:
301
- return line.replace("#", "\\#")
302
-
303
335
  def _format_bold(self, line: str) -> str:
304
- return self._bold.sub("\\1**\\3**", line)
336
+ return self._bold.sub(f"\\1{_syntax('**')}\\3{_syntax('**')}", line)
305
337
 
306
338
  def _format_italic(self, line: str) -> str:
307
- return self._italic.sub("\\1*\\3*", line)
339
+ return self._italic.sub(f"\\1{_syntax('*')}\\3{_syntax('*')}", line)
308
340
 
309
341
 
310
342
  _line_formatter = LineFormatter()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: robotcode-robot
3
- Version: 2.9.0-dev.115
3
+ Version: 2.9.0-dev.124
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://opencollective.com/robotcode
@@ -27,7 +27,7 @@ Classifier: Topic :: Utilities
27
27
  Classifier: Typing :: Typed
28
28
  Requires-Python: >=3.10
29
29
  Requires-Dist: platformdirs>=4.3
30
- Requires-Dist: robotcode-core==2.9.0-dev.115
30
+ Requires-Dist: robotcode-core==2.9.0-dev.124
31
31
  Requires-Dist: robotframework>=5.0.0
32
32
  Requires-Dist: tomli>=1.1.0; python_version < '3.11'
33
33
  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=qCMGA7BDTVyBevZ0ksnNoqFh10zoIu3X15NTXaUZz8M,30
2
+ robotcode/robot/__version__.py,sha256=kB2HCucrVBrLij9hB2UXa9rwCOmEfmeDk12kUWayuAU,30
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=HLbkZem8JiBuy9Q5naghZddAuMPOn8Tk6UBgN2T3Uyc,9073
@@ -16,7 +16,7 @@ robotcode/robot/diagnostics/errors.py,sha256=lqHDWrMNy3ZhuEIVxeEhaU0ns8PB6pVTQBf
16
16
  robotcode/robot/diagnostics/import_resolver.py,sha256=DZjUOD3uNxFAwsVUqrCTRhColjz9DRPetAvIb_Ccufs,32588
17
17
  robotcode/robot/diagnostics/imports_manager.py,sha256=opemApuCLgPNsnqVuYc9MefqHt42g_7Ozqhrmu_osH0,90401
18
18
  robotcode/robot/diagnostics/keyword_finder.py,sha256=nwTmjvhyu5H7HHzyk8gt-pny2K2xdH2qCNyKXwwXRnk,18998
19
- robotcode/robot/diagnostics/library_doc.py,sha256=ZoAiT8zwHMXo8LpEnBy_xP7Tdth_9gopntEaqWzgxHI,153578
19
+ robotcode/robot/diagnostics/library_doc.py,sha256=loLZ7cJjtrNn6YXv_lC8SmptfIUK7P5Hkdc782opN9k,153586
20
20
  robotcode/robot/diagnostics/model_helper.py,sha256=_ySXwerkK5xvjWa82T0DFy8jpxRzcaavTJiAHnk2lXQ,29740
21
21
  robotcode/robot/diagnostics/namespace.py,sha256=Rcmjk3V0vcY-DUkgjkRyuOdYXurKaTEqXJLc6efYrwY,35489
22
22
  robotcode/robot/diagnostics/namespace_analyzer.py,sha256=dGAEbbznPJlFBKN4dLkMDU7w9xc0mYdmRARvRS3Zlwg,104331
@@ -35,14 +35,14 @@ robotcode/robot/diagnostics/semantic_analyzer/serialization.py,sha256=DA-_gRbo_9
35
35
  robotcode/robot/diagnostics/semantic_analyzer/variable_tokenizer.py,sha256=vBor3GZjvZX33I9yQliUtHZbjDybWn7W0YoSd5EMO_M,33646
36
36
  robotcode/robot/utils/__init__.py,sha256=-A0YeHv7e7WK1SId7Er7Vu6frrDYWopUrcyFlyxLmPY,396
37
37
  robotcode/robot/utils/ast.py,sha256=S9xyiJZee4Y5VcroNrGIIWp8f9sutb_1o4blAlCqzdg,11218
38
- robotcode/robot/utils/markdown_docs.py,sha256=0xRa79quMMuQRKjFBHwPPDCF6_0aYOT0YDAo_2MmHNE,30075
39
- robotcode/robot/utils/markdownformatter.py,sha256=5yCrnHhJ3hIAWUjNw6yUZfhgjWGkNboWRWd64uBDzqI,16036
38
+ robotcode/robot/utils/markdown_docs.py,sha256=jR1gfOAyf0C6i-Gn9Q2cStHogAUXcM-jFNu2Uv7uhZE,31424
39
+ robotcode/robot/utils/markdownformatter.py,sha256=HjFLZK1whOghijdKAyw3NciCI_JrCmlUtst1rl8VTMA,17390
40
40
  robotcode/robot/utils/match.py,sha256=KgA6o6TbnQpOT1eAzEjhl_-FZ2XO07xiV0L7IYRzyzE,1125
41
41
  robotcode/robot/utils/robot_patching.py,sha256=jXHhnPvZx8hCQq4B_2uwX9j51Be_toWpUoTYYe8HjPo,1463
42
42
  robotcode/robot/utils/robot_path.py,sha256=ObLP8PqamTZAz5oi0eDXE_dnyZwAPXp4TJYm6ugeENs,2185
43
43
  robotcode/robot/utils/stubs.py,sha256=umugZYAyneFNgqRJBRMJPzm0u0B_TH8Sx_y-ykXnxpw,351
44
44
  robotcode/robot/utils/variables.py,sha256=9WnP3QqcHdq-zioJ_gJXJobw_2Gcto_rGPCUGmOH9YQ,9829
45
45
  robotcode/robot/utils/visitor.py,sha256=nP3O0qh3YYuxR6S8wYJRBFfNwIVgsgohURBlrnFkRYQ,2299
46
- robotcode_robot-2.9.0.dev115.dist-info/METADATA,sha256=qcJxc40nb6sN31qCIQ42UPujJyMOZX7izWc95ibtBk8,2960
47
- robotcode_robot-2.9.0.dev115.dist-info/WHEEL,sha256=2iZrJkDIkeDh0k9SikKMqFuRP-ua6mb4Ir1AsER4BEw,87
48
- robotcode_robot-2.9.0.dev115.dist-info/RECORD,,
46
+ robotcode_robot-2.9.0.dev124.dist-info/METADATA,sha256=cmy8fBqH5dS-DWv3D33XiFGSB7EF97b0iqoDFDH4xOw,2960
47
+ robotcode_robot-2.9.0.dev124.dist-info/WHEEL,sha256=2iZrJkDIkeDh0k9SikKMqFuRP-ua6mb4Ir1AsER4BEw,87
48
+ robotcode_robot-2.9.0.dev124.dist-info/RECORD,,