streamdown 0.34.0__tar.gz → 0.35.1__tar.gz

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.
Files changed (91) hide show
  1. {streamdown-0.34.0 → streamdown-0.35.1}/PKG-INFO +6 -2
  2. {streamdown-0.34.0 → streamdown-0.35.1}/README.md +5 -1
  3. {streamdown-0.34.0 → streamdown-0.35.1}/pyproject.toml +1 -1
  4. {streamdown-0.34.0 → streamdown-0.35.1}/streamdown/sd.py +52 -17
  5. streamdown-0.35.1/tests/color-restore-after-link.md +1 -0
  6. {streamdown-0.34.0 → streamdown-0.35.1}/.aider.chat.history.md +0 -0
  7. {streamdown-0.34.0 → streamdown-0.35.1}/.aider.input.history +0 -0
  8. {streamdown-0.34.0 → streamdown-0.35.1}/.aider.tags.cache.v4/cache.db +0 -0
  9. {streamdown-0.34.0 → streamdown-0.35.1}/.envrc +0 -0
  10. {streamdown-0.34.0 → streamdown-0.35.1}/.gitignore +0 -0
  11. {streamdown-0.34.0 → streamdown-0.35.1}/.vimrc +0 -0
  12. {streamdown-0.34.0 → streamdown-0.35.1}/24-bit-color.sh +0 -0
  13. {streamdown-0.34.0 → streamdown-0.35.1}/LICENSE.MIT +0 -0
  14. {streamdown-0.34.0 → streamdown-0.35.1}/assets/logo.png +0 -0
  15. {streamdown-0.34.0 → streamdown-0.35.1}/assets/logo.svg +0 -0
  16. {streamdown-0.34.0 → streamdown-0.35.1}/configurable.png +0 -0
  17. {streamdown-0.34.0 → streamdown-0.35.1}/copyable.png +0 -0
  18. {streamdown-0.34.0 → streamdown-0.35.1}/dunder.png +0 -0
  19. {streamdown-0.34.0 → streamdown-0.35.1}/error.txt +0 -0
  20. {streamdown-0.34.0 → streamdown-0.35.1}/math +0 -0
  21. {streamdown-0.34.0 → streamdown-0.35.1}/newdir/file_0.py +0 -0
  22. {streamdown-0.34.0 → streamdown-0.35.1}/newdir/file_1.rb +0 -0
  23. {streamdown-0.34.0 → streamdown-0.35.1}/newdir/file_2.jl +0 -0
  24. {streamdown-0.34.0 → streamdown-0.35.1}/passthrough.py +0 -0
  25. {streamdown-0.34.0 → streamdown-0.35.1}/pygments.util +0 -0
  26. {streamdown-0.34.0 → streamdown-0.35.1}/python-go.png +0 -0
  27. {streamdown-0.34.0 → streamdown-0.35.1}/re +0 -0
  28. {streamdown-0.34.0 → streamdown-0.35.1}/requirements.txt +0 -0
  29. {streamdown-0.34.0 → streamdown-0.35.1}/shutil +0 -0
  30. {streamdown-0.34.0 → streamdown-0.35.1}/somelog.txt +0 -0
  31. {streamdown-0.34.0 → streamdown-0.35.1}/ss-new.py +0 -0
  32. {streamdown-0.34.0 → streamdown-0.35.1}/ss.py +0 -0
  33. {streamdown-0.34.0 → streamdown-0.35.1}/streamdown/__init__.py +0 -0
  34. {streamdown-0.34.0 → streamdown-0.35.1}/streamdown/plugins/README.md +0 -0
  35. {streamdown-0.34.0 → streamdown-0.35.1}/streamdown/plugins/latex.py +0 -0
  36. {streamdown-0.34.0 → streamdown-0.35.1}/streamdown/qwen3.md +0 -0
  37. {streamdown-0.34.0 → streamdown-0.35.1}/streamdown/ss +0 -0
  38. {streamdown-0.34.0 → streamdown-0.35.1}/streamdown/ss1 +0 -0
  39. {streamdown-0.34.0 → streamdown-0.35.1}/streamdown/test.txt +0 -0
  40. {streamdown-0.34.0 → streamdown-0.35.1}/subprocess +0 -0
  41. {streamdown-0.34.0 → streamdown-0.35.1}/table.png +0 -0
  42. {streamdown-0.34.0 → streamdown-0.35.1}/temp.py +0 -0
  43. {streamdown-0.34.0 → streamdown-0.35.1}/test.py +0 -0
  44. {streamdown-0.34.0 → streamdown-0.35.1}/tester.py +0 -0
  45. {streamdown-0.34.0 → streamdown-0.35.1}/tests/README.md +0 -0
  46. {streamdown-0.34.0 → streamdown-0.35.1}/tests/backtick-with-post-spaces.md +0 -0
  47. {streamdown-0.34.0 → streamdown-0.35.1}/tests/bg-messed-up.md +0 -0
  48. {streamdown-0.34.0 → streamdown-0.35.1}/tests/blankie.md +0 -0
  49. {streamdown-0.34.0 → streamdown-0.35.1}/tests/block.md +0 -0
  50. {streamdown-0.34.0 → streamdown-0.35.1}/tests/bold_reset_with_link.md +0 -0
  51. {streamdown-0.34.0 → streamdown-0.35.1}/tests/broken-code.md +0 -0
  52. {streamdown-0.34.0 → streamdown-0.35.1}/tests/broken-example.md +0 -0
  53. {streamdown-0.34.0 → streamdown-0.35.1}/tests/chinese.md +0 -0
  54. {streamdown-0.34.0 → streamdown-0.35.1}/tests/chunk-buffer.sh +0 -0
  55. {streamdown-0.34.0 → streamdown-0.35.1}/tests/cjj.mv +0 -0
  56. {streamdown-0.34.0 → streamdown-0.35.1}/tests/cjk-comment.md +0 -0
  57. {streamdown-0.34.0 → streamdown-0.35.1}/tests/cjk-table.md +0 -0
  58. {streamdown-0.34.0 → streamdown-0.35.1}/tests/cjk-wrap.md +0 -0
  59. {streamdown-0.34.0 → streamdown-0.35.1}/tests/code.md +0 -0
  60. {streamdown-0.34.0 → streamdown-0.35.1}/tests/dimcheck.md +0 -0
  61. {streamdown-0.34.0 → streamdown-0.35.1}/tests/example.md +0 -0
  62. {streamdown-0.34.0 → streamdown-0.35.1}/tests/fizzbuzz.md +0 -0
  63. {streamdown-0.34.0 → streamdown-0.35.1}/tests/fucking-garbage.md +0 -0
  64. {streamdown-0.34.0 → streamdown-0.35.1}/tests/inline.md +0 -0
  65. {streamdown-0.34.0 → streamdown-0.35.1}/tests/jimmy_webb.md +0 -0
  66. {streamdown-0.34.0 → streamdown-0.35.1}/tests/line-buffer.sh +0 -0
  67. {streamdown-0.34.0 → streamdown-0.35.1}/tests/line-wrap.md +0 -0
  68. {streamdown-0.34.0 → streamdown-0.35.1}/tests/links.md +0 -0
  69. {streamdown-0.34.0 → streamdown-0.35.1}/tests/managerie.md +0 -0
  70. {streamdown-0.34.0 → streamdown-0.35.1}/tests/mandlebrot.md +0 -0
  71. {streamdown-0.34.0 → streamdown-0.35.1}/tests/markdown.md +0 -0
  72. {streamdown-0.34.0 → streamdown-0.35.1}/tests/nested-example.md +0 -0
  73. {streamdown-0.34.0 → streamdown-0.35.1}/tests/no-newline.md +0 -0
  74. {streamdown-0.34.0 → streamdown-0.35.1}/tests/oneline-equal.md +0 -0
  75. {streamdown-0.34.0 → streamdown-0.35.1}/tests/outline.md +0 -0
  76. {streamdown-0.34.0 → streamdown-0.35.1}/tests/output.txt +0 -0
  77. {streamdown-0.34.0 → streamdown-0.35.1}/tests/pvgo_512.jpg +0 -0
  78. {streamdown-0.34.0 → streamdown-0.35.1}/tests/pythonvgo.md +0 -0
  79. {streamdown-0.34.0 → streamdown-0.35.1}/tests/qwen3.md +0 -0
  80. {streamdown-0.34.0 → streamdown-0.35.1}/tests/rerun.zsh +0 -0
  81. {streamdown-0.34.0 → streamdown-0.35.1}/tests/run-tests.sh +0 -0
  82. {streamdown-0.34.0 → streamdown-0.35.1}/tests/slash.md +0 -0
  83. {streamdown-0.34.0 → streamdown-0.35.1}/tests/strip-chunks.sh +0 -0
  84. {streamdown-0.34.0 → streamdown-0.35.1}/tests/table-break.md +0 -0
  85. {streamdown-0.34.0 → streamdown-0.35.1}/tests/table_test.md +0 -0
  86. {streamdown-0.34.0 → streamdown-0.35.1}/tests/test.md +0 -0
  87. {streamdown-0.34.0 → streamdown-0.35.1}/tests/test_input.md +0 -0
  88. {streamdown-0.34.0 → streamdown-0.35.1}/tests/uline.md +0 -0
  89. {streamdown-0.34.0 → streamdown-0.35.1}/tests/wm.md +0 -0
  90. {streamdown-0.34.0 → streamdown-0.35.1}/tools/deploy.sh +0 -0
  91. {streamdown-0.34.0 → streamdown-0.35.1}/tty +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: streamdown
