md2txt/md2txt.py

132 lines
4.4 KiB
Python
Raw Normal View History

2025-10-19 22:58:10 +03:00
#!/usr/bin/env python3
"""
Convert Markdown into 80-column DOS-compatible plain text.
"""
from __future__ import annotations
import argparse
import re
import sys
from pathlib import Path
2025-10-20 11:40:17 +03:00
from typing import Iterable, List, Optional, Tuple
2025-10-19 22:58:10 +03:00
2025-10-20 11:40:17 +03:00
from md_types import BlockStyle, FrontMatter
from markdown_parser import MarkdownParser
from text_renderer import TextRenderer
2025-10-19 22:58:10 +03:00
FRONTMATTER_PATTERN = re.compile(r"^---\s*$")
2025-10-19 23:22:15 +03:00
def _parse_int(value: Optional[str], default: int = 0) -> int:
if value is None:
return default
match = re.search(r"-?\d+", value)
if not match:
return default
try:
return int(match.group())
except ValueError:
return default
def _parse_bool(value: Optional[str], default: bool = False) -> bool:
if value is None:
return default
lowered = value.strip().lower()
if lowered in {"true", "yes", "1", "on"}:
return True
if lowered in {"false", "no", "0", "off"}:
return False
return default
2025-10-20 11:40:17 +03:00
def convert_markdown(
lines: Iterable[str],
*,
width: int,
frontmatter: FrontMatter,
) -> List[str]:
base_style = BlockStyle(
align="left",
margin_left=max(0, frontmatter.margin_left),
margin_right=max(0, frontmatter.margin_right),
)
parser = MarkdownParser(base_style)
renderer = TextRenderer(width=width, frontmatter=frontmatter)
for event in parser.parse(lines):
renderer.handle_event(event)
return renderer.finalize()
2025-10-19 22:58:10 +03:00
def parse_frontmatter(lines: List[str]) -> Tuple[FrontMatter, List[str]]:
if not lines or not FRONTMATTER_PATTERN.match(lines[0]):
return FrontMatter(), lines
2025-10-20 11:40:17 +03:00
frontmatter: dict[str, str] = {}
2025-10-19 22:58:10 +03:00
idx = 1
while idx < len(lines):
if FRONTMATTER_PATTERN.match(lines[idx]):
break
if ":" in lines[idx]:
key, value = lines[idx].split(":", 1)
frontmatter[key.strip()] = value.strip()
idx += 1
if idx >= len(lines):
return FrontMatter(), lines
remaining = lines[idx + 1 :] if idx + 1 < len(lines) else []
2025-10-19 23:22:15 +03:00
paragraph_spacing_value = frontmatter.get("paragraph_spacing")
if paragraph_spacing_value is None:
paragraph_spacing_value = frontmatter.get("lines_between_paragraphs")
if paragraph_spacing_value is None:
paragraph_spacing_value = frontmatter.get("paragraph_lines")
2025-10-19 22:58:10 +03:00
fm = FrontMatter(
2025-10-19 23:22:15 +03:00
h1_font=frontmatter.get("h1_font", "standard").strip() or "standard",
h2_font=frontmatter.get("h2_font", "standard").strip() or "standard",
h3_font=frontmatter.get("h3_font", "standard").strip() or "standard",
margin_left=_parse_int(frontmatter.get("margin_left"), 0),
margin_right=_parse_int(frontmatter.get("margin_right"), 0),
paragraph_spacing=max(0, _parse_int(paragraph_spacing_value, 0)),
hyphenate=_parse_bool(frontmatter.get("hyphenate"), False),
hyphen_lang=(frontmatter.get("hyphen_lang") or "en_US").strip() or "en_US",
2025-10-20 01:11:29 +03:00
figlet_fallback=_parse_bool(frontmatter.get("figlet_fallback"), False),
2025-10-20 01:18:33 +03:00
header_spacing=max(0, _parse_int(frontmatter.get("header_spacing"), 2)),
2025-10-19 22:58:10 +03:00
)
return fm, remaining
def read_lines(path: Path) -> List[str]:
with path.open("r", encoding="utf-8") as handle:
return handle.readlines()
def write_output(path: Optional[Path], lines: List[str]) -> None:
content = "\r\n".join(lines)
if path is None:
sys.stdout.write(content + "\r\n")
return
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(content + "\r\n", encoding="utf-8")
def build_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(description="Convert Markdown files to 80-column DOS-compatible text.")
parser.add_argument("input_path", type=Path, help="Path to the Markdown input file.")
parser.add_argument("-o", "--output", type=Path, help="Optional path to write the resulting text file.")
parser.add_argument("--width", type=int, default=80, help="Maximum column width (default: 80).")
return parser
def main(argv: Optional[List[str]] = None) -> int:
parser = build_parser()
args = parser.parse_args(argv)
lines = read_lines(args.input_path)
frontmatter, content = parse_frontmatter(lines)
2025-10-20 11:40:17 +03:00
converted_lines = convert_markdown(content, width=args.width, frontmatter=frontmatter)
2025-10-19 22:58:10 +03:00
write_output(args.output, converted_lines)
return 0
if __name__ == "__main__":
raise SystemExit(main())
2025-10-20 11:40:17 +03:00