streamdown 0.33.0__py3-none-any.whl → 0.35.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 CHANGED
@@ -57,6 +57,8 @@ Clipboard = true
57
57
  Logging = false
58
58
  Timeout = 0.1
59
59
  Savebrace = true
60
+ Images = true
61
+ Links = true
60
62
 
61
63
  [style]
62
64
  Margin = 2
@@ -165,6 +167,7 @@ class ParseState:
165
167
  self.is_pty = False
166
168
  self.is_exec = False
167
169
  self.maybe_prompt = False
170
+ self.prompt_regex = None
168
171
  self.emit_flag = None
169
172
  self.scrape = None
170
173
  self.scrape_ix = 0
@@ -204,6 +207,7 @@ class ParseState:
204
207
  self.in_underline = False
205
208
  self.in_strikeout = False
206
209
  self.block_depth = 0
210
+ self.block_type = None
207
211
 
208
212
  self.exec_sub = None
209
213
  self.exec_master = None
@@ -496,8 +500,12 @@ def line_format(line):
496
500
  url = match.group(2)
497
501
  return f'{LINK[0]}{url}\033\\{Style.Link}{description}{UNDERLINE[1]}{LINK[1]}{FGRESET}'
498
502
 
499
- line = re.sub(r"\!\[([^\]]*)\]\(([^\)]+)\)", process_images, line)
500
- line = re.sub(r"\[([^\]]+)\]\(([^\)]+)\)", process_links, line)
503
+ if state.Images:
504
+ line = re.sub(r"\!\[([^\]]*)\]\(([^\)]+)\)", process_images, line)
505
+
506
+ if state.Links:
507
+ line = re.sub(r"\[([^\]]+)\]\(([^\)]+)\)", process_links, line)
508
+
501
509
  line = re.sub(r"\[\^(\d+)\]:?", footnotes, line)
502
510
 
503
511
  tokenList = re.finditer(r"((~~|\*\*_|_\*\*|\*{1,3}|_{1,3}|`+)|[^~_*`]+)", line)
@@ -605,6 +613,7 @@ def parse(stream):
605
613
  elif stream.fileno() in ready_in:
606
614
  byte = os.read(stream.fileno(), 1)
607
615
  TimeoutIx = 0
616
+
608
617
  elif TimeoutIx == 0:
609
618
  # This is our record separator for debugging - hands peaking
610
619
  debug_write("🫣".encode('utf-8'))
@@ -614,7 +623,13 @@ def parse(stream):
614
623
  byte = stream.read(1)
615
624
 
616
625
  if byte is not None:
617
- if byte == b'': break
626
+ # This is the eol
627
+ if byte == b'':
628
+ if len(state.buffer) == 0:
629
+ break
630
+ else:
631
+ byte = b'\n'
632
+
618
633
  state.buffer += byte
619
634
  debug_write(byte)
620
635
 
@@ -622,8 +637,8 @@ def parse(stream):
622
637
 
623
638
  line = state.buffer.decode('utf-8').replace('\t',' ')
624
639
  state.has_newline = line.endswith('\n')
625
- # I hate this. There should be better ways.
626
- state.maybe_prompt = not state.has_newline and state.current()['none'] and re.match(r'^.*>\s+$', visible(line))
640
+
641
+ state.maybe_prompt = not state.has_newline and state.current()['none'] and re.match(state.prompt_regex, visible(line))
627
642
 
628
643
  # let's wait for a newline
629
644
  if state.maybe_prompt:
