abstractcode 0.2.0__py3-none-any.whl → 0.3.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.
@@ -0,0 +1,168 @@
1
+ """Minimal, terminal-friendly Markdown renderer.
2
+
3
+ Goal: improve readability in the TUI without attempting full CommonMark compliance.
4
+ We deliberately keep this conservative:
5
+ - Only style headings, code fences, and a few inline constructs.
6
+ - Never mutate the underlying content used for copy-to-clipboard.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import re
12
+ from dataclasses import dataclass
13
+ from typing import List
14
+
15
+
16
+ @dataclass(frozen=True)
17
+ class AnsiPalette:
18
+ reset: str = "\033[0m"
19
+ dim: str = "\033[2m"
20
+ bold: str = "\033[1m"
21
+ cyan: str = "\033[36m"
22
+ green: str = "\033[32m"
23
+ blue: str = "\033[38;5;39m"
24
+
25
+
26
+ class TerminalMarkdownRenderer:
27
+ """Render a subset of Markdown to ANSI-styled plain text."""
28
+
29
+ _re_heading = re.compile(r"^(?P<hashes>#{1,6})\s+(?P<title>.+?)\s*$")
30
+ _re_hr = re.compile(r"^\s*(-{3,}|_{3,}|\*{3,})\s*$")
31
+ _re_bold = re.compile(r"\*\*(?P<txt>[^*]+)\*\*")
32
+ _re_inline_code = re.compile(r"`(?P<code>[^`]+)`")
33
+
34
+ def __init__(self, *, color: bool = True, palette: AnsiPalette | None = None) -> None:
35
+ self._color = bool(color)
36
+ self._p = palette or AnsiPalette()
37
+
38
+ def _style(self, text: str, *codes: str) -> str:
39
+ if not self._color or not codes:
40
+ return text
41
+ return "".join(codes) + text + self._p.reset
42
+
43
+ def _style_inline(self, line: str) -> str:
44
+ # Bold
45
+ def _bold(m: re.Match) -> str:
46
+ return self._style(m.group("txt"), self._p.bold)
47
+
48
+ # Inline code
49
+ def _code(m: re.Match) -> str:
50
+ return self._style(m.group("code"), self._p.blue)
51
+
52
+ out = self._re_bold.sub(_bold, line)
53
+ out = self._re_inline_code.sub(_code, out)
54
+ return out
55
+
56
+ def _unescape_newlines_if_needed(self, s: str) -> str:
57
+ """Convert literal "\\n" / "\\r" / "\\r\\n" sequences into real newlines.
58
+
59
+ Some upstream layers accidentally pass serialized strings (repr/json) where newlines are
60
+ encoded as the two characters backslash+n. We only unescape when the input has *no* real
61
+ newlines to avoid corrupting valid code like `print("a\\nb")`.
62
+ """
63
+ if "\n" in s or "\r" in s:
64
+ return s
65
+ if "\\n" not in s and "\\r" not in s:
66
+ return s
67
+
68
+ out: List[str] = []
69
+ i = 0
70
+ n = len(s)
71
+ while i < n:
72
+ ch = s[i]
73
+ if ch != "\\":
74
+ out.append(ch)
75
+ i += 1
76
+ continue
77
+
78
+ # Count consecutive backslashes.
79
+ j = i
80
+ while j < n and s[j] == "\\":
81
+ j += 1
82
+ run_len = j - i
83
+
84
+ if j >= n:
85
+ out.append("\\" * run_len)
86
+ break
87
+
88
+ nxt = s[j]
89
+
90
+ # Only treat "\n"/"\r" as escapes when the escape backslash is not itself escaped.
91
+ if nxt in ("n", "r") and (run_len % 2 == 1):
92
+ # Preserve all but the escape backslash.
93
+ if run_len > 1:
94
+ out.append("\\" * (run_len - 1))
95
+ out.append("\n")
96
+ i = j + 1
97
+
98
+ # Collapse \r\n into a single newline (Windows-style payloads).
99
+ if nxt == "r" and i < n and s[i] == "\\":
100
+ k = i
101
+ while k < n and s[k] == "\\":
102
+ k += 1
103
+ run2_len = k - i
104
+ if k < n and s[k] == "n" and (run2_len % 2 == 1):
105
+ if run2_len > 1:
106
+ out.append("\\" * (run2_len - 1))
107
+ i = k + 1
108
+ continue
109
+
110
+ # Not an escape we handle; emit literally.
111
+ out.append("\\" * run_len)
112
+ out.append(nxt)
113
+ i = j + 1
114
+
115
+ return "".join(out)
116
+
117
+ def render(self, text: str) -> str:
118
+ s = "" if text is None else str(text)
119
+ s = self._unescape_newlines_if_needed(s)
120
+ lines = s.splitlines()
121
+ out: List[str] = []
122
+
123
+ in_code = False
124
+ fence_lang = ""
125
+
126
+ for raw in lines:
127
+ line = raw.rstrip("\n")
128
+
129
+ # Code fences
130
+ if line.strip().startswith("```"):
131
+ if not in_code:
132
+ in_code = True
133
+ fence_lang = line.strip()[3:].strip()
134
+ label = f"code" + (f" ({fence_lang})" if fence_lang else "")
135
+ out.append(self._style(f"┌─ {label}", self._p.dim))
136
+ else:
137
+ in_code = False
138
+ out.append(self._style("└─", self._p.dim))
139
+ continue
140
+
141
+ if in_code:
142
+ # Keep code unmodified; add a subtle gutter.
143
+ out.append(self._style("│ ", self._p.dim) + line)
144
+ continue
145
+
146
+ # Horizontal rules
147
+ if self._re_hr.match(line):
148
+ out.append(self._style("─" * 60, self._p.dim))
149
+ continue
150
+
151
+ # Headings
152
+ m = self._re_heading.match(line)
153
+ if m:
154
+ hashes = m.group("hashes")
155
+ title = m.group("title").strip()
156
+ level = len(hashes)
157
+ if level <= 2:
158
+ out.append(self._style(title, self._p.bold, self._p.cyan))
159
+ else:
160
+ out.append(self._style(title, self._p.bold))
161
+ continue
162
+
163
+ out.append(self._style_inline(line))
164
+
165
+ return "\n".join(out)
166
+
167
+
168
+