streamdown 0.13.0__tar.gz → 0.14.0__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 (53) hide show
  1. {streamdown-0.13.0 → streamdown-0.14.0}/PKG-INFO +6 -2
  2. {streamdown-0.13.0 → streamdown-0.14.0}/README.md +5 -1
  3. {streamdown-0.13.0 → streamdown-0.14.0}/pyproject.toml +1 -1
  4. streamdown-0.14.0/streamdown/scrape/file_0.py +22 -0
  5. streamdown-0.14.0/streamdown/scrape/file_1.js +27 -0
  6. streamdown-0.14.0/streamdown/scrape/file_2.cpp +23 -0
  7. {streamdown-0.13.0 → streamdown-0.14.0}/streamdown/sd.py +116 -77
  8. {streamdown-0.13.0 → streamdown-0.14.0}/tests/chunk-buffer.sh +1 -1
  9. {streamdown-0.13.0 → streamdown-0.14.0}/.gitignore +0 -0
  10. {streamdown-0.13.0 → streamdown-0.14.0}/.vimrc +0 -0
  11. {streamdown-0.13.0 → streamdown-0.14.0}/24-bit-color.sh +0 -0
  12. {streamdown-0.13.0 → streamdown-0.14.0}/LICENSE.MIT +0 -0
  13. {streamdown-0.13.0 → streamdown-0.14.0}/configurable.png +0 -0
  14. {streamdown-0.13.0 → streamdown-0.14.0}/copyable.png +0 -0
  15. {streamdown-0.13.0 → streamdown-0.14.0}/dunder.png +0 -0
  16. {streamdown-0.13.0 → streamdown-0.14.0}/error.txt +0 -0
  17. {streamdown-0.13.0 → streamdown-0.14.0}/newdir/file_0.py +0 -0
  18. {streamdown-0.13.0 → streamdown-0.14.0}/newdir/file_1.rb +0 -0
  19. {streamdown-0.13.0 → streamdown-0.14.0}/newdir/file_2.jl +0 -0
  20. {streamdown-0.13.0 → streamdown-0.14.0}/passthrough.py +0 -0
  21. {streamdown-0.13.0 → streamdown-0.14.0}/somelog.txt +0 -0
  22. {streamdown-0.13.0 → streamdown-0.14.0}/streamdown/__init__.py +0 -0
  23. {streamdown-0.13.0 → streamdown-0.14.0}/streamdown/plugins/README.md +0 -0
  24. {streamdown-0.13.0 → streamdown-0.14.0}/streamdown/plugins/latex.py +0 -0
  25. {streamdown-0.13.0 → streamdown-0.14.0}/streamdown/tt.mds +0 -0
  26. {streamdown-0.13.0 → streamdown-0.14.0}/table.png +0 -0
  27. {streamdown-0.13.0 → streamdown-0.14.0}/temp.py +0 -0
  28. {streamdown-0.13.0 → streamdown-0.14.0}/test.py +0 -0
  29. {streamdown-0.13.0 → streamdown-0.14.0}/test_input.md +0 -0
  30. {streamdown-0.13.0 → streamdown-0.14.0}/tests/README.md +0 -0
  31. {streamdown-0.13.0 → streamdown-0.14.0}/tests/block.md +0 -0
  32. {streamdown-0.13.0 → streamdown-0.14.0}/tests/code.md +0 -0
  33. {streamdown-0.13.0 → streamdown-0.14.0}/tests/example.md +0 -0
  34. {streamdown-0.13.0 → streamdown-0.14.0}/tests/fizzbuzz.md +0 -0
  35. {streamdown-0.13.0 → streamdown-0.14.0}/tests/inline.md +0 -0
  36. {streamdown-0.13.0 → streamdown-0.14.0}/tests/line-buffer.sh +0 -0
  37. {streamdown-0.13.0 → streamdown-0.14.0}/tests/line-wrap.md +0 -0
  38. {streamdown-0.13.0 → streamdown-0.14.0}/tests/line.md +0 -0
  39. {streamdown-0.13.0 → streamdown-0.14.0}/tests/links.md +0 -0
  40. {streamdown-0.13.0 → streamdown-0.14.0}/tests/longer-example.md +0 -0
  41. {streamdown-0.13.0 → streamdown-0.14.0}/tests/mandlebrot.md +0 -0
  42. {streamdown-0.13.0 → streamdown-0.14.0}/tests/markdown.md +0 -0
  43. {streamdown-0.13.0 → streamdown-0.14.0}/tests/nested-example.md +0 -0
  44. {streamdown-0.13.0 → streamdown-0.14.0}/tests/new.md +0 -0
  45. {streamdown-0.13.0 → streamdown-0.14.0}/tests/outline.md +0 -0
  46. {streamdown-0.13.0 → streamdown-0.14.0}/tests/sd.log +0 -0
  47. {streamdown-0.13.0 → streamdown-0.14.0}/tests/table-break.md +0 -0
  48. {streamdown-0.13.0 → streamdown-0.14.0}/tests/table.md +0 -0
  49. {streamdown-0.13.0 → streamdown-0.14.0}/tests/table_test.md +0 -0
  50. {streamdown-0.13.0 → streamdown-0.14.0}/tests/test.md +0 -0
  51. {streamdown-0.13.0 → streamdown-0.14.0}/tests/test_input.md +0 -0
  52. {streamdown-0.13.0 → streamdown-0.14.0}/tests/white-space-code.md +0 -0
  53. {streamdown-0.13.0 → streamdown-0.14.0}/tests/wm.md +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: streamdown
