# 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 . 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: """Command‑line 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()