3
- Version: 0.34.0
3
+ Version: 0.35.1
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
 
@@ -156,6 +156,8 @@ $ sd --exec "llm chat"
156
156
 
157
157
  And now you have all your readline stuff. It's pretty great. (Also see the DAY50 shellwrap project.)
158
158
 
159
+ This relies on "guessing" what a prompt will look like. But don't worry, you can change that with the `--prompt` option if yours is a bit unique. It's a PCRE regex, so you can be a bit flexible.
160
+
159
161
  It's also worth noting that things like the `-c` aren't "broken" with regard to file input. You can do something like this:
160
162
 
161
163
  ```shell
@@ -188,6 +190,8 @@ optional arguments:
188
190
  -w WIDTH, --width WIDTH
189
191
  Set the width WIDTH
190
192
  -e EXEC, --exec EXEC Wrap a program EXEC for more 'proper' i/o handling
193
+ -p PROMPT, --prompt PROMPT
194
+ A PCRE regex prompt to detect (default: ^.*>\s+$)
191
195
  -s SCRAPE, --scrape SCRAPE
192
196
  Scrape code snippets to a directory SCRAPE
193
197
  -v, --version Show version information
@@ -40,7 +40,7 @@ The optional `Clipboard` feature puts the final codeblock into your clipboard. S
40
40
  [links.webm](https://github.com/user-attachments/assets/a5f71791-7c58-4183-ad3b-309f470c08a3)
41
41
 
42
42
  ### As well as everything else...
43
- 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.
43
+ 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.
44
44
 
45
45
  This allows you to interactively debug in a way that the agent doesn't just wander off doing silly things.
46
46
 
@@ -126,6 +126,8 @@ $ sd --exec "llm chat"
126
126
 
127
127
  And now you have all your readline stuff. It's pretty great. (Also see the DAY50 shellwrap project.)
128
128
 
129
+ This relies on "guessing" what a prompt will look like. But don't worry, you can change that with the `--prompt` option if yours is a bit unique. It's a PCRE regex, so you can be a bit flexible.
130
+
129
131
  It's also worth noting that things like the `-c` aren't "broken" with regard to file input. You can do something like this:
130
132
 
131
133
  ```shell
