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.
@@ -0,0 +1,8 @@
1
+ Metadata-Version: 2.3
2
+ Name: live-markdown
3
+ Version: 0.1.0
4
+ Summary: Add your description here
5
+ Requires-Dist: pygments>=2.19.2
6
+ Requires-Python: >=3.8
7
+ Description-Content-Type: text/markdown
8
+
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)