streamdown 0.19.0__py3-none-any.whl → 0.21.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
 
@@ -114,10 +115,12 @@ def debug_write(text):
114
115
 
115
116
  def savebrace():
116
117
  if state.Savebrace and state.code_buffer_raw:
118
+ tmp_dir = os.path.join(tempfile.gettempdir(), "sd")
119
+ os.makedirs(tmp_dir, exist_ok=True)
117
120
  path = os.path.join(tempfile.gettempdir(), "sd", 'savebrace')
118
121
  with open(path, "a") as f:
119
- f.write(state.code_buffer_raw)
120
-
122
+ f.write(state.code_buffer_raw + "\x00")
123
+ f.flush()
121
124
 
122
125
  class Goto(Exception):
123
126
  pass
@@ -208,7 +211,7 @@ class ParseState:
208
211
  return offset + (state.current_width(listwidth = True) if Style.PrettyBroken else self.WidthFull)
209
212
 
210
213
  def current_width(self, listwidth = False):
211
- return self.Width - (len(visible(self.space_left(listwidth))) + Style.Margin)
214
+ return self.Width - (len(visible(self.space_left(listwidth))))
212
215
 
213
216
  def space_left(self, listwidth = False):
214
217
  pre = ' ' * (len(state.list_item_stack)) * Style.ListIndent if listwidth else ''
@@ -216,6 +219,20 @@ class ParseState:
216
219
 
217
220
  state = ParseState()
218
221
 
222
+ def override_background(style_name, background_color):
223
+ base_style = get_style_by_name(style_name)
224
+ base_style.background_color = background_color
225
+ for i in base_style:
226
+ i[1]['bgcolor'] = background_color
227
+ for i,v in base_style.styles.items():
228
+ if v and 'bg' in v:
229
+ base_style.styles[i] = re.sub(r'bg:[^ ]*', '', base_style.styles[i] )
230
+ for k,v in base_style._styles.items():
231
+ if v[4] != '':
232
+ v[4] = ''
233
+
234
+ return base_style
235
+
219
236
  def format_table(rowList):
220
237
  num_cols = len(rowList)
221
238
  row_height = 0
@@ -224,15 +241,20 @@ def format_table(rowList):
224
241
  # Calculate max width per column (integer division)
225
242
  # Subtract num_cols + 1 for the vertical borders '│'
226
243
  available_width = state.current_width() - (num_cols + 1)
