print_md/print_md.py

133 lines
5.5 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
import argparse
import re
import sys
from termcolor import colored
from pygments import highlight
from pygments.lexers import get_lexer_by_name, TextLexer
from pygments.formatters import Terminal256Formatter
def render(text):
lines = text.splitlines()
formatted_text = ""
in_code_block = False
# Track code block state
code_block_lang = None
code_block_lines: list[str] = []
for line in lines:
# Check for code blocks (opening/closing)
if line.lstrip().startswith("```"):
# Opening or closing
if not in_code_block: # opening
formatted_text += colored(line.strip(), "light_grey") + "\n"
# Extract optional language
match = re.match(r"^\s*```\s*(\w+)?", line)
if match:
code_block_lang = match.group(1)
else:
code_block_lang = None
in_code_block = True
code_block_lines = []
continue # Skip fence line
else: # closing
# Highlight accumulated code
code_source = "\n".join(code_block_lines)
if code_source:
lexer = get_lexer_by_name(code_block_lang, stripall=True) if code_block_lang else TextLexer()
highlighted = highlight(code_source, lexer, Terminal256Formatter())
formatted_text += highlighted
formatted_text += colored(line.strip(), "light_grey") + "\n"
in_code_block = False
code_block_lang = None
code_block_lines = []
continue
# Inside code block: accumulate lines
if in_code_block:
code_block_lines.append(line)
continue
# Check for headers
if line.startswith("# "):
# level = len(line) - len(line.lstrip("#"))
header_text = line.strip() #.lstrip("#").strip()
formatted_text += colored(header_text, "blue", attrs=["bold", "underline"]) + "\n"
continue
if line.startswith("## "):
# level = len(line) - len(line.lstrip("#"))
header_text = line.strip() #.lstrip("#").strip()
formatted_text += colored(header_text, "blue", attrs=["bold"]) + "\n"
continue
if line.startswith("### "):
# level = len(line) - len(line.lstrip("#"))
header_text = line.strip() #.lstrip("#").strip()
formatted_text += colored(header_text, "cyan", attrs=["bold"]) + "\n"
continue
# Check for blockquotes
if line.startswith(">"):
quote_text = line.strip() #.lstrip(">").strip()
formatted_text += colored(quote_text, "yellow") + "\n"
continue
# Check for tables (rows separated by "|")
if "|" in line:
table_row = "\t| ".join(line.split("|")).strip()
formatted_text += table_row + "\n"
continue
# Inline formatting for bold, italic, and code (keeping the symbols)
# Bold (**text** or __text__)
line = re.sub(r"[^\*_](\*\*|__)(.+?)(\*\*|__)[^\*_]", lambda m: colored(m.group(), attrs=["bold"]), line)
# Italic (*text* or _text_)
line = re.sub(r"[^\*_](\*|_)([^\*_].+?[^\*_])(\*|_)[^\*_]", lambda m: colored(m.group(), attrs=["underline"]), line)
# Inline code (`code`)
line = re.sub(r"[^\*_](`)(.+?)`[^\*_]", lambda m: colored(m.group() + "`", "green"), line)
# List items (bullets and numbers)
# Bulleted list
line = re.sub(r"^(\s*[-*])\s", lambda m: colored(m.group(1), "cyan") + " ", line)
# Numbered list
line = re.sub(r"^(\s*\d+\.)\s", lambda m: colored(m.group(1), "cyan") + " ", line)
# Add processed line to formatted text
formatted_text += line + "\n"
return formatted_text
def main() -> None:
"""Commandline entry point.
The CLI accepts one or more file paths, reads each file, renders it via
:func:`render`, and prints the result to stdout. Files that cannot be
opened will emit an error on ``stderr`` but the program continues
processing subsequent files.
"""
parser = argparse.ArgumentParser(
description="Render markdown files to the terminal with ANSI color." # pragma: no cover - trivial description
)
parser.add_argument("files", nargs="+", help="Markdown files to render")
args = parser.parse_args()
for path in args.files:
try:
with open(path, "r", encoding="utf-8") as f:
content = f.read()
# Render and print
sys.stdout.write(render(content))
except Exception as exc: # pragma: no cover - error handling
sys.stderr.write(f"Error reading {path}: {exc}\n")
if __name__ == "__main__": # pragma: no cover - used only when executed as script
main()