streamdown 0.19.0__py3-none-any.whl → 0.20.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
@@ -65,7 +65,7 @@ Symbol = { H = 1.00, S = 1.00, V = 1.50 }
65
65
  Head = { H = 1.00, S = 1.00, V = 1.75 }
66
66
  Grey = { H = 1.00, S = 0.25, V = 1.37 }
67
67
  Bright = { H = 1.00, S = 2.00, V = 2.00 }
68
- Syntax = "monokai"
68
+ Syntax = "dracula"
69
69
  """
70
70
 
71
71
  def ensure_config_file():
@@ -92,6 +92,7 @@ BOLD = ["\033[1m", "\033[22m"]
92
92
  UNDERLINE = ["\033[4m", "\033[24m"]
93
93
  ITALIC = ["\033[3m", "\033[23m"]
94
94
  STRIKEOUT = ["\033[9m", "\033[29m"]
95
+ LINK = ["\033]8;;", "\033]8;;\033\\"]
95
96
  SUPER = [ 0x2070, 0x00B9, 0x00B2, 0x00B3, 0x2074, 0x2075, 0x2076, 0x2077, 0x2078, 0x2079 ]
96
97
 
97
98
  ESCAPE = r"\033\[[0-9;]*[mK]"
@@ -100,7 +101,7 @@ KEYCODE_RE = re.compile(r'\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])')
100
101
 
101
102
  visible = lambda x: re.sub(ANSIESCAPE, "", x)
102
103
  # cjk characters are double width
103
- visible_length = lambda x: len(visible(x)) + cjk_count(x)
104
+ visible_length = lambda x: len(visible(x)) + dbl_count(x)
104
105
  extract_ansi_codes = lambda text: re.findall(ESCAPE, text)
105
106
  remove_ansi = lambda line, codeList: reduce(lambda line, code: line.replace(code, ''), codeList, line)
106
107
 
@@ -116,8 +117,8 @@ def savebrace():
116
117
  if state.Savebrace and state.code_buffer_raw:
117
118
  path = os.path.join(tempfile.gettempdir(), "sd", 'savebrace')
118
119
  with open(path, "a") as f:
119
- f.write(state.code_buffer_raw)
120
-
120
+ f.write(state.code_buffer_raw + "\x00")
121
+ f.flush()
121
122
 
122
123
  class Goto(Exception):
123
124
  pass
@@ -208,7 +209,7 @@ class ParseState:
208
209
  return offset + (state.current_width(listwidth = True) if Style.PrettyBroken else self.WidthFull)
209
210
 
210
211
  def current_width(self, listwidth = False):
211
- return self.Width - (len(visible(self.space_left(listwidth))) + Style.Margin)
212
+ return self.Width - (len(visible(self.space_left(listwidth))))
212
213
 
213
214
  def space_left(self, listwidth = False):
214
215
  pre = ' ' * (len(state.list_item_stack)) * Style.ListIndent if listwidth else ''
@@ -216,6 +217,20 @@ class ParseState:
216
217
 
217
218
  state = ParseState()
218
219
 
220
+ def override_background(style_name, background_color):
221
+ base_style = get_style_by_name(style_name)
222
+ base_style.background_color = background_color
223
+ for i in base_style:
224
+ i[1]['bgcolor'] = background_color
225
+ for i,v in base_style.styles.items():
226
+ if v and 'bg' in v:
227
+ base_style.styles[i] = re.sub(r'bg:[^ ]*', '', base_style.styles[i] )
228
+ for k,v in base_style._styles.items():
229
+ if v[4] != '':
230
+ v[4] = ''
231
+
232
+ return base_style
233
+
219
234
  def format_table(rowList):
220
235
  num_cols = len(rowList)
221
236
  row_height = 0
@@ -224,15 +239,20 @@ def format_table(rowList):
224
239
  # Calculate max width per column (integer division)
225
240
  # Subtract num_cols + 1 for the vertical borders '│'
226
241
  available_width = state.current_width() - (num_cols + 1)
227
- col_width = max(1, available_width // num_cols)
242
+
243
+ width_base = available_width // num_cols
244
+ width_mod = available_width % num_cols
245
+
246
+ col_width_list = [width_base + (1 if i < width_mod else 0) for i in range(num_cols)]
228
247
  bg_color = Style.Mid if state.in_table == Style.Head else Style.Dark
229
248
  state.bg = f"{BG}{bg_color}"
230
249
 
231
250
  # First Pass: Wrap text and calculate row heights
232
251
  # Note this is where every cell is formatted so if
233
252
  # you are styling, do it before here!
234
- for row in rowList:
235
- wrapped_cell = text_wrap(row, width=col_width, force_truncate=True)
253
+ for ix in range(len(rowList)):
254
+ row = rowList[ix]
255
+ wrapped_cell = text_wrap(row, width=col_width_list[ix], force_truncate=True)
236
256
 
237
257
  # Ensure at least one line, even for empty cells
238
258
  if not wrapped_cell:
@@ -248,13 +268,14 @@ def format_table(rowList):
248
268
  line_segments = []
249
269
 
250
270
  # Now we want to snatch this row index from all our cells
251
- for cell in wrapped_cellList:
271
+ for iy in range(len(wrapped_cellList)):
272
+ cell = wrapped_cellList[iy]
252
273
  segment = ''
253
274
  if ix < len(cell):
254
275
  segment = cell[ix]
255
276
 
256
277
  # Margin logic is correctly indented here
257
- margin_needed = col_width - visible_length(segment)
278
+ margin_needed = col_width_list[iy] - visible_length(segment)
258
279
  margin_segment = segment + (" " * max(0, margin_needed))
259
280
  line_segments.append(f"{BG}{bg_color}{extra} {margin_segment}")
260
281
 
@@ -269,7 +290,7 @@ def emit_h(level, text):
269
290
  text = line_format(text)
270
291
  spaces_to_center = (state.current_width() - visible_length(text)) / 2
271
292
  if level == 1: #
272
- return f"{state.space_left()}\n{state.space_left()}{BOLD[0]}{' ' * math.floor(spaces_to_center)}{text}{BOLD[1]}"
293
+ return f"{state.space_left()}\n{state.space_left()}{BOLD[1]}{' ' * math.floor(spaces_to_center)}{text}{BOLD[1]}"
273
294
  elif level == 2: ##
274
295
  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}"
275
296
  elif level == 3: ###
@@ -338,7 +359,16 @@ def ansi_collapse(codelist, inp):
338
359
 
339
360
  def split_text(text):
340
361
  return [x for x in re.split(
341
- r'(?<=[\u3000-\u303F\u4E00-\u9FFF\u3400-\u4DBF\uF900-\uFAFF])|(?=[\u4E00-\u9FFF\u3400-\u4DBF\uF900-\uFAFF])|\s+',
362
+ r'(?<=['
363
+ r'\u3000-\u303F'
364
+ r'\u4E00-\u9FFF'
365
+ r'\u3400-\u4DBF'
366
+ r'\uF900-\uFAFF'
367
+ r'])|(?=['
368
+ #r'\u4E00-\u9FFF'
369
+ r'\u3400-\u4DBF'
370
+ r'\uF900-\uFAFF'
371
+ r'])|\s+',
342
372
  text
343
373
  ) if x]
344
374
 
@@ -347,7 +377,11 @@ def text_wrap(text, width = -1, indent = 0, first_line_prefix="", subsequent_lin
347
377
  width = state.Width
348
378
 
349
379
  # The empty word clears the buffer at the end.
350
- words = split_text(line_format(text)) + [""]
380
+ formatted = line_format(text)
381
+ #print(bytes(formatted, 'utf-8'), formatted)
382
+ words = split_text(formatted) + [""]
383
+ #print([bytes(i, 'utf-8') for i in words])
384
+
351
385
  lines = []
352
386
  current_line = ""
353
387
  current_style = []
@@ -378,6 +412,10 @@ def text_wrap(text, width = -1, indent = 0, first_line_prefix="", subsequent_lin
378
412
  margin = max(0, width - visible_length(line_content))
379
413
 
380
414
  if line_content.strip() != "":
415
+ # We make absolutely positively sure beyond any doubt
416
+ # that we have closed our hyperlink OSC
417
+ if LINK[0] in line_content:
418
+ line_content += LINK[1]
381
419
  lines.append(line_content + state.bg + ' ' * margin)
382
420
 
383
421
  current_line = (" " * indent) + "".join(current_style) + word
@@ -395,14 +433,23 @@ def text_wrap(text, width = -1, indent = 0, first_line_prefix="", subsequent_lin
395
433
 
396
434
  return lines
397
435
 
436
+ def dbl_count(s):
437
+ dbl_re = re.compile(
438
+ r'[\u2e80-\u2eff\u3000-\u303f\u3400-\u4dbf'
439
+ r'\U00004e00-\U00009fff\U0001f300-\U0001f6ff'
440
+ r'\U0001f900-\U0001f9ff\U0001fa70-\U0001faff]',
441
+ re.UNICODE
442
+ )
443
+ return len(dbl_re.findall(visible(s)))
444
+
398
445
  def cjk_count(s):
399
446
  cjk_re = re.compile(
400
447
  r'[\u4E00-\u9FFF' # CJK Unified Ideographs
401
- r'|\u3400-\u4DBF' # CJK Unified Ideographs Extension A
402
- r'|\uF900-\uFAFF' # CJK Compatibility Ideographs
403
- r'|\uFF00-\uFFEF' # CJK Compatibility Punctuation
404
- r'|\u3000-\u303F' # CJK Symbols and Punctuation
405
- r'|\U0002F800-\U0002FA1F]' # CJK Compatibility Ideographs Supplement
448
+ r'\u3400-\u4DBF' # CJK Unified Ideographs Extension A
449
+ r'\uF900-\uFAFF' # CJK Compatibility Ideographs
450
+ r'\uFF00-\uFFEF' # CJK Compatibility Punctuation
451
+ r'\u3000-\u303F' # CJK Symbols and Punctuation
452
+ r'\U0002F800-\U0002FA1F]' # CJK Compatibility Ideographs Supplement
406
453
  )
407
454
 
408
455
  return len(cjk_re.findall(visible(s)))
@@ -427,7 +474,7 @@ def line_format(line):
427
474
  def process_links(match):
428
475
  description = match.group(1)
429
476
  url = match.group(2)
430
- return f'\033]8;;{url}\033\\{Style.Link}{description}{UNDERLINE[1]}\033]8;;\033\\{FGRESET}'
477
+ return f'{LINK[0]}{url}\033\\{Style.Link}{description}{UNDERLINE[1]}{LINK[1]}{FGRESET}'
431
478
 
432
479
  line = re.sub(r"\!\[([^\]]*)\]\(([^\)]+)\)", process_images, line)
433
480
  line = re.sub(r"\[([^\]]+)\]\(([^\)]+)\)", process_links, line)
@@ -444,19 +491,24 @@ def line_format(line):
444
491
  # This trick makes sure that things like `` ` `` render right.
445
492
  if "`" in token and (not state.inline_code or state.inline_code == token):
446
493
  if state.inline_code:
494
+ if ' ' in state.inline_code:
495
+ savebrace()
447
496
  state.inline_code = False
448
497
  else:
449
498
  state.inline_code = token
499
+ state.code_buffer_raw = ''
450
500
 
451
501
  if state.inline_code:
452
502
  result += f'{BG}{Style.Mid}'
453
503
  else:
454
504
  result += state.bg
505
+ state.code_buffer_raw = ''
455
506
 
456
507
  # This is important here because we ignore formatting
457
508
  # inside of our code block.
458
509
  elif state.inline_code:
459
510
  result += token
511
+ state.code_buffer_raw += token
460
512
 
461
513
  elif token == '~~' and (state.in_strikeout or not_text(prev_token)):
462
514
  state.in_strikeout = not state.in_strikeout
@@ -559,6 +611,7 @@ def parse(stream):
559
611
  continue
560
612
 
561
613
  state.buffer = b''
614
+ """
562
615
  # Run through the plugins first