@@ -158,6 +160,8 @@ optional arguments:
158
160
  -w WIDTH, --width WIDTH
159
161
  Set the width WIDTH
160
162
  -e EXEC, --exec EXEC Wrap a program EXEC for more 'proper' i/o handling
163
+ -p PROMPT, --prompt PROMPT
164
+ A PCRE regex prompt to detect (default: ^.*>\s+$)
161
165
  -s SCRAPE, --scrape SCRAPE
162
166
  Scrape code snippets to a directory SCRAPE
163
167
  -v, --version Show version information
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "streamdown"
7
- version = "0.34.0"
7
+ version = "0.35.1"
8
8
  description = "A streaming markdown renderer for modern terminals with syntax highlighting"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.8"
@@ -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
@@ -115,6 +117,7 @@ visible = lambda x: re.sub(ANSIESCAPE, "", x)
115
117
  # many characters have different widths
116
118
  visible_length = lambda x: sum(wcwidth(c) for c in visible(x))
117
119
  extract_ansi_codes = lambda text: re.findall(ESCAPE, text)
120
+ strip_ansi = lambda line: re.sub(ANSIESCAPE, '', line)
118
121
  remove_ansi = lambda line, codeList: reduce(lambda line, code: line.replace(code, ''), codeList, line)
119
122
  split_up = lambda line: re.findall(r'(\x1b[^m]*m|[^\x1b]*)', line)
