streamdown 0.13.0__py3-none-any.whl → 0.15.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.
@@ -0,0 +1,22 @@
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)
@@ -0,0 +1,27 @@
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);
@@ -0,0 +1,23 @@
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/sd.py CHANGED
@@ -5,6 +5,7 @@
5
5
  # "pygments",
6
6
  # "pylatexenc",
7
7
  # "appdirs",
8
+ # "term-image",
8
9
  # "toml"
9
10
  # ]
10
11
  # ///
@@ -23,7 +24,9 @@ import colorsys
23
24
  import base64
24
25
  import importlib
25
26
  from io import BytesIO
27
+ from term_image.image import from_file, from_url
26
28
  import pygments.util
29
+ from functools import reduce
27
30
  from argparse import ArgumentParser
28
31
  from pygments import highlight
29
32
  from pygments.lexers import get_lexer_by_name
@@ -52,7 +55,7 @@ Dark = { H = 1.00, S = 1.50, V = 0.25 }
52
55
  Mid = { H = 1.00, S = 1.00, V = 0.50 }
53
56
  Symbol = { H = 1.00, S = 1.00, V = 1.50 }
54
57
  Head = { H = 1.00, S = 2.00, V = 1.50 }
55
- Grey = { H = 1.00, S = 0.12, V = 1.25 }
58
+ Grey = { H = 1.00, S = 0.25, V = 1.37 }
56
59
  Bright = { H = 1.00, S = 2.00, V = 2.00 }
57
60
  Syntax = "monokai"
