streamdown 0.12.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.
- {streamdown-0.12.0 → streamdown-0.14.0}/PKG-INFO +31 -4
- {streamdown-0.12.0 → streamdown-0.14.0}/README.md +30 -3
- {streamdown-0.12.0 → streamdown-0.14.0}/pyproject.toml +1 -1
- streamdown-0.14.0/streamdown/scrape/file_0.py +22 -0
- streamdown-0.14.0/streamdown/scrape/file_1.js +27 -0
- streamdown-0.14.0/streamdown/scrape/file_2.cpp +23 -0
- {streamdown-0.12.0 → streamdown-0.14.0}/streamdown/sd.py +200 -88
- streamdown-0.14.0/tests/block.md +10 -0
- {streamdown-0.12.0 → streamdown-0.14.0}/tests/chunk-buffer.sh +1 -1
- {streamdown-0.12.0 → streamdown-0.14.0}/.gitignore +0 -0
- {streamdown-0.12.0 → streamdown-0.14.0}/.vimrc +0 -0
- {streamdown-0.12.0 → streamdown-0.14.0}/24-bit-color.sh +0 -0
- {streamdown-0.12.0 → streamdown-0.14.0}/LICENSE.MIT +0 -0
- {streamdown-0.12.0 → streamdown-0.14.0}/configurable.png +0 -0
- {streamdown-0.12.0 → streamdown-0.14.0}/copyable.png +0 -0
- {streamdown-0.12.0 → streamdown-0.14.0}/dunder.png +0 -0
- {streamdown-0.12.0 → streamdown-0.14.0}/error.txt +0 -0
- {streamdown-0.12.0 → streamdown-0.14.0}/newdir/file_0.py +0 -0
- {streamdown-0.12.0 → streamdown-0.14.0}/newdir/file_1.rb +0 -0
- {streamdown-0.12.0 → streamdown-0.14.0}/newdir/file_2.jl +0 -0
- {streamdown-0.12.0 → streamdown-0.14.0}/passthrough.py +0 -0
- {streamdown-0.12.0 → streamdown-0.14.0}/somelog.txt +0 -0
- {streamdown-0.12.0 → streamdown-0.14.0}/streamdown/__init__.py +0 -0
- {streamdown-0.12.0 → streamdown-0.14.0}/streamdown/plugins/README.md +0 -0
- {streamdown-0.12.0 → streamdown-0.14.0}/streamdown/plugins/latex.py +0 -0
- {streamdown-0.12.0 → streamdown-0.14.0}/streamdown/tt.mds +0 -0
- {streamdown-0.12.0 → streamdown-0.14.0}/table.png +0 -0
- {streamdown-0.12.0 → streamdown-0.14.0}/temp.py +0 -0
- {streamdown-0.12.0 → streamdown-0.14.0}/test.py +0 -0
- {streamdown-0.12.0 → streamdown-0.14.0}/test_input.md +0 -0
- {streamdown-0.12.0 → streamdown-0.14.0}/tests/README.md +0 -0
- {streamdown-0.12.0 → streamdown-0.14.0}/tests/code.md +0 -0
- {streamdown-0.12.0 → streamdown-0.14.0}/tests/example.md +0 -0
- {streamdown-0.12.0 → streamdown-0.14.0}/tests/fizzbuzz.md +0 -0
- {streamdown-0.12.0 → streamdown-0.14.0}/tests/inline.md +0 -0
- {streamdown-0.12.0 → streamdown-0.14.0}/tests/line-buffer.sh +0 -0
- {streamdown-0.12.0 → streamdown-0.14.0}/tests/line-wrap.md +0 -0
- {streamdown-0.12.0 → streamdown-0.14.0}/tests/line.md +0 -0
- {streamdown-0.12.0 → streamdown-0.14.0}/tests/links.md +0 -0
- {streamdown-0.12.0 → streamdown-0.14.0}/tests/longer-example.md +0 -0
- {streamdown-0.12.0 → streamdown-0.14.0}/tests/mandlebrot.md +0 -0
- {streamdown-0.12.0 → streamdown-0.14.0}/tests/markdown.md +0 -0
- {streamdown-0.12.0 → streamdown-0.14.0}/tests/nested-example.md +0 -0
- {streamdown-0.12.0 → streamdown-0.14.0}/tests/new.md +0 -0
- {streamdown-0.12.0 → streamdown-0.14.0}/tests/outline.md +0 -0
- {streamdown-0.12.0 → streamdown-0.14.0}/tests/sd.log +0 -0
- {streamdown-0.12.0 → streamdown-0.14.0}/tests/table-break.md +0 -0
- {streamdown-0.12.0 → streamdown-0.14.0}/tests/table.md +0 -0
- {streamdown-0.12.0 → streamdown-0.14.0}/tests/table_test.md +0 -0
- {streamdown-0.12.0 → streamdown-0.14.0}/tests/test.md +0 -0
- {streamdown-0.12.0 → streamdown-0.14.0}/tests/test_input.md +0 -0
- {streamdown-0.12.0 → streamdown-0.14.0}/tests/white-space-code.md +0 -0
- {streamdown-0.12.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.
|
|
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
|

|
|
43
43
|
|
|
44
|
-
###
|
|
44
|
+
### Supports images, why not?
|
|
45
|
+
Here's kitty and alacritty. Try to do that in glow...
|
|
46
|
+

|
|
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_`
|
|
@@ -109,6 +113,31 @@ Width = 120
|
|
|
109
113
|
Timeout = 1.0
|
|
110
114
|
```
|
|
111
115
|
|
|
116
|
+
## Invocation
|
|
117
|
+
The most exciting feature here is `--exec` with it you can do full readline support like this:
|
|
118
|
+
|
|
119
|
+
$ sd --exec "llm chat"
|
|
120
|
+
|
|
121
|
+
And now you have all your readline stuff. It's pretty great.
|
|
122
|
+
|
|
123
|
+
```shell
|
|
124
|
+
Streamdown - A markdown renderer for modern terminals
|
|
125
|
+
|
|
126
|
+
positional arguments:
|
|
127
|
+
filenameList Input file to process (also takes stdin)
|
|
128
|
+
|
|
129
|
+
options:
|
|
130
|
+
-h, --help show this help message and exit
|
|
131
|
+
-l LOGLEVEL, --loglevel LOGLEVEL
|
|
132
|
+
Set the logging level
|
|
133
|
+
-c COLOR, --color COLOR
|
|
134
|
+
Set the hsv base: h,s,v
|
|
135
|
+
-w WIDTH, --width WIDTH
|
|
136
|
+
Set the width
|
|
137
|
+
-e EXEC, --exec EXEC Wrap a program for more 'proper' i/o handling
|
|
138
|
+
|
|
139
|
+
```
|
|
140
|
+
|
|
112
141
|
## Demo
|
|
113
142
|
Do this
|
|
114
143
|
|
|
@@ -131,5 +160,3 @@ I'm really considering using `tinycss2` and making an actual stylesheet engine.
|
|
|
131
160
|
#### scrape
|
|
132
161
|
This is already partially implemented. The idea is every code block can get extracted and put in a directory so you can have a conversation to generate every piece of a project, similar to Aider, Claude or Goose, but in the most hands-off yet still convenient way possible.
|
|
133
162
|
|
|
134
|
-
#### exec
|
|
135
|
-
I'm trying to get a readline capable wrapper so that interaction is as transparent as possible. After many days of research I've given up on trying to hack it through tty/pty hijacking and pipes and decided it has to be a standard wrapper. This should be low effort.
|
|
@@ -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
|

|
|
15
15
|
|
|
16
|
-
###
|
|
16
|
+
### Supports images, why not?
|
|
17
|
+
Here's kitty and alacritty. Try to do that in glow...
|
|
18
|
+

|
|
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_`
|
|
@@ -81,6 +85,31 @@ Width = 120
|
|
|
81
85
|
Timeout = 1.0
|
|
82
86
|
```
|
|
83
87
|
|
|
88
|
+
## Invocation
|
|
89
|
+
The most exciting feature here is `--exec` with it you can do full readline support like this:
|
|
90
|
+
|
|
91
|
+
$ sd --exec "llm chat"
|
|
92
|
+
|
|
93
|
+
And now you have all your readline stuff. It's pretty great.
|
|
94
|
+
|
|
95
|
+
```shell
|
|
96
|
+
Streamdown - A markdown renderer for modern terminals
|
|
97
|
+
|
|
98
|
+
positional arguments:
|
|
99
|
+
filenameList Input file to process (also takes stdin)
|
|
100
|
+
|
|
101
|
+
options:
|
|
102
|
+
-h, --help show this help message and exit
|
|
103
|
+
-l LOGLEVEL, --loglevel LOGLEVEL
|
|
104
|
+
Set the logging level
|
|
105
|
+
-c COLOR, --color COLOR
|
|
106
|
+
Set the hsv base: h,s,v
|
|
107
|
+
-w WIDTH, --width WIDTH
|
|
108
|
+
Set the width
|
|
109
|
+
-e EXEC, --exec EXEC Wrap a program for more 'proper' i/o handling
|
|
110
|
+
|
|
111
|
+
```
|
|
112
|
+
|
|
84
113
|
## Demo
|
|
85
114
|
Do this
|
|
86
115
|
|
|
@@ -103,5 +132,3 @@ I'm really considering using `tinycss2` and making an actual stylesheet engine.
|
|
|
103
132
|
#### scrape
|
|
104
133
|
This is already partially implemented. The idea is every code block can get extracted and put in a directory so you can have a conversation to generate every piece of a project, similar to Aider, Claude or Goose, but in the most hands-off yet still convenient way possible.
|
|
105
134
|
|
|
106
|
-
#### exec
|
|
107
|
-
I'm trying to get a readline capable wrapper so that interaction is as transparent as possible. After many days of research I've given up on trying to hack it through tty/pty hijacking and pipes and decided it has to be a standard wrapper. This should be low effort.
|
|
@@ -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
|
# ///
|
|
@@ -12,6 +13,7 @@ import appdirs, toml
|
|
|
12
13
|
import logging, tempfile
|
|
13
14
|
import os, sys
|
|
14
15
|
import pty, select
|
|
16
|
+
import termios, tty
|
|
15
17
|
|
|
16
18
|
import math
|
|
17
19
|
import re
|
|
@@ -20,7 +22,9 @@ import subprocess
|
|
|
20
22
|
import traceback
|
|
21
23
|
import colorsys
|
|
22
24
|
import base64
|
|
25
|
+
import importlib
|
|
23
26
|
from io import BytesIO
|
|
27
|
+
from term_image.image import from_file, from_url
|
|
24
28
|
import pygments.util
|
|
25
29
|
from argparse import ArgumentParser
|
|
26
30
|
from pygments import highlight
|
|
@@ -28,7 +32,10 @@ from pygments.lexers import get_lexer_by_name
|
|
|
28
32
|
from pygments.formatters import Terminal256Formatter
|
|
29
33
|
from pygments.styles import get_style_by_name
|
|
30
34
|
|
|
31
|
-
|
|
35
|
+
if __package__ is None:
|
|
36
|
+
from plugins import latex
|
|
37
|
+
else:
|
|
38
|
+
from .plugins import latex
|
|
32
39
|
|
|
33
40
|
default_toml = """
|
|
34
41
|
[features]
|
|
@@ -47,7 +54,7 @@ Dark = { H = 1.00, S = 1.50, V = 0.25 }
|
|
|
47
54
|
Mid = { H = 1.00, S = 1.00, V = 0.50 }
|
|
48
55
|
Symbol = { H = 1.00, S = 1.00, V = 1.50 }
|
|
49
56
|
Head = { H = 1.00, S = 2.00, V = 1.50 }
|
|
50
|
-
Grey = { H = 1.00, S = 0.
|
|
57
|
+
Grey = { H = 1.00, S = 0.25, V = 1.37 }
|
|
51
58
|
Bright = { H = 1.00, S = 2.00, V = 2.00 }
|
|
52
59
|
Syntax = "monokai"
|
|
53
60
|
"""
|
|
@@ -75,9 +82,11 @@ BGRESET = "\033[49m"
|
|
|
75
82
|
BOLD = ["\033[1m", "\033[22m"]
|
|
76
83
|
UNDERLINE = ["\033[4m", "\033[24m"]
|
|
77
84
|
ITALIC = ["\033[3m", "\033[23m"]
|
|
85
|
+
STRIKEOUT = ["\033[9m", "\033[29m"]
|
|
86
|
+
SUPER = [ 0x2070, 0x00B9, 0x00B2, 0x00B3, 0x2074, 0x2075, 0x2076, 0x2077, 0x2078, 0x2079 ]
|
|
78
87
|
|
|
79
88
|
ESCAPE = r"\033\[[0-9;]*[mK]"
|
|
80
|
-
ANSIESCAPE = r
|
|
89
|
+
ANSIESCAPE = r'\033(?:\[[0-9;?]*[a-zA-Z]|][0-9]*;;.*?\\|\\)'
|
|
81
90
|
KEYCODE_RE = re.compile(r'\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])')
|
|
82
91
|
|
|
83
92
|
visible = lambda x: re.sub(ANSIESCAPE, "", x)
|
|
@@ -114,16 +123,22 @@ class ParseState:
|
|
|
114
123
|
self.first_line = True
|
|
115
124
|
self.last_line_empty = False
|
|
116
125
|
self.is_pty = False
|
|
126
|
+
self.is_exec = False
|
|
117
127
|
self.maybe_prompt = False
|
|
118
128
|
self.emit_flag = None
|
|
119
129
|
self.scrape = None
|
|
120
130
|
self.scrape_ix = 0
|
|
131
|
+
self.terminal = None
|
|
121
132
|
|
|
122
133
|
self.CodeSpaces = _features.get("CodeSpaces")
|
|
123
134
|
self.Clipboard = _features.get("Clipboard")
|
|
124
135
|
self.Logging = _features.get("Logging")
|
|
125
136
|
self.Timeout = _features.get("Timeout")
|
|
126
137
|
|
|
138
|
+
self.WidthArg = None
|
|
139
|
+
self.WidthFull = None
|
|
140
|
+
self.WidthWrap = False
|
|
141
|
+
|
|
127
142
|
# If the entire block is indented this will
|
|
128
143
|
# tell us what that is
|
|
129
144
|
self.first_indent = None
|
|
@@ -150,18 +165,27 @@ class ParseState:
|
|
|
150
165
|
self.in_italic = False
|
|
151
166
|
self.in_table = False # (Code.[Header|Body] | False)
|
|
152
167
|
self.in_underline = False
|
|
153
|
-
self.
|
|
168
|
+
self.in_strikeout = False
|
|
169
|
+
self.block_depth = 0
|
|
170
|
+
|
|
171
|
+
self.exec_sub = None
|
|
172
|
+
self.exec_master = None
|
|
173
|
+
self.exec_slave = None
|
|
174
|
+
self.exec_kb = 0
|
|
154
175
|
|
|
155
176
|
self.exit = 0
|
|
156
177
|
self.where_from = None
|
|
157
178
|
|
|
158
179
|
def current(self):
|
|
159
|
-
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 }
|
|
160
181
|
state['none'] = all(item is False for item in state.values())
|
|
161
182
|
return state
|
|
162
183
|
|
|
184
|
+
def reset_inline(self):
|
|
185
|
+
self.inline_code = self.in_bold = self.in_italic = self.in_underline = False
|
|
186
|
+
|
|
163
187
|
def space_left(self):
|
|
164
|
-
return (
|
|
188
|
+
return Style.MarginSpaces + (Style.Blockquote * self.block_depth) if len(self.current_line) == 0 else ""
|
|
165
189
|
|
|
166
190
|
state = ParseState()
|
|
167
191
|
|
|
@@ -210,7 +234,7 @@ def format_table(rowList):
|
|
|
210
234
|
# Correct indentation: This should be outside the c_idx loop
|
|
211
235
|
joined_line = f"{BG}{bg_color}{extra}{FG}{Style.Symbol}│{RESET}".join(line_segments)
|
|
212
236
|
# Correct indentation and add missing characters
|
|
213
|
-
yield f"{
|
|
237
|
+
yield f"{Style.MarginSpaces}{joined_line}{RESET}"
|
|
214
238
|
|
|
215
239
|
state.bg = BGRESET
|
|
216
240
|
|
|
@@ -218,21 +242,24 @@ def emit_h(level, text):
|
|
|
218
242
|
text = line_format(text)
|
|
219
243
|
spaces_to_center = ((state.Width - visible_length(text)) / 2)
|
|
220
244
|
if level == 1: #
|
|
221
|
-
return f"\n{
|
|
245
|
+
return f"\n{state.space_left()}{BOLD[0]}{' ' * math.floor(spaces_to_center)}{text}{' ' * math.ceil(spaces_to_center)}{BOLD[1]}\n"
|
|
222
246
|
elif level == 2: ##
|
|
223
|
-
return f"\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"
|
|
224
248
|
elif level == 3: ###
|
|
225
|
-
return f"{
|
|
249
|
+
return f"{state.space_left()}{FG}{Style.Head}{BOLD[0]}{text}{RESET}"
|
|
226
250
|
elif level == 4: ####
|
|
227
|
-
return f"{
|
|
251
|
+
return f"{state.space_left()}{FG}{Style.Symbol}{text}{RESET}"
|
|
228
252
|
else: # level 5 or 6
|
|
229
|
-
return f"{
|
|
253
|
+
return f"{state.space_left()}{text}{RESET}"
|
|
230
254
|
|
|
231
255
|
def code_wrap(text_in):
|
|
256
|
+
if state.WidthWrap and len(text_in) > state.WidthFull:
|
|
257
|
+
return (0, [text_in])
|
|
258
|
+
|
|
232
259
|
# get the indentation of the first line
|
|
233
260
|
indent = len(text_in) - len(text_in.lstrip())
|
|
234
261
|
text = text_in.lstrip()
|
|
235
|
-
mywidth = state.
|
|
262
|
+
mywidth = state.WidthFull - indent
|
|
236
263
|
|
|
237
264
|
# We take special care to preserve empty lines
|
|
238
265
|
if len(text) == 0:
|
|
@@ -322,8 +349,20 @@ def text_wrap(text, width = -1, indent = 0, first_line_prefix="", subsequent_lin
|
|
|
322
349
|
return lines
|
|
323
350
|
|
|
324
351
|
def line_format(line):
|
|
325
|
-
|
|
326
|
-
|
|
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)
|
|
327
366
|
|
|
328
367
|
# Apply OSC 8 hyperlink formatting after other formatting
|
|
329
368
|
def process_links(match):
|
|
@@ -331,8 +370,11 @@ def line_format(line):
|
|
|
331
370
|
url = match.group(2)
|
|
332
371
|
return f'\033]8;;{url}\033\\{Style.Link}{description}{UNDERLINE[1]}\033]8;;\033\\{FGRESET}'
|
|
333
372
|
|
|
373
|
+
line = re.sub(r"\!\[([^\]]*)\]\(([^\)]+)\)", process_images, line)
|
|
334
374
|
line = re.sub(r"\[([^\]]+)\]\(([^\)]+)\)", process_links, line)
|
|
335
|
-
|
|
375
|
+
line = re.sub(r"\[\^(\d+)\]:?", footnotes, line)
|
|
376
|
+
|
|
377
|
+
tokenList = re.finditer(r"((~~|\*\*_|_\*\*|\*{1,3}|_{1,3}|`+)|[^~_*`]+)", line)
|
|
336
378
|
result = ""
|
|
337
379
|
|
|
338
380
|
for match in tokenList:
|
|
@@ -340,8 +382,13 @@ def line_format(line):
|
|
|
340
382
|
next_token = line[match.end()] if match.end() < len(line) else ""
|
|
341
383
|
prev_token = line[match.start()-1] if match.start() > 0 else ""
|
|
342
384
|
|
|
343
|
-
|
|
344
|
-
|
|
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
|
+
|
|
345
392
|
if state.inline_code:
|
|
346
393
|
result += f'{BG}{Style.Mid}'
|
|
347
394
|
else:
|
|
@@ -352,7 +399,17 @@ def line_format(line):
|
|
|
352
399
|
elif state.inline_code:
|
|
353
400
|
result += token
|
|
354
401
|
|
|
355
|
-
elif 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)):
|
|
356
413
|
state.in_bold = not state.in_bold
|
|
357
414
|
result += BOLD[0] if state.in_bold else BOLD[1]
|
|
358
415
|
|
|
@@ -365,7 +422,7 @@ def line_format(line):
|
|
|
365
422
|
else:
|
|
366
423
|
result += token
|
|
367
424
|
|
|
368
|
-
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())):
|
|
369
426
|
state.in_underline = not state.in_underline
|
|
370
427
|
result += UNDERLINE[0] if state.in_underline else UNDERLINE[1]
|
|
371
428
|
else:
|
|
@@ -378,11 +435,37 @@ def parse(stream):
|
|
|
378
435
|
byte = None
|
|
379
436
|
TimeoutIx = 0
|
|
380
437
|
while True:
|
|
381
|
-
if state.is_pty:
|
|
438
|
+
if state.is_pty or state.is_exec:
|
|
382
439
|
byte = None
|
|
383
|
-
|
|
440
|
+
ready_in, _, _ = select.select(
|
|
441
|
+
[stream.fileno(), state.exec_master], [], [], state.Timeout)
|
|
442
|
+
|
|
443
|
+
if state.is_exec:
|
|
444
|
+
# This is keyboard input
|
|
445
|
+
if stream.fileno() in ready_in:
|
|
446
|
+
byte = os.read(stream.fileno(), 1)
|
|
447
|
+
|
|
448
|
+
state.exec_kb += 1
|
|
449
|
+
os.write(state.exec_master, byte)
|
|
450
|
+
|
|
451
|
+
if byte == b'\n':
|
|
452
|
+
state.buffer = b''
|
|
453
|
+
print("")
|
|
454
|
+
state.exec_kb = 0
|
|
455
|
+
else:
|
|
456
|
+
continue
|
|
457
|
+
|
|
458
|
+
if state.exec_master in ready_in:
|
|
459
|
+
TimeoutIx = 0
|
|
460
|
+
byte = os.read(state.exec_master, 1)
|
|
384
461
|
|
|
385
|
-
|
|
462
|
+
if state.exec_kb:
|
|
463
|
+
os.write(sys.stdout.fileno(), byte)
|
|
464
|
+
|
|
465
|
+
if len(ready_in) == 0:
|
|
466
|
+
TimeoutIx += 1
|
|
467
|
+
|
|
468
|
+
elif stream.fileno() in ready_in:
|
|
386
469
|
byte = os.read(stream.fileno(), 1)
|
|
387
470
|
TimeoutIx = 0
|
|
388
471
|
elif TimeoutIx == 0:
|
|
@@ -402,14 +485,15 @@ def parse(stream):
|
|
|
402
485
|
|
|
403
486
|
line = state.buffer.decode('utf-8')
|
|
404
487
|
state.has_newline = line.endswith('\n')
|
|
405
|
-
|
|
488
|
+
# I hate this. There should be better ways.
|
|
489
|
+
state.maybe_prompt = not state.has_newline and state.current()['none'] and re.match(r'^.*>\s+$', visible(line))
|
|
406
490
|
|
|
407
491
|
# let's wait for a newline
|
|
408
492
|
if state.maybe_prompt:
|
|
409
493
|
state.emit_flag = Code.Flush
|
|
410
494
|
yield line
|
|
495
|
+
state.current_line = ''
|
|
411
496
|
state.buffer = b''
|
|
412
|
-
continue
|
|
413
497
|
|
|
414
498
|
if not state.has_newline:
|
|
415
499
|
continue
|
|
@@ -418,13 +502,30 @@ def parse(stream):
|
|
|
418
502
|
# Run through the plugins first
|
|
419
503
|
res = latex.Plugin(line, state, Style)
|
|
420
504
|
if res is True:
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
continue
|
|
424
|
-
elif res is not None:
|
|
425
|
-
for row in res:
|
|
426
|
-
yield row
|
|
505
|
+
# This means everything was consumed by our plugin and
|
|
506
|
+
# we should continue
|
|
427
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
|
|
428
529
|
|
|
429
530
|
# --- Collapse Multiple Empty Lines if not in code blocks ---
|
|
430
531
|
if not state.in_code:
|
|
@@ -439,7 +540,7 @@ def parse(stream):
|
|
|
439
540
|
else:
|
|
440
541
|
last_line_empty_cache = state.last_line_empty
|
|
441
542
|
state.last_line_empty = False
|
|
442
|
-
|
|
543
|
+
|
|
443
544
|
# This is to reset our top-level line-based systems
|
|
444
545
|
# \n buffer
|
|
445
546
|
if not state.in_list and len(state.ordered_list_numbers) > 0:
|
|
@@ -447,7 +548,7 @@ def parse(stream):
|
|
|
447
548
|
else:
|
|
448
549
|
state.in_list = False
|
|
449
550
|
|
|
450
|
-
if state.first_indent
|
|
551
|
+
if state.first_indent is None:
|
|
451
552
|
state.first_indent = len(line) - len(line.lstrip())
|
|
452
553
|
if len(line) - len(line.lstrip()) >= state.first_indent:
|
|
453
554
|
line = line[state.first_indent:]
|
|
@@ -462,20 +563,11 @@ def parse(stream):
|
|
|
462
563
|
if state.in_table and not state.in_code and not re.match(r"^\s*\|.+\|\s*$", line):
|
|
463
564
|
state.in_table = False
|
|
464
565
|
|
|
465
|
-
block_match = re.match(r"^<.?think>$", line)
|
|
466
|
-
if block_match:
|
|
467
|
-
state.in_blockquote = not state.in_blockquote
|
|
468
|
-
# consume and don't emit
|
|
469
|
-
if not state.in_blockquote:
|
|
470
|
-
yield( RESET)
|
|
471
|
-
continue
|
|
472
|
-
|
|
473
566
|
#
|
|
474
567
|
# <code><pre>
|
|
475
568
|
#
|
|
476
|
-
# This needs to be first
|
|
477
569
|
if not state.in_code:
|
|
478
|
-
code_match = re.match(r"\s*```\s*([^\s]+|$)", line)
|
|
570
|
+
code_match = re.match(r"\s*```\s*([^\s]+|$)$", line)
|
|
479
571
|
if code_match:
|
|
480
572
|
state.in_code = Code.Backtick
|
|
481
573
|
state.code_language = code_match.group(1) or 'Bash'
|
|
@@ -511,6 +603,7 @@ def parse(stream):
|
|
|
511
603
|
try:
|
|
512
604
|
ext = get_lexer_by_name(state.code_language).filenames[0].split('.')[-1]
|
|
513
605
|
except:
|
|
606
|
+
logging.warning(f"Can't find canonical extension for {state.code_language}")
|
|
514
607
|
pass
|
|
515
608
|
|
|
516
609
|
open(os.path.join(state.scrape, f"file_{state.scrape_ix}.{ext}"), 'w').write(state.code_buffer)
|
|
@@ -532,6 +625,7 @@ def parse(stream):
|
|
|
532
625
|
|
|
533
626
|
|
|
534
627
|
if code_type == Code.Backtick:
|
|
628
|
+
state.code_indent = len(line) - len(line.lstrip())
|
|
535
629
|
continue
|
|
536
630
|
else:
|
|
537
631
|
# otherwise we don't want to consume
|
|
@@ -548,11 +642,6 @@ def parse(stream):
|
|
|
548
642
|
custom_style = get_style_by_name("default")
|
|
549
643
|
|
|
550
644
|
formatter = Terminal256Formatter(style=custom_style)
|
|
551
|
-
for i, char in enumerate(line):
|
|
552
|
-
if char == " ":
|
|
553
|
-
state.code_indent += 1
|
|
554
|
-
else:
|
|
555
|
-
break
|
|
556
645
|
line = line[state.code_indent :]
|
|
557
646
|
|
|
558
647
|
elif line.startswith(" " * state.code_indent):
|
|
@@ -600,10 +689,10 @@ def parse(stream):
|
|
|
600
689
|
|
|
601
690
|
code_line = ' ' * indent + this_batch.strip()
|
|
602
691
|
|
|
603
|
-
margin = state.
|
|
692
|
+
margin = state.WidthFull - visible_length(code_line) % state.WidthFull
|
|
604
693
|
yield f"{Style.Codebg}{code_line}{' ' * max(0, margin)}{BGRESET}"
|
|
605
694
|
continue
|
|
606
|
-
except Goto
|
|
695
|
+
except Goto:
|
|
607
696
|
pass
|
|
608
697
|
|
|
609
698
|
except Exception as ex:
|
|
@@ -662,18 +751,18 @@ def parse(stream):
|
|
|
662
751
|
if list_type == "number":
|
|
663
752
|
state.ordered_list_numbers[-1] += 1
|
|
664
753
|
|
|
665
|
-
indent = len(state.list_item_stack) * 2
|
|
754
|
+
indent = (len(state.list_item_stack) - 1) * 2
|
|
666
755
|
|
|
667
756
|
wrap_width = state.Width - indent - (2 * Style.ListIndent)
|
|
668
757
|
|
|
669
758
|
bullet = '•'
|
|
670
759
|
if list_type == "number":
|
|
671
760
|
list_number = int(max(state.ordered_list_numbers[-1], float(list_item_match.group(2))))
|
|
672
|
-
bullet =
|
|
761
|
+
bullet = str(list_number)
|
|
673
762
|
|
|
674
763
|
wrapped_lineList = text_wrap(content, wrap_width, Style.ListIndent,
|
|
675
|
-
first_line_prefix = f"{(' ' * (indent
|
|
676
|
-
subsequent_line_prefix = " " * (indent
|
|
764
|
+
first_line_prefix = f"{(' ' * (indent ))}{FG}{Style.Symbol}{bullet}{RESET} ",
|
|
765
|
+
subsequent_line_prefix = " " * (indent)
|
|
677
766
|
)
|
|
678
767
|
for wrapped_line in wrapped_lineList:
|
|
679
768
|
yield f"{state.space_left()}{wrapped_line}\n"
|
|
@@ -690,11 +779,11 @@ def parse(stream):
|
|
|
690
779
|
#
|
|
691
780
|
# <hr>
|
|
692
781
|
#
|
|
693
|
-
hr_match = re.match(r"^[\s]*([
|
|
782
|
+
hr_match = re.match(r"^[\s]*([-\*=_]){3,}[\s]*$", line)
|
|
694
783
|
if hr_match:
|
|
695
784
|
if state.last_line_empty or last_line_empty_cache:
|
|
696
785
|
# print a horizontal rule using a unicode midline
|
|
697
|
-
yield f"{
|
|
786
|
+
yield f"{Style.MarginSpaces}{FG}{Style.Symbol}{'─' * state.Width}{RESET}"
|
|
698
787
|
else:
|
|
699
788
|
# We tell the next level up that the beginning of the buffer should be a flag.
|
|
700
789
|
# Underneath this condition it will no longer yield
|
|
@@ -712,16 +801,11 @@ def parse(stream):
|
|
|
712
801
|
for wrapped_line in wrapped_lines:
|
|
713
802
|
yield f"{state.space_left()}{wrapped_line}\n"
|
|
714
803
|
|
|
715
|
-
def get_terminal_width():
|
|
716
|
-
try:
|
|
717
|
-
return shutil.get_terminal_size().columns
|
|
718
|
-
except (AttributeError, OSError):
|
|
719
|
-
return 80
|
|
720
|
-
|
|
721
804
|
def emit(inp):
|
|
722
805
|
buffer = []
|
|
723
806
|
flush = False
|
|
724
807
|
for chunk in parse(inp):
|
|
808
|
+
width_calc()
|
|
725
809
|
if state.emit_flag:
|
|
726
810
|
if state.emit_flag == Code.Flush:
|
|
727
811
|
flush = True
|
|
@@ -742,6 +826,9 @@ def emit(inp):
|
|
|
742
826
|
state.current_line += chunk
|
|
743
827
|
|
|
744
828
|
buffer.append(chunk)
|
|
829
|
+
# This *might* be dangerous
|
|
830
|
+
state.reset_inline()
|
|
831
|
+
|
|
745
832
|
if flush:
|
|
746
833
|
chunk = "\n".join(buffer)
|
|
747
834
|
buffer = []
|
|
@@ -753,32 +840,47 @@ def emit(inp):
|
|
|
753
840
|
else:
|
|
754
841
|
chunk = buffer.pop(0)
|
|
755
842
|
|
|
756
|
-
|
|
757
|
-
print(chunk, end="", flush=True)
|
|
758
|
-
else:
|
|
759
|
-
sys.stdout.write(chunk)
|
|
843
|
+
print(chunk, end="", flush=True)
|
|
760
844
|
|
|
761
845
|
if len(buffer):
|
|
762
|
-
|
|
763
|
-
if state.is_pty:
|
|
764
|
-
print(chunk, end="", flush=True)
|
|
765
|
-
else:
|
|
766
|
-
sys.stdout.write(chunk)
|
|
846
|
+
print(buffer.pop(0), end="", flush=True)
|
|
767
847
|
|
|
768
848
|
def apply_multipliers(name, H, S, V):
|
|
769
849
|
m = _style.get(name)
|
|
770
850
|
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"]))
|
|
771
851
|
return ';'.join([str(int(x * 256)) for x in [r, g, b]]) + "m"
|
|
772
852
|
|
|
853
|
+
def width_calc():
|
|
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
|
+
|
|
868
|
+
state.Width = state.WidthFull - 2 * Style.Margin
|
|
869
|
+
Style.Codepad = [
|
|
870
|
+
f"{RESET}{FG}{Style.Dark}{'▄' * state.WidthFull}{RESET}\n",
|
|
871
|
+
f"{RESET}{FG}{Style.Dark}{'▀' * state.WidthFull}{RESET}"
|
|
872
|
+
]
|
|
873
|
+
|
|
773
874
|
def main():
|
|
774
|
-
global H, S, V
|
|
875
|
+
global H, S, V
|
|
876
|
+
|
|
775
877
|
parser = ArgumentParser(description="Streamdown - A markdown renderer for modern terminals")
|
|
776
878
|
parser.add_argument("filenameList", nargs="*", help="Input file to process (also takes stdin)")
|
|
777
879
|
parser.add_argument("-l", "--loglevel", default="INFO", help="Set the logging level")
|
|
778
880
|
parser.add_argument("-c", "--color", default=None, help="Set the hsv base: h,s,v")
|
|
779
|
-
parser.add_argument("-w", "--width", default="0", help="Set the width")
|
|
780
|
-
parser.add_argument("-e", "--exec", help="Wrap a program for more 'proper' i/o handling")
|
|
781
|
-
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")
|
|
782
884
|
args = parser.parse_args()
|
|
783
885
|
|
|
784
886
|
if args.color:
|
|
@@ -796,24 +898,26 @@ def main():
|
|
|
796
898
|
os.makedirs(args.scrape, exist_ok=True)
|
|
797
899
|
state.scrape = args.scrape
|
|
798
900
|
|
|
799
|
-
|
|
800
|
-
state.
|
|
801
|
-
|
|
901
|
+
Style.MarginSpaces = " " * Style.Margin
|
|
902
|
+
state.WidthArg = int(args.width) or _style.get("Width") or 0
|
|
903
|
+
width_calc()
|
|
904
|
+
|
|
802
905
|
Style.Codebg = f"{BG}{Style.Dark}"
|
|
803
906
|
Style.Link = f"{FG}{Style.Symbol}{UNDERLINE[0]}"
|
|
804
|
-
Style.Blockquote = f"{FG}{Style.Grey}
|
|
805
|
-
|
|
806
|
-
Style.Codepad = [
|
|
807
|
-
f"{RESET}{FG}{Style.Dark}{'▄' * state.FullWidth}{RESET}\n",
|
|
808
|
-
f"{RESET}{FG}{Style.Dark}{'▀' * state.FullWidth}{RESET}"
|
|
809
|
-
]
|
|
907
|
+
Style.Blockquote = f"{FG}{Style.Grey}│ "
|
|
810
908
|
|
|
811
909
|
logging.basicConfig(stream=sys.stdout, level=args.loglevel.upper(), format=f'%(message)s')
|
|
910
|
+
state.exec_master, state.exec_slave = pty.openpty()
|
|
812
911
|
try:
|
|
813
912
|
inp = sys.stdin
|
|
814
913
|
if args.exec:
|
|
815
|
-
state.
|
|
816
|
-
|
|
914
|
+
state.terminal = termios.tcgetattr(sys.stdin)
|
|
915
|
+
state.is_exec = True
|
|
916
|
+
state.exec_sub = subprocess.Popen(args.exec.split(' '), stdin=state.exec_slave, stdout=state.exec_slave, stderr=state.exec_slave, close_fds=True)
|
|
917
|
+
os.close(state.exec_slave) # We don't need slave in parent
|
|
918
|
+
# Set stdin to raw mode so we don't need to press enter
|
|
919
|
+
tty.setcbreak(sys.stdin.fileno())
|
|
920
|
+
emit(sys.stdin)
|
|
817
921
|
|
|
818
922
|
elif args.filenameList:
|
|
819
923
|
# Let's say we only care about logging in streams
|
|
@@ -832,11 +936,13 @@ def main():
|
|
|
832
936
|
os.set_blocking(inp.fileno(), False)
|
|
833
937
|
emit(inp)
|
|
834
938
|
|
|
835
|
-
except KeyboardInterrupt:
|
|
939
|
+
except (OSError, KeyboardInterrupt):
|
|
836
940
|
state.exit = 130
|
|
837
941
|
|
|
838
942
|
except Exception as ex:
|
|
839
|
-
|
|
943
|
+
if state.terminal:
|
|
944
|
+
termios.tcsetattr(sys.stdin, termios.TCSADRAIN, state.terminal)
|
|
945
|
+
logging.warning(f"Exception thrown: {type(ex)} {ex}")
|
|
840
946
|
traceback.print_exc()
|
|
841
947
|
|
|
842
948
|
if state.Clipboard and state.code_buffer:
|
|
@@ -847,6 +953,12 @@ def main():
|
|
|
847
953
|
base64_string = base64_bytes.decode('utf-8')
|
|
848
954
|
print(f"\033]52;c;{base64_string}\a", end="", flush=True)
|
|
849
955
|
|
|
956
|
+
|
|
957
|
+
if state.terminal:
|
|
958
|
+
termios.tcsetattr(sys.stdin, termios.TCSADRAIN, state.terminal)
|
|
959
|
+
os.close(state.exec_master)
|
|
960
|
+
if state.exec_sub:
|
|
961
|
+
state.exec_sub.wait()
|
|
850
962
|
sys.exit(state.exit)
|
|
851
963
|
|
|
852
964
|
if __name__ == "__main__":
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
So here is some text
|
|
2
|
+
|
|
3
|
+
> and technically we are in blockquote
|
|
4
|
+
> territory with these. Blockquote is one
|
|
5
|
+
> > of the few things that can be embedded
|
|
6
|
+
> > in markdown. Stylistically they are
|
|
7
|
+
> > different than lists, but
|
|
8
|
+
> not by much.
|
|
9
|
+
|
|
10
|
+
Now we are out of the blockquote
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|