120
123
 
@@ -127,7 +130,10 @@ def gettmpdir():
127
130
  else:
128
131
  tmp_dir = tmp_dir_all
129
132
 
133
+ prev_mask = os.umask(0)
134
+ # This is shared among all users
130
135
  os.makedirs(tmp_dir, exist_ok=True)
136
+ os.umask(prev_mask)
131
137
  return tmp_dir
132
138
 
133
139
  def debug_write(text):
@@ -136,6 +142,10 @@ def debug_write(text):
136
142
  state.Logging = tempfile.NamedTemporaryFile(dir=gettmpdir(), prefix="dbg", delete=False, mode="wb")
137
143
  state.Logging.write(text)
138
144
 
145
+ def sub_extract(line, upto):
146
+ # this takes the line up to the upto string and then returns the ansi codes
147
+ return ''.join(extract_ansi_codes(line[:line.find(upto)]))
148
+
139
149
  def savebrace():
140
150
  if state.Savebrace and state.code_buffer_raw and os.name != 'nt':
141
151
  path = os.path.join(gettmpdir(), 'savebrace')
@@ -165,6 +175,7 @@ class ParseState:
165
175
  self.is_pty = False
166
176
  self.is_exec = False
167
177
  self.maybe_prompt = False
178
+ self.prompt_regex = None
168
179
  self.emit_flag = None
169
180
  self.scrape = None
170
181
  self.scrape_ix = 0
@@ -204,6 +215,7 @@ class ParseState:
204
215
  self.in_underline = False
205
216
  self.in_strikeout = False
206
217
  self.block_depth = 0
218
+ self.block_type = None
207
219
 
208
220
  self.exec_sub = None
209
221
  self.exec_master = None
