termflow-md 0.1.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.
termflow/__init__.py ADDED
@@ -0,0 +1,58 @@
1
+ """termflow - A streaming markdown renderer for modern terminals.
2
+
3
+ A Python port of streamdown-rs, providing beautiful markdown rendering
4
+ directly in your terminal with streaming support, syntax highlighting,
5
+ and rich formatting.
6
+
7
+ Basic usage:
8
+ >>> from termflow import render_markdown
9
+ >>> render_markdown("# Hello World!")
10
+
11
+ Streaming usage:
12
+ >>> from termflow import Parser, Renderer
13
+ >>> parser = Parser()
14
+ >>> renderer = Renderer(width=80)
15
+ >>>
16
+ >>> for line in markdown_lines:
17
+ ... events = parser.parse_line(line)
18
+ ... renderer.render_all(events)
19
+ >>>
20
+ >>> renderer.render_all(parser.finalize())
21
+
22
+ With configuration:
23
+ >>> from termflow import Config, Renderer, RenderStyle
24
+ >>> config = Config.load()
25
+ >>> style = RenderStyle.dracula()
26
+ >>> renderer = Renderer(style=style, features=config.features)
27
+
28
+ CLI usage:
29
+ $ cat README.md | tf
30
+ $ tf document.md
31
+ $ tf --style dracula README.md
32
+ """
33
+
34
+ __version__ = "0.1.0"
35
+
36
+ from termflow.config import Config
37
+ from termflow.parser import Parser
38
+ from termflow.parser.events import ParseEvent
39
+ from termflow.render import Renderer, RenderFeatures, RenderStyle, render_markdown
40
+ from termflow.syntax import Highlighter, highlight_code
41
+
42
+ __all__ = [
43
+ # Core classes
44
+ "Config",
45
+ "Highlighter",
46
+ # Events
47
+ "ParseEvent",
48
+ "Parser",
49
+ # Style
50
+ "RenderFeatures",
51
+ "RenderStyle",
52
+ "Renderer",
53
+ # Version
54
+ "__version__",
55
+ # Convenience functions
56
+ "highlight_code",
57
+ "render_markdown",
58
+ ]
termflow/__main__.py ADDED
@@ -0,0 +1,6 @@
1
+ """Entry point for python -m termflow."""
2
+
3
+ from termflow.cli import main
4
+
5
+ if __name__ == "__main__":
6
+ raise SystemExit(main())
@@ -0,0 +1,103 @@
1
+ """ANSI module - terminal escape codes and styling.
2
+
3
+ This module provides comprehensive ANSI escape code support for terminal
4
+ rendering, including:
5
+
6
+ - Raw escape code constants (codes.py)
7
+ - Color conversion utilities (color.py)
8
+ - Style pairs for easy toggling (style.py)
9
+ - Text processing utilities (utils.py)
10
+
11
+ Example:
12
+ >>> from termflow.ansi import BOLD, fg_color, visible_length
13
+ >>> styled = f"{BOLD[0]}{fg_color('#FF5500')}Orange Bold{BOLD[1]}"
14
+ >>> print(visible_length(styled)) # Returns 11, not counting ANSI codes
15
+ """
16
+
17
+ from termflow.ansi.codes import (
18
+ BGRESET,
19
+ BOLD_OFF,
20
+ BOLD_ON,
21
+ DIM_OFF,
22
+ DIM_ON,
23
+ ITALIC_OFF,
24
+ ITALIC_ON,
25
+ RESET,
26
+ STRIKEOUT_OFF,
27
+ STRIKEOUT_ON,
28
+ SUPERSCRIPTS,
29
+ UNDERLINE_OFF,
30
+ UNDERLINE_ON,
31
+ digit_to_superscript,
32
+ number_to_superscript,
33
+ )
34
+ from termflow.ansi.color import (
35
+ bg_color,
36
+ fg_color,
37
+ hex2rgb,
38
+ hsv_to_rgb,
39
+ rgb2hex,
40
+ )
41
+ from termflow.ansi.style import (
42
+ BOLD,
43
+ DIM,
44
+ ITALIC,
45
+ LINK,
46
+ STRIKEOUT,
47
+ UNDERLINE,
48
+ make_link,
49
+ )
50
+ from termflow.ansi.utils import (
51
+ ANSI_CSI_RE,
52
+ ANSI_ESCAPE_RE,
53
+ ANSI_SGR_RE,
54
+ extract_ansi_codes,
55
+ is_ansi_code,
56
+ parse_sgr_params,
57
+ split_ansi,
58
+ truncate_ansi,
59
+ visible,
60
+ visible_length,
61
+ wrap_ansi,
62
+ )
63
+
64
+ __all__ = [
65
+ "ANSI_CSI_RE",
66
+ "ANSI_ESCAPE_RE",
67
+ "ANSI_SGR_RE",
68
+ "BGRESET",
69
+ "BOLD",
70
+ "BOLD_OFF",
71
+ "BOLD_ON",
72
+ "DIM",
73
+ "DIM_OFF",
74
+ "DIM_ON",
75
+ "ITALIC",
76
+ "ITALIC_OFF",
77
+ "ITALIC_ON",
78
+ "LINK",
79
+ "RESET",
80
+ "STRIKEOUT",
81
+ "STRIKEOUT_OFF",
82
+ "STRIKEOUT_ON",
83
+ "SUPERSCRIPTS",
84
+ "UNDERLINE",
85
+ "UNDERLINE_OFF",
86
+ "UNDERLINE_ON",
87
+ "bg_color",
88
+ "digit_to_superscript",
89
+ "extract_ansi_codes",
90
+ "fg_color",
91
+ "hex2rgb",
92
+ "hsv_to_rgb",
93
+ "is_ansi_code",
94
+ "make_link",
95
+ "number_to_superscript",
96
+ "parse_sgr_params",
97
+ "rgb2hex",
98
+ "split_ansi",
99
+ "truncate_ansi",
100
+ "visible",
101
+ "visible_length",
102
+ "wrap_ansi",
103
+ ]
termflow/ansi/codes.py ADDED
@@ -0,0 +1,92 @@
1
+ """ANSI escape code constants and superscript utilities.
2
+
3
+ This module provides all the raw ANSI escape sequences needed for terminal
4
+ text styling, along with utilities for converting digits to Unicode superscripts.
5
+ """
6
+
7
+ # =============================================================================
8
+ # Reset
9
+ # =============================================================================
10
+ RESET = "\x1b[0m"
11
+
12
+ # =============================================================================
13
+ # Text Styles - On/Off pairs
14
+ # =============================================================================
15
+ BOLD_ON = "\x1b[1m"
16
+ BOLD_OFF = "\x1b[22m" # Also turns off DIM
17
+
18
+ DIM_ON = "\x1b[2m"
19
+ DIM_OFF = "\x1b[22m" # Also turns off BOLD
20
+
21
+ ITALIC_ON = "\x1b[3m"
22
+ ITALIC_OFF = "\x1b[23m"
23
+
24
+ UNDERLINE_ON = "\x1b[4m"
25
+ UNDERLINE_OFF = "\x1b[24m"
26
+
27
+ STRIKEOUT_ON = "\x1b[9m"
28
+ STRIKEOUT_OFF = "\x1b[29m"
29
+
30
+ # =============================================================================
31
+ # Background
32
+ # =============================================================================
33
+ BGRESET = "\x1b[49m"
34
+
35
+ # =============================================================================
36
+ # Superscript Unicode Characters
37
+ # =============================================================================
38
+ SUPERSCRIPTS = "⁰¹²³⁴⁵⁶⁷⁸⁹"
39
+
40
+
41
+ def digit_to_superscript(digit: int) -> str:
42
+ """Convert a digit (0-9) to its superscript Unicode character.
43
+
44
+ Args:
45
+ digit: An integer from 0 to 9.
46
+
47
+ Returns:
48
+ The corresponding superscript Unicode character.
49
+
50
+ Raises:
51
+ ValueError: If digit is not in range 0-9.
52
+
53
+ Example:
54
+ >>> digit_to_superscript(2)
55
+ '²'
56
+ >>> digit_to_superscript(0)
57
+ '⁰'
58
+ """
59
+ if not 0 <= digit <= 9:
60
+ raise ValueError(f"Digit must be 0-9, got {digit}")
61
+ return SUPERSCRIPTS[digit]
62
+
63
+
64
+ def number_to_superscript(num: int) -> str:
65
+ """Convert an integer to its superscript string representation.
66
+
67
+ Handles negative numbers by prefixing with superscript minus (⁻).
68
+
69
+ Args:
70
+ num: Any integer.
71
+
72
+ Returns:
73
+ The superscript representation of the number.
74
+
75
+ Example:
76
+ >>> number_to_superscript(42)
77
+ '⁴²'
78
+ >>> number_to_superscript(-5)
79
+ '⁻⁵'
80
+ >>> number_to_superscript(0)
81
+ '⁰'
82
+ """
83
+ if num < 0:
84
+ return "⁻" + number_to_superscript(-num)
85
+ if num == 0:
86
+ return SUPERSCRIPTS[0]
87
+
88
+ result = []
89
+ while num > 0:
90
+ result.append(SUPERSCRIPTS[num % 10])
91
+ num //= 10
92
+ return "".join(reversed(result))
termflow/ansi/color.py ADDED
@@ -0,0 +1,160 @@
1
+ """HSV/RGB color utilities for ANSI terminal colors.
2
+
3
+ This module provides color conversion functions and ANSI escape sequence
4
+ generators for 24-bit (truecolor) terminal support.
5
+ """
6
+
7
+ import re
8
+
9
+ # Regex for hex color validation
10
+ _HEX_COLOR_RE = re.compile(r"^#?([0-9a-fA-F]{6})$")
11
+
12
+
13
+ def hex2rgb(hex_color: str) -> tuple[int, int, int] | None:
14
+ """Convert hex color (#RRGGBB or RRGGBB) to RGB tuple.
15
+
16
+ Args:
17
+ hex_color: Hex color string, with or without leading '#'.
18
+
19
+ Returns:
20
+ Tuple of (R, G, B) values (0-255 each), or None if invalid.
21
+
22
+ Example:
23
+ >>> hex2rgb("#FF5500")
24
+ (255, 85, 0)
25
+ >>> hex2rgb("00FF00")
26
+ (0, 255, 0)
27
+ >>> hex2rgb("invalid")
28
+ None
29
+ """
30
+ match = _HEX_COLOR_RE.match(hex_color)
31
+ if not match:
32
+ return None
33
+
34
+ hex_str = match.group(1)
35
+ return (
36
+ int(hex_str[0:2], 16),
37
+ int(hex_str[2:4], 16),
38
+ int(hex_str[4:6], 16),
39
+ )
40
+
41
+
42
+ def rgb2hex(r: int, g: int, b: int) -> str:
43
+ """Convert RGB values to hex string (#RRGGBB).
44
+
45
+ Args:
46
+ r: Red component (0-255).
47
+ g: Green component (0-255).
48
+ b: Blue component (0-255).
49
+
50
+ Returns:
51
+ Hex color string with leading '#'.
52
+
53
+ Example:
54
+ >>> rgb2hex(255, 85, 0)
55
+ '#FF5500'
56
+ >>> rgb2hex(0, 0, 0)
57
+ '#000000'
58
+ """
59
+ # Clamp values to valid range
60
+ r = max(0, min(255, r))
61
+ g = max(0, min(255, g))
62
+ b = max(0, min(255, b))
63
+ return f"#{r:02X}{g:02X}{b:02X}"
64
+
65
+
66
+ def hsv_to_rgb(h: float, s: float, v: float) -> tuple[int, int, int]:
67
+ """Convert HSV color to RGB.
68
+
69
+ Args:
70
+ h: Hue (0.0-1.0).
71
+ s: Saturation (0.0-1.0).
72
+ v: Value/brightness (0.0-1.0).
73
+
74
+ Returns:
75
+ Tuple of (R, G, B) values (0-255 each).
76
+
77
+ Example:
78
+ >>> hsv_to_rgb(0.0, 1.0, 1.0) # Red
79
+ (255, 0, 0)
80
+ >>> hsv_to_rgb(0.333, 1.0, 1.0) # Green-ish
81
+ (0, 255, 2)
82
+ >>> hsv_to_rgb(0.0, 0.0, 1.0) # White
83
+ (255, 255, 255)
84
+ """
85
+ if s == 0.0:
86
+ # Achromatic (grey)
87
+ val = int(v * 255)
88
+ return (val, val, val)
89
+
90
+ h = h % 1.0 # Wrap hue to 0-1 range
91
+ h *= 6.0
92
+ i = int(h)
93
+ f = h - i
94
+
95
+ p = v * (1.0 - s)
96
+ q = v * (1.0 - s * f)
97
+ t = v * (1.0 - s * (1.0 - f))
98
+
99
+ if i == 0:
100
+ r, g, b = v, t, p
101
+ elif i == 1:
102
+ r, g, b = q, v, p
103
+ elif i == 2:
104
+ r, g, b = p, v, t
105
+ elif i == 3:
106
+ r, g, b = p, q, v
107
+ elif i == 4:
108
+ r, g, b = t, p, v
109
+ else:
110
+ r, g, b = v, p, q
111
+
112
+ return (int(r * 255), int(g * 255), int(b * 255))
113
+
114
+
115
+ def fg_color(hex_color: str) -> str:
116
+ """Generate foreground ANSI escape sequence from hex color.
117
+
118
+ Uses 24-bit truecolor ANSI escape codes (\x1b[38;2;R;G;Bm).
119
+
120
+ Args:
121
+ hex_color: Hex color string (#RRGGBB or RRGGBB).
122
+
123
+ Returns:
124
+ ANSI escape sequence for foreground color, or empty string if invalid.
125
+
126
+ Example:
127
+ >>> fg_color("#FF5500")
128
+ '\\x1b[38;2;255;85;0m'
129
+ >>> fg_color("invalid")
130
+ ''
131
+ """
132
+ rgb = hex2rgb(hex_color)
133
+ if rgb is None:
134
+ return ""
135
+ r, g, b = rgb
136
+ return f"\x1b[38;2;{r};{g};{b}m"
137
+
138
+
139
+ def bg_color(hex_color: str) -> str:
140
+ """Generate background ANSI escape sequence from hex color.
141
+
142
+ Uses 24-bit truecolor ANSI escape codes (\x1b[48;2;R;G;Bm).
143
+
144
+ Args:
145
+ hex_color: Hex color string (#RRGGBB or RRGGBB).
146
+
147
+ Returns:
148
+ ANSI escape sequence for background color, or empty string if invalid.
149
+
150
+ Example:
151
+ >>> bg_color("#FF5500")
152
+ '\\x1b[48;2;255;85;0m'
153
+ >>> bg_color("invalid")
154
+ ''
155
+ """
156
+ rgb = hex2rgb(hex_color)
157
+ if rgb is None:
158
+ return ""
159
+ r, g, b = rgb
160
+ return f"\x1b[48;2;{r};{g};{b}m"
termflow/ansi/style.py ADDED
@@ -0,0 +1,51 @@
1
+ """Style pairs (on, off) tuples for easy toggling.
2
+
3
+ These tuples make it easy to wrap text with styles:
4
+ styled_text = f"{BOLD[0]}bold text{BOLD[1]}"
5
+
6
+ Each tuple is (on_code, off_code) for symmetric style application.
7
+ """
8
+
9
+ # =============================================================================
10
+ # Text Style Pairs: (on, off)
11
+ # =============================================================================
12
+
13
+ #: Bold text style - Note: BOLD_OFF (22m) also turns off DIM
14
+ BOLD: tuple[str, str] = ("\x1b[1m", "\x1b[22m")
15
+
16
+ #: Dim/faint text style - Note: DIM_OFF (22m) also turns off BOLD
17
+ DIM: tuple[str, str] = ("\x1b[2m", "\x1b[22m")
18
+
19
+ #: Italic text style
20
+ ITALIC: tuple[str, str] = ("\x1b[3m", "\x1b[23m")
21
+
22
+ #: Underline text style
23
+ UNDERLINE: tuple[str, str] = ("\x1b[4m", "\x1b[24m")
24
+
25
+ #: Strikethrough/strikeout text style
26
+ STRIKEOUT: tuple[str, str] = ("\x1b[9m", "\x1b[29m")
27
+
28
+ # =============================================================================
29
+ # OSC 8 Hyperlink
30
+ # =============================================================================
31
+
32
+ #: OSC 8 hyperlink - Usage: f"{LINK[0]}{url}\x1b\\{text}{LINK[1]}"
33
+ #: The URL goes after LINK[0], then ST (\x1b\\), then visible text, then LINK[1]
34
+ LINK: tuple[str, str] = ("\x1b]8;;", "\x1b]8;;\x1b\\")
35
+
36
+
37
+ def make_link(url: str, text: str) -> str:
38
+ """Create an OSC 8 hyperlink.
39
+
40
+ Args:
41
+ url: The URL to link to.
42
+ text: The visible text to display.
43
+
44
+ Returns:
45
+ ANSI-escaped hyperlink string.
46
+
47
+ Example:
48
+ >>> link = make_link("https://example.com", "Click here")
49
+ >>> # Renders as clickable "Click here" in supported terminals
50
+ """
51
+ return f"{LINK[0]}{url}\x1b\\{text}{LINK[1]}"