3
- Version: 0.13.0
3
+ Version: 0.14.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
@@ -41,7 +41,11 @@ This will work with [simonw's llm](https://github.com/simonw/llm) unlike with [r
41
41
  ### Provides clean copyable code for long code blocks and short terminals.
42
42
  ![copyable](https://github.com/user-attachments/assets/4a3539c5-b5d1-4d6a-8bce-032724d8909d)
43
43
 
44
- ### Does OSC 8 links for modern terminals.
44
+ ### Supports images, why not?
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)
47
+
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
51
  ### Doesn't consume characters like _ and * as style when they are in `blocks like this` because `_they_can_be_varaiables_`
@@ -13,7 +13,11 @@ This will work with [simonw's llm](https://github.com/simonw/llm) unlike with [r
13
13
  ### Provides clean copyable code for long code blocks and short terminals.
14
14
  ![copyable](https://github.com/user-attachments/assets/4a3539c5-b5d1-4d6a-8bce-032724d8909d)
15
15
 
16
- ### Does OSC 8 links for modern terminals.
16
+ ### Supports images, why not?
17
+ Here's kitty and alacritty. Try to do that in glow...
18
+ ![doggie](https://github.com/user-attachments/assets/9a392929-b6c2-4204-b257-e09305acb7af)
19
+
20
+ ### Does OSC 8 links for modern terminals (and optionally OSC 52 for clipboard)
17
21
  [links.webm](https://github.com/user-attachments/assets/a5f71791-7c58-4183-ad3b-309f470c08a3)
18
22
 
19
23
  ### Doesn't consume characters like _ and * as style when they are in `blocks like this` because `_they_can_be_varaiables_`
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "streamdown"
7
- version = "0.13.0"
7
+ version = "0.14.0"
8
8
  description = "A streaming markdown renderer for modern terminals with syntax highlighting"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.8"
@@ -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
+ }
@@ -5,6 +5,7 @@
5
5
  # "pygments",
6
6
  # "pylatexenc",
7
7
  # "appdirs",
8
+ # "term-image",
8
9
  # "toml"
9
10
  # ]
10
11
  # ///
@@ -23,6 +24,7 @@ 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
27
29
  from argparse import ArgumentParser
28
30
  from pygments import highlight
@@ -52,7 +54,7 @@ Dark = { H = 1.00, S = 1.50, V = 0.25 }
52
54
  Mid = { H = 1.00, S = 1.00, V = 0.50 }
53
55
  Symbol = { H = 1.00, S = 1.00, V = 1.50 }
54
56
  Head = { H = 1.00, S = 2.00, V = 1.50 }
55
- Grey = { H = 1.00, S = 0.12, V = 1.25 }
57
+ Grey = { H = 1.00, S = 0.25, V = 1.37 }
56
58
  Bright = { H = 1.00, S = 2.00, V = 2.00 }
57
59
  Syntax = "monokai"
58
60
  """
@@ -80,10 +82,11 @@ BGRESET = "\033[49m"
80
82
  BOLD = ["\033[1m", "\033[22m"]
81
83
  UNDERLINE = ["\033[4m", "\033[24m"]
82
84
  ITALIC = ["\033[3m", "\033[23m"]
85
+ STRIKEOUT = ["\033[9m", "\033[29m"]
86
+ SUPER = [ 0x2070, 0x00B9, 0x00B2, 0x00B3, 0x2074, 0x2075, 0x2076, 0x2077, 0x2078, 0x2079 ]
83
87
 
84
88
  ESCAPE = r"\033\[[0-9;]*[mK]"
85
89
  ANSIESCAPE = r'\033(?:\[[0-9;?]*[a-zA-Z]|][0-9]*;;.*?\\|\\)'
86
- #r"\033(\[[0-9;]*[mK]|][0-9]*;;.*?\\|\\)"
87
90
  KEYCODE_RE = re.compile(r'\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])')
88
91
 
89
92
  visible = lambda x: re.sub(ANSIESCAPE, "", x)
@@ -131,7 +134,10 @@ class ParseState:
131
134
  self.Clipboard = _features.get("Clipboard")
132
135
  self.Logging = _features.get("Logging")
133
136
  self.Timeout = _features.get("Timeout")
137
+
134
138
  self.WidthArg = None
139
+ self.WidthFull = None
140
+ self.WidthWrap = False
135
141
 
136
142
  # If the entire block is indented this will
137
143
  # tell us what that is
@@ -159,24 +165,27 @@ class ParseState:
159
165
  self.in_italic = False
160
166
  self.in_table = False # (Code.[Header|Body] | False)
161
167
  self.in_underline = False
168
+ self.in_strikeout = False
162
169
  self.block_depth = 0
163
170
 
164
171
  self.exec_sub = None
165
172
  self.exec_master = None
166
173
  self.exec_slave = None
167
174
  self.exec_kb = 0
168
- self.exec_israw = False
169
175
 
170
176
  self.exit = 0
171
177
  self.where_from = None
172
178
 
173
179
  def current(self):
174
- state = { 'code': self.in_code, 'bold': self.in_bold, 'italic': self.in_italic, 'underline': self.in_underline }
180
+ state = { 'inline': self.inline_code, 'code': self.in_code, 'bold': self.in_bold, 'italic': self.in_italic, 'underline': self.in_underline }
175
181
  state['none'] = all(item is False for item in state.values())
176
182
  return state
177
183
 
184
+ def reset_inline(self):
185
+ self.inline_code = self.in_bold = self.in_italic = self.in_underline = False
186
+
178
187
  def space_left(self):
179
- return (Style.MarginSpaces if len(self.current_line) == 0 else "") + (Style.Blockquote * self.block_depth)
188
+ return Style.MarginSpaces + (Style.Blockquote * self.block_depth) if len(self.current_line) == 0 else ""
180
189
 
181
190
  state = ParseState()
182
191
 
@@ -233,17 +242,20 @@ def emit_h(level, text):
233
242
  text = line_format(text)
234
243
  spaces_to_center = ((state.Width - visible_length(text)) / 2)
235
244
  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"
245
+ return f"\n{state.space_left()}{BOLD[0]}{' ' * math.floor(spaces_to_center)}{text}{' ' * math.ceil(spaces_to_center)}{BOLD[1]}\n"
237
246
  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"
247
+ 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
248
  elif level == 3: ###
240
- return f"{Style.MarginSpaces}{FG}{Style.Head}{BOLD[0]}{text}{RESET}"
249
+ return f"{state.space_left()}{FG}{Style.Head}{BOLD[0]}{text}{RESET}"
241
250
  elif level == 4: ####
242
- return f"{Style.MarginSpaces}{FG}{Style.Symbol}{text}{RESET}"
251
+ return f"{state.space_left()}{FG}{Style.Symbol}{text}{RESET}"
243
252
  else: # level 5 or 6
244
- return f"{Style.MarginSpaces}{text}{RESET}"
253
+ return f"{state.space_left()}{text}{RESET}"
245
254
 
246
255
  def code_wrap(text_in):
256
+ if state.WidthWrap and len(text_in) > state.WidthFull:
257
+ return (0, [text_in])
258
+
247
259
  # get the indentation of the first line
248
260
  indent = len(text_in) - len(text_in.lstrip())
249
261
  text = text_in.lstrip()
@@ -337,8 +349,20 @@ def text_wrap(text, width = -1, indent = 0, first_line_prefix="", subsequent_lin
337
349
  return lines
338
350
 
339
351
  def line_format(line):
340
- def not_text(token):
341
- return not token or len(token.rstrip()) != len(token)
352
+ not_text = lambda token: not token or len(token.rstrip()) != len(token)
353
+ footnotes = lambda match: ''.join([chr(SUPER[int(i)]) for i in match.group(1)])
354
+
355
+ def process_images(match):
356
+ url = match.group(2)
357
+ try:
358
+ if re.match(r"https://", url.lower()):
359
+ image = from_url(url)
360
+ else:
361
+ image = from_file(url)
362
+ image.height = 20
363
+ print(f"{image:|.-1#}")
364
+ except:
365
+ return match.group(2)
342
366
 
343
367
  # Apply OSC 8 hyperlink formatting after other formatting
344
368
  def process_links(match):
@@ -346,8 +370,11 @@ def line_format(line):
346
370
  url = match.group(2)
347
371
  return f'\033]8;;{url}\033\\{Style.Link}{description}{UNDERLINE[1]}\033]8;;\033\\{FGRESET}'
348
372
 
373
+ line = re.sub(r"\!\[([^\]]*)\]\(([^\)]+)\)", process_images, line)
349
374
  line = re.sub(r"\[([^\]]+)\]\(([^\)]+)\)", process_links, line)
350
- tokenList = re.finditer(r"((\*\*|\*|_|`)|[^_*`]+)", line)
375
+ line = re.sub(r"\[\^(\d+)\]:?", footnotes, line)
376
+
377
+ tokenList = re.finditer(r"((~~|\*\*_|_\*\*|\*{1,3}|_{1,3}|`+)|[^~_*`]+)", line)
351
378
  result = ""
352
379
 
353
380
  for match in tokenList:
@@ -355,8 +382,13 @@ def line_format(line):
355
382
  next_token = line[match.end()] if match.end() < len(line) else ""
356
383
  prev_token = line[match.start()-1] if match.start() > 0 else ""
357
384
 
358
- if token == "`":
359
- state.inline_code = not state.inline_code
385
+ # This trick makes sure that things like `` ` `` render right.
386
+ if "`" in token and (not state.inline_code or state.inline_code == token):
387
+ if state.inline_code:
388
+ state.inline_code = False
389
+ else:
390
+ state.inline_code = token
391
+
360
392
  if state.inline_code:
361
393
  result += f'{BG}{Style.Mid}'
362
394
  else:
@@ -367,7 +399,17 @@ def line_format(line):
367
399
  elif state.inline_code:
368
400
  result += token
369
401
 
370
- elif token == "**" and (state.in_bold or not_text(prev_token)):
402
+ elif token == '~~' and (state.in_strikeout or not_text(prev_token)):
403
+ state.in_strikeout = not state.in_strikeout
404
+ result += STRIKEOUT[0] if state.in_strikeout else STRIKEOUT[1]
405
+
406
+ elif token in ['**_','_**','___','***'] and (state.in_bold or not_text(prev_token)):
407
+ state.in_bold = not state.in_bold
408
+ result += BOLD[0] if state.in_bold else BOLD[1]
409
+ state.in_italic = not state.in_italic
410
+ result += ITALIC[0] if state.in_italic else ITALIC[1]
411
+
412
+ elif (token == '__' or token == "**") and (state.in_bold or not_text(prev_token)):
371
413
  state.in_bold = not state.in_bold
372
414
  result += BOLD[0] if state.in_bold else BOLD[1]
373
415
 
@@ -380,7 +422,7 @@ def line_format(line):
380
422
  else:
381
423
  result += token
382
424
 
383
- elif token == "_" and (state.in_underline or not_text(prev_token)):
425
+ elif token == "_" and (state.in_underline or (not_text(prev_token) and next_token.isalnum())):
384
426
  state.in_underline = not state.in_underline
385
427
  result += UNDERLINE[0] if state.in_underline else UNDERLINE[1]
386
428
  else:
@@ -422,8 +464,6 @@ def parse(stream):
422
464
 
423
465
  if len(ready_in) == 0:
424
466
  TimeoutIx += 1
425
-
426
-
427
467
 
428
468
  elif stream.fileno() in ready_in:
429
469
  byte = os.read(stream.fileno(), 1)
@@ -462,13 +502,30 @@ def parse(stream):
462
502
  # Run through the plugins first
463
503
  res = latex.Plugin(line, state, Style)
464
504
  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
505
+ # This means everything was consumed by our plugin and
506
+ # we should continue
471
507
  continue
508
+ elif res is not None:
509
+ for row in res:
510
+ yield row
511
+ continue
512
+
513
+ # running this here avoids stray |
514
+ block_match = re.match(r"^\s*((>\s*)+|<.?think>)", line)
515
+ if not state.in_code and block_match:
516
+ if block_match.group(1) == '</think>':
517
+ state.block_depth = 0
518
+ yield RESET
519
+ elif block_match.group(1) == '<think>':
520
+ state.block_depth = 1
521
+ else:
522
+ state.block_depth = block_match.group(0).count('>')
523
+ # we also need to consume those tokens
524
+ line = line[len(block_match.group(0)):]
525
+ else:
526
+ if state.block_depth > 0:
527
+ line = FGRESET + line
528
+ state.block_depth = 0
472
529
 
473
530
  # --- Collapse Multiple Empty Lines if not in code blocks ---
474
531
  if not state.in_code:
@@ -483,7 +540,7 @@ def parse(stream):
483
540
  else:
484
541
  last_line_empty_cache = state.last_line_empty
485
542
  state.last_line_empty = False
486
-
543
+
487
544
  # This is to reset our top-level line-based systems
488
545
  # \n buffer
489
546
  if not state.in_list and len(state.ordered_list_numbers) > 0:
@@ -506,28 +563,11 @@ def parse(stream):
506
563
  if state.in_table and not state.in_code and not re.match(r"^\s*\|.+\|\s*$", line):
507
564
  state.in_table = False
508
565
 
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
566
  #
526
567
  # <code><pre>
527
568
  #
528
- # This needs to be first
529
569
  if not state.in_code:
530
- code_match = re.match(r"\s*```\s*([^\s]+|$)", line)
570
+ code_match = re.match(r"\s*```\s*([^\s]+|$)$", line)
531
571
  if code_match:
532
572
  state.in_code = Code.Backtick
533
573
  state.code_language = code_match.group(1) or 'Bash'
@@ -563,6 +603,7 @@ def parse(stream):
563
603
  try:
564
604
  ext = get_lexer_by_name(state.code_language).filenames[0].split('.')[-1]
565
605
  except:
606
+ logging.warning(f"Can't find canonical extension for {state.code_language}")
566
607
  pass
567
608
 
568
609
  open(os.path.join(state.scrape, f"file_{state.scrape_ix}.{ext}"), 'w').write(state.code_buffer)
@@ -584,7 +625,8 @@ def parse(stream):
584
625
 
585
626
 
586
627
  if code_type == Code.Backtick:
587
- continue
628
+ state.code_indent = len(line) - len(line.lstrip())
629
+ continue
588
630
  else:
589
631
  # otherwise we don't want to consume
590
632
  # nor do we want to be here.
@@ -600,11 +642,6 @@ def parse(stream):
600
642
  custom_style = get_style_by_name("default")
601
643
 
602
644
  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
645
  line = line[state.code_indent :]
609
646
 
610
647
  elif line.startswith(" " * state.code_indent):
@@ -652,7 +689,7 @@ def parse(stream):
652
689
 
653
690
  code_line = ' ' * indent + this_batch.strip()
654
691
 
655
- margin = state.WidthFull - visible_length(code_line)
692
+ margin = state.WidthFull - visible_length(code_line) % state.WidthFull
656
693
  yield f"{Style.Codebg}{code_line}{' ' * max(0, margin)}{BGRESET}"
657
694
  continue
658
695
  except Goto:
@@ -714,18 +751,18 @@ def parse(stream):
714
751
  if list_type == "number":
715
752
  state.ordered_list_numbers[-1] += 1
716
753
 
717
- indent = len(state.list_item_stack) * 2
754
+ indent = (len(state.list_item_stack) - 1) * 2
718
755
 
719
756
  wrap_width = state.Width - indent - (2 * Style.ListIndent)
720
757
 
721
758
  bullet = '•'
722
759
  if list_type == "number":
723
760
  list_number = int(max(state.ordered_list_numbers[-1], float(list_item_match.group(2))))
724
- bullet = f"{list_number}"
761
+ bullet = str(list_number)
725
762
 
726
763
  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)
764
+ first_line_prefix = f"{(' ' * (indent ))}{FG}{Style.Symbol}{bullet}{RESET} ",
765
+ subsequent_line_prefix = " " * (indent)
729
766
  )
730
767
  for wrapped_line in wrapped_lineList:
731
768
  yield f"{state.space_left()}{wrapped_line}\n"
@@ -742,7 +779,7 @@ def parse(stream):
742
779
  #
743
780
  # <hr>
744
781
  #
745
- hr_match = re.match(r"^[\s]*([-=_]){3,}[\s]*$", line)
782
+ hr_match = re.match(r"^[\s]*([-\*=_]){3,}[\s]*$", line)
746
783
  if hr_match:
747
784
  if state.last_line_empty or last_line_empty_cache:
748
785
  # print a horizontal rule using a unicode midline
@@ -764,12 +801,6 @@ def parse(stream):
764
801
  for wrapped_line in wrapped_lines:
765
802
  yield f"{state.space_left()}{wrapped_line}\n"
766
803
 
767
- def get_terminal_width():
768
- try:
769
- return shutil.get_terminal_size().columns
770
- except (AttributeError, OSError):
771
- return 80
772
-
773
804
  def emit(inp):
774
805
  buffer = []
775
806
  flush = False
@@ -795,6 +826,9 @@ def emit(inp):
795
826
  state.current_line += chunk
796
827
 
797
828
  buffer.append(chunk)
829
+ # This *might* be dangerous
830
+ state.reset_inline()
831
+
798
832
  if flush:
799
833
  chunk = "\n".join(buffer)
800
834
  buffer = []
@@ -806,17 +840,10 @@ def emit(inp):
806
840
  else:
807
841
  chunk = buffer.pop(0)
808
842
 
809
- if state.is_pty:
810
- print(chunk, end="", flush=True)
811
- else:
812
- sys.stdout.write(chunk)
843
+ print(chunk, end="", flush=True)
813
844
 
814
845
  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)
846
+ print(buffer.pop(0), end="", flush=True)
820
847
 
821
848
  def apply_multipliers(name, H, S, V):
822
849
  m = _style.get(name)
@@ -824,7 +851,20 @@ def apply_multipliers(name, H, S, V):
824
851
  return ';'.join([str(int(x * 256)) for x in [r, g, b]]) + "m"
825
852
 
826
853
  def width_calc():
827
- state.WidthFull = state.WidthArg or int(get_terminal_width())
854
+ if not state.WidthFull or not state.WidthArg:
855
+ if state.WidthArg:
856
+ state.WidthFull = state.WidthArg
857
+ else:
858
+ width = 80
859
+
860
+ try:
861
+ width = shutil.get_terminal_size().columns
862
+ state.WidthWrap = True
863
+ except (AttributeError, OSError):
864
+ pass
865
+
866
+ state.WidthFull = width
867
+
828
868
  state.Width = state.WidthFull - 2 * Style.Margin
829
869
  Style.Codepad = [
830
870
  f"{RESET}{FG}{Style.Dark}{'▄' * state.WidthFull}{RESET}\n",
@@ -838,9 +878,9 @@ def main():
838
878
  parser.add_argument("filenameList", nargs="*", help="Input file to process (also takes stdin)")
839
879
  parser.add_argument("-l", "--loglevel", default="INFO", help="Set the logging level")
840
880
  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")
881
+ parser.add_argument("-w", "--width", default="0", help="Set the width WIDTH")
882
+ parser.add_argument("-e", "--exec", help="Wrap a program EXEC for more 'proper' i/o handling")
883
+ parser.add_argument("-s", "--scrape", help="Scrape code snippets to a directory SCRAPE")
844
884
  args = parser.parse_args()
845
885
 
846
886
  if args.color:
@@ -864,8 +904,7 @@ def main():
864
904
 
865
905
  Style.Codebg = f"{BG}{Style.Dark}"
866
906
  Style.Link = f"{FG}{Style.Symbol}{UNDERLINE[0]}"
867
- Style.Blockquote = f"{FG}{Style.Grey} \u258E "
868
-
907
+ Style.Blockquote = f"{FG}{Style.Grey} "
869
908
 
870
909
  logging.basicConfig(stream=sys.stdout, level=args.loglevel.upper(), format=f'%(message)s')
871
910
  state.exec_master, state.exec_slave = pty.openpty()
@@ -1,5 +1,5 @@
1
1
  #!/bin/bash
2
- TIMEOUT=${TIMEOUT:-0.1}
2
+ TIMEOUT=${TIMEOUT:-0.01}
3
3
 
4
4
  while [[ $# -gt 0 ]]; do
5
5
  echo "## File: $1"
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