@@ -650,23 +665,31 @@ def parse(stream):
650
665
  """
651
666
 
652
667
  # running this here avoids stray |
653
- block_match = re.match(r"^\s*((>\s*)+|<.?think>)", line)
668
+ # So kimi doesn't newline after the <think> token and it uses some unicode triangle?. They'll
669
+ # newline at the end of it, but not the beginning.
670
+ block_match = re.match(r"^\s*((>\s*)+|[◁<].?think[>▷])(.*)", line)
654
671
  if not state.in_code and block_match:
655
- if block_match.group(1) == '</think>':
672
+ # wtf is this you might ask! Not all thinking models use < and > ...
673
+ # because why make life easy?
674
+ if block_match.group(1)[1:7] == '/think':
675
+ line = ''
656
676
  state.block_depth = 0
657
677
  yield RESET
658
- elif block_match.group(1) == '<think>':
678
+ elif block_match.group(1)[1:6] == 'think':
679
+ line = block_match.group(3)
659
680
  state.block_depth = 1
681
+ state.block_type = 'think'
660
682
  else:
661
- state.block_depth = block_match.group(0).count('>')
683
+ state.block_depth = block_match.group(1).count('>')
684
+ state.block_type = '>'
662
685
  # we also need to consume those tokens
663
- line = line[len(block_match.group(0)):]
686
+ line = line[len(block_match.group(1)):]
664
687
  else:
665
- if state.block_depth > 0:
688
+ if state.block_type == '>' and state.block_depth > 0:
666
689
  yield FGRESET
667
690
  state.block_depth = 0
668
691
 
669
- # --- Collapse Multiple Empty Lines if not in code blocks ---
692
+ # Collapse Multiple Empty Lines if not in code blocks
670
693
  if not state.in_code:
671
694
  is_empty = line.strip() == ""
672
695
 
@@ -831,7 +854,7 @@ def parse(stream):
831
854
  while parts[-1] in [FGRESET, FORMATRESET]:
832
855
  parts.pop()
833
856
 
834
- tline_len = visible_length(tline)
857
+ tline_len = visible_length(tline.rstrip('\r\n'))
835
858
 
836
859
  # now we find the new stuff:
837
860
  ttl = 0
@@ -842,7 +865,7 @@ def parse(stream):
842
865
 
843
866
  ttl += len(idx) if idx[0] != '\x1b' else 0
844
867
 
845
- if ttl > 1+tline_len:
868
+ if ttl > tline_len:
846
869
  break
847
870
 
848
871
 
@@ -1101,6 +1124,7 @@ def main():
1101
1124
  parser.add_argument("-c", "--config", default=None, help="Use a custom config override")
1102
1125
  parser.add_argument("-w", "--width", default="0", help="Set the width WIDTH")
1103
1126
  parser.add_argument("-e", "--exec", help="Wrap a program EXEC for more 'proper' i/o handling")
1127
+ parser.add_argument("-p", "--prompt", default="^.*>\\s+$", help="A PCRE regex prompt to detect (default: %(default)s)")
1104
1128
  parser.add_argument("-s", "--scrape", help="Scrape code snippets to a directory SCRAPE")
1105
1129
  parser.add_argument("-v", "--version", action="store_true", help="Show version information")
1106
1130
  args = parser.parse_args()
@@ -1134,7 +1158,7 @@ def main():
1134
1158
  setattr(Style, color, apply_multipliers(style, color, H, S, V))
1135
1159
  for attr in ['PrettyPad', 'PrettyBroken', 'Margin', 'ListIndent', 'Syntax']:
1136
1160
  setattr(Style, attr, style.get(attr))
1137
- for attr in ['CodeSpaces', 'Clipboard', 'Logging', 'Timeout', 'Savebrace']:
1161
+ for attr in ['Links', 'Images', 'CodeSpaces', 'Clipboard', 'Logging', 'Timeout', 'Savebrace']:
1138
1162
  setattr(state, attr, features.get(attr))
1139
1163
 
1140
1164
 
@@ -1144,6 +1168,7 @@ def main():
1144
1168
 
1145
1169
  Style.MarginSpaces = " " * Style.Margin
1146
1170
  state.WidthArg = int(args.width) or style.get("Width") or 0
1171
+ state.prompt_regex = re.compile(args.prompt)
1147
1172
  Style.Blockquote = f"{FG}{Style.Grey}│ "
1148
1173
  width_calc()
1149
1174
 
streamdown/test.txt ADDED
@@ -0,0 +1 @@
1
+ hi
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: streamdown
3
- Version: 0.33.0
3
+ Version: 0.35.0
4
4
  Summary: A streaming markdown renderer for modern terminals with syntax highlighting
5
5
  Project-URL: Homepage, https://github.com/day50-dev/Streamdown
6
6
  Project-URL: Bug Tracker, https://github.com/day50-dev/Streamdown/issues
@@ -70,7 +70,7 @@ The optional `Clipboard` feature puts the final codeblock into your clipboard. S
70
70
  [links.webm](https://github.com/user-attachments/assets/a5f71791-7c58-4183-ad3b-309f470c08a3)
71
71
 
72
72
  ### As well as everything else...
73
- Here's the `Savebrace` feature with `screen-query` and `sq-picker` from [llmehelp](https://github.com/kristopolous/llmehelp). You can have an ongoing conversation in tmux with your terminal session. Then use popups and fzf to insert command or coding blocks all with a keystroke.
73
+ Here's the `Savebrace` feature with [`sidechat` and `sc-picker`](https://github.com/day50-dev/sidechat). You can have an ongoing conversation in tmux with your terminal session. Then use popups and fzf to insert command or coding blocks all with a keystroke.
74
74
 
75
75
  This allows you to interactively debug in a way that the agent doesn't just wander off doing silly things.
76
76
 
@@ -1,12 +1,13 @@
1
1
  streamdown/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
2
  streamdown/qwen3.md,sha256=1e7ELkK-quwUeOmBDwXodFH-DlnfAcQWj32rjK6Zex4,542
3
- streamdown/sd.py,sha256=jdMBN1iQ-L-t4h2yjETGS97s-6e50BEFv4C14ToS5AM,44961
3
+ streamdown/sd.py,sha256=38nDZjHXQKorpUesXlTa0LkeR-gP6DPEw9h6exTthJo,45901
4
4
  streamdown/ss,sha256=sel_phpaecrw6WGIHRLROsD7BFShf0rSDHheflwdUn8,277
5
5
  streamdown/ss1,sha256=CUVf86_2zeAle2oQCeTfWYqtHBrAFR_UgvptuYMQzFU,3151
6
+ streamdown/test.txt,sha256=j0NDRmSPa5bfid2pAcUXaxCm2Dlh3TwayItZstwyeqQ,2
6
7
  streamdown/plugins/README.md,sha256=KWqYELs9WkKJmuDzYv3cvPlZMkArsNCBUe4XDoTLjLA,1143
7
8
  streamdown/plugins/latex.py,sha256=xZMGMdx_Sw4X1piZejXFHfEG9qazU4fGeceiMI0h13Y,648
8
- streamdown-0.33.0.dist-info/METADATA,sha256=Xq6ccpX4MUuXKwXOAVWV09WXuqYdEpTj52-QRVQZB9E,10216
9
- streamdown-0.33.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
10
- streamdown-0.33.0.dist-info/entry_points.txt,sha256=HroKFsFMGf_h9PRTE96NjvjJQWupMW5TGP5RGUr1O_Q,74
11
- streamdown-0.33.0.dist-info/licenses/LICENSE.MIT,sha256=SnY46EPirUsF20dZDR8HpyVgS2_4Tjxuc6f-4OdqO7U,1070
12
- streamdown-0.33.0.dist-info/RECORD,,
9
+ streamdown-0.35.0.dist-info/METADATA,sha256=LnPBbC-QwFbxttWL3Kf46lwCYEpt8RPCZsLm5IHo47Y,10195
10
+ streamdown-0.35.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
11
+ streamdown-0.35.0.dist-info/entry_points.txt,sha256=HroKFsFMGf_h9PRTE96NjvjJQWupMW5TGP5RGUr1O_Q,74
12
+ streamdown-0.35.0.dist-info/licenses/LICENSE.MIT,sha256=SnY46EPirUsF20dZDR8HpyVgS2_4Tjxuc6f-4OdqO7U,1070
13
+ streamdown-0.35.0.dist-info/RECORD,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: hatchling 1.27.0
2
+ Generator: hatchling 1.28.0
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any