streamdown 0.18.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: ###
@@ -337,21 +358,35 @@ def ansi_collapse(codelist, inp):
337
358
 
338
359
 
339
360
  def split_text(text):
340
- return re.split(
341
- r'(?<=[\u4E00-\u9FFF\u3400-\u4DBF\uF900-\uFAFF])|(?=[\u4E00-\u9FFF\u3400-\u4DBF\uF900-\uFAFF])|\s+',
361
+ return [x for x in re.split(
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
 
345
375
  def text_wrap(text, width = -1, indent = 0, first_line_prefix="", subsequent_line_prefix="", force_truncate=False):
346
376
  if width == -1:
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 = []
354
388
 
389
+ oldword = ''
355
390
  for word in words:
356
391
  # we apply the style if we see it at the beginning of the word
357
392
  codes = extract_ansi_codes(word)
@@ -360,8 +395,12 @@ def text_wrap(text, width = -1, indent = 0, first_line_prefix="", subsequent_lin
360
395
  current_style.append(codes.pop(0))
361
396
 
362
397
  if len(word) and visible_length(current_line) + visible_length(word) + 1 <= width: # +1 for space
363
-
364
- current_line += (" " if len(visible(word)) > 0 and current_line and not cjk_count(word) else "") + word
398
+ space = ""
399
+ if len(visible(word)) > 0 and current_line:
400
+ space = " "
401
+ if (":" in visible(word) or cjk_count(word)) and cjk_count(oldword):
402
+ space = ""
403
+ current_line += space + word
365
404
  else:
366
405
  # Word doesn't fit, finalize the previous line
367
406
  prefix = first_line_prefix if not lines else subsequent_line_prefix
@@ -373,6 +412,10 @@ def text_wrap(text, width = -1, indent = 0, first_line_prefix="", subsequent_lin
373
412
  margin = max(0, width - visible_length(line_content))
374
413
 
375
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]
376
419
  lines.append(line_content + state.bg + ' ' * margin)
377
420
 
378
421
  current_line = (" " * indent) + "".join(current_style) + word
@@ -383,19 +426,30 @@ def text_wrap(text, width = -1, indent = 0, first_line_prefix="", subsequent_lin
383
426
  if codes:
384
427
  current_style = ansi_collapse(current_style, codes)
385
428
 
429
+ oldword = word
430
+
386
431
  if len(lines) < 1:
387
432
  return []
388
433
 
389
434
  return lines
390
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
+
391
445
  def cjk_count(s):
392
446
  cjk_re = re.compile(
393
447
  r'[\u4E00-\u9FFF' # CJK Unified Ideographs
394
- r'|\u3400-\u4DBF' # CJK Unified Ideographs Extension A
395
- r'|\uF900-\uFAFF' # CJK Compatibility Ideographs
396
- r'|\uFF00-\uFFEF' # CJK Compatibility Punctuation
397
- r'|\u3000-\u303F' # CJK Symbols and Punctuation
398
- 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
399
453
  )
400
454
 
401
455
  return len(cjk_re.findall(visible(s)))
@@ -420,7 +474,7 @@ def line_format(line):
420
474
  def process_links(match):
421
475
  description = match.group(1)
422
476
  url = match.group(2)
423
- 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}'
424
478
 
425
479
  line = re.sub(r"\!\[([^\]]*)\]\(([^\)]+)\)", process_images, line)
426
480
  line = re.sub(r"\[([^\]]+)\]\(([^\)]+)\)", process_links, line)
@@ -437,19 +491,24 @@ def line_format(line):
437
491
  # This trick makes sure that things like `` ` `` render right.
438
492
  if "`" in token and (not state.inline_code or state.inline_code == token):
439
493
  if state.inline_code:
494
+ if ' ' in state.inline_code:
495
+ savebrace()
440
496
  state.inline_code = False
441
497
  else:
442
498
  state.inline_code = token
499
+ state.code_buffer_raw = ''
443
500
 
444
501
  if state.inline_code:
445
502
  result += f'{BG}{Style.Mid}'
446
503
  else:
447
504
  result += state.bg
505
+ state.code_buffer_raw = ''
448
506
 
449
507
  # This is important here because we ignore formatting
450
508
  # inside of our code block.
451
509
  elif state.inline_code:
452
510
  result += token
511
+ state.code_buffer_raw += token
453
512
 
454
513
  elif token == '~~' and (state.in_strikeout or not_text(prev_token)):
455
514
  state.in_strikeout = not state.in_strikeout
@@ -552,6 +611,7 @@ def parse(stream):
552
611
  continue
553
612
 
554
613
  state.buffer = b''
614
+ """
555
615
  # Run through the plugins first
556
616
  res = latex.Plugin(line, state, Style)
557
617
  if res is True:
@@ -562,6 +622,7 @@ def parse(stream):
562
622
  for row in res:
563
623
  yield row
564
624
  continue
625
+ """
565
626
 
566
627
  # running this here avoids stray |
567
628
  block_match = re.match(r"^\s*((>\s*)+|<.?think>)", line)
@@ -632,7 +693,6 @@ def parse(stream):
632
693
  state.code_language = 'Bash'
633
694
 
634
695
  if state.in_code:
635
- savebrace()
636
696
  state.code_buffer = state.code_buffer_raw = ""
637
697
  state.code_gen = 0
638
698
  state.code_first_line = True
@@ -664,6 +724,7 @@ def parse(stream):
664
724
  open(os.path.join(state.scrape, f"file_{state.scrape_ix}.{ext}"), 'w').write(state.code_buffer_raw)
665
725
  state.scrape_ix += 1
666
726
 
727
+ savebrace()
667
728
  state.code_language = None
668
729
  state.code_indent = 0
669
730
  code_type = state.in_code
@@ -691,10 +752,10 @@ def parse(stream):
691
752
  state.code_first_line = False
692
753
  try:
693
754
  lexer = get_lexer_by_name(state.code_language)
694
- custom_style = get_style_by_name(Style.Syntax)
755
+ custom_style = override_background(Style.Syntax, ansi2hex(Style.Dark))
695
756
  except pygments.util.ClassNotFound:
696
757
  lexer = get_lexer_by_name("Bash")
697
- custom_style = get_style_by_name("default")
758
+ custom_style = override_background("default", ansi2hex(Style.Dark))
698
759
 
699
760
  formatter = TerminalTrueColorFormatter(style=custom_style)
700
761
  if line.startswith(' ' * state.code_indent):
@@ -713,6 +774,7 @@ def parse(stream):
713
774
  else:
714
775
  continue
715
776
 
777
+ highlighted_code = highlight(line, lexer, formatter)
716
778
  indent, line_wrap = code_wrap(line)
717
779
 
718
780
  state.where_from = "in code"
@@ -724,15 +786,18 @@ def parse(stream):
724
786
  # then naively search back until our visible_lengths() match. This is not fast and there's certainly smarter
725
787
  # ways of doing it but this thing is way trickery than you think
726
788
  highlighted_code = highlight(state.code_buffer + tline, lexer, formatter)
789
+ #print("(",highlighted_code,")")
727
790
 
728
791
  # Sometimes the highlighter will do things like a full reset or a background reset.
729
792
  # This is not what we want
730
- highlighted_code = re.sub(r"\033\[49(;00|)m", '', highlighted_code)
793
+ highlighted_code = re.sub(r"\033\[[34]9(;00|)m", '', highlighted_code)
731
794
 
732
795
  # Since we are streaming we ignore the resets and newlines at the end
733
796
  if highlighted_code.endswith(FGRESET + "\n"):
734
797
  highlighted_code = highlighted_code[: -(1 + len(FGRESET))]
735
798
 
799
+ #print(bytes(highlighted_code, 'utf-8'))
800
+
736
801
  # turns out highlight will eat leading newlines on empty lines
737
802
  vislen = visible_length(state.code_buffer.lstrip())
738
803
 
@@ -834,7 +899,7 @@ def parse(stream):
834
899
  # This is intentional ... we can get here in llama 4 using
835
900
  # a weird thing
836
901
  if state.in_list:
837
- indent = (len(state.list_item_stack) - 1) * Style.ListIndent + (len(bullet) - 1)
902
+ indent = (len(state.list_item_stack) - 1) * Style.ListIndent #+ (len(bullet) - 1)
838
903
  wrap_width = state.current_width() - indent - (2 * Style.ListIndent)
839
904
 
840
905
  wrapped_lineList = text_wrap(content, wrap_width, Style.ListIndent,
@@ -920,6 +985,11 @@ def emit(inp):
920
985
  if len(buffer):
921
986
  print(buffer.pop(0), file=sys.stdout, end="", flush=True)
922
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
+
923
993
  def apply_multipliers(name, H, S, V):
924
994
  m = _style.get(name)
925
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"]))
@@ -1035,6 +1105,8 @@ def main():
1035
1105
  os.close(state.exec_master)
1036
1106
  if state.exec_sub:
1037
1107
  state.exec_sub.wait()
1108
+
1109
+ print(RESET, end="")
1038
1110
  sys.exit(state.exit)
1039
1111
 
1040
1112
  if __name__ == "__main__":
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: streamdown
3
- Version: 0.18.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,43 +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
- Some *inferior* 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
- Here's kitty and alacritty. Try to do that in glow...
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
- Very ... Carefully ... Supported ...
65
- ![cjk1](https://github.com/user-attachments/assets/75162ade-4734-440e-aaa3-5ffc17a0dd46)
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)
66
74
 
67
75
  ### Colors are highly (and quickly) configurable for people who care a lot, or just a little.
68
76
  ![configurable](https://github.com/user-attachments/assets/19ca2ec9-8ea1-4a79-87ca-8352789269fe)
69
77
 
70
- ### 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.
71
79
  For instance, here is the [latex plugin](https://github.com/kristopolous/Streamdown/blob/main/streamdown/plugins/latex.py) doing math inside a table:
72
80
  ![calc](https://github.com/user-attachments/assets/0b0027ca-8ef0-4b4a-b4ae-e36ff623a683)
73
81
 
74
82
 
75
- ## TOML Configuration
83
+ ## Configuration
76
84
 
77
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.
78
86
 
@@ -80,7 +88,9 @@ Here are the sections:
80
88
 
81
89
  **`[style]`**
82
90
 
83
- 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).
84
94
 
85
95
  * `HSV`: [ 0.0 - 1.0, 0.0 - 1.0, 0.0 - 1.0 ]
86
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=gFm6WqrWsMqV8EW9tcq7ebGoXpDFrTd89_ka21AaHm8,39118
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.18.0.dist-info/METADATA,sha256=Lyrf0k6BjC4wjiwwWz5b7aOFPtLR5uJ41e-SRGr1JC0,8062
8
- streamdown-0.18.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
9
- streamdown-0.18.0.dist-info/entry_points.txt,sha256=HroKFsFMGf_h9PRTE96NjvjJQWupMW5TGP5RGUr1O_Q,74
10
- streamdown-0.18.0.dist-info/licenses/LICENSE.MIT,sha256=SnY46EPirUsF20dZDR8HpyVgS2_4Tjxuc6f-4OdqO7U,1070
11
- streamdown-0.18.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,,