563
616
  res = latex.Plugin(line, state, Style)
564
617
  if res is True:
@@ -569,6 +622,7 @@ def parse(stream):
569
622
  for row in res:
570
623
  yield row
571
624
  continue
625
+ """
572
626
 
573
627
  # running this here avoids stray |
574
628
  block_match = re.match(r"^\s*((>\s*)+|<.?think>)", line)
@@ -639,7 +693,6 @@ def parse(stream):
639
693
  state.code_language = 'Bash'
640
694
 
641
695
  if state.in_code:
642
- savebrace()
643
696
  state.code_buffer = state.code_buffer_raw = ""
644
697
  state.code_gen = 0
645
698
  state.code_first_line = True
@@ -671,6 +724,7 @@ def parse(stream):
671
724
  open(os.path.join(state.scrape, f"file_{state.scrape_ix}.{ext}"), 'w').write(state.code_buffer_raw)
672
725
  state.scrape_ix += 1
673
726
 
727
+ savebrace()
674
728
  state.code_language = None
675
729
  state.code_indent = 0
676
730
  code_type = state.in_code
@@ -698,10 +752,10 @@ def parse(stream):
698
752
  state.code_first_line = False
699
753
  try:
700
754
  lexer = get_lexer_by_name(state.code_language)
701
- custom_style = get_style_by_name(Style.Syntax)
755
+ custom_style = override_background(Style.Syntax, ansi2hex(Style.Dark))
702
756
  except pygments.util.ClassNotFound:
703
757
  lexer = get_lexer_by_name("Bash")
704
- custom_style = get_style_by_name("default")
758
+ custom_style = override_background("default", ansi2hex(Style.Dark))
705
759
 
706
760
  formatter = TerminalTrueColorFormatter(style=custom_style)
707
761
  if line.startswith(' ' * state.code_indent):
@@ -720,6 +774,7 @@ def parse(stream):
720
774
  else:
721
775
  continue
722
776
 
777
+ highlighted_code = highlight(line, lexer, formatter)
723
778
  indent, line_wrap = code_wrap(line)
724
779
 
725
780
  state.where_from = "in code"
@@ -731,15 +786,18 @@ def parse(stream):
731
786
  # then naively search back until our visible_lengths() match. This is not fast and there's certainly smarter
732
787
  # ways of doing it but this thing is way trickery than you think
733
788
  highlighted_code = highlight(state.code_buffer + tline, lexer, formatter)
789
+ #print("(",highlighted_code,")")
734
790
 
735
791
  # Sometimes the highlighter will do things like a full reset or a background reset.
736
792
  # This is not what we want
737
- highlighted_code = re.sub(r"\033\[49(;00|)m", '', highlighted_code)
793
+ highlighted_code = re.sub(r"\033\[[34]9(;00|)m", '', highlighted_code)
738
794
 
739
795
  # Since we are streaming we ignore the resets and newlines at the end
740
796
  if highlighted_code.endswith(FGRESET + "\n"):
741
797
  highlighted_code = highlighted_code[: -(1 + len(FGRESET))]
742
798
 
799
+ #print(bytes(highlighted_code, 'utf-8'))
800
+
743
801
  # turns out highlight will eat leading newlines on empty lines
744
802
  vislen = visible_length(state.code_buffer.lstrip())
745
803
 
@@ -927,6 +985,11 @@ def emit(inp):
927
985
  if len(buffer):
928
986
  print(buffer.pop(0), file=sys.stdout, end="", flush=True)
929
987
 
988
+ def ansi2hex(ansi_code):
989
+ parts = ansi_code.strip('m').split(";")
990
+ r, g, b = map(int, parts)
991
+ return f"#{r:02x}{g:02x}{b:02x}"
992
+
930
993
  def apply_multipliers(name, H, S, V):
931
994
  m = _style.get(name)
932
995
  r, g, b = colorsys.hsv_to_rgb(min(1.0, H * m["H"]), min(1.0, S * m["S"]), min(1.0, V * m["V"]))
@@ -1042,6 +1105,8 @@ def main():
1042
1105
  os.close(state.exec_master)
1043
1106
  if state.exec_sub:
1044
1107
  state.exec_sub.wait()
1108
+
1109
+ print(RESET, end="")
1045
1110
  sys.exit(state.exit)
1046
1111
 
1047
1112
  if __name__ == "__main__":
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: streamdown
3
- Version: 0.19.0
3
+ Version: 0.20.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
@@ -36,44 +36,51 @@ Description-Content-Type: text/markdown
36
36
  </p>
37
37
 
38
38
 
39
- Streamdown works with [simonw's llm](https://github.com/simonw/llm) along with any other streaming markdown, even something basic like curl.
40
- It supports standard piping like any normal pager and a clean `execvp` option for robustly wrapping around interactive programs with readline or their own ANSI stuff to manage.
39
+ Streamdown works with any streaming markdown such as [simonw's llm](https://github.com/simonw/llm) or even something basic like curl.
40
+
41
+ It supports standard piping and files as arguments like any normal pager but can also run as a wrapper so you retain full keyboard interactivity. Arrow keys, control, alt, all still work.
41
42
  ```bash
42
43
  $ pip install streamdown
43
44
  ```
44
45
  ![Streamdown is Amazing](https://github.com/user-attachments/assets/268cb340-78cc-4df0-a773-c5ac95eceeeb)
45
46
 
46
47
  ### Provides clean copyable code for long code lines
47
- Other renderers inject line breaks when copying code that wraps around. We're better and now you are too!
48
+ Other renderers inject line breaks when copying code that wraps around. Streamdown's better and now you are too!
48
49
  ![Handle That Mandle](https://github.com/user-attachments/assets/a27aa70c-f691-4796-84f0-c2eb18c7de23)
49
- **Tip**: You can make things prettier if you don't mind if this guarantee is broken. See the `PrettyBroken` flag below!
50
+ **Tip**: You can make things prettier if you don't mind if this guarantee is broken. See the `PrettyBroken` flag below! (There's still 2 other convenient ways of getting code blocks out.)
50
51
 
51
52
  ### Supports images
52
53
  Here's kitty and alacritty.
53
54
  ![doggie](https://github.com/user-attachments/assets/81c43983-68cd-40c1-b1d5-aa3a52004504)
54
55
 
55
56
  ### Supports hyperlinks (OSC 8) and clipboard (OSC 52)
57
+ The optional `Clipboard` feature puts the final codeblock into your clipboard. See below for details.
58
+
56
59
  [links.webm](https://github.com/user-attachments/assets/a5f71791-7c58-4183-ad3b-309f470c08a3)
57
60
 
58
- ### Supports tables
59
- ![table](https://github.com/user-attachments/assets/dbe3d13e-6bac-4f45-bf30-f1857ed98898)
61
+ ### As well as everything else...
62
+ Here's the `Savebrace` feature with `screen-query` and `sd-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.
63
+
64
+ This allows you to interactively debug in a way that the agent doesn't just wander off doing silly things.
60
65
 
61
- #### As well as everything else...
62
- ![dunder](https://github.com/user-attachments/assets/d41d7fec-6dec-4387-b53d-f2098f269a5e)
66
+ It takes about 2 minutes to set up and about 0.2s to use. Fast, fluid and free.
67
+ ![screenquery](https://github.com/user-attachments/assets/517be4fe-6962-4e4c-b2f2-563471bc48d0)
63
68
 
64
- #### ...even CJK
65
- Compare how streamdown wraps around and spaces this tabular Chinese description of programming languages to the same file using glow.
66
- ![cjk](https://github.com/user-attachments/assets/b831a5f3-7ef0-48b8-8d17-bb7b605df16a)
69
+ ### ...It even supports CJK
70
+ Compare how streamdown wraps and spaces this tabular Chinese description of programming languages to other leading markdown renderers.
71
+
72
+ Only one generates the text without truncation. 很美!
73
+ ![cjk](https://github.com/user-attachments/assets/cae485d7-c478-4836-9732-d9fa49e13bc9)
67
74
 
68
75
  ### Colors are highly (and quickly) configurable for people who care a lot, or just a little.
69
76
  ![configurable](https://github.com/user-attachments/assets/19ca2ec9-8ea1-4a79-87ca-8352789269fe)
70
77
 
71
- ### Has a [Plugin](https://github.com/kristopolous/Streamdown/tree/main/streamdown/plugins) system to extend the parser and renderer.
78
+ ### Has a [Plugin](https://github.com/kristopolous/Streamdown/tree/main/streamdown/plugins) system to extend the parser and renderers.
72
79
  For instance, here is the [latex plugin](https://github.com/kristopolous/Streamdown/blob/main/streamdown/plugins/latex.py) doing math inside a table:
73
80
  ![calc](https://github.com/user-attachments/assets/0b0027ca-8ef0-4b4a-b4ae-e36ff623a683)
74
81
 
75
82
 
76
- ## TOML Configuration
83
+ ## Configuration
77
84
 
78
85
  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.
79
86
 
@@ -81,7 +88,9 @@ Here are the sections:
81
88
 
82
89
  **`[style]`**
83
90
 
84
- Defines the base Hue (H), Saturation (S), and Value (V) from which all other palette colors are derived. The defaults are [at the beginning of the source](https://github.com/kristopolous/Streamdown/blob/main/streamdown/sd.py#L33).
91
+ Defines the base Hue (H), Saturation (S), and Value (V) from which all other palette colors are derived. This can also be specified at runtime via command line arguments. See below!
92
+
93
+ The default values are [at the beginning of the source](https://github.com/kristopolous/Streamdown/blob/main/streamdown/sd.py#L33).
85
94
 
86
95
  * `HSV`: [ 0.0 - 1.0, 0.0 - 1.0, 0.0 - 1.0 ]
87
96
  * `Dark`: Multipliers for background elements, code blocks.
@@ -1,11 +1,11 @@
1
1
  streamdown/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
- streamdown/sd.py,sha256=kXN5K_0pBKkDJfIthqUf8WVL1J0fqAamfl3g_mRp_VM,39330
2
+ streamdown/sd.py,sha256=ggXlCGovA6cS3686BLnk0ISzf2xXrx7UwuKjwjs6RUM,41475
3
3
  streamdown/ss,sha256=sel_phpaecrw6WGIHRLROsD7BFShf0rSDHheflwdUn8,277
4
4
  streamdown/ss1,sha256=CUVf86_2zeAle2oQCeTfWYqtHBrAFR_UgvptuYMQzFU,3151
5
5
  streamdown/plugins/README.md,sha256=KWqYELs9WkKJmuDzYv3cvPlZMkArsNCBUe4XDoTLjLA,1143
6
6
  streamdown/plugins/latex.py,sha256=xZMGMdx_Sw4X1piZejXFHfEG9qazU4fGeceiMI0h13Y,648
7
- streamdown-0.19.0.dist-info/METADATA,sha256=murXBmNpC449_wu6JFtQ5STo4br8eqGVVFD5ljCGxy0,8145
8
- streamdown-0.19.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
9
- streamdown-0.19.0.dist-info/entry_points.txt,sha256=HroKFsFMGf_h9PRTE96NjvjJQWupMW5TGP5RGUr1O_Q,74
10
- streamdown-0.19.0.dist-info/licenses/LICENSE.MIT,sha256=SnY46EPirUsF20dZDR8HpyVgS2_4Tjxuc6f-4OdqO7U,1070
11
- streamdown-0.19.0.dist-info/RECORD,,
7
+ streamdown-0.20.0.dist-info/METADATA,sha256=LpTl5FFiZJMk_9OC9Kqqq72satHDEHn19rGnJ93XSKY,8843
8
+ streamdown-0.20.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
9
+ streamdown-0.20.0.dist-info/entry_points.txt,sha256=HroKFsFMGf_h9PRTE96NjvjJQWupMW5TGP5RGUr1O_Q,74
10
+ streamdown-0.20.0.dist-info/licenses/LICENSE.MIT,sha256=SnY46EPirUsF20dZDR8HpyVgS2_4Tjxuc6f-4OdqO7U,1070
11
+ streamdown-0.20.0.dist-info/RECORD,,