227
- col_width = max(1, available_width // num_cols)
244
+
245
+ width_base = available_width // num_cols
246
+ width_mod = available_width % num_cols
247
+
248
+ col_width_list = [width_base + (1 if i < width_mod else 0) for i in range(num_cols)]
228
249
  bg_color = Style.Mid if state.in_table == Style.Head else Style.Dark
229
250
  state.bg = f"{BG}{bg_color}"
230
251
 
231
252
  # First Pass: Wrap text and calculate row heights
232
253
  # Note this is where every cell is formatted so if
233
254
  # you are styling, do it before here!
234
- for row in rowList:
235
- wrapped_cell = text_wrap(row, width=col_width, force_truncate=True)
255
+ for ix in range(len(rowList)):
256
+ row = rowList[ix]
257
+ wrapped_cell = text_wrap(row, width=col_width_list[ix], force_truncate=True)
236
258
 
237
259
  # Ensure at least one line, even for empty cells
238
260
  if not wrapped_cell:
@@ -248,13 +270,14 @@ def format_table(rowList):
248
270
  line_segments = []
249
271
 
250
272
  # Now we want to snatch this row index from all our cells
251
- for cell in wrapped_cellList:
273
+ for iy in range(len(wrapped_cellList)):
274
+ cell = wrapped_cellList[iy]
252
275
  segment = ''
253
276
  if ix < len(cell):
254
277
  segment = cell[ix]
255
278
 
256
279
  # Margin logic is correctly indented here
257
- margin_needed = col_width - visible_length(segment)
280
+ margin_needed = col_width_list[iy] - visible_length(segment)
258
281
  margin_segment = segment + (" " * max(0, margin_needed))
259
282
  line_segments.append(f"{BG}{bg_color}{extra} {margin_segment}")
260
283
 
@@ -269,7 +292,7 @@ def emit_h(level, text):
269
292
  text = line_format(text)
270
293
  spaces_to_center = (state.current_width() - visible_length(text)) / 2
271
294
  if level == 1: #
272
- return f"{state.space_left()}\n{state.space_left()}{BOLD[0]}{' ' * math.floor(spaces_to_center)}{text}{BOLD[1]}"
295
+ return f"{state.space_left()}\n{state.space_left()}{BOLD[1]}{' ' * math.floor(spaces_to_center)}{text}{BOLD[1]}"
273
296
  elif level == 2: ##
274
297
  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
298
  elif level == 3: ###
@@ -338,7 +361,16 @@ def ansi_collapse(codelist, inp):
338
361
 
339
362
  def split_text(text):
340
363
  return [x for x in re.split(
341
- r'(?<=[\u3000-\u303F\u4E00-\u9FFF\u3400-\u4DBF\uF900-\uFAFF])|(?=[\u4E00-\u9FFF\u3400-\u4DBF\uF900-\uFAFF])|\s+',
364
+ r'(?<=['
365
+ r'\u3000-\u303F'
366
+ r'\u4E00-\u9FFF'
367
+ r'\u3400-\u4DBF'
368
+ r'\uF900-\uFAFF'
369
+ r'])|(?=['
370
+ #r'\u4E00-\u9FFF'
371
+ r'\u3400-\u4DBF'
372
+ r'\uF900-\uFAFF'
373
+ r'])|\s+',
342
374
  text
343
375
  ) if x]
344
376
 
@@ -347,7 +379,11 @@ def text_wrap(text, width = -1, indent = 0, first_line_prefix="", subsequent_lin
347
379
  width = state.Width
348
380
 
349
381
  # The empty word clears the buffer at the end.
350
- words = split_text(line_format(text)) + [""]
382
+ formatted = line_format(text)
383
+ #print(bytes(formatted, 'utf-8'), formatted)
384
+ words = split_text(formatted) + [""]
385
+ #print([bytes(i, 'utf-8') for i in words])
386
+
351
387
  lines = []
352
388
  current_line = ""
353
389
  current_style = []
@@ -378,6 +414,10 @@ def text_wrap(text, width = -1, indent = 0, first_line_prefix="", subsequent_lin
378
414
  margin = max(0, width - visible_length(line_content))
379
415
 
380
416
  if line_content.strip() != "":
417
+ # We make absolutely positively sure beyond any doubt
418
+ # that we have closed our hyperlink OSC
419
+ if LINK[0] in line_content:
420
+ line_content += LINK[1]
381
421
  lines.append(line_content + state.bg + ' ' * margin)
382
422
 
383
423
  current_line = (" " * indent) + "".join(current_style) + word
@@ -395,14 +435,23 @@ def text_wrap(text, width = -1, indent = 0, first_line_prefix="", subsequent_lin
395
435
 
396
436
  return lines
397
437
 
438
+ def dbl_count(s):
439
+ dbl_re = re.compile(
440
+ r'[\u2e80-\u2eff\u3000-\u303f\u3400-\u4dbf'
441
+ r'\U00004e00-\U00009fff\U0001f300-\U0001f6ff'
442
+ r'\U0001f900-\U0001f9ff\U0001fa70-\U0001faff]',
443
+ re.UNICODE
444
+ )
445
+ return len(dbl_re.findall(visible(s)))
446
+
398
447
  def cjk_count(s):
399
448
  cjk_re = re.compile(
400
449
  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
450
+ r'\u3400-\u4DBF' # CJK Unified Ideographs Extension A
451
+ r'\uF900-\uFAFF' # CJK Compatibility Ideographs
452
+ r'\uFF00-\uFFEF' # CJK Compatibility Punctuation
453
+ r'\u3000-\u303F' # CJK Symbols and Punctuation
454
+ r'\U0002F800-\U0002FA1F]' # CJK Compatibility Ideographs Supplement
406
455
  )
407
456
 
408
457
  return len(cjk_re.findall(visible(s)))
@@ -427,7 +476,7 @@ def line_format(line):
427
476
  def process_links(match):
428
477
  description = match.group(1)
429
478
  url = match.group(2)
430
- return f'\033]8;;{url}\033\\{Style.Link}{description}{UNDERLINE[1]}\033]8;;\033\\{FGRESET}'
479
+ return f'{LINK[0]}{url}\033\\{Style.Link}{description}{UNDERLINE[1]}{LINK[1]}{FGRESET}'
431
480
 
432
481
  line = re.sub(r"\!\[([^\]]*)\]\(([^\)]+)\)", process_images, line)
433
482
  line = re.sub(r"\[([^\]]+)\]\(([^\)]+)\)", process_links, line)
@@ -444,19 +493,24 @@ def line_format(line):
444
493
  # This trick makes sure that things like `` ` `` render right.
445
494
  if "`" in token and (not state.inline_code or state.inline_code == token):
446
495
  if state.inline_code:
496
+ if ' ' in state.inline_code:
497
+ savebrace()
447
498
  state.inline_code = False
448
499
  else:
449
500
  state.inline_code = token
501
+ state.code_buffer_raw = ''
450
502
 
451
503
  if state.inline_code:
452
504
  result += f'{BG}{Style.Mid}'
453
505
  else:
454
506
  result += state.bg
507
+ state.code_buffer_raw = ''
455
508
 
456
509
  # This is important here because we ignore formatting
457
510
  # inside of our code block.
458
511
  elif state.inline_code:
459
512
  result += token
513
+ state.code_buffer_raw += token
460
514
 
461
515
  elif token == '~~' and (state.in_strikeout or not_text(prev_token)):
462
516
  state.in_strikeout = not state.in_strikeout
@@ -559,6 +613,7 @@ def parse(stream):
559
613
  continue
560
614
 
561
615
  state.buffer = b''
616
+ """
562
617
  # Run through the plugins first
563
618
  res = latex.Plugin(line, state, Style)
564
619
  if res is True:
@@ -569,6 +624,7 @@ def parse(stream):
569
624
  for row in res:
570
625
  yield row
571
626
  continue
627
+ """
572
628
 
573
629
  # running this here avoids stray |
574
630
  block_match = re.match(r"^\s*((>\s*)+|<.?think>)", line)
@@ -639,7 +695,6 @@ def parse(stream):
639
695
  state.code_language = 'Bash'
640
696
 
641
697
  if state.in_code:
642
- savebrace()
643
698
  state.code_buffer = state.code_buffer_raw = ""
644
699
  state.code_gen = 0
645
700
  state.code_first_line = True
@@ -671,6 +726,7 @@ def parse(stream):
671
726
  open(os.path.join(state.scrape, f"file_{state.scrape_ix}.{ext}"), 'w').write(state.code_buffer_raw)
672
727
  state.scrape_ix += 1
673
728
 
729
+ savebrace()
674
730
  state.code_language = None
675
731
  state.code_indent = 0
676
732
  code_type = state.in_code
@@ -698,10 +754,10 @@ def parse(stream):
698
754
  state.code_first_line = False
699
755
  try:
700
756
  lexer = get_lexer_by_name(state.code_language)
701
- custom_style = get_style_by_name(Style.Syntax)
757
+ custom_style = override_background(Style.Syntax, ansi2hex(Style.Dark))
702
758
  except pygments.util.ClassNotFound:
703
759
  lexer = get_lexer_by_name("Bash")
704
- custom_style = get_style_by_name("default")
760
+ custom_style = override_background("default", ansi2hex(Style.Dark))
705
761
 
706
762
  formatter = TerminalTrueColorFormatter(style=custom_style)
707
763
  if line.startswith(' ' * state.code_indent):
@@ -720,6 +776,7 @@ def parse(stream):
720
776
  else:
721
777
  continue
722
778
 
779
+ highlighted_code = highlight(line, lexer, formatter)
723
780
  indent, line_wrap = code_wrap(line)
724
781
 
725
782
  state.where_from = "in code"
@@ -731,15 +788,18 @@ def parse(stream):
731
788
  # then naively search back until our visible_lengths() match. This is not fast and there's certainly smarter
732
789
  # ways of doing it but this thing is way trickery than you think
733
790
  highlighted_code = highlight(state.code_buffer + tline, lexer, formatter)
791
+ #print("(",highlighted_code,")")
734
792
 
735
793
  # Sometimes the highlighter will do things like a full reset or a background reset.
736
794
  # This is not what we want
737
- highlighted_code = re.sub(r"\033\[49(;00|)m", '', highlighted_code)
795
+ highlighted_code = re.sub(r"\033\[[34]9(;00|)m", '', highlighted_code)
738
796
 
739
797
  # Since we are streaming we ignore the resets and newlines at the end
740
798
  if highlighted_code.endswith(FGRESET + "\n"):
741
799
  highlighted_code = highlighted_code[: -(1 + len(FGRESET))]
742
800
 
801
+ #print(bytes(highlighted_code, 'utf-8'))
802
+
743
803
  # turns out highlight will eat leading newlines on empty lines
744
804
  vislen = visible_length(state.code_buffer.lstrip())
745
805
 
@@ -927,6 +987,11 @@ def emit(inp):
927
987
  if len(buffer):
928
988
  print(buffer.pop(0), file=sys.stdout, end="", flush=True)
929
989
 
990
+ def ansi2hex(ansi_code):
991
+ parts = ansi_code.strip('m').split(";")
992
+ r, g, b = map(int, parts)
993
+ return f"#{r:02x}{g:02x}{b:02x}"
994
+
930
995
  def apply_multipliers(name, H, S, V):
931
996
  m = _style.get(name)
932
997
  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 +1107,8 @@ def main():
1042
1107
  os.close(state.exec_master)
1043
1108
  if state.exec_sub:
1044
1109
  state.exec_sub.wait()
1110
+
1111
+ print(RESET, end="")
1045
1112
  sys.exit(state.exit)
1046
1113
 
1047
1114
  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.21.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=GmxttpBmMR3yJU2UGM9ESOYqFQMU28HYhVwAKcsMERM,41579
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.21.0.dist-info/METADATA,sha256=AcCbhBJvkAvdcB-4_1qDhXkrAp02-4vsKWPaoLW2jXg,8843
8
+ streamdown-0.21.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
9
+ streamdown-0.21.0.dist-info/entry_points.txt,sha256=HroKFsFMGf_h9PRTE96NjvjJQWupMW5TGP5RGUr1O_Q,74
10
+ streamdown-0.21.0.dist-info/licenses/LICENSE.MIT,sha256=SnY46EPirUsF20dZDR8HpyVgS2_4Tjxuc6f-4OdqO7U,1070
11
+ streamdown-0.21.0.dist-info/RECORD,,