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 sys
|
|
|
|
|
from pathlib import Path
|
2025-10-20 15:03:34 +03:00
|
|
|
from typing import Any, Dict, Iterable, List, Optional, Tuple
|
2025-10-19 22:58:10 +03:00
|
|
|
|
2025-10-20 15:03:34 +03:00
|
|
|
from conversion_core import parse_frontmatter, read_lines, run_conversion
|
2025-10-20 11:40:17 +03:00
|
|
|
from md_types import BlockStyle, FrontMatter
|
|
|
|
|
from markdown_parser import MarkdownParser
|
2025-10-20 15:03:34 +03:00
|
|
|
from plugins import (
|
|
|
|
|
available_parsers,
|
|
|
|
|
available_renderers,
|
|
|
|
|
get_parser_factory,
|
|
|
|
|
get_renderer_factory,
|
|
|
|
|
register_parser,
|
|
|
|
|
register_renderer,
|
|
|
|
|
)
|
2025-10-20 11:40:17 +03:00
|
|
|
from text_renderer import TextRenderer
|
2025-10-19 22:58:10 +03:00
|
|
|
|
|
|
|
|
|
2025-10-20 15:03:34 +03:00
|
|
|
def _split_option(token: str) -> Tuple[str, str]:
|
|
|
|
|
if "=" not in token:
|
|
|
|
|
raise argparse.ArgumentTypeError("Expected KEY=VALUE format.")
|
|
|
|
|
key, value = token.split("=", 1)
|
|
|
|
|
key = key.strip()
|
|
|
|
|
if not key:
|
|
|
|
|
raise argparse.ArgumentTypeError("Option key cannot be empty.")
|
|
|
|
|
return key, value
|
2025-10-19 22:58:10 +03:00
|
|
|
|
|
|
|
|
|
2025-10-20 15:03:34 +03:00
|
|
|
def _markdown_parser_factory(*, base_style: BlockStyle, **_: Any) -> MarkdownParser:
|
|
|
|
|
return MarkdownParser(base_style)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _text_renderer_factory(*, frontmatter: FrontMatter, width: int = 80, **_: Any) -> TextRenderer:
|
|
|
|
|
return TextRenderer(width=width, frontmatter=frontmatter)
|
2025-10-19 23:22:15 +03:00
|
|
|
|
|
|
|
|
|
2025-10-20 15:03:34 +03:00
|
|
|
try:
|
|
|
|
|
register_parser("markdown", _markdown_parser_factory)
|
|
|
|
|
except ValueError:
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
register_renderer("text", _text_renderer_factory)
|
|
|
|
|
except ValueError:
|
|
|
|
|
pass
|
2025-10-19 23:22:15 +03:00
|
|
|
|
|
|
|
|
|
2025-10-20 11:40:17 +03:00
|
|
|
def convert_markdown(
|
|
|
|
|
lines: Iterable[str],
|
|
|
|
|
*,
|
|
|
|
|
width: int,
|
|
|
|
|
frontmatter: FrontMatter,
|
2025-10-20 14:14:07 +03:00
|
|
|
base_path: Optional[Path] = None,
|
2025-10-20 15:03:34 +03:00
|
|
|
parser_name: str = "markdown",
|
|
|
|
|
renderer_name: str = "text",
|
|
|
|
|
parser_options: Optional[Dict[str, Any]] = None,
|
|
|
|
|
renderer_options: Optional[Dict[str, Any]] = None,
|
2025-10-20 11:40:17 +03:00
|
|
|
) -> List[str]:
|
2025-10-20 15:03:34 +03:00
|
|
|
parser_factory = get_parser_factory(parser_name)
|
|
|
|
|
renderer_factory = get_renderer_factory(renderer_name)
|
|
|
|
|
effective_renderer_options: Dict[str, Any] = {"width": width}
|
|
|
|
|
if renderer_options:
|
|
|
|
|
effective_renderer_options.update(renderer_options)
|
|
|
|
|
rendered = run_conversion(
|
|
|
|
|
lines,
|
|
|
|
|
frontmatter=frontmatter,
|
|
|
|
|
parser_factory=parser_factory,
|
|
|
|
|
parser_options=parser_options,
|
|
|
|
|
renderer_factory=renderer_factory,
|
|
|
|
|
renderer_options=effective_renderer_options,
|
|
|
|
|
base_path=base_path,
|
2025-10-19 22:58:10 +03:00
|
|
|
)
|
2025-10-20 15:03:34 +03:00
|
|
|
if not isinstance(rendered, list):
|
|
|
|
|
raise TypeError("Renderer plugin returned unsupported output for md2txt CLI.")
|
|
|
|
|
return rendered
|
2025-10-19 22:58:10 +03:00
|
|
|
|
|
|
|
|
|
|
|
|
|
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).")
|
2025-10-20 15:03:34 +03:00
|
|
|
parser.add_argument(
|
|
|
|
|
"--parser",
|
|
|
|
|
default="markdown",
|
|
|
|
|
choices=available_parsers() or ["markdown"],
|
|
|
|
|
help="Name of the parser plugin to use.",
|
|
|
|
|
)
|
|
|
|
|
parser.add_argument(
|
|
|
|
|
"--renderer",
|
|
|
|
|
default="text",
|
|
|
|
|
choices=available_renderers() or ["text"],
|
|
|
|
|
help="Name of the renderer plugin to use.",
|
|
|
|
|
)
|
|
|
|
|
parser.add_argument(
|
|
|
|
|
"--parser-option",
|
|
|
|
|
action="append",
|
|
|
|
|
default=[],
|
|
|
|
|
type=_split_option,
|
|
|
|
|
metavar="KEY=VALUE",
|
|
|
|
|
help="Additional parser option in KEY=VALUE form (may repeat).",
|
|
|
|
|
)
|
|
|
|
|
parser.add_argument(
|
|
|
|
|
"--renderer-option",
|
|
|
|
|
action="append",
|
|
|
|
|
default=[],
|
|
|
|
|
type=_split_option,
|
|
|
|
|
metavar="KEY=VALUE",
|
|
|
|
|
help="Additional renderer option in KEY=VALUE form (may repeat).",
|
|
|
|
|
)
|
2025-10-19 22:58:10 +03:00
|
|
|
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 15:03:34 +03:00
|
|
|
parser_options = dict(args.parser_option or [])
|
|
|
|
|
renderer_options = dict(args.renderer_option or [])
|
|
|
|
|
try:
|
|
|
|
|
converted_lines = convert_markdown(
|
|
|
|
|
content,
|
|
|
|
|
width=args.width,
|
|
|
|
|
frontmatter=frontmatter,
|
|
|
|
|
base_path=args.input_path.parent,
|
|
|
|
|
parser_name=args.parser,
|
|
|
|
|
renderer_name=args.renderer,
|
|
|
|
|
parser_options=parser_options or None,
|
|
|
|
|
renderer_options=renderer_options or None,
|
|
|
|
|
)
|
|
|
|
|
except (KeyError, TypeError) as exc:
|
|
|
|
|
sys.stderr.write(f"{exc}\n")
|
|
|
|
|
return 2
|
2025-10-19 22:58:10 +03:00
|
|
|
write_output(args.output, converted_lines)
|
|
|
|
|
return 0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if __name__ == "__main__":
|
|
|
|
|
raise SystemExit(main())
|