@@ -442,7 +454,7 @@ def text_wrap(text, width = -1, indent = 0, first_line_prefix="", subsequent_lin
442
454
  # that we have closed our hyperlink OSC
443
455
  if LINK[0] in line_content:
444
456
  line_content += LINK[1]
445
- lines.append(line_content + resetter + state.bg + ' ' * margin)
457
+ lines.append(line_content + resetter + sub_extract(line_content, LINK[0]) + state.bg + ' ' * margin)
446
458
 
447
459
  current_line = (" " * indent) + "".join(current_style) + word
448
460
 
@@ -492,12 +504,19 @@ def line_format(line):
492
504
 
493
505
  # Apply OSC 8 hyperlink formatting after other formatting
494
506
  def process_links(match):
507
+ #import pdb
508
+ #pdb.set_trace()
509
+ print(match)
495
510
  description = match.group(1)
496
511
  url = match.group(2)
497
512
  return f'{LINK[0]}{url}\033\\{Style.Link}{description}{UNDERLINE[1]}{LINK[1]}{FGRESET}'
498
513
 
499
- line = re.sub(r"\!\[([^\]]*)\]\(([^\)]+)\)", process_images, line)
500
- line = re.sub(r"\[([^\]]+)\]\(([^\)]+)\)", process_links, line)
514
+ if state.Images:
515
+ line = re.sub(r"\!\[([^\]]*)\]\(([^\)]+)\)", process_images, line)
516
+
517
+ if state.Links:
518
+ line = re.sub(r"\[([^\]]+)\]\(([^\)]+)\)", process_links, line)
519
+
501
520
  line = re.sub(r"\[\^(\d+)\]:?", footnotes, line)
502
521
 
503
522
  tokenList = re.finditer(r"((~~|\*\*_|_\*\*|\*{1,3}|_{1,3}|`+)|[^~_*`]+)", line)
@@ -630,8 +649,7 @@ def parse(stream):
630
649
  line = state.buffer.decode('utf-8').replace('\t',' ')
631
650
  state.has_newline = line.endswith('\n')
632
651
 
633
- # I hate this. There should be better ways.
634
- state.maybe_prompt = not state.has_newline and state.current()['none'] and re.match(r'^.*>\s+$', visible(line))
652
+ state.maybe_prompt = not state.has_newline and state.current()['none'] and re.match(state.prompt_regex, visible(line))
635
653
 
636
654
  # let's wait for a newline
637
655
  if state.maybe_prompt:
@@ -658,19 +676,27 @@ def parse(stream):
658
676
  """
659
677
 
660
678
  # running this here avoids stray |
661
- block_match = re.match(r"^\s*((>\s*)+|<.?think>)", line)
679
+ # So kimi doesn't newline after the <think> token and it uses some unicode triangle?. They'll
680
+ # newline at the end of it, but not the beginning.
681
+ block_match = re.match(r"^\s*((>\s*)+|[◁<].?think[>▷])(.*)", line)
662
682
  if not state.in_code and block_match:
663
- if block_match.group(1) == '</think>':
683
+ # wtf is this you might ask! Not all thinking models use < and > ...
684
+ # because why make life easy?
685
+ if block_match.group(1)[1:7] == '/think':
686
+ line = ''
664
687
  state.block_depth = 0
665
688
  yield RESET
666
- elif block_match.group(1) == '<think>':
689
+ elif block_match.group(1)[1:6] == 'think':
690
+ line = block_match.group(3)
667
691
  state.block_depth = 1
692
+ state.block_type = 'think'
668
693
  else:
669
- state.block_depth = block_match.group(0).count('>')
694
+ state.block_depth = block_match.group(1).count('>')
695
+ state.block_type = '>'
670
696
  # we also need to consume those tokens
671
- line = line[len(block_match.group(0)):]
697
+ line = line[len(block_match.group(1)):]
672
698
  else:
673
- if state.block_depth > 0:
699
+ if state.block_type == '>' and state.block_depth > 0:
674
700
  yield FGRESET
675
701
  state.block_depth = 0
676
702
 
@@ -839,7 +865,7 @@ def parse(stream):
839
865
  while parts[-1] in [FGRESET, FORMATRESET]:
840
866
  parts.pop()
841
867
 
842
- tline_len = visible_length(tline)
868
+ tline_len = visible_length(tline.rstrip('\r\n'))
843
869
 
844
870
  # now we find the new stuff:
845
871
  ttl = 0
@@ -850,7 +876,7 @@ def parse(stream):
850
876
 
851
877
  ttl += len(idx) if idx[0] != '\x1b' else 0
852
878
 
853
- if ttl > 1+tline_len:
879
+ if ttl > tline_len:
854
880
  break
855
881
 
856
882
 
@@ -1011,6 +1037,11 @@ def parse(stream):
1011
1037
  for wrapped_line in wrapped_lines:
1012
1038
  yield f"{state.space_left()}{wrapped_line}\n"
1013
1039
 
1040
+ def terminal_prep(what):
1041
+ if Style.Plaintext:
1042
+ return strip_ansi(what)
1043
+ return what
1044
+
1014
1045
  def emit(inp):
1015
1046
  buffer = []
1016
1047
  flush = False
@@ -1050,10 +1081,10 @@ def emit(inp):
1050
1081
  else:
1051
1082
  chunk = buffer.pop(0)
1052
1083
 
1053
- print(chunk, end="", file=sys.stdout, flush=True)
1084
+ print(terminal_prep(chunk), end="", file=sys.stdout, flush=True)
1054
1085
 
1055
1086
  if len(buffer):
1056
- print(buffer.pop(0), file=sys.stdout, end="", flush=True)
1087
+ print(terminal_prep(buffer.pop(0)), file=sys.stdout, end="", flush=True)
1057
1088
 
1058
1089
  def ansi2hex(ansi_code):
1059
1090
  parts = ansi_code.strip('m').split(";")
@@ -1109,8 +1140,10 @@ def main():
1109
1140
  parser.add_argument("-c", "--config", default=None, help="Use a custom config override")
1110
1141
  parser.add_argument("-w", "--width", default="0", help="Set the width WIDTH")
1111
1142
  parser.add_argument("-e", "--exec", help="Wrap a program EXEC for more 'proper' i/o handling")
1143
+ parser.add_argument("-p", "--prompt", default="^.*>\\s+$", help="A PCRE regex prompt to detect (default: %(default)s)")
1112
1144
  parser.add_argument("-s", "--scrape", help="Scrape code snippets to a directory SCRAPE")
1113
1145
  parser.add_argument("-v", "--version", action="store_true", help="Show version information")
1146
+ parser.add_argument("--strip", action="store_true", help="Just strip the markdown and output plaintext")
1114
1147
  args = parser.parse_args()
1115
1148
 
1116
1149
  if args.version:
@@ -1140,9 +1173,9 @@ def main():
1140
1173
 
1141
1174
  for color in ["Dark", "Mid", "Symbol", "Head", "Grey", "Bright"]:
1142
1175
  setattr(Style, color, apply_multipliers(style, color, H, S, V))
1143
- for attr in ['PrettyPad', 'PrettyBroken', 'Margin', 'ListIndent', 'Syntax']:
1176
+ for attr in ['PrettyPad', 'PrettyBroken', 'Margin', 'ListIndent', 'Syntax', 'Plaintext']:
1144
1177
  setattr(Style, attr, style.get(attr))
1145
- for attr in ['CodeSpaces', 'Clipboard', 'Logging', 'Timeout', 'Savebrace']:
1178
+ for attr in ['Links', 'Images', 'CodeSpaces', 'Clipboard', 'Logging', 'Timeout', 'Savebrace']:
1146
1179
  setattr(state, attr, features.get(attr))
1147
1180
 
1148
1181
 
@@ -1150,8 +1183,10 @@ def main():
1150
1183
  os.makedirs(args.scrape, exist_ok=True)
1151
1184
  state.scrape = args.scrape
1152
1185
 
1186
+ Style.Plaintext = args.strip
1153
1187
  Style.MarginSpaces = " " * Style.Margin
1154
1188
  state.WidthArg = int(args.width) or style.get("Width") or 0
1189
+ state.prompt_regex = re.compile(args.prompt)
1155
1190
  Style.Blockquote = f"{FG}{Style.Grey}│ "
1156
1191
  width_calc()
1157
1192
 
@@ -0,0 +1 @@
1
+ ### Before link. [The Link!](link) Should retain color
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes