live-markdown 0.1.0__tar.gz
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.
- live_markdown-0.1.0/PKG-INFO +8 -0
- live_markdown-0.1.0/README.md +0 -0
- live_markdown-0.1.0/pyproject.toml +15 -0
- live_markdown-0.1.0/src/live_markdown/__init__.py +356 -0
- live_markdown-0.1.0/src/live_markdown/pipe.py +29 -0
- live_markdown-0.1.0/src/live_markdown/py.typed +0 -0
- live_markdown-0.1.0/src/live_markdown/test_cases.py +100 -0
- live_markdown-0.1.0/src/live_markdown/test_parser.py +408 -0
|
File without changes
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "live-markdown"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Add your description here"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.8"
|
|
7
|
+
dependencies = [
|
|
8
|
+
"pygments>=2.19.2",
|
|
9
|
+
]
|
|
10
|
+
|
|
11
|
+
[build-system]
|
|
12
|
+
requires = ["uv_build>=0.8.4,<0.9.0"]
|
|
13
|
+
build-backend = "uv_build"
|
|
14
|
+
[project.scripts]
|
|
15
|
+
lm = "live_markdown.pipe:main"
|
|
@@ -0,0 +1,356 @@
|
|
|
1
|
+
import re
|
|
2
|
+
import shutil
|
|
3
|
+
import sys
|
|
4
|
+
|
|
5
|
+
# Note: In this environment, pygments is available for syntax highlighting.
|
|
6
|
+
try:
|
|
7
|
+
from pygments import highlight
|
|
8
|
+
from pygments.formatters import Terminal256Formatter
|
|
9
|
+
from pygments.lexers import (
|
|
10
|
+
BashLexer,
|
|
11
|
+
JavascriptLexer,
|
|
12
|
+
MarkdownLexer,
|
|
13
|
+
PythonLexer,
|
|
14
|
+
get_lexer_by_name,
|
|
15
|
+
)
|
|
16
|
+
from pygments.util import ClassNotFound
|
|
17
|
+
except ImportError:
|
|
18
|
+
# Fallback for environments without pygments
|
|
19
|
+
print("Pygments not found. Highlighting will be disabled.")
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class MarkdownStreamer:
|
|
23
|
+
"""
|
|
24
|
+
A terminal-based streamer that parses Markdown syntax on the fly.
|
|
25
|
+
Supports recursive rendering with visual indentation for text, but
|
|
26
|
+
keeps code blocks left-aligned for better readability and copying.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
# Palette for nesting depths
|
|
30
|
+
BGS = [
|
|
31
|
+
"\033[48;5;235m", # Depth 1: Darkest
|
|
32
|
+
"\033[48;5;237m", # Depth 2
|
|
33
|
+
"\033[48;5;239m", # Depth 3
|
|
34
|
+
"\033[48;5;241m", # Depth 4: Lightest
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
ANSI = {
|
|
38
|
+
"reset": "\033[0m",
|
|
39
|
+
"bold": "\033[1m",
|
|
40
|
+
"italic": "\033[3m",
|
|
41
|
+
"strikethrough": "\033[9m",
|
|
42
|
+
"code_in": "\033[36m",
|
|
43
|
+
"header": "\033[1;95m",
|
|
44
|
+
"quote": "\033[1;34m",
|
|
45
|
+
"hr": "\033[90m",
|
|
46
|
+
"bullet": "\033[1;33m",
|
|
47
|
+
"clear_line": "\033[2K",
|
|
48
|
+
"clear_down": "\033[J",
|
|
49
|
+
"fill_bg": "\033[K",
|
|
50
|
+
"move_right": "\033[{}C",
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
def __init__(self):
|
|
54
|
+
self.buffer = ""
|
|
55
|
+
self.code_block_depth = 0
|
|
56
|
+
self.depth_stack = []
|
|
57
|
+
self.current_code_lang = ""
|
|
58
|
+
self.current_code_line = ""
|
|
59
|
+
self.code_history = "" # Crucial for multiline Python strings/docstrings
|
|
60
|
+
self.at_line_start = True
|
|
61
|
+
self.active_styles = set()
|
|
62
|
+
|
|
63
|
+
self.word_buffer = ""
|
|
64
|
+
self.line_pos = 0
|
|
65
|
+
self.term_width = shutil.get_terminal_size().columns
|
|
66
|
+
self.last_rendered_line_hl = ""
|
|
67
|
+
|
|
68
|
+
try:
|
|
69
|
+
self.formatter = Terminal256Formatter(style="monokai")
|
|
70
|
+
self.lexers = {
|
|
71
|
+
"python": PythonLexer(),
|
|
72
|
+
"bash": BashLexer(),
|
|
73
|
+
"javascript": JavascriptLexer(),
|
|
74
|
+
"js": JavascriptLexer(),
|
|
75
|
+
}
|
|
76
|
+
except:
|
|
77
|
+
self.formatter = None
|
|
78
|
+
self.lexers = {}
|
|
79
|
+
|
|
80
|
+
def get_block_bg(self):
|
|
81
|
+
if self.code_block_depth == 0:
|
|
82
|
+
return ""
|
|
83
|
+
idx = min(self.code_block_depth - 1, len(self.BGS) - 1)
|
|
84
|
+
return self.BGS[idx]
|
|
85
|
+
|
|
86
|
+
def apply_indentation(self, depth=None):
|
|
87
|
+
"""
|
|
88
|
+
Moves cursor right for text content.
|
|
89
|
+
NO indentation is applied if inside a code block.
|
|
90
|
+
"""
|
|
91
|
+
if self.code_block_depth > 0:
|
|
92
|
+
return
|
|
93
|
+
|
|
94
|
+
d = depth if depth is not None else self.code_block_depth
|
|
95
|
+
if d <= 1:
|
|
96
|
+
return
|
|
97
|
+
move_len = (d - 1) * 4
|
|
98
|
+
sys.stdout.write(self.ANSI["move_right"].format(move_len))
|
|
99
|
+
self.line_pos += move_len
|
|
100
|
+
|
|
101
|
+
def apply_pygments(self, text, lang="python"):
|
|
102
|
+
if not self.formatter or not text:
|
|
103
|
+
return f"{self.get_block_bg()}{text}"
|
|
104
|
+
|
|
105
|
+
lang = lang.lower()
|
|
106
|
+
bg = self.get_block_bg()
|
|
107
|
+
if lang == "markdown":
|
|
108
|
+
return f"{bg}{text}"
|
|
109
|
+
|
|
110
|
+
lexer = self.lexers.get(lang)
|
|
111
|
+
if not lexer:
|
|
112
|
+
try:
|
|
113
|
+
lexer = get_lexer_by_name(lang)
|
|
114
|
+
self.lexers[lang] = lexer
|
|
115
|
+
except:
|
|
116
|
+
return f"{bg}{text}"
|
|
117
|
+
|
|
118
|
+
full_context = self.code_history + text
|
|
119
|
+
full_hl = highlight(full_context, lexer, self.formatter).rstrip("\n")
|
|
120
|
+
|
|
121
|
+
if self.code_history:
|
|
122
|
+
hist_hl = highlight(self.code_history, lexer, self.formatter).rstrip("\n")
|
|
123
|
+
clean_hl = full_hl[len(hist_hl) :].lstrip("\n")
|
|
124
|
+
else:
|
|
125
|
+
clean_hl = full_hl
|
|
126
|
+
|
|
127
|
+
clean_hl = clean_hl.replace("\033[0m", "\033[0m" + bg)
|
|
128
|
+
return f"{bg}{clean_hl}"
|
|
129
|
+
|
|
130
|
+
def _clear_and_move_up(self, line_text):
|
|
131
|
+
visible_text = re.sub(r"\033\[[0-9;]*m", "", line_text)
|
|
132
|
+
# Indent is 0 for code blocks
|
|
133
|
+
indent = 0 if self.code_block_depth > 0 else (self.code_block_depth - 1) * 4
|
|
134
|
+
total_len = max(0, indent) + len(visible_text)
|
|
135
|
+
visual_rows = (max(0, total_len - 1)) // self.term_width
|
|
136
|
+
|
|
137
|
+
sys.stdout.write("\r")
|
|
138
|
+
for _ in range(visual_rows):
|
|
139
|
+
sys.stdout.write("\033[F")
|
|
140
|
+
|
|
141
|
+
sys.stdout.write(self.ANSI["clear_down"])
|
|
142
|
+
sys.stdout.flush()
|
|
143
|
+
|
|
144
|
+
def _flush_word(self, prefix="", depth=None):
|
|
145
|
+
if not self.word_buffer:
|
|
146
|
+
return
|
|
147
|
+
visible_word = re.sub(r"\033\[[0-9;]*m", "", self.word_buffer)
|
|
148
|
+
if self.line_pos + len(visible_word) >= self.term_width - 1:
|
|
149
|
+
sys.stdout.write("\n")
|
|
150
|
+
self.line_pos = 0
|
|
151
|
+
self.apply_indentation(depth)
|
|
152
|
+
sys.stdout.write(prefix)
|
|
153
|
+
sys.stdout.write(self.word_buffer)
|
|
154
|
+
self.line_pos += len(visible_word)
|
|
155
|
+
self.word_buffer = ""
|
|
156
|
+
|
|
157
|
+
def process_buffer(self, final=False):
|
|
158
|
+
while len(self.buffer) > 0 or (final and self.word_buffer):
|
|
159
|
+
if final and not self.buffer and self.word_buffer:
|
|
160
|
+
bg = self.get_block_bg()
|
|
161
|
+
self._flush_word(prefix=bg, depth=self.code_block_depth)
|
|
162
|
+
sys.stdout.flush()
|
|
163
|
+
break
|
|
164
|
+
|
|
165
|
+
if not final and len(self.buffer) < 25:
|
|
166
|
+
break
|
|
167
|
+
bg = self.get_block_bg()
|
|
168
|
+
|
|
169
|
+
# --- 1. CODE BLOCK HANDLING ---
|
|
170
|
+
if self.code_block_depth > 0 and self.current_code_lang != "markdown":
|
|
171
|
+
close_match = re.match(r"^[ \t]*```[ \t]*\n?", self.buffer)
|
|
172
|
+
if close_match and self.at_line_start:
|
|
173
|
+
if self.current_code_line:
|
|
174
|
+
self._clear_and_move_up(self.current_code_line)
|
|
175
|
+
self.apply_indentation() # No-op for code
|
|
176
|
+
sys.stdout.write(
|
|
177
|
+
self.apply_pygments(
|
|
178
|
+
self.current_code_line, self.current_code_lang
|
|
179
|
+
)
|
|
180
|
+
)
|
|
181
|
+
sys.stdout.write(f"{self.ANSI['fill_bg']}\n")
|
|
182
|
+
self.code_block_depth -= 1
|
|
183
|
+
self.current_code_lang = (
|
|
184
|
+
self.depth_stack.pop() if self.depth_stack else ""
|
|
185
|
+
)
|
|
186
|
+
self.current_code_line = ""
|
|
187
|
+
self.code_history = ""
|
|
188
|
+
self.buffer = self.buffer[close_match.end() :]
|
|
189
|
+
self.at_line_start = True
|
|
190
|
+
sys.stdout.write(self.ANSI["reset"])
|
|
191
|
+
if self.code_block_depth > 0:
|
|
192
|
+
sys.stdout.write(self.get_block_bg())
|
|
193
|
+
self.line_pos = 0
|
|
194
|
+
continue
|
|
195
|
+
|
|
196
|
+
char = self.buffer[0]
|
|
197
|
+
self.buffer = self.buffer[1:]
|
|
198
|
+
if char == "\n":
|
|
199
|
+
self._clear_and_move_up(self.current_code_line)
|
|
200
|
+
self.apply_indentation() # No-op for code
|
|
201
|
+
sys.stdout.write(
|
|
202
|
+
self.apply_pygments(
|
|
203
|
+
self.current_code_line, self.current_code_lang
|
|
204
|
+
)
|
|
205
|
+
)
|
|
206
|
+
sys.stdout.write(f"{self.ANSI['fill_bg']}\n")
|
|
207
|
+
self.code_history += self.current_code_line + "\n"
|
|
208
|
+
self.current_code_line = ""
|
|
209
|
+
self.last_rendered_line_hl = ""
|
|
210
|
+
self.at_line_start = True
|
|
211
|
+
self.line_pos = 0
|
|
212
|
+
else:
|
|
213
|
+
new_line_content = self.current_code_line + char
|
|
214
|
+
new_hl = self.apply_pygments(
|
|
215
|
+
new_line_content, self.current_code_lang
|
|
216
|
+
)
|
|
217
|
+
visible_len = len(re.sub(r"\033\[[0-9;]*m", "", new_line_content))
|
|
218
|
+
# Check wrap without indentation for code
|
|
219
|
+
is_wrap = (visible_len) % self.term_width == 0
|
|
220
|
+
if not new_hl.startswith(self.last_rendered_line_hl) or is_wrap:
|
|
221
|
+
self._clear_and_move_up(self.current_code_line)
|
|
222
|
+
self.apply_indentation() # No-op for code
|
|
223
|
+
sys.stdout.write(new_hl)
|
|
224
|
+
else:
|
|
225
|
+
delta = new_hl[len(self.last_rendered_line_hl) :]
|
|
226
|
+
sys.stdout.write(delta)
|
|
227
|
+
self.current_code_line = new_line_content
|
|
228
|
+
self.last_rendered_line_hl = new_hl
|
|
229
|
+
self.at_line_start = False
|
|
230
|
+
sys.stdout.flush()
|
|
231
|
+
continue
|
|
232
|
+
|
|
233
|
+
# --- 2. MARKDOWN / ROOT HANDLING ---
|
|
234
|
+
if self.at_line_start:
|
|
235
|
+
open_match = re.match(
|
|
236
|
+
r"^[ \t]*```([a-zA-Z0-9\-\+#]+).*\n?", self.buffer
|
|
237
|
+
)
|
|
238
|
+
if open_match:
|
|
239
|
+
new_lang = open_match.group(1).lower()
|
|
240
|
+
self.depth_stack.append(self.current_code_lang)
|
|
241
|
+
self.code_block_depth += 1
|
|
242
|
+
self.current_code_lang = new_lang
|
|
243
|
+
self.code_history = ""
|
|
244
|
+
self.last_rendered_line_hl = ""
|
|
245
|
+
self.buffer = self.buffer[open_match.end() :]
|
|
246
|
+
self.at_line_start = True
|
|
247
|
+
sys.stdout.write(self.get_block_bg())
|
|
248
|
+
continue
|
|
249
|
+
# HR
|
|
250
|
+
if self.buffer.startswith(("---", "***")):
|
|
251
|
+
self.apply_indentation()
|
|
252
|
+
sys.stdout.write(
|
|
253
|
+
f"{bg}{self.ANSI['hr']}{'─' * (self.term_width - self.line_pos)}{self.ANSI['reset']}\n"
|
|
254
|
+
)
|
|
255
|
+
self.buffer = self.buffer[3:]
|
|
256
|
+
self.line_pos = 0
|
|
257
|
+
continue
|
|
258
|
+
# Headers
|
|
259
|
+
header_match = re.match(r"^(#+) ", self.buffer)
|
|
260
|
+
if header_match:
|
|
261
|
+
self.apply_indentation()
|
|
262
|
+
self.word_buffer = (
|
|
263
|
+
f"{bg}{self.ANSI['header']}{header_match.group()}"
|
|
264
|
+
)
|
|
265
|
+
self._flush_word(prefix=bg)
|
|
266
|
+
self.buffer = self.buffer[len(header_match.group()) :]
|
|
267
|
+
self.at_line_start = False
|
|
268
|
+
continue
|
|
269
|
+
# Lists
|
|
270
|
+
list_match = re.match(r"^[ \t]*([\*\-\+]) ", self.buffer)
|
|
271
|
+
if list_match:
|
|
272
|
+
self.apply_indentation()
|
|
273
|
+
sys.stdout.write(
|
|
274
|
+
f"{self.ANSI['bullet']}{list_match.group(1)}{self.ANSI['reset']}{bg} "
|
|
275
|
+
)
|
|
276
|
+
self.line_pos += 2
|
|
277
|
+
self.buffer = self.buffer[len(list_match.group()) :]
|
|
278
|
+
self.at_line_start = False
|
|
279
|
+
continue
|
|
280
|
+
# Quotes
|
|
281
|
+
if self.buffer.startswith("> "):
|
|
282
|
+
self.apply_indentation()
|
|
283
|
+
sys.stdout.write(f"{self.ANSI['quote']}┃ {self.ANSI['reset']}{bg}")
|
|
284
|
+
self.line_pos += 2
|
|
285
|
+
self.buffer = self.buffer[2:]
|
|
286
|
+
self.at_line_start = False
|
|
287
|
+
continue
|
|
288
|
+
|
|
289
|
+
# Inline processing
|
|
290
|
+
char = self.buffer[0]
|
|
291
|
+
if char == "\\":
|
|
292
|
+
if len(self.buffer) > 1:
|
|
293
|
+
self.word_buffer += self.buffer[1]
|
|
294
|
+
self.buffer = self.buffer[2:]
|
|
295
|
+
else:
|
|
296
|
+
self.buffer = self.buffer[1:]
|
|
297
|
+
elif self.buffer.startswith("**"):
|
|
298
|
+
self._toggle_style("bold", bg)
|
|
299
|
+
self.buffer = self.buffer[2:]
|
|
300
|
+
elif self.buffer.startswith(("*", "_")):
|
|
301
|
+
self._toggle_style("italic", bg)
|
|
302
|
+
self.buffer = self.buffer[1:]
|
|
303
|
+
elif self.buffer.startswith("~~"):
|
|
304
|
+
self._toggle_style("strikethrough", bg)
|
|
305
|
+
self.buffer = self.buffer[2:]
|
|
306
|
+
elif self.buffer.startswith("`"):
|
|
307
|
+
self._toggle_style("code_in", bg)
|
|
308
|
+
self.buffer = self.buffer[1:]
|
|
309
|
+
elif char in (" ", "\n"):
|
|
310
|
+
self._flush_word(prefix=bg)
|
|
311
|
+
if char == "\n":
|
|
312
|
+
sys.stdout.write(f"{self.ANSI['fill_bg']}\n")
|
|
313
|
+
self.line_pos = 0
|
|
314
|
+
self.at_line_start = True
|
|
315
|
+
sys.stdout.write(self.ANSI["reset"] + bg)
|
|
316
|
+
self.active_styles.clear()
|
|
317
|
+
else:
|
|
318
|
+
sys.stdout.write(f"{bg} ")
|
|
319
|
+
self.line_pos += 1
|
|
320
|
+
self.buffer = self.buffer[1:]
|
|
321
|
+
else:
|
|
322
|
+
self.word_buffer += char
|
|
323
|
+
self.buffer = self.buffer[1:]
|
|
324
|
+
self.at_line_start = False
|
|
325
|
+
sys.stdout.flush()
|
|
326
|
+
|
|
327
|
+
def _toggle_style(self, style, base_prefix):
|
|
328
|
+
if style in self.active_styles:
|
|
329
|
+
self.active_styles.remove(style)
|
|
330
|
+
else:
|
|
331
|
+
self.active_styles.add(style)
|
|
332
|
+
style_string = self.ANSI["reset"] + base_prefix
|
|
333
|
+
for s in self.active_styles:
|
|
334
|
+
style_string += self.ANSI[s]
|
|
335
|
+
self.word_buffer += style_string
|
|
336
|
+
|
|
337
|
+
def terminal_stream(self, text):
|
|
338
|
+
self.term_width = shutil.get_terminal_size().columns
|
|
339
|
+
for char in text:
|
|
340
|
+
self.buffer += char
|
|
341
|
+
self.process_buffer()
|
|
342
|
+
self.process_buffer(final=True)
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
parser = MarkdownStreamer()
|
|
346
|
+
if __name__ == "__main__":
|
|
347
|
+
parser = MarkdownStreamer()
|
|
348
|
+
test_text = """# Nested Test
|
|
349
|
+
> Inside a quote
|
|
350
|
+
> ```javascript
|
|
351
|
+
> function hello() {
|
|
352
|
+
> console.log("This block should be left-aligned.");
|
|
353
|
+
> }
|
|
354
|
+
> ```
|
|
355
|
+
"""
|
|
356
|
+
parser.terminal_stream(test_text)
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def read_stdin_chunks(chunk_size=16):
|
|
5
|
+
"""
|
|
6
|
+
Reads from sys.stdin in chunks of a specified size.
|
|
7
|
+
"""
|
|
8
|
+
buffer = ""
|
|
9
|
+
while True:
|
|
10
|
+
# Read a chunk of data from stdin
|
|
11
|
+
chunk = sys.stdin.read(chunk_size)
|
|
12
|
+
buffer += chunk
|
|
13
|
+
if not chunk:
|
|
14
|
+
break
|
|
15
|
+
yield chunk
|
|
16
|
+
return buffer
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
# Example usage: read in 4096 byte chunks (a common buffer size)
|
|
20
|
+
def main():
|
|
21
|
+
try:
|
|
22
|
+
from live_markdown import parser
|
|
23
|
+
|
|
24
|
+
parser.terminal_stream(read_stdin_chunks(1))
|
|
25
|
+
except EOFError:
|
|
26
|
+
pass
|
|
27
|
+
except KeyboardInterrupt:
|
|
28
|
+
# Handle cases where input is interactive and needs manual termination (Ctrl+D/Ctrl+Z)
|
|
29
|
+
pass
|
|
File without changes
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
"""
|
|
2
|
+
A collection of high-complexity Markdown strings designed to stress-test
|
|
3
|
+
terminal-based streamers and parsers.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
TEST_SUITE = {
|
|
7
|
+
"indentation_hell": """# 🏗️ Indentation Hell
|
|
8
|
+
This test checks if the parser can handle shifting indentation levels without losing track of the background.
|
|
9
|
+
|
|
10
|
+
```python
|
|
11
|
+
def nested_indent():
|
|
12
|
+
if True:
|
|
13
|
+
for i in range(1):
|
|
14
|
+
print("Level 1 (4 spaces)")
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
```javascript
|
|
18
|
+
console.log("Level 2 (8 spaces)");
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
echo "Level 3 (12 spaces)"
|
|
23
|
+
```
|
|
24
|
+
""",
|
|
25
|
+
"ansi_collision": """# 💥 ANSI Collision Test
|
|
26
|
+
This test places high-density formatting right at the typical 80-character terminal wrap boundary.
|
|
27
|
+
|
|
28
|
+
**Bold**_Italic_`Code`**Bold**_Italic_`Code`**Bold**_Italic_`Code`**Bold**_Italic_`Code`**Bold**_Italic_`Code`**Bold**_Italic_`Code`**Bold**_Italic_`Code`**Bold**_Italic_`Code`
|
|
29
|
+
|
|
30
|
+
> > > > Deeply nested quotes to see if the prefixing logic correctly calculates width and wraps the text to the next line with the correct prefix.
|
|
31
|
+
""",
|
|
32
|
+
"code_in_code": """# 🪆 Matryoshka Code Blocks
|
|
33
|
+
Markdown-in-Markdown recursion test.
|
|
34
|
+
|
|
35
|
+
```markdown
|
|
36
|
+
# Level 1
|
|
37
|
+
This is a markdown block.
|
|
38
|
+
|
|
39
|
+
```python
|
|
40
|
+
# Level 2
|
|
41
|
+
print("This is python inside markdown")
|
|
42
|
+
```
|
|
43
|
+
```
|
|
44
|
+
""",
|
|
45
|
+
"mixed_list_logic": """# 📋 Mixed List Logic
|
|
46
|
+
1. First item
|
|
47
|
+
* Nested bullet
|
|
48
|
+
* Another bullet with `inline code`
|
|
49
|
+
2. Second item
|
|
50
|
+
> A blockquote inside a list item.
|
|
51
|
+
> ```javascript
|
|
52
|
+
> console.log("Code inside quote inside list");
|
|
53
|
+
> ```
|
|
54
|
+
3. Third item
|
|
55
|
+
---
|
|
56
|
+
Horizontal rule inside list.
|
|
57
|
+
""",
|
|
58
|
+
"function_call_chaos": """# 🚀 Function Call Chaos
|
|
59
|
+
This test case features a massive, deeply nested function call with extreme line lengths to stress-test horizontal wrapping and vertical clearing.
|
|
60
|
+
|
|
61
|
+
```python
|
|
62
|
+
def main_orchestrator():
|
|
63
|
+
# Deeply nested call within an indented block
|
|
64
|
+
result = external_api_service.process_complex_request(
|
|
65
|
+
header_data={"auth_token": "abc_123_xyz_pdq_long_token_string_that_keeps_going_and_going_to_force_a_wrap_within_a_dictionary_definition"},
|
|
66
|
+
payload=[
|
|
67
|
+
{
|
|
68
|
+
"id": 1,
|
|
69
|
+
"message": "The quick brown fox jumps over the lazy dog while the streamer tries to maintain the background color across four physical lines of terminal output.",
|
|
70
|
+
"metadata": {
|
|
71
|
+
"nested_call": compute_secondary_metrics(
|
|
72
|
+
source_id="SRC-999-X-RAY-ZULU-OMEGA-LONG-ID-0001",
|
|
73
|
+
threshold=0.999999999999,
|
|
74
|
+
callback=lambda x: print(f"Recursive string inside callback with **Markdown** markers: {x}")
|
|
75
|
+
)
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
],
|
|
79
|
+
timeout=None,
|
|
80
|
+
retry_policy="exponential_backoff_with_jitter_and_multiple_other_parameters_that_extend_this_line_to_the_absolute_edge_of_the_screen_buffer"
|
|
81
|
+
)
|
|
82
|
+
return result
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
> **Note**: If the background color "leaks" or the lines above the function call are duplicated during the streaming of the `payload` section, the wrapping-aware clearing logic has failed.
|
|
86
|
+
""",
|
|
87
|
+
"unclosed_chaos": """# ⚠️ Unclosed Chaos
|
|
88
|
+
Testing how the streamer handles incomplete syntax.
|
|
89
|
+
|
|
90
|
+
```python
|
|
91
|
+
def unfinished():
|
|
92
|
+
print("This block never closes...
|
|
93
|
+
""",
|
|
94
|
+
}
|
|
95
|
+
from live_markdown import parser
|
|
96
|
+
|
|
97
|
+
for key, val in TEST_SUITE.items():
|
|
98
|
+
print(key)
|
|
99
|
+
parser.terminal_stream(val)
|
|
100
|
+
pass
|
|
@@ -0,0 +1,408 @@
|
|
|
1
|
+
import re
|
|
2
|
+
import shutil
|
|
3
|
+
import sys
|
|
4
|
+
import time
|
|
5
|
+
|
|
6
|
+
# Note: In this environment, pygments is available for syntax highlighting.
|
|
7
|
+
try:
|
|
8
|
+
from pygments import highlight
|
|
9
|
+
from pygments.formatters import Terminal256Formatter
|
|
10
|
+
from pygments.lexers import (
|
|
11
|
+
BashLexer,
|
|
12
|
+
JavascriptLexer,
|
|
13
|
+
MarkdownLexer,
|
|
14
|
+
PythonLexer,
|
|
15
|
+
get_lexer_by_name,
|
|
16
|
+
)
|
|
17
|
+
from pygments.util import ClassNotFound
|
|
18
|
+
except ImportError:
|
|
19
|
+
# Fallback for environments without pygments (though usually available here)
|
|
20
|
+
print("Pygments not found. Highlighting will be disabled.")
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class MarkdownStreamer:
|
|
24
|
+
"""
|
|
25
|
+
A terminal-based streamer that parses Markdown syntax on the fly.
|
|
26
|
+
Supports recursive rendering with visual indentation and symmetrical colored dividers.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
# Palette for nesting depths
|
|
30
|
+
BGS = [
|
|
31
|
+
"\033[48;5;235m", # Depth 1: Darkest
|
|
32
|
+
"\033[48;5;237m", # Depth 2
|
|
33
|
+
"\033[48;5;239m", # Depth 3
|
|
34
|
+
"\033[48;5;241m", # Depth 4: Lightest
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
# Palette for divider lines (slightly darker/different than the block itself)
|
|
38
|
+
DIVIDER_BGS = [
|
|
39
|
+
"\033[48;5;232m", # Divider 1
|
|
40
|
+
"\033[48;5;234m", # Divider 2
|
|
41
|
+
"\033[48;5;236m", # Divider 3
|
|
42
|
+
"\033[48;5;238m", # Divider 4
|
|
43
|
+
]
|
|
44
|
+
|
|
45
|
+
ANSI = {
|
|
46
|
+
"reset": "\033[0m",
|
|
47
|
+
"bold": "\033[1m",
|
|
48
|
+
"italic": "\033[3m",
|
|
49
|
+
"code_in": "\033[36m",
|
|
50
|
+
"header": "\033[1;95m",
|
|
51
|
+
"quote": "\033[1;34m",
|
|
52
|
+
"hr": "\033[90m",
|
|
53
|
+
"clear_line": "\033[2K",
|
|
54
|
+
"fill_bg": "\033[K",
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
def __init__(self):
|
|
58
|
+
self.buffer = ""
|
|
59
|
+
self.code_block_depth = 0
|
|
60
|
+
self.depth_stack = [] # Tracks language history to allow exiting markdown
|
|
61
|
+
self.current_code_lang = ""
|
|
62
|
+
self.current_code_line = ""
|
|
63
|
+
self.at_line_start = True
|
|
64
|
+
self.active_styles = set()
|
|
65
|
+
|
|
66
|
+
# Word Wrapping State
|
|
67
|
+
self.word_buffer = ""
|
|
68
|
+
self.line_pos = 0
|
|
69
|
+
self.term_width = shutil.get_terminal_size().columns
|
|
70
|
+
|
|
71
|
+
# Pygments setup
|
|
72
|
+
try:
|
|
73
|
+
self.formatter = Terminal256Formatter(style="monokai")
|
|
74
|
+
self.lexers = {
|
|
75
|
+
"python": PythonLexer(),
|
|
76
|
+
"bash": BashLexer(),
|
|
77
|
+
"javascript": JavascriptLexer(),
|
|
78
|
+
"js": JavascriptLexer(),
|
|
79
|
+
}
|
|
80
|
+
except:
|
|
81
|
+
self.formatter = None
|
|
82
|
+
self.lexers = {}
|
|
83
|
+
|
|
84
|
+
def get_block_bg(self):
|
|
85
|
+
"""Returns the background ANSI code based on current depth."""
|
|
86
|
+
if self.code_block_depth == 0:
|
|
87
|
+
return ""
|
|
88
|
+
idx = min(self.code_block_depth - 1, len(self.BGS) - 1)
|
|
89
|
+
return self.BGS[idx]
|
|
90
|
+
|
|
91
|
+
def get_divider_bg(self, depth=None):
|
|
92
|
+
"""Returns the background ANSI code for the divider based on specific or current depth."""
|
|
93
|
+
d = depth if depth is not None else self.code_block_depth
|
|
94
|
+
if d == 0:
|
|
95
|
+
return ""
|
|
96
|
+
idx = min(d - 1, len(self.DIVIDER_BGS) - 1)
|
|
97
|
+
return self.DIVIDER_BGS[idx]
|
|
98
|
+
|
|
99
|
+
def get_indent(self, depth=None):
|
|
100
|
+
"""Returns pure spaces for indentation based on depth."""
|
|
101
|
+
d = depth if depth is not None else self.code_block_depth
|
|
102
|
+
if d <= 1:
|
|
103
|
+
return ""
|
|
104
|
+
return " " * (d - 1)
|
|
105
|
+
|
|
106
|
+
def draw_background_divider(self, depth=None):
|
|
107
|
+
"""Prints an empty line with a unique divider background color."""
|
|
108
|
+
d = depth if depth is not None else self.code_block_depth
|
|
109
|
+
if d == 0:
|
|
110
|
+
return
|
|
111
|
+
bg = self.get_divider_bg(d)
|
|
112
|
+
divider = f"{bg}{self.ANSI['fill_bg']}\n"
|
|
113
|
+
sys.stdout.write(divider)
|
|
114
|
+
|
|
115
|
+
def apply_pygments(self, text, lang="python"):
|
|
116
|
+
"""
|
|
117
|
+
Applies syntax highlighting. Always ensures the block background is present.
|
|
118
|
+
"""
|
|
119
|
+
if not self.formatter:
|
|
120
|
+
return text
|
|
121
|
+
|
|
122
|
+
lang = lang.lower()
|
|
123
|
+
bg = self.get_block_bg()
|
|
124
|
+
indent = self.get_indent()
|
|
125
|
+
|
|
126
|
+
if lang == "markdown":
|
|
127
|
+
return f"{bg}{indent}{text}"
|
|
128
|
+
|
|
129
|
+
lexer = self.lexers.get(lang)
|
|
130
|
+
if not lexer:
|
|
131
|
+
try:
|
|
132
|
+
lexer = get_lexer_by_name(lang)
|
|
133
|
+
self.lexers[lang] = lexer
|
|
134
|
+
except:
|
|
135
|
+
return f"{bg}{indent}{text}"
|
|
136
|
+
|
|
137
|
+
if not text:
|
|
138
|
+
return f"{bg}{indent}"
|
|
139
|
+
|
|
140
|
+
highlighted = highlight(text, lexer, self.formatter)
|
|
141
|
+
clean_hl = highlighted.rstrip("\n")
|
|
142
|
+
# Ensure resetting style doesn't lose the block background
|
|
143
|
+
clean_hl = clean_hl.replace("\033[0m", "\033[0m" + bg)
|
|
144
|
+
|
|
145
|
+
return f"{bg}{indent}{clean_hl}"
|
|
146
|
+
|
|
147
|
+
def _clear_and_move_up(self, highlighted_text):
|
|
148
|
+
"""
|
|
149
|
+
Clears the current line(s) by calculating how many visual lines
|
|
150
|
+
the text occupied based on terminal width.
|
|
151
|
+
"""
|
|
152
|
+
if not highlighted_text:
|
|
153
|
+
sys.stdout.write(f"\r{self.ANSI['clear_line']}")
|
|
154
|
+
return
|
|
155
|
+
|
|
156
|
+
# Strip ANSI to calculate actual visible length
|
|
157
|
+
visible_text = re.sub(r"\033\[[0-9;]*m", "", highlighted_text)
|
|
158
|
+
|
|
159
|
+
# Calculate rows: visible text length / terminal width
|
|
160
|
+
visual_rows = (
|
|
161
|
+
(len(visible_text) - 1) // self.term_width if len(visible_text) > 0 else 0
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
# Move up to the start of the wrapped block
|
|
165
|
+
for _ in range(visual_rows):
|
|
166
|
+
sys.stdout.write("\033[F")
|
|
167
|
+
|
|
168
|
+
sys.stdout.write("\r")
|
|
169
|
+
|
|
170
|
+
# Clear every line that was part of the wrapped text
|
|
171
|
+
for i in range(visual_rows + 1):
|
|
172
|
+
sys.stdout.write(self.ANSI["clear_line"])
|
|
173
|
+
if i < visual_rows:
|
|
174
|
+
sys.stdout.write("\n")
|
|
175
|
+
|
|
176
|
+
# Return to the original top-left position of the cleared block
|
|
177
|
+
for _ in range(visual_rows):
|
|
178
|
+
sys.stdout.write("\033[F")
|
|
179
|
+
sys.stdout.write("\r")
|
|
180
|
+
|
|
181
|
+
def _flush_word(self, prefix=""):
|
|
182
|
+
if not self.word_buffer:
|
|
183
|
+
return
|
|
184
|
+
|
|
185
|
+
visible_word = re.sub(r"\033\[[0-9;]*m", "", self.word_buffer)
|
|
186
|
+
|
|
187
|
+
if self.line_pos + len(visible_word) >= self.term_width - 1:
|
|
188
|
+
sys.stdout.write("\n" + prefix)
|
|
189
|
+
self.line_pos = len(re.sub(r"\033\[[0-9;]*m", "", prefix))
|
|
190
|
+
|
|
191
|
+
sys.stdout.write(self.word_buffer)
|
|
192
|
+
self.line_pos += len(visible_word)
|
|
193
|
+
self.word_buffer = ""
|
|
194
|
+
|
|
195
|
+
def process_buffer(self, final=False):
|
|
196
|
+
while len(self.buffer) >= (1 if final else 30):
|
|
197
|
+
bg = self.get_block_bg()
|
|
198
|
+
indent = self.get_indent()
|
|
199
|
+
|
|
200
|
+
# --- 1. CODE BLOCK HANDLING (Literal Highlighting Mode) ---
|
|
201
|
+
# If we are in a literal code block (NOT markdown), backticks should close it.
|
|
202
|
+
if self.code_block_depth > 0 and self.current_code_lang != "markdown":
|
|
203
|
+
close_match = re.match(r"^[ \t]*```[ \t]*\n?", self.buffer)
|
|
204
|
+
if close_match and self.at_line_start:
|
|
205
|
+
if self.current_code_line:
|
|
206
|
+
typing_hl = self.apply_pygments(
|
|
207
|
+
self.current_code_line, self.current_code_lang
|
|
208
|
+
)
|
|
209
|
+
self._clear_and_move_up(typing_hl)
|
|
210
|
+
final_hl = self.apply_pygments(
|
|
211
|
+
self.current_code_line, self.current_code_lang
|
|
212
|
+
)
|
|
213
|
+
sys.stdout.write(f"{final_hl}{self.ANSI['fill_bg']}\n")
|
|
214
|
+
|
|
215
|
+
self.code_block_depth -= 1
|
|
216
|
+
# Restore previous language from stack
|
|
217
|
+
if self.depth_stack:
|
|
218
|
+
self.current_code_lang = self.depth_stack.pop()
|
|
219
|
+
else:
|
|
220
|
+
self.current_code_lang = ""
|
|
221
|
+
|
|
222
|
+
self.current_code_line = ""
|
|
223
|
+
self.buffer = self.buffer[close_match.end() :]
|
|
224
|
+
self.at_line_start = True
|
|
225
|
+
sys.stdout.write(self.ANSI["reset"])
|
|
226
|
+
if self.code_block_depth > 0:
|
|
227
|
+
sys.stdout.write(self.get_block_bg())
|
|
228
|
+
self.line_pos = 0
|
|
229
|
+
continue
|
|
230
|
+
|
|
231
|
+
# Normal character processing for literal code blocks
|
|
232
|
+
char = self.buffer[0]
|
|
233
|
+
self.buffer = self.buffer[1:]
|
|
234
|
+
if char == "\n":
|
|
235
|
+
typing_hl = self.apply_pygments(
|
|
236
|
+
self.current_code_line, self.current_code_lang
|
|
237
|
+
)
|
|
238
|
+
self._clear_and_move_up(typing_hl)
|
|
239
|
+
final_hl = self.apply_pygments(
|
|
240
|
+
self.current_code_line, self.current_code_lang
|
|
241
|
+
)
|
|
242
|
+
sys.stdout.write(f"{final_hl}{self.ANSI['fill_bg']}\n")
|
|
243
|
+
self.current_code_line = ""
|
|
244
|
+
self.at_line_start = True
|
|
245
|
+
else:
|
|
246
|
+
old_typing_hl = self.apply_pygments(
|
|
247
|
+
self.current_code_line, self.current_code_lang
|
|
248
|
+
)
|
|
249
|
+
self._clear_and_move_up(old_typing_hl)
|
|
250
|
+
self.current_code_line += char
|
|
251
|
+
new_typing_hl = self.apply_pygments(
|
|
252
|
+
self.current_code_line, self.current_code_lang
|
|
253
|
+
)
|
|
254
|
+
sys.stdout.write(f"{new_typing_hl}{self.ANSI['fill_bg']}")
|
|
255
|
+
self.at_line_start = False
|
|
256
|
+
sys.stdout.flush()
|
|
257
|
+
continue
|
|
258
|
+
|
|
259
|
+
# --- 2. MARKDOWN / ROOT HANDLING ---
|
|
260
|
+
if self.at_line_start:
|
|
261
|
+
# Priority 1: Opening a new block (at depth 0 or depth 1 markdown)
|
|
262
|
+
open_match = re.match(r"^[ \t]*```([a-zA-Z]+)[ \t]*\n?", self.buffer)
|
|
263
|
+
if open_match:
|
|
264
|
+
new_lang = open_match.group(1).lower()
|
|
265
|
+
self.depth_stack.append(self.current_code_lang)
|
|
266
|
+
self.code_block_depth += 1
|
|
267
|
+
self.current_code_lang = new_lang
|
|
268
|
+
self.buffer = self.buffer[open_match.end() :]
|
|
269
|
+
self.at_line_start = True
|
|
270
|
+
sys.stdout.write(self.get_block_bg())
|
|
271
|
+
continue
|
|
272
|
+
|
|
273
|
+
# Priority 2: Closing current markdown block
|
|
274
|
+
# We only close a markdown block if we see plain backticks (no language)
|
|
275
|
+
if self.current_code_lang == "markdown":
|
|
276
|
+
close_match = re.match(r"^[ \t]*```[ \t]*(\n|$)", self.buffer)
|
|
277
|
+
if close_match:
|
|
278
|
+
self.code_block_depth -= 1
|
|
279
|
+
if self.depth_stack:
|
|
280
|
+
self.current_code_lang = self.depth_stack.pop()
|
|
281
|
+
else:
|
|
282
|
+
self.current_code_lang = ""
|
|
283
|
+
self.buffer = self.buffer[close_match.end() :]
|
|
284
|
+
self.at_line_start = True
|
|
285
|
+
sys.stdout.write(self.ANSI["reset"])
|
|
286
|
+
if self.code_block_depth > 0:
|
|
287
|
+
sys.stdout.write(self.get_block_bg())
|
|
288
|
+
self.line_pos = 0
|
|
289
|
+
continue
|
|
290
|
+
|
|
291
|
+
if self.buffer.startswith("---"):
|
|
292
|
+
sys.stdout.write(
|
|
293
|
+
f"{bg}{self.ANSI['hr']}{'─' * self.term_width}{self.ANSI['reset']}\n"
|
|
294
|
+
)
|
|
295
|
+
self.buffer = self.buffer[3:]
|
|
296
|
+
self.line_pos = 0
|
|
297
|
+
continue
|
|
298
|
+
|
|
299
|
+
if self.buffer.startswith("#"):
|
|
300
|
+
match = re.match(r"^(#+) ", self.buffer)
|
|
301
|
+
if match:
|
|
302
|
+
self.word_buffer = (
|
|
303
|
+
f"{bg}{indent}{self.ANSI['header']}{match.group()}"
|
|
304
|
+
)
|
|
305
|
+
self._flush_word(prefix=bg + indent)
|
|
306
|
+
self.buffer = self.buffer[len(match.group()) :]
|
|
307
|
+
self.at_line_start = False
|
|
308
|
+
continue
|
|
309
|
+
|
|
310
|
+
# Standard Inline Markdown
|
|
311
|
+
is_quote = "> " in self.buffer[:5] or (
|
|
312
|
+
self.at_line_start and self.buffer.startswith("> ")
|
|
313
|
+
)
|
|
314
|
+
quote_prefix = (
|
|
315
|
+
f"{self.ANSI['quote']}┃ {self.ANSI['reset']}{bg}" if is_quote else ""
|
|
316
|
+
)
|
|
317
|
+
full_prefix = bg + indent + quote_prefix
|
|
318
|
+
|
|
319
|
+
if self.at_line_start:
|
|
320
|
+
sys.stdout.write(bg + indent)
|
|
321
|
+
self.line_pos += len(indent)
|
|
322
|
+
if self.buffer.startswith("> "):
|
|
323
|
+
sys.stdout.write(quote_prefix)
|
|
324
|
+
self.line_pos += 2
|
|
325
|
+
self.buffer = self.buffer[2:]
|
|
326
|
+
|
|
327
|
+
char = self.buffer[0]
|
|
328
|
+
if char == "\\":
|
|
329
|
+
if len(self.buffer) > 1:
|
|
330
|
+
self.word_buffer += self.buffer[1]
|
|
331
|
+
self.buffer = self.buffer[2:]
|
|
332
|
+
else:
|
|
333
|
+
self.buffer = self.buffer[1:]
|
|
334
|
+
elif self.buffer.startswith("**"):
|
|
335
|
+
self._toggle_style("bold", bg + indent)
|
|
336
|
+
self.buffer = self.buffer[2:]
|
|
337
|
+
elif self.buffer.startswith("_"):
|
|
338
|
+
self._toggle_style("italic", bg + indent)
|
|
339
|
+
self.buffer = self.buffer[1:]
|
|
340
|
+
elif self.buffer.startswith("`"):
|
|
341
|
+
self._toggle_style("code_in", bg + indent)
|
|
342
|
+
self.buffer = self.buffer[1:]
|
|
343
|
+
elif char in (" ", "\n"):
|
|
344
|
+
self._flush_word(prefix=full_prefix)
|
|
345
|
+
if char == "\n":
|
|
346
|
+
sys.stdout.write(f"{self.ANSI['fill_bg']}\n")
|
|
347
|
+
self.line_pos = 0
|
|
348
|
+
self.at_line_start = True
|
|
349
|
+
sys.stdout.write(self.ANSI["reset"] + bg)
|
|
350
|
+
self.active_styles.clear()
|
|
351
|
+
else:
|
|
352
|
+
sys.stdout.write(f"{bg} ")
|
|
353
|
+
self.line_pos += 1
|
|
354
|
+
self.buffer = self.buffer[1:]
|
|
355
|
+
else:
|
|
356
|
+
self.word_buffer += char
|
|
357
|
+
self.buffer = self.buffer[1:]
|
|
358
|
+
self.at_line_start = False
|
|
359
|
+
|
|
360
|
+
sys.stdout.flush()
|
|
361
|
+
|
|
362
|
+
def _toggle_style(self, style, base_prefix):
|
|
363
|
+
if style in self.active_styles:
|
|
364
|
+
self.active_styles.remove(style)
|
|
365
|
+
else:
|
|
366
|
+
self.active_styles.add(style)
|
|
367
|
+
style_string = self.ANSI["reset"] + base_prefix
|
|
368
|
+
for s in self.active_styles:
|
|
369
|
+
style_string += self.ANSI[s]
|
|
370
|
+
self.word_buffer += style_string
|
|
371
|
+
|
|
372
|
+
def terminal_stream(self, text):
|
|
373
|
+
self.term_width = shutil.get_terminal_size().columns
|
|
374
|
+
for char in text:
|
|
375
|
+
self.buffer += char
|
|
376
|
+
self.process_buffer()
|
|
377
|
+
self.process_buffer(final=True)
|
|
378
|
+
|
|
379
|
+
def end_of_stream(self, text):
|
|
380
|
+
sys.stdout.write(self.ANSI["reset"])
|
|
381
|
+
print("\n\n" + "=" * 20)
|
|
382
|
+
print("STREAM FINISHED")
|
|
383
|
+
print("=" * 20)
|
|
384
|
+
|
|
385
|
+
|
|
386
|
+
if __name__ == "__main__":
|
|
387
|
+
parser = MarkdownStreamer()
|
|
388
|
+
# Test case showing nested code blocks where 'markdown' is the parent
|
|
389
|
+
nested_test = """# Recursive Logic Test
|
|
390
|
+
Encountering backticks with a language should deepen the nest.
|
|
391
|
+
Encountering plain backticks should close the CURRENT nest level.
|
|
392
|
+
|
|
393
|
+
```markdown
|
|
394
|
+
This is depth 1 (Markdown).
|
|
395
|
+
|
|
396
|
+
```python
|
|
397
|
+
def hello():
|
|
398
|
+
print("I am at depth 2")
|
|
399
|
+
```
|
|
400
|
+
|
|
401
|
+
Back to depth 1 markdown.
|
|
402
|
+
```
|
|
403
|
+
|
|
404
|
+
Finished the test at root level.
|
|
405
|
+
"""
|
|
406
|
+
parser.terminal_stream(nested_test)
|
|
407
|
+
# sys.stdout.write(parser.ANSI["reset"])
|
|
408
|
+
# parser.end_of_stream(nested_test)
|