streamdown 0.13.0__py3-none-any.whl → 0.15.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.
- streamdown/scrape/file_0.py +22 -0
- streamdown/scrape/file_1.js +27 -0
- streamdown/scrape/file_2.cpp +23 -0
- streamdown/sd.py +123 -78
- {streamdown-0.13.0.dist-info → streamdown-0.15.0.dist-info}/METADATA +19 -16
- streamdown-0.15.0.dist-info/RECORD +13 -0
- streamdown-0.13.0.dist-info/RECORD +0 -10
- {streamdown-0.13.0.dist-info → streamdown-0.15.0.dist-info}/WHEEL +0 -0
- {streamdown-0.13.0.dist-info → streamdown-0.15.0.dist-info}/entry_points.txt +0 -0
- {streamdown-0.13.0.dist-info → streamdown-0.15.0.dist-info}/licenses/LICENSE.MIT +0 -0
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
def fizzbuzz(n):
|
|
2
|
+
for i in range(1, n + 1):
|
|
3
|
+
if i % 3 == 0 and i % 5 == 0:
|
|
4
|
+
print("FizzBuzz")
|
|
5
|
+
elif i % 3 == 0:
|
|
6
|
+
print("Fizz")
|
|
7
|
+
elif i % 5 == 0:
|
|
8
|
+
print("Buzz")
|
|
9
|
+
else:
|
|
10
|
+
print(i)
|
|
11
|
+
|
|
12
|
+
# Example usage: Print FizzBuzz up to 100 Example usage: Print FizzBuzz up to 100 Example usage: Print FizzBuzz up to 100 Example usage: Print FizzBuzz up to 100
|
|
13
|
+
fizzbuzz(100)
|
|
14
|
+
|
|
15
|
+
# Example usage: different range:
|
|
16
|
+
fizzbuzz(20)
|
|
17
|
+
|
|
18
|
+
#Example usage: one line output (list comprehension)
|
|
19
|
+
def fizzbuzz_oneline(n):
|
|
20
|
+
print(["FizzBuzz" if i%3==0 and i%5==0 else "Fizz" if i%3==0 else "Buzz" if i%5==0 else i for i in range(1,n+1)])
|
|
21
|
+
|
|
22
|
+
fizzbuzz_oneline(30)
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
function fizzBuzz(n) {
|
|
2
|
+
for (let i = 1; i <= n; i++) {
|
|
3
|
+
if (i % 3 === 0 && i % 5 === 0) {
|
|
4
|
+
console.log("FizzBuzz");
|
|
5
|
+
} else if (i % 3 === 0) {
|
|
6
|
+
console.log("Fizz");
|
|
7
|
+
} else if (i % 5 === 0) {
|
|
8
|
+
console.log("Buzz");
|
|
9
|
+
} else {
|
|
10
|
+
console.log(i);
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// Example usage:
|
|
16
|
+
fizzBuzz(100);
|
|
17
|
+
|
|
18
|
+
// Example usage: different range
|
|
19
|
+
fizzBuzz(25);
|
|
20
|
+
|
|
21
|
+
// Example one-line output. (arrow function & ternary operator)
|
|
22
|
+
const fizzBuzzOneLine = n => {
|
|
23
|
+
for (let i = 1; i <= n; i++) {
|
|
24
|
+
console.log((i % 3 === 0 ? (i % 5 === 0 ? "FizzBuzz" : "Fizz") : (i % 5 === 0 ? "Buzz" : i)));
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
fizzBuzzOneLine(30);
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
#include <iostream>
|
|
2
|
+
|
|
3
|
+
void fizzBuzz(int n) {
|
|
4
|
+
for (int i = 1; i <= n; i++) {
|
|
5
|
+
if (i % 3 == 0 && i % 5 == 0) {
|
|
6
|
+
std::cout << "FizzBuzz" << std::endl;
|
|
7
|
+
} else if (i % 3 == 0) {
|
|
8
|
+
std::cout << "Fizz" << std::endl;
|
|
9
|
+
} else if (i % 5 == 0) {
|
|
10
|
+
std::cout << "Buzz" << std::endl;
|
|
11
|
+
} else {
|
|
12
|
+
std::cout << i << std::endl;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
int main() {
|
|
18
|
+
fizzBuzz(100);
|
|
19
|
+
|
|
20
|
+
// Example usage: different range
|
|
21
|
+
fizzBuzz(35);
|
|
22
|
+
return 0;
|
|
23
|
+
}
|
streamdown/sd.py
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
# "pygments",
|
|
6
6
|
# "pylatexenc",
|
|
7
7
|
# "appdirs",
|
|
8
|
+
# "term-image",
|
|
8
9
|
# "toml"
|
|
9
10
|
# ]
|
|
10
11
|
# ///
|
|
@@ -23,7 +24,9 @@ import colorsys
|
|
|
23
24
|
import base64
|
|
24
25
|
import importlib
|
|
25
26
|
from io import BytesIO
|
|
27
|
+
from term_image.image import from_file, from_url
|
|
26
28
|
import pygments.util
|
|
29
|
+
from functools import reduce
|
|
27
30
|
from argparse import ArgumentParser
|
|
28
31
|
from pygments import highlight
|
|
29
32
|
from pygments.lexers import get_lexer_by_name
|
|
@@ -52,7 +55,7 @@ Dark = { H = 1.00, S = 1.50, V = 0.25 }
|
|
|
52
55
|
Mid = { H = 1.00, S = 1.00, V = 0.50 }
|
|
53
56
|
Symbol = { H = 1.00, S = 1.00, V = 1.50 }
|
|
54
57
|
Head = { H = 1.00, S = 2.00, V = 1.50 }
|
|
55
|
-
Grey = { H = 1.00, S = 0.
|
|
58
|
+
Grey = { H = 1.00, S = 0.25, V = 1.37 }
|
|
56
59
|
Bright = { H = 1.00, S = 2.00, V = 2.00 }
|
|
57
60
|
Syntax = "monokai"
|
|
58
61
|
"""
|
|
@@ -80,16 +83,17 @@ BGRESET = "\033[49m"
|
|
|
80
83
|
BOLD = ["\033[1m", "\033[22m"]
|
|
81
84
|
UNDERLINE = ["\033[4m", "\033[24m"]
|
|
82
85
|
ITALIC = ["\033[3m", "\033[23m"]
|
|
86
|
+
STRIKEOUT = ["\033[9m", "\033[29m"]
|
|
87
|
+
SUPER = [ 0x2070, 0x00B9, 0x00B2, 0x00B3, 0x2074, 0x2075, 0x2076, 0x2077, 0x2078, 0x2079 ]
|
|
83
88
|
|
|
84
89
|
ESCAPE = r"\033\[[0-9;]*[mK]"
|
|
85
90
|
ANSIESCAPE = r'\033(?:\[[0-9;?]*[a-zA-Z]|][0-9]*;;.*?\\|\\)'
|
|
86
|
-
#r"\033(\[[0-9;]*[mK]|][0-9]*;;.*?\\|\\)"
|
|
87
91
|
KEYCODE_RE = re.compile(r'\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])')
|
|
88
92
|
|
|
89
93
|
visible = lambda x: re.sub(ANSIESCAPE, "", x)
|
|
90
94
|
visible_length = lambda x: len(visible(x))
|
|
91
|
-
|
|
92
95
|
extract_ansi_codes = lambda text: re.findall(ESCAPE, text)
|
|
96
|
+
remove_ansi = lambda line, codeList: reduce(lambda line, code: line.replace(code, ''), codeList, line)
|
|
93
97
|
|
|
94
98
|
def debug_write(text):
|
|
95
99
|
if state.Logging:
|
|
@@ -131,7 +135,10 @@ class ParseState:
|
|
|
131
135
|
self.Clipboard = _features.get("Clipboard")
|
|
132
136
|
self.Logging = _features.get("Logging")
|
|
133
137
|
self.Timeout = _features.get("Timeout")
|
|
138
|
+
|
|
134
139
|
self.WidthArg = None
|
|
140
|
+
self.WidthFull = None
|
|
141
|
+
self.WidthWrap = False
|
|
135
142
|
|
|
136
143
|
# If the entire block is indented this will
|
|
137
144
|
# tell us what that is
|
|
@@ -159,24 +166,27 @@ class ParseState:
|
|
|
159
166
|
self.in_italic = False
|
|
160
167
|
self.in_table = False # (Code.[Header|Body] | False)
|
|
161
168
|
self.in_underline = False
|
|
169
|
+
self.in_strikeout = False
|
|
162
170
|
self.block_depth = 0
|
|
163
171
|
|
|
164
172
|
self.exec_sub = None
|
|
165
173
|
self.exec_master = None
|
|
166
174
|
self.exec_slave = None
|
|
167
175
|
self.exec_kb = 0
|
|
168
|
-
self.exec_israw = False
|
|
169
176
|
|
|
170
177
|
self.exit = 0
|
|
171
178
|
self.where_from = None
|
|
172
179
|
|
|
173
180
|
def current(self):
|
|
174
|
-
state = { 'code': self.in_code, 'bold': self.in_bold, 'italic': self.in_italic, 'underline': self.in_underline }
|
|
181
|
+
state = { 'inline': self.inline_code, 'code': self.in_code, 'bold': self.in_bold, 'italic': self.in_italic, 'underline': self.in_underline }
|
|
175
182
|
state['none'] = all(item is False for item in state.values())
|
|
176
183
|
return state
|
|
177
184
|
|
|
185
|
+
def reset_inline(self):
|
|
186
|
+
self.inline_code = self.in_bold = self.in_italic = self.in_underline = False
|
|
187
|
+
|
|
178
188
|
def space_left(self):
|
|
179
|
-
return
|
|
189
|
+
return Style.MarginSpaces + (Style.Blockquote * self.block_depth) if len(self.current_line) == 0 else ""
|
|
180
190
|
|
|
181
191
|
state = ParseState()
|
|
182
192
|
|
|
@@ -233,17 +243,20 @@ def emit_h(level, text):
|
|
|
233
243
|
text = line_format(text)
|
|
234
244
|
spaces_to_center = ((state.Width - visible_length(text)) / 2)
|
|
235
245
|
if level == 1: #
|
|
236
|
-
return f"\n{
|
|
246
|
+
return f"\n{state.space_left()}{BOLD[0]}{' ' * math.floor(spaces_to_center)}{text}{' ' * math.ceil(spaces_to_center)}{BOLD[1]}\n"
|
|
237
247
|
elif level == 2: ##
|
|
238
|
-
return f"\n{
|
|
248
|
+
return f"\n{state.space_left()}{BOLD[0]}{FG}{Style.Bright}{' ' * math.floor(spaces_to_center)}{text}{' ' * math.ceil(spaces_to_center)}{RESET}\n\n"
|
|
239
249
|
elif level == 3: ###
|
|
240
|
-
return f"{
|
|
250
|
+
return f"{state.space_left()}{FG}{Style.Head}{BOLD[0]}{text}{RESET}"
|
|
241
251
|
elif level == 4: ####
|
|
242
|
-
return f"{
|
|
252
|
+
return f"{state.space_left()}{FG}{Style.Symbol}{text}{RESET}"
|
|
243
253
|
else: # level 5 or 6
|
|
244
|
-
return f"{
|
|
254
|
+
return f"{state.space_left()}{text}{RESET}"
|
|
245
255
|
|
|
246
256
|
def code_wrap(text_in):
|
|
257
|
+
if state.WidthWrap and len(text_in) > state.WidthFull:
|
|
258
|
+
return (0, [text_in])
|
|
259
|
+
|
|
247
260
|
# get the indentation of the first line
|
|
248
261
|
indent = len(text_in) - len(text_in.lstrip())
|
|
249
262
|
text = text_in.lstrip()
|
|
@@ -259,6 +272,7 @@ def code_wrap(text_in):
|
|
|
259
272
|
|
|
260
273
|
return (indent, res)
|
|
261
274
|
|
|
275
|
+
|
|
262
276
|
# This marvelously obscure code "compacts" long lines of repetitive ANSI format strings by
|
|
263
277
|
# removing duplicates. Here's how it works
|
|
264
278
|
def ansi_collapse(codelist, inp):
|
|
@@ -337,8 +351,20 @@ def text_wrap(text, width = -1, indent = 0, first_line_prefix="", subsequent_lin
|
|
|
337
351
|
return lines
|
|
338
352
|
|
|
339
353
|
def line_format(line):
|
|
340
|
-
|
|
341
|
-
|
|
354
|
+
not_text = lambda token: not token or len(token.rstrip()) != len(token)
|
|
355
|
+
footnotes = lambda match: ''.join([chr(SUPER[int(i)]) for i in match.group(1)])
|
|
356
|
+
|
|
357
|
+
def process_images(match):
|
|
358
|
+
url = match.group(2)
|
|
359
|
+
try:
|
|
360
|
+
if re.match(r"https://", url.lower()):
|
|
361
|
+
image = from_url(url)
|
|
362
|
+
else:
|
|
363
|
+
image = from_file(url)
|
|
364
|
+
image.height = 20
|
|
365
|
+
print(f"{image:|.-1#}")
|
|
366
|
+
except:
|
|
367
|
+
return match.group(2)
|
|
342
368
|
|
|
343
369
|
# Apply OSC 8 hyperlink formatting after other formatting
|
|
344
370
|
def process_links(match):
|
|
@@ -346,8 +372,11 @@ def line_format(line):
|
|
|
346
372
|
url = match.group(2)
|
|
347
373
|
return f'\033]8;;{url}\033\\{Style.Link}{description}{UNDERLINE[1]}\033]8;;\033\\{FGRESET}'
|
|
348
374
|
|
|
375
|
+
line = re.sub(r"\!\[([^\]]*)\]\(([^\)]+)\)", process_images, line)
|
|
349
376
|
line = re.sub(r"\[([^\]]+)\]\(([^\)]+)\)", process_links, line)
|
|
350
|
-
|
|
377
|
+
line = re.sub(r"\[\^(\d+)\]:?", footnotes, line)
|
|
378
|
+
|
|
379
|
+
tokenList = re.finditer(r"((~~|\*\*_|_\*\*|\*{1,3}|_{1,3}|`+)|[^~_*`]+)", line)
|
|
351
380
|
result = ""
|
|
352
381
|
|
|
353
382
|
for match in tokenList:
|
|
@@ -355,8 +384,13 @@ def line_format(line):
|
|
|
355
384
|
next_token = line[match.end()] if match.end() < len(line) else ""
|
|
356
385
|
prev_token = line[match.start()-1] if match.start() > 0 else ""
|
|
357
386
|
|
|
358
|
-
|
|
359
|
-
|
|
387
|
+
# This trick makes sure that things like `` ` `` render right.
|
|
388
|
+
if "`" in token and (not state.inline_code or state.inline_code == token):
|
|
389
|
+
if state.inline_code:
|
|
390
|
+
state.inline_code = False
|
|
391
|
+
else:
|
|
392
|
+
state.inline_code = token
|
|
393
|
+
|
|
360
394
|
if state.inline_code:
|
|
361
395
|
result += f'{BG}{Style.Mid}'
|
|
362
396
|
else:
|
|
@@ -367,7 +401,17 @@ def line_format(line):
|
|
|
367
401
|
elif state.inline_code:
|
|
368
402
|
result += token
|
|
369
403
|
|
|
370
|
-
elif token ==
|
|
404
|
+
elif token == '~~' and (state.in_strikeout or not_text(prev_token)):
|
|
405
|
+
state.in_strikeout = not state.in_strikeout
|
|
406
|
+
result += STRIKEOUT[0] if state.in_strikeout else STRIKEOUT[1]
|
|
407
|
+
|
|
408
|
+
elif token in ['**_','_**','___','***'] and (state.in_bold or not_text(prev_token)):
|
|
409
|
+
state.in_bold = not state.in_bold
|
|
410
|
+
result += BOLD[0] if state.in_bold else BOLD[1]
|
|
411
|
+
state.in_italic = not state.in_italic
|
|
412
|
+
result += ITALIC[0] if state.in_italic else ITALIC[1]
|
|
413
|
+
|
|
414
|
+
elif (token == '__' or token == "**") and (state.in_bold or not_text(prev_token)):
|
|
371
415
|
state.in_bold = not state.in_bold
|
|
372
416
|
result += BOLD[0] if state.in_bold else BOLD[1]
|
|
373
417
|
|
|
@@ -380,7 +424,7 @@ def line_format(line):
|
|
|
380
424
|
else:
|
|
381
425
|
result += token
|
|
382
426
|
|
|
383
|
-
elif token == "_" and (state.in_underline or not_text(prev_token)):
|
|
427
|
+
elif token == "_" and (state.in_underline or (not_text(prev_token) and next_token.isalnum())):
|
|
384
428
|
state.in_underline = not state.in_underline
|
|
385
429
|
result += UNDERLINE[0] if state.in_underline else UNDERLINE[1]
|
|
386
430
|
else:
|
|
@@ -422,8 +466,6 @@ def parse(stream):
|
|
|
422
466
|
|
|
423
467
|
if len(ready_in) == 0:
|
|
424
468
|
TimeoutIx += 1
|
|
425
|
-
|
|
426
|
-
|
|
427
469
|
|
|
428
470
|
elif stream.fileno() in ready_in:
|
|
429
471
|
byte = os.read(stream.fileno(), 1)
|
|
@@ -462,13 +504,30 @@ def parse(stream):
|
|
|
462
504
|
# Run through the plugins first
|
|
463
505
|
res = latex.Plugin(line, state, Style)
|
|
464
506
|
if res is True:
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
continue
|
|
468
|
-
elif res is not None:
|
|
469
|
-
for row in res:
|
|
470
|
-
yield row
|
|
507
|
+
# This means everything was consumed by our plugin and
|
|
508
|
+
# we should continue
|
|
471
509
|
continue
|
|
510
|
+
elif res is not None:
|
|
511
|
+
for row in res:
|
|
512
|
+
yield row
|
|
513
|
+
continue
|
|
514
|
+
|
|
515
|
+
# running this here avoids stray |
|
|
516
|
+
block_match = re.match(r"^\s*((>\s*)+|<.?think>)", line)
|
|
517
|
+
if not state.in_code and block_match:
|
|
518
|
+
if block_match.group(1) == '</think>':
|
|
519
|
+
state.block_depth = 0
|
|
520
|
+
yield RESET
|
|
521
|
+
elif block_match.group(1) == '<think>':
|
|
522
|
+
state.block_depth = 1
|
|
523
|
+
else:
|
|
524
|
+
state.block_depth = block_match.group(0).count('>')
|
|
525
|
+
# we also need to consume those tokens
|
|
526
|
+
line = line[len(block_match.group(0)):]
|
|
527
|
+
else:
|
|
528
|
+
if state.block_depth > 0:
|
|
529
|
+
line = FGRESET + line
|
|
530
|
+
state.block_depth = 0
|
|
472
531
|
|
|
473
532
|
# --- Collapse Multiple Empty Lines if not in code blocks ---
|
|
474
533
|
if not state.in_code:
|
|
@@ -483,7 +542,7 @@ def parse(stream):
|
|
|
483
542
|
else:
|
|
484
543
|
last_line_empty_cache = state.last_line_empty
|
|
485
544
|
state.last_line_empty = False
|
|
486
|
-
|
|
545
|
+
|
|
487
546
|
# This is to reset our top-level line-based systems
|
|
488
547
|
# \n buffer
|
|
489
548
|
if not state.in_list and len(state.ordered_list_numbers) > 0:
|
|
@@ -506,28 +565,11 @@ def parse(stream):
|
|
|
506
565
|
if state.in_table and not state.in_code and not re.match(r"^\s*\|.+\|\s*$", line):
|
|
507
566
|
state.in_table = False
|
|
508
567
|
|
|
509
|
-
block_match = re.match(r"^((> )*|<.?think>)", line)
|
|
510
|
-
if block_match:
|
|
511
|
-
if block_match.group(1) == '</think>':
|
|
512
|
-
state.block_depth = 0
|
|
513
|
-
yield(RESET)
|
|
514
|
-
elif block_match.group(1) == '<think>':
|
|
515
|
-
state.block_depth = 1
|
|
516
|
-
else:
|
|
517
|
-
state.block_depth = int(len(block_match.group(0)) / 2)
|
|
518
|
-
# we also need to consume those tokens
|
|
519
|
-
line = line[state.block_depth * 2:]
|
|
520
|
-
else:
|
|
521
|
-
if state.block_depth > 0:
|
|
522
|
-
yield RESET
|
|
523
|
-
state.block_depth = 0
|
|
524
|
-
|
|
525
568
|
#
|
|
526
569
|
# <code><pre>
|
|
527
570
|
#
|
|
528
|
-
# This needs to be first
|
|
529
571
|
if not state.in_code:
|
|
530
|
-
code_match = re.match(r"\s*```\s*([^\s]+|$)", line)
|
|
572
|
+
code_match = re.match(r"\s*```\s*([^\s]+|$)$", line)
|
|
531
573
|
if code_match:
|
|
532
574
|
state.in_code = Code.Backtick
|
|
533
575
|
state.code_language = code_match.group(1) or 'Bash'
|
|
@@ -563,6 +605,7 @@ def parse(stream):
|
|
|
563
605
|
try:
|
|
564
606
|
ext = get_lexer_by_name(state.code_language).filenames[0].split('.')[-1]
|
|
565
607
|
except:
|
|
608
|
+
logging.warning(f"Can't find canonical extension for {state.code_language}")
|
|
566
609
|
pass
|
|
567
610
|
|
|
568
611
|
open(os.path.join(state.scrape, f"file_{state.scrape_ix}.{ext}"), 'w').write(state.code_buffer)
|
|
@@ -584,7 +627,8 @@ def parse(stream):
|
|
|
584
627
|
|
|
585
628
|
|
|
586
629
|
if code_type == Code.Backtick:
|
|
587
|
-
|
|
630
|
+
state.code_indent = len(line) - len(line.lstrip())
|
|
631
|
+
continue
|
|
588
632
|
else:
|
|
589
633
|
# otherwise we don't want to consume
|
|
590
634
|
# nor do we want to be here.
|
|
@@ -600,11 +644,6 @@ def parse(stream):
|
|
|
600
644
|
custom_style = get_style_by_name("default")
|
|
601
645
|
|
|
602
646
|
formatter = Terminal256Formatter(style=custom_style)
|
|
603
|
-
for i, char in enumerate(line):
|
|
604
|
-
if char == " ":
|
|
605
|
-
state.code_indent += 1
|
|
606
|
-
else:
|
|
607
|
-
break
|
|
608
647
|
line = line[state.code_indent :]
|
|
609
648
|
|
|
610
649
|
elif line.startswith(" " * state.code_indent):
|
|
@@ -628,6 +667,10 @@ def parse(stream):
|
|
|
628
667
|
# then naively search back until our visible_lengths() match. This is not fast and there's certainly smarter
|
|
629
668
|
# ways of doing it but this thing is way trickery than you think
|
|
630
669
|
highlighted_code = highlight(state.code_buffer + tline, lexer, formatter)
|
|
670
|
+
|
|
671
|
+
# Sometimes the highlighter will do things like a full reset or a background reset.
|
|
672
|
+
# This is not what we want
|
|
673
|
+
highlighted_code = re.sub(r"\033\[39(;00|)m", '', highlighted_code)
|
|
631
674
|
|
|
632
675
|
# Since we are streaming we ignore the resets and newlines at the end
|
|
633
676
|
if highlighted_code.endswith(FGRESET + "\n"):
|
|
@@ -652,7 +695,7 @@ def parse(stream):
|
|
|
652
695
|
|
|
653
696
|
code_line = ' ' * indent + this_batch.strip()
|
|
654
697
|
|
|
655
|
-
margin = state.WidthFull - visible_length(code_line)
|
|
698
|
+
margin = state.WidthFull - visible_length(code_line) % state.WidthFull
|
|
656
699
|
yield f"{Style.Codebg}{code_line}{' ' * max(0, margin)}{BGRESET}"
|
|
657
700
|
continue
|
|
658
701
|
except Goto:
|
|
@@ -714,18 +757,18 @@ def parse(stream):
|
|
|
714
757
|
if list_type == "number":
|
|
715
758
|
state.ordered_list_numbers[-1] += 1
|
|
716
759
|
|
|
717
|
-
indent = len(state.list_item_stack) * 2
|
|
760
|
+
indent = (len(state.list_item_stack) - 1) * 2
|
|
718
761
|
|
|
719
762
|
wrap_width = state.Width - indent - (2 * Style.ListIndent)
|
|
720
763
|
|
|
721
764
|
bullet = '•'
|
|
722
765
|
if list_type == "number":
|
|
723
766
|
list_number = int(max(state.ordered_list_numbers[-1], float(list_item_match.group(2))))
|
|
724
|
-
bullet =
|
|
767
|
+
bullet = str(list_number)
|
|
725
768
|
|
|
726
769
|
wrapped_lineList = text_wrap(content, wrap_width, Style.ListIndent,
|
|
727
|
-
first_line_prefix = f"{(' ' * (indent
|
|
728
|
-
subsequent_line_prefix = " " * (indent
|
|
770
|
+
first_line_prefix = f"{(' ' * (indent ))}{FG}{Style.Symbol}{bullet}{RESET} ",
|
|
771
|
+
subsequent_line_prefix = " " * (indent)
|
|
729
772
|
)
|
|
730
773
|
for wrapped_line in wrapped_lineList:
|
|
731
774
|
yield f"{state.space_left()}{wrapped_line}\n"
|
|
@@ -742,7 +785,7 @@ def parse(stream):
|
|
|
742
785
|
#
|
|
743
786
|
# <hr>
|
|
744
787
|
#
|
|
745
|
-
hr_match = re.match(r"^[\s]*([
|
|
788
|
+
hr_match = re.match(r"^[\s]*([-\*=_]){3,}[\s]*$", line)
|
|
746
789
|
if hr_match:
|
|
747
790
|
if state.last_line_empty or last_line_empty_cache:
|
|
748
791
|
# print a horizontal rule using a unicode midline
|
|
@@ -764,12 +807,6 @@ def parse(stream):
|
|
|
764
807
|
for wrapped_line in wrapped_lines:
|
|
765
808
|
yield f"{state.space_left()}{wrapped_line}\n"
|
|
766
809
|
|
|
767
|
-
def get_terminal_width():
|
|
768
|
-
try:
|
|
769
|
-
return shutil.get_terminal_size().columns
|
|
770
|
-
except (AttributeError, OSError):
|
|
771
|
-
return 80
|
|
772
|
-
|
|
773
810
|
def emit(inp):
|
|
774
811
|
buffer = []
|
|
775
812
|
flush = False
|
|
@@ -795,6 +832,9 @@ def emit(inp):
|
|
|
795
832
|
state.current_line += chunk
|
|
796
833
|
|
|
797
834
|
buffer.append(chunk)
|
|
835
|
+
# This *might* be dangerous
|
|
836
|
+
state.reset_inline()
|
|
837
|
+
|
|
798
838
|
if flush:
|
|
799
839
|
chunk = "\n".join(buffer)
|
|
800
840
|
buffer = []
|
|
@@ -806,17 +846,10 @@ def emit(inp):
|
|
|
806
846
|
else:
|
|
807
847
|
chunk = buffer.pop(0)
|
|
808
848
|
|
|
809
|
-
|
|
810
|
-
print(chunk, end="", flush=True)
|
|
811
|
-
else:
|
|
812
|
-
sys.stdout.write(chunk)
|
|
849
|
+
print(chunk, end="", flush=True)
|
|
813
850
|
|
|
814
851
|
if len(buffer):
|
|
815
|
-
|
|
816
|
-
if state.is_pty:
|
|
817
|
-
print(chunk, end="", flush=True)
|
|
818
|
-
else:
|
|
819
|
-
sys.stdout.write(chunk)
|
|
852
|
+
print(buffer.pop(0), end="", flush=True)
|
|
820
853
|
|
|
821
854
|
def apply_multipliers(name, H, S, V):
|
|
822
855
|
m = _style.get(name)
|
|
@@ -824,7 +857,20 @@ def apply_multipliers(name, H, S, V):
|
|
|
824
857
|
return ';'.join([str(int(x * 256)) for x in [r, g, b]]) + "m"
|
|
825
858
|
|
|
826
859
|
def width_calc():
|
|
827
|
-
state.WidthFull
|
|
860
|
+
if not state.WidthFull or not state.WidthArg:
|
|
861
|
+
if state.WidthArg:
|
|
862
|
+
state.WidthFull = state.WidthArg
|
|
863
|
+
else:
|
|
864
|
+
width = 80
|
|
865
|
+
|
|
866
|
+
try:
|
|
867
|
+
width = shutil.get_terminal_size().columns
|
|
868
|
+
state.WidthWrap = True
|
|
869
|
+
except (AttributeError, OSError):
|
|
870
|
+
pass
|
|
871
|
+
|
|
872
|
+
state.WidthFull = width
|
|
873
|
+
|
|
828
874
|
state.Width = state.WidthFull - 2 * Style.Margin
|
|
829
875
|
Style.Codepad = [
|
|
830
876
|
f"{RESET}{FG}{Style.Dark}{'▄' * state.WidthFull}{RESET}\n",
|
|
@@ -838,9 +884,9 @@ def main():
|
|
|
838
884
|
parser.add_argument("filenameList", nargs="*", help="Input file to process (also takes stdin)")
|
|
839
885
|
parser.add_argument("-l", "--loglevel", default="INFO", help="Set the logging level")
|
|
840
886
|
parser.add_argument("-c", "--color", default=None, help="Set the hsv base: h,s,v")
|
|
841
|
-
parser.add_argument("-w", "--width", default="0", help="Set the width")
|
|
842
|
-
parser.add_argument("-e", "--exec", help="Wrap a program for more 'proper' i/o handling")
|
|
843
|
-
parser.add_argument("-s", "--scrape", help="Scrape code snippets to a directory")
|
|
887
|
+
parser.add_argument("-w", "--width", default="0", help="Set the width WIDTH")
|
|
888
|
+
parser.add_argument("-e", "--exec", help="Wrap a program EXEC for more 'proper' i/o handling")
|
|
889
|
+
parser.add_argument("-s", "--scrape", help="Scrape code snippets to a directory SCRAPE")
|
|
844
890
|
args = parser.parse_args()
|
|
845
891
|
|
|
846
892
|
if args.color:
|
|
@@ -864,8 +910,7 @@ def main():
|
|
|
864
910
|
|
|
865
911
|
Style.Codebg = f"{BG}{Style.Dark}"
|
|
866
912
|
Style.Link = f"{FG}{Style.Symbol}{UNDERLINE[0]}"
|
|
867
|
-
Style.Blockquote = f"{FG}{Style.Grey}
|
|
868
|
-
|
|
913
|
+
Style.Blockquote = f"{FG}{Style.Grey}│ "
|
|
869
914
|
|
|
870
915
|
logging.basicConfig(stream=sys.stdout, level=args.loglevel.upper(), format=f'%(message)s')
|
|
871
916
|
state.exec_master, state.exec_slave = pty.openpty()
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: streamdown
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.15.0
|
|
4
4
|
Summary: A streaming markdown renderer for modern terminals with syntax highlighting
|
|
5
5
|
Project-URL: Homepage, https://github.com/kristopolous/Streamdown
|
|
6
6
|
Project-URL: Bug Tracker, https://github.com/kristopolous/Streamdown/issues
|
|
@@ -23,6 +23,7 @@ Requires-Python: >=3.8
|
|
|
23
23
|
Requires-Dist: appdirs
|
|
24
24
|
Requires-Dist: pygments
|
|
25
25
|
Requires-Dist: pylatexenc
|
|
26
|
+
Requires-Dist: term-image
|
|
26
27
|
Requires-Dist: toml
|
|
27
28
|
Description-Content-Type: text/markdown
|
|
28
29
|
|
|
@@ -30,26 +31,30 @@ Description-Content-Type: text/markdown
|
|
|
30
31
|
|
|
31
32
|
[](https://badge.fury.io/py/streamdown)
|
|
32
33
|
|
|
33
|
-
|
|
34
|
+
Streamdown is the streaming markdown renderer for the terminal that rocks.
|
|
35
|
+
This will work with [simonw's llm](https://github.com/simonw/llm). You even get full readline and keyboard navigation support.
|
|
34
36
|
|
|
35
|
-
|
|
37
|
+
It's fully streaming and does not block
|
|
38
|
+

|
|
36
39
|
|
|
37
|
-
|
|
40
|
+
### Provides clean copyable code for long code lines
|
|
41
|
+
You may have noticed that other, *inferior* renderers will inject line breaks when copying code that wraps around. We're better and now, you can be as well.
|
|
42
|
+

|
|
38
43
|
|
|
39
|
-
|
|
44
|
+
### Supports images
|
|
45
|
+
Here's kitty and alacritty. Try to do that in glow...
|
|
46
|
+

|
|
40
47
|
|
|
41
|
-
###
|
|
42
|
-

|
|
43
|
-
|
|
44
|
-
### Does OSC 8 links for modern terminals.
|
|
48
|
+
### Does OSC 8 links for modern terminals (and optionally OSC 52 for clipboard)
|
|
45
49
|
[links.webm](https://github.com/user-attachments/assets/a5f71791-7c58-4183-ad3b-309f470c08a3)
|
|
46
50
|
|
|
47
|
-
### Doesn't consume characters like _ and * as style when they are in `blocks like this` because `_they_can_be_varaiables_`
|
|
48
|
-

|
|
49
|
-
|
|
50
51
|
### Tables are carefully supported
|
|
51
52
|

|
|
52
53
|
|
|
54
|
+
As well as everything else...
|
|
55
|
+
|
|
56
|
+

|
|
57
|
+
|
|
53
58
|
### Colors are highly (and quickly) configurable for people who care a lot, or just a little.
|
|
54
59
|

|
|
55
60
|
|
|
@@ -80,7 +85,7 @@ Defines the base Hue (H), Saturation (S), and Value (V) from which all other pal
|
|
|
80
85
|
* `Width` (integer, default: `0`): Along with the `Margin`, `Width` specifies the base width of the content, which when set to 0, means use the terminal width. See [#6](https://github.com/kristopolous/Streamdown/issues/6) for more details
|
|
81
86
|
* `PrettyPad` (boolean, default: `false`): Uses a unicode vertical pad trick to add a half height background to code blocks. This makes copy/paste have artifacts. See [#2](https://github.com/kristopolous/Streamdown/issues/2). I like it on. But that's just me
|
|
82
87
|
* `ListIndent` (integer, default: `2`): This is the recursive indent for the list styles.
|
|
83
|
-
* `Syntax` (string, default `monokai`): This the syntax [highlighting theme which come via pygments](https://pygments.org/styles/).
|
|
88
|
+
* `Syntax` (string, default `monokai`): This is the syntax [highlighting theme which come via pygments](https://pygments.org/styles/).
|
|
84
89
|
|
|
85
90
|
Example:
|
|
86
91
|
```toml
|
|
@@ -139,10 +144,8 @@ Do this
|
|
|
139
144
|
|
|
140
145
|
$ ./streamdown/sd.py tests/*md
|
|
141
146
|
|
|
142
|
-
Certainly room for improvement and I'll probably continue to make them
|
|
143
|
-
|
|
144
147
|
## Install from source
|
|
145
|
-
|
|
148
|
+
After the git clone least one of these should work, hopefully. it's using the modern uv pip tool.
|
|
146
149
|
|
|
147
150
|
$ pipx install -e .
|
|
148
151
|
$ pip install -e .
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
streamdown/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
+
streamdown/sd.py,sha256=TqW_kbt1YxeWOByaMuJD86XAfWq_Z9Lq1YFsCo0Apjk,35622
|
|
3
|
+
streamdown/tt.mds,sha256=srDldQ9KnMJd5P8GdTXTJl4mjTowwV9y58ZIaBVbtFY,359
|
|
4
|
+
streamdown/plugins/README.md,sha256=KWqYELs9WkKJmuDzYv3cvPlZMkArsNCBUe4XDoTLjLA,1143
|
|
5
|
+
streamdown/plugins/latex.py,sha256=xZMGMdx_Sw4X1piZejXFHfEG9qazU4fGeceiMI0h13Y,648
|
|
6
|
+
streamdown/scrape/file_0.py,sha256=OiFxFGGHu2C2iO9LVnhXKCybqCsnw0bu8MmI2E0vs_s,610
|
|
7
|
+
streamdown/scrape/file_1.js,sha256=JnXSvlsk9UmU5LsGOfXkP3sGId8VNEJRJo8-uRohRCM,569
|
|
8
|
+
streamdown/scrape/file_2.cpp,sha256=4hbT9TJzDNmrU7BVwaIuCMlI2BvUEVeTKoH6wUJRkrI,397
|
|
9
|
+
streamdown-0.15.0.dist-info/METADATA,sha256=-cJ1HR7jWmuxlwPYInt78MDl6i6r3asDtt2OAFDVxPs,7893
|
|
10
|
+
streamdown-0.15.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
11
|
+
streamdown-0.15.0.dist-info/entry_points.txt,sha256=HroKFsFMGf_h9PRTE96NjvjJQWupMW5TGP5RGUr1O_Q,74
|
|
12
|
+
streamdown-0.15.0.dist-info/licenses/LICENSE.MIT,sha256=SnY46EPirUsF20dZDR8HpyVgS2_4Tjxuc6f-4OdqO7U,1070
|
|
13
|
+
streamdown-0.15.0.dist-info/RECORD,,
|
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
streamdown/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
-
streamdown/sd.py,sha256=efsejJg_wCaEnbGSoAveIUTojtQ0FPCmSrkupwNvluc,33346
|
|
3
|
-
streamdown/tt.mds,sha256=srDldQ9KnMJd5P8GdTXTJl4mjTowwV9y58ZIaBVbtFY,359
|
|
4
|
-
streamdown/plugins/README.md,sha256=KWqYELs9WkKJmuDzYv3cvPlZMkArsNCBUe4XDoTLjLA,1143
|
|
5
|
-
streamdown/plugins/latex.py,sha256=xZMGMdx_Sw4X1piZejXFHfEG9qazU4fGeceiMI0h13Y,648
|
|
6
|
-
streamdown-0.13.0.dist-info/METADATA,sha256=JxVOmiPebQTidKCv1genLIwlS4j9-mVlCX0R3njk5dA,7841
|
|
7
|
-
streamdown-0.13.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
8
|
-
streamdown-0.13.0.dist-info/entry_points.txt,sha256=HroKFsFMGf_h9PRTE96NjvjJQWupMW5TGP5RGUr1O_Q,74
|
|
9
|
-
streamdown-0.13.0.dist-info/licenses/LICENSE.MIT,sha256=SnY46EPirUsF20dZDR8HpyVgS2_4Tjxuc6f-4OdqO7U,1070
|
|
10
|
-
streamdown-0.13.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|