58
61
  """
@@ -80,16 +83,17 @@ BGRESET = "\033[49m"
80
83
  BOLD = ["\033[1m", "\033[22m"]
81
84
  UNDERLINE = ["\033[4m", "\033[24m"]
82
85
  ITALIC = ["\033[3m", "\033[23m"]
86
+ STRIKEOUT = ["\033[9m", "\033[29m"]
87
+ SUPER = [ 0x2070, 0x00B9, 0x00B2, 0x00B3, 0x2074, 0x2075, 0x2076, 0x2077, 0x2078, 0x2079 ]
83
88
 
84
89
  ESCAPE = r"\033\[[0-9;]*[mK]"
85
90
  ANSIESCAPE = r'\033(?:\[[0-9;?]*[a-zA-Z]|][0-9]*;;.*?\\|\\)'
86
- #r"\033(\[[0-9;]*[mK]|][0-9]*;;.*?\\|\\)"
87
91
  KEYCODE_RE = re.compile(r'\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])')
88
92
 
89
93
  visible = lambda x: re.sub(ANSIESCAPE, "", x)
90
94
  visible_length = lambda x: len(visible(x))
91
-
92
95
  extract_ansi_codes = lambda text: re.findall(ESCAPE, text)
96
+ remove_ansi = lambda line, codeList: reduce(lambda line, code: line.replace(code, ''), codeList, line)
93
97
 
94
98
  def debug_write(text):
95
99
  if state.Logging:
@@ -131,7 +135,10 @@ class ParseState:
131
135
  self.Clipboard = _features.get("Clipboard")
132
136
  self.Logging = _features.get("Logging")
133
137
  self.Timeout = _features.get("Timeout")
138
+
134
139
  self.WidthArg = None
140
+ self.WidthFull = None
141
+ self.WidthWrap = False
135
142
 
136
143
  # If the entire block is indented this will
137
144
  # tell us what that is
@@ -159,24 +166,27 @@ class ParseState:
159
166
  self.in_italic = False
160
167
  self.in_table = False # (Code.[Header|Body] | False)
161
168
  self.in_underline = False
169
+ self.in_strikeout = False
162
170
  self.block_depth = 0
163
171
 
164
172
  self.exec_sub = None
165
173
  self.exec_master = None
166
174
  self.exec_slave = None
167
175
  self.exec_kb = 0
168
- self.exec_israw = False
169
176
 
170
177
  self.exit = 0
171
178
  self.where_from = None
172
179
 
173
180
  def current(self):
174
- state = { 'code': self.in_code, 'bold': self.in_bold, 'italic': self.in_italic, 'underline': self.in_underline }
181
+ state = { 'inline': self.inline_code, 'code': self.in_code, 'bold': self.in_bold, 'italic': self.in_italic, 'underline': self.in_underline }
175
182
  state['none'] = all(item is False for item in state.values())
176
183
  return state
177
184
 
185
+ def reset_inline(self):
186
+ self.inline_code = self.in_bold = self.in_italic = self.in_underline = False
187
+
178
188
  def space_left(self):
179
- return (Style.MarginSpaces if len(self.current_line) == 0 else "") + (Style.Blockquote * self.block_depth)
189
+ return Style.MarginSpaces + (Style.Blockquote * self.block_depth) if len(self.current_line) == 0 else ""
180
190
 
181
191
  state = ParseState()
182
192
 
@@ -233,17 +243,20 @@ def emit_h(level, text):
233
243
  text = line_format(text)
234
244
  spaces_to_center = ((state.Width - visible_length(text)) / 2)
235
245
  if level == 1: #
236
- return f"\n{Style.MarginSpaces}{BOLD[0]}{' ' * math.floor(spaces_to_center)}{text}{' ' * math.ceil(spaces_to_center)}{BOLD[1]}\n"
246
+ return f"\n{state.space_left()}{BOLD[0]}{' ' * math.floor(spaces_to_center)}{text}{' ' * math.ceil(spaces_to_center)}{BOLD[1]}\n"
237
247
  elif level == 2: ##
238
- return f"\n{Style.MarginSpaces}{BOLD[0]}{FG}{Style.Bright}{' ' * math.floor(spaces_to_center)}{text}{' ' * math.ceil(spaces_to_center)}{RESET}\n\n"
248
+ return f"\n{state.space_left()}{BOLD[0]}{FG}{Style.Bright}{' ' * math.floor(spaces_to_center)}{text}{' ' * math.ceil(spaces_to_center)}{RESET}\n\n"
239
249
  elif level == 3: ###
240
- return f"{Style.MarginSpaces}{FG}{Style.Head}{BOLD[0]}{text}{RESET}"
250
+ return f"{state.space_left()}{FG}{Style.Head}{BOLD[0]}{text}{RESET}"
241
251
  elif level == 4: ####
242
- return f"{Style.MarginSpaces}{FG}{Style.Symbol}{text}{RESET}"
252
+ return f"{state.space_left()}{FG}{Style.Symbol}{text}{RESET}"
243
253
  else: # level 5 or 6
244
- return f"{Style.MarginSpaces}{text}{RESET}"
254
+ return f"{state.space_left()}{text}{RESET}"
245
255
 
246
256
  def code_wrap(text_in):
257
+ if state.WidthWrap and len(text_in) > state.WidthFull:
258
+ return (0, [text_in])
259
+
247
260
  # get the indentation of the first line
248
261
  indent = len(text_in) - len(text_in.lstrip())
249
262
  text = text_in.lstrip()
@@ -259,6 +272,7 @@ def code_wrap(text_in):
259
272
 
260
273
  return (indent, res)
261
274
 
275
+
262
276
  # This marvelously obscure code "compacts" long lines of repetitive ANSI format strings by
263
277
  # removing duplicates. Here's how it works
264
278
  def ansi_collapse(codelist, inp):
@@ -337,8 +351,20 @@ def text_wrap(text, width = -1, indent = 0, first_line_prefix="", subsequent_lin
337
351
  return lines
338
352
 
339
353
  def line_format(line):
340
- def not_text(token):
341
- return not token or len(token.rstrip()) != len(token)
354
+ not_text = lambda token: not token or len(token.rstrip()) != len(token)
355
+ footnotes = lambda match: ''.join([chr(SUPER[int(i)]) for i in match.group(1)])
356
+
357
+ def process_images(match):
358
+ url = match.group(2)
359
+ try:
360
+ if re.match(r"https://", url.lower()):
361
+ image = from_url(url)
362
+ else:
363
+ image = from_file(url)
364
+ image.height = 20
365
+ print(f"{image:|.-1#}")
366
+ except:
367
+ return match.group(2)
342
368
 
343
369
  # Apply OSC 8 hyperlink formatting after other formatting
344
370
  def process_links(match):
@@ -346,8 +372,11 @@ def line_format(line):
346
372
  url = match.group(2)
347
373
  return f'\033]8;;{url}\033\\{Style.Link}{description}{UNDERLINE[1]}\033]8;;\033\\{FGRESET}'
348
374
 
375
+ line = re.sub(r"\!\[([^\]]*)\]\(([^\)]+)\)", process_images, line)
349
376
  line = re.sub(r"\[([^\]]+)\]\(([^\)]+)\)", process_links, line)
350
- tokenList = re.finditer(r"((\*\*|\*|_|`)|[^_*`]+)", line)
377
+ line = re.sub(r"\[\^(\d+)\]:?", footnotes, line)
378
+
379
+ tokenList = re.finditer(r"((~~|\*\*_|_\*\*|\*{1,3}|_{1,3}|`+)|[^~_*`]+)", line)
351
380
  result = ""
352
381
 
353
382
  for match in tokenList:
@@ -355,8 +384,13 @@ def line_format(line):
355
384
  next_token = line[match.end()] if match.end() < len(line) else ""
356
385
  prev_token = line[match.start()-1] if match.start() > 0 else ""
357
386
 
358
- if token == "`":
359
- state.inline_code = not state.inline_code
387
+ # This trick makes sure that things like `` ` `` render right.
388
+ if "`" in token and (not state.inline_code or state.inline_code == token):
389
+ if state.inline_code:
390
+ state.inline_code = False
391
+ else:
392
+ state.inline_code = token
393
+
360
394
  if state.inline_code:
361
395
  result += f'{BG}{Style.Mid}'
362
396
  else:
@@ -367,7 +401,17 @@ def line_format(line):
367
401
  elif state.inline_code:
368
402
  result += token
369
403
 
370
- elif token == "**" and (state.in_bold or not_text(prev_token)):
404
+ elif token == '~~' and (state.in_strikeout or not_text(prev_token)):
405
+ state.in_strikeout = not state.in_strikeout
406
+ result += STRIKEOUT[0] if state.in_strikeout else STRIKEOUT[1]
407
+
408
+ elif token in ['**_','_**','___','***'] and (state.in_bold or not_text(prev_token)):
409
+ state.in_bold = not state.in_bold
410
+ result += BOLD[0] if state.in_bold else BOLD[1]
411
+ state.in_italic = not state.in_italic
412
+ result += ITALIC[0] if state.in_italic else ITALIC[1]
413
+
414
+ elif (token == '__' or token == "**") and (state.in_bold or not_text(prev_token)):
371
415
  state.in_bold = not state.in_bold
372
416
  result += BOLD[0] if state.in_bold else BOLD[1]
373
417
 
@@ -380,7 +424,7 @@ def line_format(line):
380
424
  else:
381
425
  result += token
382
426
 
383
- elif token == "_" and (state.in_underline or not_text(prev_token)):
427
+ elif token == "_" and (state.in_underline or (not_text(prev_token) and next_token.isalnum())):
384
428
  state.in_underline = not state.in_underline
385
429
  result += UNDERLINE[0] if state.in_underline else UNDERLINE[1]
386
430
  else:
@@ -422,8 +466,6 @@ def parse(stream):
422
466
 
423
467
  if len(ready_in) == 0:
424
468
  TimeoutIx += 1
425
-
426
-
427
469
 
428
470
  elif stream.fileno() in ready_in:
429
471
  byte = os.read(stream.fileno(), 1)
@@ -462,13 +504,30 @@ def parse(stream):
462
504
  # Run through the plugins first
463
505
  res = latex.Plugin(line, state, Style)
464
506
  if res is True:
465
- # This means everything was consumed by our plugin and
466
- # we should continue
467
- continue
468
- elif res is not None:
469
- for row in res:
470
- yield row
507
+ # This means everything was consumed by our plugin and
508
+ # we should continue
471
509
  continue
510
+ elif res is not None:
511
+ for row in res:
512
+ yield row
513
+ continue
514
+
515
+ # running this here avoids stray |
516
+ block_match = re.match(r"^\s*((>\s*)+|<.?think>)", line)
517
+ if not state.in_code and block_match:
518
+ if block_match.group(1) == '</think>':
519
+ state.block_depth = 0
520
+ yield RESET
521
+ elif block_match.group(1) == '<think>':
522
+ state.block_depth = 1
523
+ else:
524
+ state.block_depth = block_match.group(0).count('>')
525
+ # we also need to consume those tokens
526
+ line = line[len(block_match.group(0)):]
527
+ else:
528
+ if state.block_depth > 0:
529
+ line = FGRESET + line
530
+ state.block_depth = 0
472
531
 
473
532
  # --- Collapse Multiple Empty Lines if not in code blocks ---
474
533
  if not state.in_code:
@@ -483,7 +542,7 @@ def parse(stream):
483
542
  else:
484
543
  last_line_empty_cache = state.last_line_empty
485
544
  state.last_line_empty = False
486
-
545
+
487
546
  # This is to reset our top-level line-based systems
488
547
  # \n buffer
489
548
  if not state.in_list and len(state.ordered_list_numbers) > 0:
@@ -506,28 +565,11 @@ def parse(stream):
506
565
  if state.in_table and not state.in_code and not re.match(r"^\s*\|.+\|\s*$", line):
507
566
  state.in_table = False
508
567
 
509
- block_match = re.match(r"^((> )*|<.?think>)", line)
510
- if block_match:
511
- if block_match.group(1) == '</think>':
512
- state.block_depth = 0
513
- yield(RESET)
514
- elif block_match.group(1) == '<think>':
515
- state.block_depth = 1
516
- else:
517
- state.block_depth = int(len(block_match.group(0)) / 2)
518
- # we also need to consume those tokens
519
- line = line[state.block_depth * 2:]
520
- else:
521
- if state.block_depth > 0:
522
- yield RESET
523
- state.block_depth = 0
524
-
525
568
  #
526
569
  # <code><pre>
527
570
  #
528
- # This needs to be first
529
571
  if not state.in_code:
530
- code_match = re.match(r"\s*```\s*([^\s]+|$)", line)
572
+ code_match = re.match(r"\s*```\s*([^\s]+|$)$", line)
531
573
  if code_match:
532
574
  state.in_code = Code.Backtick
533
575
  state.code_language = code_match.group(1) or 'Bash'
@@ -563,6 +605,7 @@ def parse(stream):
563
605
  try:
564
606
  ext = get_lexer_by_name(state.code_language).filenames[0].split('.')[-1]
565
607
  except:
608
+ logging.warning(f"Can't find canonical extension for {state.code_language}")
566
609
  pass
567
610
 
568
611
  open(os.path.join(state.scrape, f"file_{state.scrape_ix}.{ext}"), 'w').write(state.code_buffer)
@@ -584,7 +627,8 @@ def parse(stream):
584
627
 
585
628
 
586
629
  if code_type == Code.Backtick:
587
- continue
630
+ state.code_indent = len(line) - len(line.lstrip())
631
+ continue
588
632
  else:
589
633
  # otherwise we don't want to consume
590
634
  # nor do we want to be here.
@@ -600,11 +644,6 @@ def parse(stream):
600
644
  custom_style = get_style_by_name("default")
601
645
 
602
646
  formatter = Terminal256Formatter(style=custom_style)
603
- for i, char in enumerate(line):
604
- if char == " ":
605
- state.code_indent += 1
606
- else:
607
- break
608
647
  line = line[state.code_indent :]
609
648
 
610
649
  elif line.startswith(" " * state.code_indent):
@@ -628,6 +667,10 @@ def parse(stream):
628
667
  # then naively search back until our visible_lengths() match. This is not fast and there's certainly smarter
629
668
  # ways of doing it but this thing is way trickery than you think
630
669
  highlighted_code = highlight(state.code_buffer + tline, lexer, formatter)
670
+
671
+ # Sometimes the highlighter will do things like a full reset or a background reset.
672
+ # This is not what we want
673
+ highlighted_code = re.sub(r"\033\[39(;00|)m", '', highlighted_code)
631
674
 
632
675
  # Since we are streaming we ignore the resets and newlines at the end
633
676
  if highlighted_code.endswith(FGRESET + "\n"):
@@ -652,7 +695,7 @@ def parse(stream):
652
695
 
653
696
  code_line = ' ' * indent + this_batch.strip()
654
697
 
655
- margin = state.WidthFull - visible_length(code_line)
698
+ margin = state.WidthFull - visible_length(code_line) % state.WidthFull
656
699
  yield f"{Style.Codebg}{code_line}{' ' * max(0, margin)}{BGRESET}"
657
700
  continue
658
701
  except Goto:
@@ -714,18 +757,18 @@ def parse(stream):
714
757
  if list_type == "number":
715
758
  state.ordered_list_numbers[-1] += 1
716
759
 
717
- indent = len(state.list_item_stack) * 2
760
+ indent = (len(state.list_item_stack) - 1) * 2
718
761
 
719
762
  wrap_width = state.Width - indent - (2 * Style.ListIndent)
720
763
 
721
764
  bullet = '•'
722
765
  if list_type == "number":
723
766
  list_number = int(max(state.ordered_list_numbers[-1], float(list_item_match.group(2))))
724
- bullet = f"{list_number}"
767
+ bullet = str(list_number)
725
768
 
726
769
  wrapped_lineList = text_wrap(content, wrap_width, Style.ListIndent,
727
- first_line_prefix = f"{(' ' * (indent - len(bullet)))}{FG}{Style.Symbol}{bullet}{RESET} ",
728
- subsequent_line_prefix = " " * (indent - 1)
770
+ first_line_prefix = f"{(' ' * (indent ))}{FG}{Style.Symbol}{bullet}{RESET} ",
771
+ subsequent_line_prefix = " " * (indent)
729
772
  )
730
773
  for wrapped_line in wrapped_lineList:
731
774
  yield f"{state.space_left()}{wrapped_line}\n"
@@ -742,7 +785,7 @@ def parse(stream):
742
785
  #
743
786
  # <hr>
744
787
  #
745
- hr_match = re.match(r"^[\s]*([-=_]){3,}[\s]*$", line)
788
+ hr_match = re.match(r"^[\s]*([-\*=_]){3,}[\s]*$", line)
746
789
  if hr_match:
747
790
  if state.last_line_empty or last_line_empty_cache:
748
791
  # print a horizontal rule using a unicode midline
@@ -764,12 +807,6 @@ def parse(stream):
764
807
  for wrapped_line in wrapped_lines:
765
808
  yield f"{state.space_left()}{wrapped_line}\n"
766
809
 
767
- def get_terminal_width():
768
- try:
769
- return shutil.get_terminal_size().columns
770
- except (AttributeError, OSError):
771
- return 80
772
-
773
810
  def emit(inp):
774
811
  buffer = []
775
812
  flush = False
@@ -795,6 +832,9 @@ def emit(inp):
795
832
  state.current_line += chunk
796
833
 
797
834
  buffer.append(chunk)
835
+ # This *might* be dangerous
836
+ state.reset_inline()
837
+
798
838
  if flush:
799
839
  chunk = "\n".join(buffer)
800
840
  buffer = []
@@ -806,17 +846,10 @@ def emit(inp):
806
846
  else:
807
847
  chunk = buffer.pop(0)
808
848
 
809
- if state.is_pty:
810
- print(chunk, end="", flush=True)
811
- else:
812
- sys.stdout.write(chunk)
849
+ print(chunk, end="", flush=True)
813
850
 
814
851
  if len(buffer):
815
- chunk = buffer.pop(0)
816
- if state.is_pty:
817
- print(chunk, end="", flush=True)
818
- else:
819
- sys.stdout.write(chunk)
852
+ print(buffer.pop(0), end="", flush=True)
820
853
 
821
854
  def apply_multipliers(name, H, S, V):
822
855
  m = _style.get(name)
@@ -824,7 +857,20 @@ def apply_multipliers(name, H, S, V):
824
857
  return ';'.join([str(int(x * 256)) for x in [r, g, b]]) + "m"
825
858
 
826
859
  def width_calc():
827
- state.WidthFull = state.WidthArg or int(get_terminal_width())
860
+ if not state.WidthFull or not state.WidthArg:
861
+ if state.WidthArg:
862
+ state.WidthFull = state.WidthArg
863
+ else:
864
+ width = 80
865
+
866
+ try:
867
+ width = shutil.get_terminal_size().columns
868
+ state.WidthWrap = True
869
+ except (AttributeError, OSError):
870
+ pass
871
+
872
+ state.WidthFull = width
873
+
828
874
  state.Width = state.WidthFull - 2 * Style.Margin
829
875
  Style.Codepad = [
830
876
  f"{RESET}{FG}{Style.Dark}{'▄' * state.WidthFull}{RESET}\n",
@@ -838,9 +884,9 @@ def main():
838
884
  parser.add_argument("filenameList", nargs="*", help="Input file to process (also takes stdin)")
839
885
  parser.add_argument("-l", "--loglevel", default="INFO", help="Set the logging level")
840
886
  parser.add_argument("-c", "--color", default=None, help="Set the hsv base: h,s,v")
841
- parser.add_argument("-w", "--width", default="0", help="Set the width")
842
- parser.add_argument("-e", "--exec", help="Wrap a program for more 'proper' i/o handling")
843
- parser.add_argument("-s", "--scrape", help="Scrape code snippets to a directory")
887
+ parser.add_argument("-w", "--width", default="0", help="Set the width WIDTH")
888
+ parser.add_argument("-e", "--exec", help="Wrap a program EXEC for more 'proper' i/o handling")
889
+ parser.add_argument("-s", "--scrape", help="Scrape code snippets to a directory SCRAPE")
844
890
  args = parser.parse_args()
845
891
 
846
892
  if args.color:
@@ -864,8 +910,7 @@ def main():
864
910
 
865
911
  Style.Codebg = f"{BG}{Style.Dark}"
866
912
  Style.Link = f"{FG}{Style.Symbol}{UNDERLINE[0]}"
867
- Style.Blockquote = f"{FG}{Style.Grey} \u258E "
868
-
913
+ Style.Blockquote = f"{FG}{Style.Grey} "
869
914
 
870
915
  logging.basicConfig(stream=sys.stdout, level=args.loglevel.upper(), format=f'%(message)s')
871
916
  state.exec_master, state.exec_slave = pty.openpty()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: streamdown
3
- Version: 0.13.0
3
+ Version: 0.15.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
@@ -23,6 +23,7 @@ Requires-Python: >=3.8
23
23
  Requires-Dist: appdirs
24
24
  Requires-Dist: pygments
25
25
  Requires-Dist: pylatexenc
26
+ Requires-Dist: term-image
26
27
  Requires-Dist: toml
27
28
  Description-Content-Type: text/markdown
28
29
 
@@ -30,26 +31,30 @@ Description-Content-Type: text/markdown
30
31
 
31
32
  [![PyPI version](https://badge.fury.io/py/streamdown.svg)](https://badge.fury.io/py/streamdown)
32
33
 
33
- I needed a streaming Markdown renderer and I couldn't find one. So here we go. From the ground up. It's a bad idea but it has to be done.
34
+ Streamdown is the streaming markdown renderer for the terminal that rocks.
35
+ This will work with [simonw's llm](https://github.com/simonw/llm). You even get full readline and keyboard navigation support.
34
36
 
35
- [sd demo](https://github.com/user-attachments/assets/48dba6fa-2282-4be9-8087-a2ad8e7c7d12)
37
+ It's fully streaming and does not block
38
+ ![Streamdown is Amazing](https://github.com/user-attachments/assets/268cb340-78cc-4df0-a773-c5ac95eceeeb)
36
39
 
37
- This will work with [simonw's llm](https://github.com/simonw/llm) unlike with [richify.py](https://github.com/gianlucatruda/richify) which rerenders the whole buffer and blocks with an elipses or [glow](https://github.com/charmbracelet/glow) which buffers everything, this streams and does exactly what it says.
40
+ ### Provides clean copyable code for long code lines
41
+ You may have noticed that other, *inferior* renderers will inject line breaks when copying code that wraps around. We're better and now, you can be as well.
42
+ ![Handle That Mandle](https://github.com/user-attachments/assets/a27aa70c-f691-4796-84f0-c2eb18c7de23)
38
43
 
39
- ## Some Features
44
+ ### Supports images
45
+ Here's kitty and alacritty. Try to do that in glow...
46
+ ![doggie](https://github.com/user-attachments/assets/9a392929-b6c2-4204-b257-e09305acb7af)
40
47
 
41
- ### Provides clean copyable code for long code blocks and short terminals.
42
- ![copyable](https://github.com/user-attachments/assets/4a3539c5-b5d1-4d6a-8bce-032724d8909d)
43
-
44
- ### Does OSC 8 links for modern terminals.
48
+ ### Does OSC 8 links for modern terminals (and optionally OSC 52 for clipboard)
45
49
  [links.webm](https://github.com/user-attachments/assets/a5f71791-7c58-4183-ad3b-309f470c08a3)
46
50
 
47
- ### Doesn't consume characters like _ and * as style when they are in `blocks like this` because `_they_can_be_varaiables_`
48
- ![dunder](https://github.com/user-attachments/assets/d41d7fec-6dec-4387-b53d-f2098f269a5e)
49
-
50
51
  ### Tables are carefully supported
51
52
  ![table](https://github.com/user-attachments/assets/dbe3d13e-6bac-4f45-bf30-f1857ed98898)
52
53
 
54
+ As well as everything else...
55
+
56
+ ![dunder](https://github.com/user-attachments/assets/d41d7fec-6dec-4387-b53d-f2098f269a5e)
57
+
53
58
  ### Colors are highly (and quickly) configurable for people who care a lot, or just a little.
54
59
  ![configurable](https://github.com/user-attachments/assets/04b36749-4bb8-4c14-9758-84eb6e19b704)
55
60
 
@@ -80,7 +85,7 @@ Defines the base Hue (H), Saturation (S), and Value (V) from which all other pal
80
85
  * `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
81
86
  * `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
82
87
  * `ListIndent` (integer, default: `2`): This is the recursive indent for the list styles.
83
- * `Syntax` (string, default `monokai`): This the syntax [highlighting theme which come via pygments](https://pygments.org/styles/).
88
+ * `Syntax` (string, default `monokai`): This is the syntax [highlighting theme which come via pygments](https://pygments.org/styles/).
84
89
 
85
90
  Example:
86
91
  ```toml
@@ -139,10 +144,8 @@ Do this
139
144
 
140
145
  $ ./streamdown/sd.py tests/*md
141
146
 
142
- Certainly room for improvement and I'll probably continue to make them
143
-
144
147
  ## Install from source
145
- At least one of these should work, hopefully
148
+ After the git clone least one of these should work, hopefully. it's using the modern uv pip tool.
146
149
 
147
150
  $ pipx install -e .
148
151
  $ pip install -e .
@@ -0,0 +1,13 @@
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,,
@@ -1,10 +0,0 @@
1
- streamdown/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
- streamdown/sd.py,sha256=efsejJg_wCaEnbGSoAveIUTojtQ0FPCmSrkupwNvluc,33346
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-0.13.0.dist-info/METADATA,sha256=JxVOmiPebQTidKCv1genLIwlS4j9-mVlCX0R3njk5dA,7841
7
- streamdown-0.13.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
8
- streamdown-0.13.0.dist-info/entry_points.txt,sha256=HroKFsFMGf_h9PRTE96NjvjJQWupMW5TGP5RGUr1O_Q,74
9
- streamdown-0.13.0.dist-info/licenses/LICENSE.MIT,sha256=SnY46EPirUsF20dZDR8HpyVgS2_4Tjxuc6f-4OdqO7U,1070
10
- streamdown-0.13.0.dist-info/RECORD,,