mdsyntax 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.
- mdsyntax/__init__.py +26 -0
- mdsyntax/cli.py +82 -0
- mdsyntax/py.typed +0 -0
- mdsyntax/renderer.py +368 -0
- mdsyntax-0.1.0.dist-info/METADATA +176 -0
- mdsyntax-0.1.0.dist-info/RECORD +9 -0
- mdsyntax-0.1.0.dist-info/WHEEL +4 -0
- mdsyntax-0.1.0.dist-info/entry_points.txt +2 -0
- mdsyntax-0.1.0.dist-info/licenses/LICENSE +21 -0
mdsyntax/__init__.py
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""
|
|
2
|
+
mdsyntax: Render markdown with syntax highlighting in the terminal.
|
|
3
|
+
|
|
4
|
+
Usage:
|
|
5
|
+
>>> from mdsyntax import md_print, md_render
|
|
6
|
+
>>> md_print("# Hello **world**")
|
|
7
|
+
>>> output = md_render("Some `code` here")
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from mdsyntax.renderer import (
|
|
11
|
+
LANG_ALIASES,
|
|
12
|
+
MarkdownRenderer,
|
|
13
|
+
SyntaxHighlighter,
|
|
14
|
+
md_print,
|
|
15
|
+
md_render,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
__version__ = "0.1.0"
|
|
19
|
+
__all__ = [
|
|
20
|
+
"md_print",
|
|
21
|
+
"md_render",
|
|
22
|
+
"MarkdownRenderer",
|
|
23
|
+
"SyntaxHighlighter",
|
|
24
|
+
"LANG_ALIASES",
|
|
25
|
+
"__version__",
|
|
26
|
+
]
|
mdsyntax/cli.py
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"""Command-line interface for md-print."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import sys
|
|
7
|
+
|
|
8
|
+
from mdsyntax import __version__, md_print
|
|
9
|
+
from mdsyntax.renderer import SyntaxHighlighter
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def main(argv: list[str] | None = None) -> int:
|
|
13
|
+
"""Main CLI entry point."""
|
|
14
|
+
parser = argparse.ArgumentParser(
|
|
15
|
+
prog="mdsyntax",
|
|
16
|
+
description="Render markdown with syntax highlighting in the terminal.",
|
|
17
|
+
)
|
|
18
|
+
parser.add_argument(
|
|
19
|
+
"file",
|
|
20
|
+
nargs="?",
|
|
21
|
+
type=argparse.FileType("r"),
|
|
22
|
+
default=sys.stdin,
|
|
23
|
+
help="Markdown file to render (default: stdin)",
|
|
24
|
+
)
|
|
25
|
+
parser.add_argument(
|
|
26
|
+
"-s",
|
|
27
|
+
"--style",
|
|
28
|
+
default="monokai",
|
|
29
|
+
metavar="STYLE",
|
|
30
|
+
help="Pygments style for code blocks (default: monokai)",
|
|
31
|
+
)
|
|
32
|
+
parser.add_argument(
|
|
33
|
+
"-w",
|
|
34
|
+
"--width",
|
|
35
|
+
type=int,
|
|
36
|
+
default=None,
|
|
37
|
+
metavar="N",
|
|
38
|
+
help="Width for code blocks (default: terminal width)",
|
|
39
|
+
)
|
|
40
|
+
parser.add_argument(
|
|
41
|
+
"--no-true-color",
|
|
42
|
+
action="store_true",
|
|
43
|
+
help="Disable 24-bit true color (use 256 colors)",
|
|
44
|
+
)
|
|
45
|
+
parser.add_argument(
|
|
46
|
+
"--list-styles",
|
|
47
|
+
action="store_true",
|
|
48
|
+
help="List available syntax highlighting styles and exit",
|
|
49
|
+
)
|
|
50
|
+
parser.add_argument(
|
|
51
|
+
"-V",
|
|
52
|
+
"--version",
|
|
53
|
+
action="version",
|
|
54
|
+
version=f"%(prog)s {__version__}",
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
args = parser.parse_args(argv)
|
|
58
|
+
|
|
59
|
+
if args.list_styles:
|
|
60
|
+
print("Available styles:")
|
|
61
|
+
for style in sorted(SyntaxHighlighter.available_styles()):
|
|
62
|
+
print(f" {style}")
|
|
63
|
+
return 0
|
|
64
|
+
|
|
65
|
+
text = args.file.read()
|
|
66
|
+
if args.file is not sys.stdin:
|
|
67
|
+
args.file.close()
|
|
68
|
+
|
|
69
|
+
true_color = None if not args.no_true_color else False
|
|
70
|
+
|
|
71
|
+
md_print(
|
|
72
|
+
text,
|
|
73
|
+
code_style=args.style,
|
|
74
|
+
code_width=args.width,
|
|
75
|
+
true_color=true_color,
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
return 0
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
if __name__ == "__main__":
|
|
82
|
+
sys.exit(main())
|
mdsyntax/py.typed
ADDED
|
File without changes
|
mdsyntax/renderer.py
ADDED
|
@@ -0,0 +1,368 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Terminal markdown renderer with syntax highlighting.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import os
|
|
8
|
+
import re
|
|
9
|
+
import shutil
|
|
10
|
+
from collections.abc import Iterator
|
|
11
|
+
from dataclasses import dataclass, field
|
|
12
|
+
|
|
13
|
+
from colorama import Back, Fore, Style, init
|
|
14
|
+
from pygments import highlight
|
|
15
|
+
from pygments.formatters import Terminal256Formatter, TerminalTrueColorFormatter
|
|
16
|
+
from pygments.lexers import TextLexer, get_lexer_by_name, guess_lexer
|
|
17
|
+
from pygments.styles import get_all_styles, get_style_by_name
|
|
18
|
+
|
|
19
|
+
init(autoreset=True)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class Ansi:
|
|
23
|
+
"""ANSI escape codes not exposed by colorama."""
|
|
24
|
+
|
|
25
|
+
ITALIC = "\033[3m"
|
|
26
|
+
ITALIC_OFF = "\033[23m"
|
|
27
|
+
UNDERLINE = "\033[4m"
|
|
28
|
+
UNDERLINE_OFF = "\033[24m"
|
|
29
|
+
DIM = "\033[2m"
|
|
30
|
+
DIM_OFF = "\033[22m"
|
|
31
|
+
STRIKETHROUGH = "\033[9m"
|
|
32
|
+
STRIKETHROUGH_OFF = "\033[29m"
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
LANG_ALIASES: dict[str, str] = {
|
|
36
|
+
"py": "python",
|
|
37
|
+
"js": "javascript",
|
|
38
|
+
"ts": "typescript",
|
|
39
|
+
"sh": "bash",
|
|
40
|
+
"shell": "bash",
|
|
41
|
+
"yml": "yaml",
|
|
42
|
+
"md": "markdown",
|
|
43
|
+
"c++": "cpp",
|
|
44
|
+
"c#": "csharp",
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _detect_true_color() -> bool:
|
|
49
|
+
"""Check if terminal supports 24-bit color."""
|
|
50
|
+
colorterm = os.environ.get("COLORTERM", "")
|
|
51
|
+
return colorterm in ("truecolor", "24bit")
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _get_style_bg(style_name: str) -> str:
|
|
55
|
+
"""Extract background color from pygments style as ANSI escape."""
|
|
56
|
+
try:
|
|
57
|
+
style = get_style_by_name(style_name)
|
|
58
|
+
bg = style.background_color
|
|
59
|
+
if bg and bg.startswith("#") and len(bg) == 7:
|
|
60
|
+
r, g, b = int(bg[1:3], 16), int(bg[3:5], 16), int(bg[5:7], 16)
|
|
61
|
+
return f"\033[48;2;{r};{g};{b}m"
|
|
62
|
+
except Exception:
|
|
63
|
+
pass
|
|
64
|
+
return "\033[48;5;236m" # fallback gray
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _visible_len(s: str) -> int:
|
|
68
|
+
"""Length of string excluding ANSI escape sequences."""
|
|
69
|
+
return len(re.sub(r"\033\[[0-9;]*m", "", s))
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _pad_to_width(text: str, width: int) -> str:
|
|
73
|
+
"""Pad string to width, accounting for ANSI codes."""
|
|
74
|
+
padding = width - _visible_len(text)
|
|
75
|
+
return text + " " * max(0, padding)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class SyntaxHighlighter:
|
|
79
|
+
"""Syntax highlighter using pygments."""
|
|
80
|
+
|
|
81
|
+
def __init__(self, style: str = "monokai", true_color: bool | None = None):
|
|
82
|
+
"""
|
|
83
|
+
Args:
|
|
84
|
+
style: Pygments style name (monokai, dracula, gruvbox-dark, one-dark, etc.)
|
|
85
|
+
true_color: Use 24-bit color. None = auto-detect from COLORTERM env var.
|
|
86
|
+
"""
|
|
87
|
+
if true_color is None:
|
|
88
|
+
true_color = _detect_true_color()
|
|
89
|
+
|
|
90
|
+
formatter_cls = (
|
|
91
|
+
TerminalTrueColorFormatter if true_color else Terminal256Formatter
|
|
92
|
+
)
|
|
93
|
+
self.formatter = formatter_cls(style=style)
|
|
94
|
+
self.style = style
|
|
95
|
+
|
|
96
|
+
def highlight(self, code: str, language: str = "") -> str:
|
|
97
|
+
"""Highlight code and return ANSI-formatted string."""
|
|
98
|
+
lexer = self._get_lexer(code, language)
|
|
99
|
+
return highlight(code, lexer, self.formatter).rstrip("\n")
|
|
100
|
+
|
|
101
|
+
def _get_lexer(self, code: str, language: str):
|
|
102
|
+
language = LANG_ALIASES.get(language.lower(), language.lower())
|
|
103
|
+
|
|
104
|
+
if language:
|
|
105
|
+
try:
|
|
106
|
+
return get_lexer_by_name(language)
|
|
107
|
+
except Exception:
|
|
108
|
+
pass
|
|
109
|
+
|
|
110
|
+
try:
|
|
111
|
+
return guess_lexer(code)
|
|
112
|
+
except Exception:
|
|
113
|
+
return TextLexer()
|
|
114
|
+
|
|
115
|
+
@staticmethod
|
|
116
|
+
def available_styles() -> list[str]:
|
|
117
|
+
"""Return list of available pygments style names."""
|
|
118
|
+
return list(get_all_styles())
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
@dataclass
|
|
122
|
+
class MarkdownRenderer:
|
|
123
|
+
"""Renders markdown to ANSI-formatted terminal output."""
|
|
124
|
+
|
|
125
|
+
code_style: str = "monokai"
|
|
126
|
+
code_width: int | None = None # None = terminal width
|
|
127
|
+
true_color: bool | None = None # None = auto-detect
|
|
128
|
+
|
|
129
|
+
_highlighter: SyntaxHighlighter = field(init=False, repr=False)
|
|
130
|
+
_code_bg: str = field(init=False, repr=False)
|
|
131
|
+
|
|
132
|
+
def __post_init__(self):
|
|
133
|
+
self._highlighter = SyntaxHighlighter(
|
|
134
|
+
style=self.code_style, true_color=self.true_color
|
|
135
|
+
)
|
|
136
|
+
self._code_bg = _get_style_bg(self.code_style)
|
|
137
|
+
|
|
138
|
+
def render(self, text: str) -> str:
|
|
139
|
+
"""Render markdown text to ANSI-formatted string."""
|
|
140
|
+
# Normalize line endings
|
|
141
|
+
text = text.replace("\r\n", "\n").replace("\r", "\n")
|
|
142
|
+
return "\n".join(self._render_blocks(text))
|
|
143
|
+
|
|
144
|
+
def _render_blocks(self, text: str) -> Iterator[str]:
|
|
145
|
+
"""Process block-level elements."""
|
|
146
|
+
lines = text.split("\n")
|
|
147
|
+
i = 0
|
|
148
|
+
|
|
149
|
+
while i < len(lines):
|
|
150
|
+
line = lines[i]
|
|
151
|
+
stripped = line.strip()
|
|
152
|
+
|
|
153
|
+
# Code block
|
|
154
|
+
if stripped.startswith("```"):
|
|
155
|
+
lang = stripped[3:].strip()
|
|
156
|
+
code_lines = []
|
|
157
|
+
i += 1
|
|
158
|
+
|
|
159
|
+
while i < len(lines) and not lines[i].strip().startswith("```"):
|
|
160
|
+
code_lines.append(lines[i])
|
|
161
|
+
i += 1
|
|
162
|
+
|
|
163
|
+
yield from self._render_code_block(code_lines, lang)
|
|
164
|
+
i += 1 # skip closing ```
|
|
165
|
+
continue
|
|
166
|
+
|
|
167
|
+
yield self._render_line(line)
|
|
168
|
+
i += 1
|
|
169
|
+
|
|
170
|
+
def _get_code_width(self) -> int:
|
|
171
|
+
if self.code_width:
|
|
172
|
+
return self.code_width
|
|
173
|
+
return shutil.get_terminal_size().columns
|
|
174
|
+
|
|
175
|
+
def _render_code_block(self, code_lines: list[str], language: str) -> Iterator[str]:
|
|
176
|
+
"""Render a fenced code block with syntax highlighting."""
|
|
177
|
+
width = self._get_code_width()
|
|
178
|
+
bg = self._code_bg
|
|
179
|
+
reset = Style.RESET_ALL
|
|
180
|
+
|
|
181
|
+
# Header with language label
|
|
182
|
+
label = f"{language}" if language else ""
|
|
183
|
+
if label:
|
|
184
|
+
yield f"{bg}{Fore.LIGHTBLACK_EX}{_pad_to_width(label, width)}{reset}"
|
|
185
|
+
|
|
186
|
+
# Highlighted code
|
|
187
|
+
if code_lines:
|
|
188
|
+
code = "\n".join(code_lines)
|
|
189
|
+
highlighted = self._highlighter.highlight(code, language)
|
|
190
|
+
|
|
191
|
+
for hl_line in highlighted.split("\n"):
|
|
192
|
+
yield f"{bg}{_pad_to_width(hl_line, width)}{reset}"
|
|
193
|
+
|
|
194
|
+
def _render_line(self, line: str) -> str:
|
|
195
|
+
"""Render a single line of markdown."""
|
|
196
|
+
stripped = line.strip()
|
|
197
|
+
|
|
198
|
+
if not stripped:
|
|
199
|
+
return ""
|
|
200
|
+
|
|
201
|
+
# Horizontal rule
|
|
202
|
+
if re.match(r"^[-*_]{3,}$", stripped):
|
|
203
|
+
return f"{Ansi.DIM}{Fore.WHITE}{'─' * 50}{Style.RESET_ALL}"
|
|
204
|
+
|
|
205
|
+
# Headers
|
|
206
|
+
if m := re.match(r"^(#{1,6})\s+(.+)$", stripped):
|
|
207
|
+
return self._render_header(len(m.group(1)), m.group(2))
|
|
208
|
+
|
|
209
|
+
# Blockquotes
|
|
210
|
+
if stripped.startswith(">"):
|
|
211
|
+
content = stripped.lstrip(">").strip()
|
|
212
|
+
rendered = self._render_inline(content)
|
|
213
|
+
return f"{Fore.MAGENTA}│ {Ansi.ITALIC}{rendered}{Ansi.ITALIC_OFF}{Style.RESET_ALL}"
|
|
214
|
+
|
|
215
|
+
# Task lists
|
|
216
|
+
if m := re.match(r"^[-*]\s+\[([ xX])\]\s+(.+)$", stripped):
|
|
217
|
+
checked = m.group(1).lower() == "x"
|
|
218
|
+
marker = f"{Fore.GREEN}✓" if checked else f"{Fore.RED}○"
|
|
219
|
+
return f" {marker} {self._render_inline(m.group(2))}{Style.RESET_ALL}"
|
|
220
|
+
|
|
221
|
+
# Unordered lists
|
|
222
|
+
if m := re.match(r"^[-*+]\s+(.+)$", stripped):
|
|
223
|
+
indent = len(line) - len(line.lstrip())
|
|
224
|
+
return f"{' ' * indent}{Fore.GREEN}• {Style.RESET_ALL}{self._render_inline(m.group(1))}"
|
|
225
|
+
|
|
226
|
+
# Ordered lists
|
|
227
|
+
if m := re.match(r"^(\d+)\.\s+(.+)$", stripped):
|
|
228
|
+
indent = len(line) - len(line.lstrip())
|
|
229
|
+
return f"{' ' * indent}{Fore.GREEN}{m.group(1)}. {Style.RESET_ALL}{self._render_inline(m.group(2))}"
|
|
230
|
+
|
|
231
|
+
return self._render_inline(line)
|
|
232
|
+
|
|
233
|
+
def _render_header(self, level: int, text: str) -> str:
|
|
234
|
+
"""Render a header with level-appropriate styling."""
|
|
235
|
+
colors = [
|
|
236
|
+
Fore.CYAN,
|
|
237
|
+
Fore.BLUE,
|
|
238
|
+
Fore.MAGENTA,
|
|
239
|
+
Fore.GREEN,
|
|
240
|
+
Fore.YELLOW,
|
|
241
|
+
Fore.WHITE,
|
|
242
|
+
]
|
|
243
|
+
color = colors[min(level, 6) - 1]
|
|
244
|
+
|
|
245
|
+
# Visual prefix for h1-h3
|
|
246
|
+
prefix = "█" * (4 - level) + " " if level <= 3 else ""
|
|
247
|
+
|
|
248
|
+
rendered_text = self._render_inline(text)
|
|
249
|
+
return f"{color}{Style.BRIGHT}{prefix}{rendered_text}{Style.RESET_ALL}"
|
|
250
|
+
|
|
251
|
+
def _render_inline(self, text: str) -> str:
|
|
252
|
+
"""Render inline markdown elements."""
|
|
253
|
+
# Order matters: process from most specific to least specific
|
|
254
|
+
|
|
255
|
+
# Inline code first (protects contents from further processing)
|
|
256
|
+
code_spans: list[str] = []
|
|
257
|
+
|
|
258
|
+
def extract_code(m):
|
|
259
|
+
code_spans.append(
|
|
260
|
+
f"{Back.BLACK}{Fore.YELLOW} {m.group(1)} {Style.RESET_ALL}"
|
|
261
|
+
)
|
|
262
|
+
return f"\x00CODE{len(code_spans) - 1}\x00"
|
|
263
|
+
|
|
264
|
+
text = re.sub(r"`([^`]+)`", extract_code, text)
|
|
265
|
+
|
|
266
|
+
# Bold + italic (must come before bold and italic)
|
|
267
|
+
text = re.sub(
|
|
268
|
+
r"\*\*\*(.+?)\*\*\*",
|
|
269
|
+
lambda m: f"{Style.BRIGHT}{Ansi.ITALIC}{m.group(1)}{Ansi.ITALIC_OFF}{Style.NORMAL}",
|
|
270
|
+
text,
|
|
271
|
+
)
|
|
272
|
+
|
|
273
|
+
# Bold
|
|
274
|
+
text = re.sub(
|
|
275
|
+
r"\*\*(.+?)\*\*",
|
|
276
|
+
lambda m: f"{Style.BRIGHT}{m.group(1)}{Style.NORMAL}",
|
|
277
|
+
text,
|
|
278
|
+
)
|
|
279
|
+
text = re.sub(
|
|
280
|
+
r"__(.+?)__",
|
|
281
|
+
lambda m: f"{Style.BRIGHT}{m.group(1)}{Style.NORMAL}",
|
|
282
|
+
text,
|
|
283
|
+
)
|
|
284
|
+
|
|
285
|
+
# Italic with asterisks (works anywhere)
|
|
286
|
+
text = re.sub(
|
|
287
|
+
r"(?<!\*)\*(?!\*)(.+?)(?<!\*)\*(?!\*)",
|
|
288
|
+
lambda m: f"{Ansi.ITALIC}{m.group(1)}{Ansi.ITALIC_OFF}",
|
|
289
|
+
text,
|
|
290
|
+
)
|
|
291
|
+
|
|
292
|
+
# Italic with underscores (only at word boundaries)
|
|
293
|
+
text = re.sub(
|
|
294
|
+
r"(?<!\w)_(?!_)(.+?)(?<!_)_(?!\w)",
|
|
295
|
+
lambda m: f"{Ansi.ITALIC}{m.group(1)}{Ansi.ITALIC_OFF}",
|
|
296
|
+
text,
|
|
297
|
+
)
|
|
298
|
+
|
|
299
|
+
# Strikethrough
|
|
300
|
+
text = re.sub(
|
|
301
|
+
r"~~(.+?)~~",
|
|
302
|
+
lambda m: f"{Ansi.STRIKETHROUGH}{m.group(1)}{Ansi.STRIKETHROUGH_OFF}",
|
|
303
|
+
text,
|
|
304
|
+
)
|
|
305
|
+
|
|
306
|
+
# Links
|
|
307
|
+
text = re.sub(
|
|
308
|
+
r"\[([^\]]+)\]\(([^)]+)\)",
|
|
309
|
+
lambda m: f"{Ansi.UNDERLINE}{Fore.BLUE}{m.group(1)}{Ansi.UNDERLINE_OFF}{Style.RESET_ALL}{Ansi.DIM} ({m.group(2)}){Ansi.DIM_OFF}",
|
|
310
|
+
text,
|
|
311
|
+
)
|
|
312
|
+
|
|
313
|
+
# Restore code spans
|
|
314
|
+
for i, code in enumerate(code_spans):
|
|
315
|
+
text = text.replace(f"\x00CODE{i}\x00", code)
|
|
316
|
+
|
|
317
|
+
return text
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
def md_print(
|
|
321
|
+
text: str,
|
|
322
|
+
*,
|
|
323
|
+
code_style: str = "monokai",
|
|
324
|
+
code_width: int | None = None,
|
|
325
|
+
true_color: bool | None = None,
|
|
326
|
+
) -> None:
|
|
327
|
+
"""
|
|
328
|
+
Print markdown-formatted text to the terminal.
|
|
329
|
+
|
|
330
|
+
Args:
|
|
331
|
+
text: Markdown text to render.
|
|
332
|
+
code_style: Pygments style for code blocks.
|
|
333
|
+
code_width: Width for code blocks (None = terminal width).
|
|
334
|
+
true_color: Use 24-bit color (None = auto-detect).
|
|
335
|
+
"""
|
|
336
|
+
renderer = MarkdownRenderer(
|
|
337
|
+
code_style=code_style,
|
|
338
|
+
code_width=code_width,
|
|
339
|
+
true_color=true_color,
|
|
340
|
+
)
|
|
341
|
+
print(renderer.render(text))
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
def md_render(
|
|
345
|
+
text: str,
|
|
346
|
+
*,
|
|
347
|
+
code_style: str = "monokai",
|
|
348
|
+
code_width: int | None = None,
|
|
349
|
+
true_color: bool | None = None,
|
|
350
|
+
) -> str:
|
|
351
|
+
"""
|
|
352
|
+
Render markdown text to ANSI-formatted string.
|
|
353
|
+
|
|
354
|
+
Args:
|
|
355
|
+
text: Markdown text to render.
|
|
356
|
+
code_style: Pygments style for code blocks.
|
|
357
|
+
code_width: Width for code blocks (None = terminal width).
|
|
358
|
+
true_color: Use 24-bit color (None = auto-detect).
|
|
359
|
+
|
|
360
|
+
Returns:
|
|
361
|
+
ANSI-formatted string ready for terminal output.
|
|
362
|
+
"""
|
|
363
|
+
renderer = MarkdownRenderer(
|
|
364
|
+
code_style=code_style,
|
|
365
|
+
code_width=code_width,
|
|
366
|
+
true_color=true_color,
|
|
367
|
+
)
|
|
368
|
+
return renderer.render(text)
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: mdsyntax
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Render markdown with syntax highlighting in the terminal
|
|
5
|
+
Project-URL: Homepage, https://github.com/Azaias/mdsyntax
|
|
6
|
+
Project-URL: Repository, https://github.com/Azaias/mdsyntax
|
|
7
|
+
Project-URL: Issues, https://github.com/Azaias/mdsyntax/issues
|
|
8
|
+
Author-email: Izaiah Meyer <lolduderlly@gmail.com>
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: ansi,cli,console,markdown,syntax-highlighting,terminal
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Environment :: Console
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
16
|
+
Classifier: Operating System :: OS Independent
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
22
|
+
Classifier: Topic :: Terminals
|
|
23
|
+
Classifier: Topic :: Text Processing :: Markup :: Markdown
|
|
24
|
+
Classifier: Typing :: Typed
|
|
25
|
+
Requires-Python: >=3.10
|
|
26
|
+
Requires-Dist: colorama>=0.4.6
|
|
27
|
+
Requires-Dist: pygments>=2.17.0
|
|
28
|
+
Provides-Extra: dev
|
|
29
|
+
Requires-Dist: build; extra == 'dev'
|
|
30
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
31
|
+
Requires-Dist: ruff>=0.4; extra == 'dev'
|
|
32
|
+
Requires-Dist: twine; extra == 'dev'
|
|
33
|
+
Description-Content-Type: text/markdown
|
|
34
|
+
|
|
35
|
+
# mdsyntax
|
|
36
|
+
|
|
37
|
+
[](https://pypi.org/project/mdsyntax/)
|
|
38
|
+
[](https://pypi.org/project/mdsyntax/)
|
|
39
|
+
[](https://github.com/Azaias/mdsyntax/actions/workflows/ci.yml)
|
|
40
|
+
[](https://opensource.org/licenses/MIT)
|
|
41
|
+
|
|
42
|
+
Render markdown with syntax highlighting in the terminal.
|
|
43
|
+
|
|
44
|
+
## Installation
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
pip install mdsyntax
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Usage
|
|
51
|
+
|
|
52
|
+
### Python API
|
|
53
|
+
|
|
54
|
+
```python
|
|
55
|
+
from mdsyntax import md_print, md_render
|
|
56
|
+
|
|
57
|
+
# Print directly to terminal
|
|
58
|
+
md_print("""
|
|
59
|
+
# Hello World
|
|
60
|
+
|
|
61
|
+
This is **bold** and *italic* text.
|
|
62
|
+
|
|
63
|
+
```python
|
|
64
|
+
def greet(name):
|
|
65
|
+
return f"Hello, {name}!"
|
|
66
|
+
```
|
|
67
|
+
""")
|
|
68
|
+
|
|
69
|
+
# Get ANSI string for further processing
|
|
70
|
+
output = md_render("Some `inline code` here")
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
### Command Line
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
# Render a file
|
|
77
|
+
mdsyntax README.md
|
|
78
|
+
|
|
79
|
+
# Pipe from stdin
|
|
80
|
+
echo "# Hello" | mdsyntax
|
|
81
|
+
|
|
82
|
+
# Use a different syntax theme
|
|
83
|
+
mdsyntax --style dracula document.md
|
|
84
|
+
|
|
85
|
+
# List available themes
|
|
86
|
+
mdsyntax --list-styles
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
## Features
|
|
90
|
+
|
|
91
|
+
- Headers (h1-h6) with color coding
|
|
92
|
+
- **Bold**, *italic*, ***bold italic***
|
|
93
|
+
- ~~Strikethrough~~
|
|
94
|
+
- `Inline code`
|
|
95
|
+
- Fenced code blocks with syntax highlighting
|
|
96
|
+
- [Links](https://example.com)
|
|
97
|
+
- Unordered and ordered lists
|
|
98
|
+
- Task lists
|
|
99
|
+
- Blockquotes
|
|
100
|
+
- Horizontal rules
|
|
101
|
+
|
|
102
|
+
## Configuration
|
|
103
|
+
|
|
104
|
+
### Code Styles
|
|
105
|
+
|
|
106
|
+
Any [Pygments style](https://pygments.org/styles/) is supported. Popular options:
|
|
107
|
+
|
|
108
|
+
- `monokai` (default)
|
|
109
|
+
- `dracula`
|
|
110
|
+
- `one-dark`
|
|
111
|
+
- `gruvbox-dark`
|
|
112
|
+
- `nord`
|
|
113
|
+
- `github-dark`
|
|
114
|
+
|
|
115
|
+
### True Color
|
|
116
|
+
|
|
117
|
+
By default, md-print auto-detects 24-bit color support via the `COLORTERM` environment variable. You can override this:
|
|
118
|
+
|
|
119
|
+
```python
|
|
120
|
+
# Force 256-color mode
|
|
121
|
+
md_print(text, true_color=False)
|
|
122
|
+
|
|
123
|
+
# Force true color
|
|
124
|
+
md_print(text, true_color=True)
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
## API Reference
|
|
128
|
+
|
|
129
|
+
### `md_print(text, *, code_style="monokai", code_width=None, true_color=None)`
|
|
130
|
+
|
|
131
|
+
Print markdown to terminal.
|
|
132
|
+
|
|
133
|
+
- `text`: Markdown string to render
|
|
134
|
+
- `code_style`: Pygments style name for code blocks
|
|
135
|
+
- `code_width`: Fixed width for code blocks (default: terminal width)
|
|
136
|
+
- `true_color`: Use 24-bit color (default: auto-detect)
|
|
137
|
+
|
|
138
|
+
### `md_render(...) -> str`
|
|
139
|
+
|
|
140
|
+
Same arguments as `md_print`, but returns the ANSI-formatted string instead of printing.
|
|
141
|
+
|
|
142
|
+
### `MarkdownRenderer`
|
|
143
|
+
|
|
144
|
+
Dataclass for more control:
|
|
145
|
+
|
|
146
|
+
```python
|
|
147
|
+
from mdsyntax import MarkdownRenderer
|
|
148
|
+
|
|
149
|
+
renderer = MarkdownRenderer(
|
|
150
|
+
code_style="dracula",
|
|
151
|
+
code_width=80,
|
|
152
|
+
true_color=True,
|
|
153
|
+
)
|
|
154
|
+
output = renderer.render(markdown_text)
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
### `SyntaxHighlighter`
|
|
158
|
+
|
|
159
|
+
Standalone code highlighter:
|
|
160
|
+
|
|
161
|
+
```python
|
|
162
|
+
from mdsyntax import SyntaxHighlighter
|
|
163
|
+
|
|
164
|
+
hl = SyntaxHighlighter(style="monokai")
|
|
165
|
+
print(hl.highlight("print('hello')", "python"))
|
|
166
|
+
print(SyntaxHighlighter.available_styles())
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
## License
|
|
170
|
+
|
|
171
|
+
MIT
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
## Contributing
|
|
175
|
+
|
|
176
|
+
Contributions are welcome! See [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines.
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
mdsyntax/__init__.py,sha256=UyRtUQZPOR9Ju9jQXYM1o98yToDCv4FGes3Ug0TYXlU,503
|
|
2
|
+
mdsyntax/cli.py,sha256=tGXBjAeibKwphdHi5saSWuNV3mAKee-82koUC6uxeMo,1977
|
|
3
|
+
mdsyntax/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
4
|
+
mdsyntax/renderer.py,sha256=WInbdSCepcWw7l_ziuG13ncfGM17Xf4P0-haxe-lmwg,11434
|
|
5
|
+
mdsyntax-0.1.0.dist-info/METADATA,sha256=0ktiKUppK9eYCLZhLye8GML4REg09-8r5CIkwGcVATY,4306
|
|
6
|
+
mdsyntax-0.1.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
7
|
+
mdsyntax-0.1.0.dist-info/entry_points.txt,sha256=X3XBqbjP3t5OnEsK5RniLQZNpK3_Z1Q5nQnYQbeXxcE,47
|
|
8
|
+
mdsyntax-0.1.0.dist-info/licenses/LICENSE,sha256=KAch0UTtju_-x0eNps82_go4gq-FssWkDk-eJlNkKMU,1069
|
|
9
|
+
mdsyntax-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Izaiah Meyer
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|