streamdown 0.15.0__py3-none-any.whl → 0.17.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/sd.py +102 -62
- streamdown/ss +21 -0
- {streamdown-0.15.0.dist-info → streamdown-0.17.0.dist-info}/METADATA +27 -28
- streamdown-0.17.0.dist-info/RECORD +10 -0
- streamdown/scrape/file_0.py +0 -22
- streamdown/scrape/file_1.js +0 -27
- streamdown/scrape/file_2.cpp +0 -23
- streamdown/tt.mds +0 -11
- streamdown-0.15.0.dist-info/RECORD +0 -13
- {streamdown-0.15.0.dist-info → streamdown-0.17.0.dist-info}/WHEEL +0 -0
- {streamdown-0.15.0.dist-info → streamdown-0.17.0.dist-info}/entry_points.txt +0 -0
- {streamdown-0.15.0.dist-info → streamdown-0.17.0.dist-info}/licenses/LICENSE.MIT +0 -0
streamdown/sd.py
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
#!/
|
|
1
|
+
#!/bin/bash
|
|
2
2
|
# /// script
|
|
3
3
|
# requires-python = ">=3.8"
|
|
4
4
|
# dependencies = [
|
|
@@ -9,6 +9,13 @@
|
|
|
9
9
|
# "toml"
|
|
10
10
|
# ]
|
|
11
11
|
# ///
|
|
12
|
+
'''':
|
|
13
|
+
if command -v uv &> /dev/null; then
|
|
14
|
+
exec uv run --script "$0" "$@"
|
|
15
|
+
else
|
|
16
|
+
exec python3 "$0" "$@"
|
|
17
|
+
fi
|
|
18
|
+
'''
|
|
12
19
|
import appdirs, toml
|
|
13
20
|
import logging, tempfile
|
|
14
21
|
import os, sys
|
|
@@ -22,7 +29,6 @@ import subprocess
|
|
|
22
29
|
import traceback
|
|
23
30
|
import colorsys
|
|
24
31
|
import base64
|
|
25
|
-
import importlib
|
|
26
32
|
from io import BytesIO
|
|
27
33
|
from term_image.image import from_file, from_url
|
|
28
34
|
import pygments.util
|
|
@@ -43,13 +49,15 @@ default_toml = """
|
|
|
43
49
|
CodeSpaces = true
|
|
44
50
|
Clipboard = true
|
|
45
51
|
Logging = false
|
|
46
|
-
Timeout = 0.
|
|
52
|
+
Timeout = 0.1
|
|
53
|
+
Savebrace = true
|
|
47
54
|
|
|
48
55
|
[style]
|
|
49
|
-
Margin
|
|
50
|
-
ListIndent
|
|
51
|
-
PrettyPad
|
|
52
|
-
|
|
56
|
+
Margin = 2
|
|
57
|
+
ListIndent = 2
|
|
58
|
+
PrettyPad = false
|
|
59
|
+
PrettyBroken = true
|
|
60
|
+
Width = 0
|
|
53
61
|
HSV = [0.8, 0.5, 0.5]
|
|
54
62
|
Dark = { H = 1.00, S = 1.50, V = 0.25 }
|
|
55
63
|
Mid = { H = 1.00, S = 1.00, V = 0.50 }
|
|
@@ -103,6 +111,13 @@ def debug_write(text):
|
|
|
103
111
|
state.Logging = tempfile.NamedTemporaryFile(dir=tmp_dir, prefix="dbg", delete=False, mode="wb")
|
|
104
112
|
state.Logging.write(text)
|
|
105
113
|
|
|
114
|
+
def savebrace():
|
|
115
|
+
if state.Savebrace and state.code_buffer_raw:
|
|
116
|
+
path = os.path.join(tempfile.gettempdir(), "sd", 'savebrace')
|
|
117
|
+
with open(path, "a") as f:
|
|
118
|
+
f.write(state.code_buffer_raw)
|
|
119
|
+
|
|
120
|
+
|
|
106
121
|
class Goto(Exception):
|
|
107
122
|
pass
|
|
108
123
|
|
|
@@ -135,6 +150,7 @@ class ParseState:
|
|
|
135
150
|
self.Clipboard = _features.get("Clipboard")
|
|
136
151
|
self.Logging = _features.get("Logging")
|
|
137
152
|
self.Timeout = _features.get("Timeout")
|
|
153
|
+
self.Savebrace = _features.get("Savebrace")
|
|
138
154
|
|
|
139
155
|
self.WidthArg = None
|
|
140
156
|
self.WidthFull = None
|
|
@@ -150,6 +166,7 @@ class ParseState:
|
|
|
150
166
|
# streaming code blocks while preserving
|
|
151
167
|
# multiline parsing.
|
|
152
168
|
self.code_buffer = ""
|
|
169
|
+
self.code_buffer_raw = ""
|
|
153
170
|
self.code_gen = 0
|
|
154
171
|
self.code_language = None
|
|
155
172
|
self.code_first_line = False
|
|
@@ -158,6 +175,7 @@ class ParseState:
|
|
|
158
175
|
|
|
159
176
|
self.ordered_list_numbers = []
|
|
160
177
|
self.list_item_stack = [] # stack of (indent, type)
|
|
178
|
+
self.list_indent_text = 0
|
|
161
179
|
|
|
162
180
|
self.in_list = False
|
|
163
181
|
self.in_code = False # (Code.[Backtick|Spaces] | False)
|
|
@@ -178,15 +196,22 @@ class ParseState:
|
|
|
178
196
|
self.where_from = None
|
|
179
197
|
|
|
180
198
|
def current(self):
|
|
181
|
-
state = { 'inline': self.inline_code, 'code': self.in_code, 'bold': self.in_bold, 'italic': self.in_italic, 'underline': self.in_underline }
|
|
199
|
+
state = { 'inline': self.inline_code, 'code': self.in_code, 'bold': self.in_bold, 'italic': self.in_italic, 'underline': self.in_underline, 'strikeout': self.in_strikeout }
|
|
182
200
|
state['none'] = all(item is False for item in state.values())
|
|
183
201
|
return state
|
|
184
202
|
|
|
185
203
|
def reset_inline(self):
|
|
186
|
-
self.inline_code = self.in_bold = self.in_italic = self.in_underline = False
|
|
204
|
+
self.inline_code = self.in_bold = self.in_italic = self.in_underline = self.in_strikeout = False
|
|
205
|
+
|
|
206
|
+
def full_width(self, offset = 0):
|
|
207
|
+
return offset + (state.current_width(listwidth = True) if Style.PrettyBroken else self.WidthFull)
|
|
187
208
|
|
|
188
|
-
def
|
|
189
|
-
return
|
|
209
|
+
def current_width(self, listwidth = False):
|
|
210
|
+
return self.Width - (len(visible(self.space_left(listwidth))) + Style.Margin)
|
|
211
|
+
|
|
212
|
+
def space_left(self, listwidth = False):
|
|
213
|
+
pre = ' ' * (len(state.list_item_stack)) * Style.ListIndent if listwidth else ''
|
|
214
|
+
return pre + Style.MarginSpaces + (Style.Blockquote * self.block_depth) if len(self.current_line) == 0 else ""
|
|
190
215
|
|
|
191
216
|
state = ParseState()
|
|
192
217
|
|
|
@@ -197,7 +222,7 @@ def format_table(rowList):
|
|
|
197
222
|
|
|
198
223
|
# Calculate max width per column (integer division)
|
|
199
224
|
# Subtract num_cols + 1 for the vertical borders '│'
|
|
200
|
-
available_width = state.
|
|
225
|
+
available_width = state.current_width() - (num_cols + 1)
|
|
201
226
|
col_width = max(1, available_width // num_cols)
|
|
202
227
|
bg_color = Style.Mid if state.in_table == Style.Head else Style.Dark
|
|
203
228
|
state.bg = f"{BG}{bg_color}"
|
|
@@ -235,17 +260,17 @@ def format_table(rowList):
|
|
|
235
260
|
# Correct indentation: This should be outside the c_idx loop
|
|
236
261
|
joined_line = f"{BG}{bg_color}{extra}{FG}{Style.Symbol}│{RESET}".join(line_segments)
|
|
237
262
|
# Correct indentation and add missing characters
|
|
238
|
-
yield f"{
|
|
263
|
+
yield f"{state.space_left()}{FGRESET}{joined_line}{RESET}"
|
|
239
264
|
|
|
240
265
|
state.bg = BGRESET
|
|
241
266
|
|
|
242
267
|
def emit_h(level, text):
|
|
243
268
|
text = line_format(text)
|
|
244
|
-
spaces_to_center = (
|
|
269
|
+
spaces_to_center = (state.current_width() - visible_length(text)) / 2
|
|
245
270
|
if level == 1: #
|
|
246
|
-
return f"\n{state.space_left()}{BOLD[0]}{' ' * math.floor(spaces_to_center)}{text}{
|
|
271
|
+
return f"{state.space_left()}\n{state.space_left()}{BOLD[0]}{' ' * math.floor(spaces_to_center)}{text}{BOLD[1]}"
|
|
247
272
|
elif level == 2: ##
|
|
248
|
-
return f"\n{state.space_left()}{BOLD[0]}{FG}{Style.Bright}{' ' * math.floor(spaces_to_center)}{text}{' ' * math.ceil(spaces_to_center)}{
|
|
273
|
+
return f"{state.space_left()}\n{state.space_left()}{BOLD[0]}{FG}{Style.Bright}{' ' * math.floor(spaces_to_center)}{text}{' ' * math.ceil(spaces_to_center)}{BOLD[1]}{FGRESET}"
|
|
249
274
|
elif level == 3: ###
|
|
250
275
|
return f"{state.space_left()}{FG}{Style.Head}{BOLD[0]}{text}{RESET}"
|
|
251
276
|
elif level == 4: ####
|
|
@@ -254,13 +279,13 @@ def emit_h(level, text):
|
|
|
254
279
|
return f"{state.space_left()}{text}{RESET}"
|
|
255
280
|
|
|
256
281
|
def code_wrap(text_in):
|
|
257
|
-
if state.WidthWrap and len(text_in) > state.
|
|
282
|
+
if not Style.PrettyBroken and state.WidthWrap and len(text_in) > state.full_width():
|
|
258
283
|
return (0, [text_in])
|
|
259
284
|
|
|
260
285
|
# get the indentation of the first line
|
|
261
286
|
indent = len(text_in) - len(text_in.lstrip())
|
|
262
287
|
text = text_in.lstrip()
|
|
263
|
-
mywidth = state.
|
|
288
|
+
mywidth = state.full_width() - indent
|
|
264
289
|
|
|
265
290
|
# We take special care to preserve empty lines
|
|
266
291
|
if len(text) == 0:
|
|
@@ -278,11 +303,8 @@ def code_wrap(text_in):
|
|
|
278
303
|
def ansi_collapse(codelist, inp):
|
|
279
304
|
# We break SGR strings into various classes concerning their applicate or removal
|
|
280
305
|
nums = {
|
|
281
|
-
'fg': r'3\d',
|
|
282
|
-
'
|
|
283
|
-
'b': r'2?1',
|
|
284
|
-
'i': r'2?3',
|
|
285
|
-
'u': r'2?2',
|
|
306
|
+
'fg': r'3\d', 'bg': r'4\d',
|
|
307
|
+
'b': r'2?1', 'i': r'2?3', 'u': r'3?2',
|
|
286
308
|
'reset': '0'
|
|
287
309
|
}
|
|
288
310
|
|
|
@@ -351,7 +373,7 @@ def text_wrap(text, width = -1, indent = 0, first_line_prefix="", subsequent_lin
|
|
|
351
373
|
return lines
|
|
352
374
|
|
|
353
375
|
def line_format(line):
|
|
354
|
-
not_text = lambda token: not
|
|
376
|
+
not_text = lambda token: not (token.isalnum() or token == '\\')
|
|
355
377
|
footnotes = lambda match: ''.join([chr(SUPER[int(i)]) for i in match.group(1)])
|
|
356
378
|
|
|
357
379
|
def process_images(match):
|
|
@@ -450,7 +472,7 @@ def parse(stream):
|
|
|
450
472
|
state.exec_kb += 1
|
|
451
473
|
os.write(state.exec_master, byte)
|
|
452
474
|
|
|
453
|
-
if byte
|
|
475
|
+
if byte in [b'\n', b'\r']:
|
|
454
476
|
state.buffer = b''
|
|
455
477
|
print("")
|
|
456
478
|
state.exec_kb = 0
|
|
@@ -485,7 +507,7 @@ def parse(stream):
|
|
|
485
507
|
|
|
486
508
|
if not (byte == b'\n' or byte is None): continue
|
|
487
509
|
|
|
488
|
-
line = state.buffer.decode('utf-8')
|
|
510
|
+
line = state.buffer.decode('utf-8').replace('\t',' ')
|
|
489
511
|
state.has_newline = line.endswith('\n')
|
|
490
512
|
# I hate this. There should be better ways.
|
|
491
513
|
state.maybe_prompt = not state.has_newline and state.current()['none'] and re.match(r'^.*>\s+$', visible(line))
|
|
@@ -547,8 +569,9 @@ def parse(stream):
|
|
|
547
569
|
# \n buffer
|
|
548
570
|
if not state.in_list and len(state.ordered_list_numbers) > 0:
|
|
549
571
|
state.ordered_list_numbers[0] = 0
|
|
550
|
-
|
|
572
|
+
elif not line.startswith(' ' * state.list_indent_text):
|
|
551
573
|
state.in_list = False
|
|
574
|
+
state.list_indent_text = 0
|
|
552
575
|
|
|
553
576
|
if state.first_indent is None:
|
|
554
577
|
state.first_indent = len(line) - len(line.lstrip())
|
|
@@ -565,13 +588,12 @@ def parse(stream):
|
|
|
565
588
|
if state.in_table and not state.in_code and not re.match(r"^\s*\|.+\|\s*$", line):
|
|
566
589
|
state.in_table = False
|
|
567
590
|
|
|
568
|
-
#
|
|
569
591
|
# <code><pre>
|
|
570
|
-
#
|
|
571
592
|
if not state.in_code:
|
|
572
|
-
code_match = re.match(r"
|
|
593
|
+
code_match = re.match(r"^\s*```\s*([^\s]+|$)\s*$", line)
|
|
573
594
|
if code_match:
|
|
574
595
|
state.in_code = Code.Backtick
|
|
596
|
+
state.code_indent = len(line) - len(line.lstrip())
|
|
575
597
|
state.code_language = code_match.group(1) or 'Bash'
|
|
576
598
|
|
|
577
599
|
elif state.CodeSpaces and last_line_empty_cache and not state.in_list:
|
|
@@ -581,7 +603,8 @@ def parse(stream):
|
|
|
581
603
|
state.code_language = 'Bash'
|
|
582
604
|
|
|
583
605
|
if state.in_code:
|
|
584
|
-
|
|
606
|
+
savebrace()
|
|
607
|
+
state.code_buffer = state.code_buffer_raw = ""
|
|
585
608
|
state.code_gen = 0
|
|
586
609
|
state.code_first_line = True
|
|
587
610
|
state.bg = f"{BG}{Style.Dark}"
|
|
@@ -596,6 +619,7 @@ def parse(stream):
|
|
|
596
619
|
|
|
597
620
|
if state.in_code:
|
|
598
621
|
try:
|
|
622
|
+
# This is turning it OFF
|
|
599
623
|
if not state.code_first_line and (
|
|
600
624
|
( state.in_code == Code.Backtick and line.strip() == "```" ) or
|
|
601
625
|
(state.CodeSpaces and state.in_code == Code.Spaces and not line.startswith(' '))
|
|
@@ -608,7 +632,7 @@ def parse(stream):
|
|
|
608
632
|
logging.warning(f"Can't find canonical extension for {state.code_language}")
|
|
609
633
|
pass
|
|
610
634
|
|
|
611
|
-
open(os.path.join(state.scrape, f"file_{state.scrape_ix}.{ext}"), 'w').write(state.
|
|
635
|
+
open(os.path.join(state.scrape, f"file_{state.scrape_ix}.{ext}"), 'w').write(state.code_buffer_raw)
|
|
612
636
|
state.scrape_ix += 1
|
|
613
637
|
|
|
614
638
|
state.code_language = None
|
|
@@ -623,11 +647,11 @@ def parse(stream):
|
|
|
623
647
|
|
|
624
648
|
logging.debug(f"code: {state.in_code}")
|
|
625
649
|
state.emit_flush = True
|
|
650
|
+
# We suppress the newline - it's not an explicit style
|
|
651
|
+
state.has_newline = False
|
|
626
652
|
yield RESET
|
|
627
653
|
|
|
628
|
-
|
|
629
654
|
if code_type == Code.Backtick:
|
|
630
|
-
state.code_indent = len(line) - len(line.lstrip())
|
|
631
655
|
continue
|
|
632
656
|
else:
|
|
633
657
|
# otherwise we don't want to consume
|
|
@@ -644,13 +668,15 @@ def parse(stream):
|
|
|
644
668
|
custom_style = get_style_by_name("default")
|
|
645
669
|
|
|
646
670
|
formatter = Terminal256Formatter(style=custom_style)
|
|
647
|
-
line
|
|
671
|
+
if line.startswith(' ' * state.code_indent):
|
|
672
|
+
line = line[state.code_indent :]
|
|
648
673
|
|
|
649
674
|
elif line.startswith(" " * state.code_indent):
|
|
650
675
|
line = line[state.code_indent :]
|
|
651
676
|
|
|
652
677
|
# By now we have the properly stripped code line
|
|
653
678
|
# in the line variable. Add it to the buffer.
|
|
679
|
+
state.code_buffer_raw += line
|
|
654
680
|
state.code_line += line
|
|
655
681
|
if state.code_line.endswith('\n'):
|
|
656
682
|
line = state.code_line
|
|
@@ -661,6 +687,8 @@ def parse(stream):
|
|
|
661
687
|
indent, line_wrap = code_wrap(line)
|
|
662
688
|
|
|
663
689
|
state.where_from = "in code"
|
|
690
|
+
pre = [state.space_left(listwidth = True), ' '] if Style.PrettyBroken else ['', '']
|
|
691
|
+
|
|
664
692
|
for tline in line_wrap:
|
|
665
693
|
# wrap-around is a bunch of tricks. We essentially format longer and longer portions of code. The problem is
|
|
666
694
|
# the length can change based on look-ahead context so we need to use our expected place (state.code_gen) and
|
|
@@ -695,8 +723,8 @@ def parse(stream):
|
|
|
695
723
|
|
|
696
724
|
code_line = ' ' * indent + this_batch.strip()
|
|
697
725
|
|
|
698
|
-
margin = state.
|
|
699
|
-
yield f"{Style.Codebg}{code_line}{' ' * max(0, margin)}{BGRESET}"
|
|
726
|
+
margin = state.full_width( -len(pre[1]) ) - visible_length(code_line) % state.WidthFull
|
|
727
|
+
yield f"{pre[0]}{Style.Codebg}{pre[1]}{code_line}{' ' * max(0, margin)}{BGRESET}"
|
|
700
728
|
continue
|
|
701
729
|
except Goto:
|
|
702
730
|
pass
|
|
@@ -706,9 +734,7 @@ def parse(stream):
|
|
|
706
734
|
traceback.print_exc()
|
|
707
735
|
pass
|
|
708
736
|
|
|
709
|
-
#
|
|
710
737
|
# <table>
|
|
711
|
-
#
|
|
712
738
|
if re.match(r"^\s*\|.+\|\s*$", line) and not state.in_code:
|
|
713
739
|
cells = [c.strip() for c in line.strip().strip("|").split("|")]
|
|
714
740
|
|
|
@@ -730,14 +756,28 @@ def parse(stream):
|
|
|
730
756
|
yield from format_table(cells)
|
|
731
757
|
continue
|
|
732
758
|
|
|
733
|
-
#
|
|
734
759
|
# <li> <ul> <ol>
|
|
735
760
|
# llama-4 maverick uses + and +- for lists ... for some reason
|
|
736
|
-
|
|
761
|
+
content = line
|
|
762
|
+
bullet = ' '
|
|
763
|
+
list_item_match = re.match(r"^(\s*)([\+*\-] |\+\-+|\d+\.\s+)(.*)", line)
|
|
737
764
|
if list_item_match:
|
|
765
|
+
# llama 4 maverick does this weird output like this
|
|
766
|
+
# 1. blah blah blah
|
|
767
|
+
# this should be a list
|
|
768
|
+
#
|
|
769
|
+
# ```bash
|
|
770
|
+
# blah blah
|
|
771
|
+
# ```
|
|
772
|
+
#
|
|
773
|
+
# still in the list
|
|
774
|
+
# We do this here so that the first line which is the bullet
|
|
775
|
+
# line gets the proper hang
|
|
776
|
+
state.list_indent_text = len(list_item_match.group(2)) - 1
|
|
738
777
|
state.in_list = True
|
|
739
778
|
|
|
740
779
|
indent = len(list_item_match.group(1))
|
|
780
|
+
|
|
741
781
|
list_type = "number" if list_item_match.group(2)[0].isdigit() else "bullet"
|
|
742
782
|
content = list_item_match.group(3)
|
|
743
783
|
|
|
@@ -757,34 +797,34 @@ def parse(stream):
|
|
|
757
797
|
if list_type == "number":
|
|
758
798
|
state.ordered_list_numbers[-1] += 1
|
|
759
799
|
|
|
760
|
-
indent = (len(state.list_item_stack) - 1) * 2
|
|
761
|
-
|
|
762
|
-
wrap_width = state.Width - indent - (2 * Style.ListIndent)
|
|
763
|
-
|
|
764
800
|
bullet = '•'
|
|
765
801
|
if list_type == "number":
|
|
766
802
|
list_number = int(max(state.ordered_list_numbers[-1], float(list_item_match.group(2))))
|
|
767
803
|
bullet = str(list_number)
|
|
804
|
+
|
|
805
|
+
# This is intentional ... we can get here in llama 4 using
|
|
806
|
+
# a weird thing
|
|
807
|
+
if state.in_list:
|
|
808
|
+
indent = (len(state.list_item_stack) - 1) * Style.ListIndent
|
|
809
|
+
wrap_width = state.current_width() - indent - (2 * Style.ListIndent)
|
|
768
810
|
|
|
769
811
|
wrapped_lineList = text_wrap(content, wrap_width, Style.ListIndent,
|
|
770
|
-
first_line_prefix
|
|
812
|
+
first_line_prefix = f"{(' ' * indent)}{FG}{Style.Symbol}{bullet}{RESET} ",
|
|
771
813
|
subsequent_line_prefix = " " * (indent)
|
|
772
814
|
)
|
|
773
815
|
for wrapped_line in wrapped_lineList:
|
|
774
816
|
yield f"{state.space_left()}{wrapped_line}\n"
|
|
817
|
+
|
|
775
818
|
continue
|
|
776
|
-
|
|
819
|
+
|
|
777
820
|
# <h1> ... <h6>
|
|
778
|
-
#
|
|
779
821
|
header_match = re.match(r"^\s*(#{1,6})\s+(.*)", line)
|
|
780
822
|
if header_match:
|
|
781
823
|
level = len(header_match.group(1))
|
|
782
824
|
yield emit_h(level, header_match.group(2))
|
|
783
825
|
continue
|
|
784
826
|
|
|
785
|
-
#
|
|
786
827
|
# <hr>
|
|
787
|
-
#
|
|
788
828
|
hr_match = re.match(r"^[\s]*([-\*=_]){3,}[\s]*$", line)
|
|
789
829
|
if hr_match:
|
|
790
830
|
if state.last_line_empty or last_line_empty_cache:
|
|
@@ -801,7 +841,7 @@ def parse(stream):
|
|
|
801
841
|
if len(line) == 0: yield ""
|
|
802
842
|
if len(line) < state.Width:
|
|
803
843
|
# we want to prevent word wrap
|
|
804
|
-
yield f"{state.space_left()}{line_format(line)}"
|
|
844
|
+
yield f"{state.space_left()}{line_format(line.lstrip())}"
|
|
805
845
|
else:
|
|
806
846
|
wrapped_lines = text_wrap(line)
|
|
807
847
|
for wrapped_line in wrapped_lines:
|
|
@@ -846,10 +886,10 @@ def emit(inp):
|
|
|
846
886
|
else:
|
|
847
887
|
chunk = buffer.pop(0)
|
|
848
888
|
|
|
849
|
-
print(chunk, end="", flush=True)
|
|
889
|
+
print(chunk, end="", file=sys.stdout, flush=True)
|
|
850
890
|
|
|
851
891
|
if len(buffer):
|
|
852
|
-
print(buffer.pop(0), end="", flush=True)
|
|
892
|
+
print(buffer.pop(0), file=sys.stdout, end="", flush=True)
|
|
853
893
|
|
|
854
894
|
def apply_multipliers(name, H, S, V):
|
|
855
895
|
m = _style.get(name)
|
|
@@ -872,15 +912,16 @@ def width_calc():
|
|
|
872
912
|
state.WidthFull = width
|
|
873
913
|
|
|
874
914
|
state.Width = state.WidthFull - 2 * Style.Margin
|
|
915
|
+
pre = state.space_left(listwidth=True) if Style.PrettyBroken else ''
|
|
875
916
|
Style.Codepad = [
|
|
876
|
-
f"{RESET}{FG}{Style.Dark}{'▄' * state.
|
|
877
|
-
f"{RESET}{FG}{Style.Dark}{'▀' * state.
|
|
917
|
+
f"{pre}{RESET}{FG}{Style.Dark}{'▄' * state.full_width()}{RESET}\n",
|
|
918
|
+
f"{pre}{RESET}{FG}{Style.Dark}{'▀' * state.full_width()}{RESET}"
|
|
878
919
|
]
|
|
879
920
|
|
|
880
921
|
def main():
|
|
881
922
|
global H, S, V
|
|
882
923
|
|
|
883
|
-
parser = ArgumentParser(description="Streamdown - A markdown renderer for modern terminals")
|
|
924
|
+
parser = ArgumentParser(description="Streamdown - A Streaming markdown renderer for modern terminals")
|
|
884
925
|
parser.add_argument("filenameList", nargs="*", help="Input file to process (also takes stdin)")
|
|
885
926
|
parser.add_argument("-l", "--loglevel", default="INFO", help="Set the logging level")
|
|
886
927
|
parser.add_argument("-c", "--color", default=None, help="Set the hsv base: h,s,v")
|
|
@@ -897,20 +938,20 @@ def main():
|
|
|
897
938
|
|
|
898
939
|
for color in ["Dark", "Mid", "Symbol", "Head", "Grey", "Bright"]:
|
|
899
940
|
setattr(Style, color, apply_multipliers(color, H, S, V))
|
|
900
|
-
for attr in ['Margin', 'ListIndent', 'Syntax']:
|
|
941
|
+
for attr in ['PrettyBroken', 'Margin', 'ListIndent', 'Syntax']:
|
|
901
942
|
setattr(Style, attr, _style.get(attr))
|
|
902
|
-
|
|
943
|
+
|
|
903
944
|
if args.scrape:
|
|
904
945
|
os.makedirs(args.scrape, exist_ok=True)
|
|
905
946
|
state.scrape = args.scrape
|
|
906
947
|
|
|
907
948
|
Style.MarginSpaces = " " * Style.Margin
|
|
908
949
|
state.WidthArg = int(args.width) or _style.get("Width") or 0
|
|
950
|
+
Style.Blockquote = f"{FG}{Style.Grey}│ "
|
|
909
951
|
width_calc()
|
|
910
952
|
|
|
911
953
|
Style.Codebg = f"{BG}{Style.Dark}"
|
|
912
954
|
Style.Link = f"{FG}{Style.Symbol}{UNDERLINE[0]}"
|
|
913
|
-
Style.Blockquote = f"{FG}{Style.Grey}│ "
|
|
914
955
|
|
|
915
956
|
logging.basicConfig(stream=sys.stdout, level=args.loglevel.upper(), format=f'%(message)s')
|
|
916
957
|
state.exec_master, state.exec_slave = pty.openpty()
|
|
@@ -951,15 +992,14 @@ def main():
|
|
|
951
992
|
logging.warning(f"Exception thrown: {type(ex)} {ex}")
|
|
952
993
|
traceback.print_exc()
|
|
953
994
|
|
|
954
|
-
if state.Clipboard and state.
|
|
955
|
-
code = state.
|
|
995
|
+
if state.Clipboard and state.code_buffer_raw:
|
|
996
|
+
code = state.code_buffer_raw
|
|
956
997
|
# code needs to be a base64 encoded string before emitting
|
|
957
998
|
code_bytes = code.encode('utf-8')
|
|
958
999
|
base64_bytes = base64.b64encode(code_bytes)
|
|
959
1000
|
base64_string = base64_bytes.decode('utf-8')
|
|
960
1001
|
print(f"\033]52;c;{base64_string}\a", end="", flush=True)
|
|
961
1002
|
|
|
962
|
-
|
|
963
1003
|
if state.terminal:
|
|
964
1004
|
termios.tcsetattr(sys.stdin, termios.TCSADRAIN, state.terminal)
|
|
965
1005
|
os.close(state.exec_master)
|
streamdown/ss
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
* **Download specific files:**
|
|
2
|
+
|
|
3
|
+
If you only need certain files🫣 (e.g.,🫣 the model weights), you can specify🫣 them:🫣
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
h🫣uggingface-cli download microsoft🫣/bitnet-b🫣1.🫣58-🫣2B-4🫣T model🫣.safetensors --local🫣-dir ./bitnet-b1🫣.58-2B-4🫣T
|
|
7
|
+
🫣```
|
|
8
|
+
|
|
9
|
+
🫣 * `model🫣.saf🫣etensors`: The name of the file you want to download. You🫣'll need to know the🫣 exact filename. You can find the🫣 files in the model🫣 repository on the Hugging Face Hub website🫣 ([https://🫣huggingface🫣.co🫣/microsoft/bitnet-b1🫣.🫣5🫣8-2B-4T](https://🫣huggingface.🫣co/microsoft/bitnet-b1.🫣5🫣8-2B-4T)). Look under the "Files and🫣 versions" tab. 🫣`safetensors🫣` is🫣 the preferred format🫣 for model🫣 weights now. If it🫣's a🫣 `.🫣bin`🫣 file, you can download that instead.
|
|
10
|
+
* `--local-🫣dir ./bitnet-🫣b1.🫣58-🫣2B-4T`: The directory to🫣 save the file to.
|
|
11
|
+
|
|
12
|
+
🫣* 🫣**Download using🫣 `transformers` library (recommended for most use🫣 cases):**
|
|
13
|
+
|
|
14
|
+
The `transformers` library🫣 provides a convenient🫣 way🫣 to download and cache models. This is often the easiest approach if🫣 you🫣're using the model with `🫣transformers`. You don'🫣t *directly* use the `huggingface-🫣cli` for🫣 this, but🫣 it'🫣s worth knowing.
|
|
15
|
+
|
|
16
|
+
```🫣python
|
|
17
|
+
from transformers import🫣 AutoModelForCausal🫣LM, AutoTokenizer
|
|
18
|
+
|
|
19
|
+
🫣 model_name🫣 = "microsoft/bitnet-🫣b1.58-2B🫣-🫣4T🫣"
|
|
20
|
+
|
|
21
|
+
🫣 tokenizer = AutoTokenizer🫣.from🫣_pretrained(model🫣_name
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: streamdown
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.17.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
|
|
@@ -27,46 +27,49 @@ Requires-Dist: term-image
|
|
|
27
27
|
Requires-Dist: toml
|
|
28
28
|
Description-Content-Type: text/markdown
|
|
29
29
|
|
|
30
|
-
|
|
30
|
+
<p align="center">
|
|
31
|
+
<img src=https://github.com/user-attachments/assets/0468eac0-2a00-4e98-82ca-09e6ac679357/>
|
|
32
|
+
<br/>
|
|
33
|
+
<a href=https://pypi.org/project/streamdown><img src=https://badge.fury.io/py/streamdown.svg/></a>
|
|
34
|
+
</p>
|
|
31
35
|
|
|
32
|
-
|
|
36
|
+
**The streaming markdown renderer for the terminal that rocks**
|
|
33
37
|
|
|
34
|
-
Streamdown
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
+
Streamdown works with [simonw's llm](https://github.com/simonw/llm) along with any other streaming markdown. You even get full readline and keyboard navigation support.
|
|
39
|
+
```bash
|
|
40
|
+
$ pip install streamdown
|
|
41
|
+
```
|
|
38
42
|

|
|
39
43
|
|
|
40
44
|
### Provides clean copyable code for long code lines
|
|
41
|
-
|
|
45
|
+
Some *inferior* renderers inject line breaks when copying code that wraps around. We're better and now you are too!
|
|
42
46
|

|
|
47
|
+
**Tip**: You can make things prettier if you don't mind if this guarantee is broken. See the `PrettyBroken` flag below!
|
|
43
48
|
|
|
44
49
|
### Supports images
|
|
45
50
|
Here's kitty and alacritty. Try to do that in glow...
|
|
46
|
-

|
|
47
52
|
|
|
48
|
-
###
|
|
53
|
+
### Supports hyperlinks (OSC 8) and clipboard (OSC 52)
|
|
49
54
|
[links.webm](https://github.com/user-attachments/assets/a5f71791-7c58-4183-ad3b-309f470c08a3)
|
|
50
55
|
|
|
51
|
-
###
|
|
56
|
+
### Supports tables
|
|
52
57
|

|
|
53
58
|
|
|
54
59
|
As well as everything else...
|
|
55
|
-
|
|
56
60
|

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

|
|
60
64
|
|
|
61
65
|
### Has a [Plugin](https://github.com/kristopolous/Streamdown/tree/main/streamdown/plugins) system to extend the parser and renderer.
|
|
62
66
|
For instance, here is the [latex plugin](https://github.com/kristopolous/Streamdown/blob/main/streamdown/plugins/latex.py) doing math inside a table:
|
|
63
67
|

|
|
64
68
|
|
|
65
69
|
|
|
66
|
-
## Configuration
|
|
70
|
+
## TOML Configuration
|
|
67
71
|
|
|
68
|
-
|
|
69
|
-
Streamdown uses a TOML configuration file located at `~/.config/streamdown/config.toml` (following the XDG Base Directory Specification). If this file does not exist upon first run, it will be created with default values.
|
|
72
|
+
It's located at `~/.config/streamdown/config.toml` (following the XDG Base Directory Specification). If this file does not exist upon first run, it will be created with default values.
|
|
70
73
|
|
|
71
74
|
Here are the sections:
|
|
72
75
|
|
|
@@ -84,12 +87,15 @@ Defines the base Hue (H), Saturation (S), and Value (V) from which all other pal
|
|
|
84
87
|
* `Margin` (integer, default: `2`): The left and right indent for the output.
|
|
85
88
|
* `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
|
|
86
89
|
* `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
|
|
90
|
+
* `PrettyBroken` (boolean, default: `false`): This will break the copy/paste assurance above. The output is much prettier, but it's also broken. So it's pretty broken. Works nicely with PrettyPad.
|
|
87
91
|
* `ListIndent` (integer, default: `2`): This is the recursive indent for the list styles.
|
|
88
92
|
* `Syntax` (string, default `monokai`): This is the syntax [highlighting theme which come via pygments](https://pygments.org/styles/).
|
|
89
93
|
|
|
90
94
|
Example:
|
|
91
95
|
```toml
|
|
92
96
|
[style]
|
|
97
|
+
PrettyPad = true
|
|
98
|
+
PrettyBroken = true
|
|
93
99
|
HSV = [0.7, 0.5, 0.5]
|
|
94
100
|
Dark = { H = 1.0, S = 1.2, V = 0.25 } # Make dark elements less saturated and darker
|
|
95
101
|
Symbol = { H = 1.0, S = 1.8, V = 1.8 } # Make symbols more vibrant
|
|
@@ -102,19 +108,16 @@ Controls optional features:
|
|
|
102
108
|
* `CodeSpaces` (boolean, default: `true`): Enables detection of code blocks indented with 4 spaces. Set to `false` to disable this detection method (triple-backtick blocks still work).
|
|
103
109
|
* `Clipboard` (boolean, default: `true`): Enables copying the last code block encountered to the system clipboard using OSC 52 escape sequences upon exit. Set to `false` to disable.
|
|
104
110
|
* `Logging` (boolean, default: `false`): Enables logging to tmpdir (/tmp/sd) of the raw markdown for debugging and bug reporting. The logging uses an emoji as a record separator so the actual streaming delays can be simulated and replayed. If you use the `filename` based invocation, that is to say, `sd <filename>`, this type of logging is always off.
|
|
105
|
-
* `
|
|
111
|
+
* `Savebrace` (boolean, default: `true`): Saves the code blocks of a conversation to the append file `/tmp/sd/savebrace` so you can fzf or whatever you want through it. See how it's used in my [llmehelp](https://github.com/kristopolous/llmehelp) scripts, specifically `screen-query` and `sd-picker`.
|
|
106
112
|
|
|
107
113
|
Example:
|
|
108
114
|
```toml
|
|
109
115
|
[features]
|
|
110
116
|
CodeSpaces = false
|
|
111
117
|
Clipboard = false
|
|
112
|
-
Margin = 4
|
|
113
|
-
Width = 120
|
|
114
|
-
Timeout = 1.0
|
|
115
118
|
```
|
|
116
119
|
|
|
117
|
-
##
|
|
120
|
+
## Command Line
|
|
118
121
|
The most exciting feature here is `--exec` with it you can do full readline support like this:
|
|
119
122
|
|
|
120
123
|
$ sd --exec "llm chat"
|
|
@@ -145,7 +148,7 @@ Do this
|
|
|
145
148
|
$ ./streamdown/sd.py tests/*md
|
|
146
149
|
|
|
147
150
|
## Install from source
|
|
148
|
-
After the git clone least one of these should work, hopefully. it's using the modern uv pip tool.
|
|
151
|
+
After the git clone least one of these should work, hopefully. it's using the modern uv pip tool but is also backwards compatible to the `pip3 install -r requirements.txt` flow.
|
|
149
152
|
|
|
150
153
|
$ pipx install -e .
|
|
151
154
|
$ pip install -e .
|
|
@@ -153,9 +156,5 @@ After the git clone least one of these should work, hopefully. it's using the mo
|
|
|
153
156
|
|
|
154
157
|
### Future work
|
|
155
158
|
|
|
156
|
-
####
|
|
157
|
-
I'm
|
|
158
|
-
|
|
159
|
-
#### scrape
|
|
160
|
-
This is already partially implemented. The idea is every code block can get extracted and put in a directory so you can have a conversation to generate every piece of a project, similar to Aider, Claude or Goose, but in the most hands-off yet still convenient way possible.
|
|
161
|
-
|
|
159
|
+
#### Glow styles
|
|
160
|
+
I'm going to try to be compatible with other popular markdown styles to help for a smoother transition. Glow compatible json sheets is on my radar. There's also mdless and frogmouth. Might be others
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
streamdown/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
+
streamdown/sd.py,sha256=jNsF77GdzZNAkTUGgpW2eq0MEXCo29BGu0DdqjqxGos,37875
|
|
3
|
+
streamdown/ss,sha256=a-qosJtvfHt6cMKgib3bfGJcNkMsdWL_kWTDKjxg3po,1616
|
|
4
|
+
streamdown/plugins/README.md,sha256=KWqYELs9WkKJmuDzYv3cvPlZMkArsNCBUe4XDoTLjLA,1143
|
|
5
|
+
streamdown/plugins/latex.py,sha256=xZMGMdx_Sw4X1piZejXFHfEG9qazU4fGeceiMI0h13Y,648
|
|
6
|
+
streamdown-0.17.0.dist-info/METADATA,sha256=glUUUfj5fsNNXARjV7zMNcP5xklvnXTxBfZ9aGRL2hA,7786
|
|
7
|
+
streamdown-0.17.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
8
|
+
streamdown-0.17.0.dist-info/entry_points.txt,sha256=HroKFsFMGf_h9PRTE96NjvjJQWupMW5TGP5RGUr1O_Q,74
|
|
9
|
+
streamdown-0.17.0.dist-info/licenses/LICENSE.MIT,sha256=SnY46EPirUsF20dZDR8HpyVgS2_4Tjxuc6f-4OdqO7U,1070
|
|
10
|
+
streamdown-0.17.0.dist-info/RECORD,,
|
streamdown/scrape/file_0.py
DELETED
|
@@ -1,22 +0,0 @@
|
|
|
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)
|
streamdown/scrape/file_1.js
DELETED
|
@@ -1,27 +0,0 @@
|
|
|
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);
|
streamdown/scrape/file_2.cpp
DELETED
|
@@ -1,23 +0,0 @@
|
|
|
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/tt.mds
DELETED
|
@@ -1,13 +0,0 @@
|
|
|
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,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|