From 5067b9ec65db318239651f33c7acd0ca64e3dc16 Mon Sep 17 00:00:00 2001 From: randogoth Date: Mon, 20 Oct 2025 17:26:38 +0300 Subject: [PATCH] packaged --- README.md | 43 ++- lorem.amb | Bin 14840 -> 0 bytes loremipsum.mu | 237 --------------- md2amb.py | 282 ------------------ output.amb | Bin 14840 -> 0 bytes pyproject.toml | 14 +- src/md2txt/__init__.py | 5 + src/md2txt/__main__.py | 7 + md2txt.py => src/md2txt/cli.py | 14 +- src/md2txt/conversion/__init__.py | 19 ++ .../md2txt/conversion/core.py | 2 +- md_types.py => src/md2txt/models.py | 0 src/md2txt/parsers/__init__.py | 5 + .../md2txt/parsers/markdown.py | 4 +- plugins.py => src/md2txt/plugins/__init__.py | 14 +- .../md2txt/plugins/registry.py | 0 src/md2txt/renderers/__init__.py | 7 + .../md2txt/renderers/ama.py | 6 +- .../md2txt/renderers/micron.py | 15 +- .../md2txt/renderers/text.py | 2 +- uv.lock | 4 +- 21 files changed, 114 insertions(+), 566 deletions(-) delete mode 100644 lorem.amb delete mode 100644 loremipsum.mu delete mode 100644 md2amb.py delete mode 100644 output.amb create mode 100644 src/md2txt/__init__.py create mode 100644 src/md2txt/__main__.py rename md2txt.py => src/md2txt/cli.py (92%) create mode 100644 src/md2txt/conversion/__init__.py rename conversion_core.py => src/md2txt/conversion/core.py (99%) rename md_types.py => src/md2txt/models.py (100%) create mode 100644 src/md2txt/parsers/__init__.py rename markdown_parser.py => src/md2txt/parsers/markdown.py (99%) rename plugins.py => src/md2txt/plugins/__init__.py (73%) rename plugin_registry.py => src/md2txt/plugins/registry.py (100%) create mode 100644 src/md2txt/renderers/__init__.py rename ama_renderer.py => src/md2txt/renderers/ama.py (99%) rename micron_renderer.py => src/md2txt/renderers/micron.py (95%) rename text_renderer.py => src/md2txt/renderers/text.py (99%) diff --git a/README.md b/README.md index 48e1c3f..483a03c 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,10 @@ -# md2amb utilities +# md2txt -This repository contains command line helpers for transforming Markdown into formats that work well on retro hardware or constrained text viewers. A small plugin API lets you mix-and-match parsers and renderers so additional formats can plug into the same preprocessing pipeline. +This repository contains the `md2txt` command line tool and supporting libraries for transforming Markdown into formats that work well on retro hardware or constrained text viewers. A small plugin API lets you mix-and-match parsers and renderers so additional formats can plug into the same preprocessing pipeline. -## Tools +## CLI -- `md2amb.py` – converts Markdown into Amber-screen formatted text (see script for details). -- `md2txt.py` – converts Markdown into 80-column, DOS-compatible plain text with extensive formatting support. It ships with the default `markdown` parser and `text` renderer plugins, registers optional `micron` and `ama` renderers for Micron/Ancient Machine Book output, and exposes the core pipeline so you can add your own parser or renderer modules: +- `md2txt` – converts Markdown into 80-column, DOS-compatible plain text with extensive formatting support. It ships with the default `markdown` parser and `text` renderer plugins, registers optional `micron` and `ama` renderers for Micron/Ancient Machine Book output, and exposes the core pipeline so you can add your own parser or renderer modules: - FIGlet-rendered headings (H1–H3) driven by optional YAML frontmatter (`h1_font`, `h2_font`, `h3_font`). - H4+ headings rendered in uppercase with dashed underlines. - Emphasis styles converted to spaced or delimited characters, e.g. `**bold**` → `B O L D`, `__strong__` → `_s_t_r_o_n_g_`, `~~strike~~` → `~s~t~r~i~k~e~`. @@ -13,7 +12,6 @@ This repository contains command line helpers for transforming Markdown into for - Code blocks numbered (`01 | line`), with both fenced and indented fences supported (see “Rendering Controls” for customisation). - Links transformed into `[label](n)` references, with footnote-style URL list at the end. - Optional alignment and margin controls via HTML `

` attributes or MultiMarkdown attribute blocks (e.g. `{:.center margin=20px}`) applied as leading spaces. - - Recursive file includes using `![[file.md]]` or `{.include file.md}` (frontmatter inside the included file is ignored). - ASCII art injection with `#[label :align](art.txt)` syntax, supporting multiple art pieces per line and optional `{: .right}` style annotations. - Per-document toggles for code block wrapping, numbering, blockquote decoration, and list indentation spacing. @@ -26,21 +24,18 @@ This repository contains command line helpers for transforming Markdown into for ## Usage ```bash -python md2txt.py input.md -o output.txt # convert to DOS-friendly text -python md2txt.py input.md # write result to stdout -python md2txt.py input.md --width 72 # override column width -python md2txt.py input.md --parser markdown --renderer micron # emit Micron-formatted output -python md2txt.py input.md --renderer ama # emit AMB/AMA markup -python md2txt.py input.md --renderer-option width=68 # pass KEY=VALUE to a renderer +md2txt input.md -o output.txt # convert to DOS-friendly text +md2txt input.md # write result to stdout +md2txt input.md --width 72 # override column width +md2txt input.md --parser markdown --renderer micron # emit Micron-formatted output +md2txt input.md --renderer ama # emit AMB/AMA markup +md2txt input.md --renderer-option width=68 # pass KEY=VALUE to a renderer + +# If the project is not installed yet: +python -m md2txt input.md ``` -- `md2amb.py` – package Markdown (and linked Markdown files) into a self-contained `.amb` archive composed of `.ama` articles that honour the 78-column/64 KiB AMA constraints. - -```bash -python md2amb.py --title "Your Manual" docs/index.md output/manual.amb -``` - -`--parser` and `--renderer` select a plugin by name (defaults are `markdown` and `text`). Repeatable `--parser-option KEY=VALUE` and `--renderer-option KEY=VALUE` pairs are forwarded to the plugin factories as keyword arguments in addition to the defaults supplied by the CLI. Both scripts accept `--help` for the full option list. +`--parser` and `--renderer` select a plugin by name (defaults are `markdown` and `text`). Repeatable `--parser-option KEY=VALUE` and `--renderer-option KEY=VALUE` pairs are forwarded to the plugin factories as keyword arguments in addition to the defaults supplied by the CLI. The CLI accepts `--help` for the full option list. ## FIGlet Fonts via Frontmatter @@ -126,15 +121,15 @@ Generated text uses CRLF line endings to maintain DOS compatibility. ## Plugin Architecture -The conversion pipeline lives in `conversion_core.py` and is exposed via `run_conversion`. It is designed around lightweight factories: +The conversion pipeline lives in `src/md2txt/conversion/core.py` and is exposed via `run_conversion`. It is designed around lightweight factories: - **Parser factories** receive `base_style: BlockStyle` plus any extra keyword arguments and must return an object with a `parse(lines: Iterable[str]) -> Iterator[BlockEvent | StyleUpdateEvent]` method. The bundled `MarkdownParser` implements this interface. - **Renderer factories** receive `frontmatter: FrontMatter` and arbitrary keyword arguments and must return an object that provides `handle_event(event)` and `finalize() -> Any`. The `TextRenderer` returns a list of DOS-friendly output lines; other renderers may return any data appropriate for their target format. -Plugins register themselves through the helpers in `plugins.py`: +Plugins register themselves through the helpers in `md2txt.plugins`: ```python -from plugins import register_parser, register_renderer +from md2txt.plugins import register_parser, register_renderer def my_parser_factory(*, base_style, **options): return MyParser(base_style, **options) @@ -149,6 +144,6 @@ def my_renderer_factory(*, frontmatter, **options): register_renderer("ansi", my_renderer_factory) ``` -Once registered (for example in a small module that imports `md2txt.py`), the new plugins are available via `--parser my-markdown` or `--renderer ansi`. The CLI lists registered plugin names in `--help`, and the helper functions `available_parsers()` / `available_renderers()` return the sorted names if you need to build higher-level tooling. +Once registered (for example in a small module that imports `md2txt.cli`), the new plugins are available via `--parser my-markdown` or `--renderer ansi`. The CLI lists registered plugin names in `--help`, and the helper functions `available_parsers()` / `available_renderers()` return the sorted names if you need to build higher-level tooling. -The shared preprocessing helpers—YAML frontmatter parsing, recursive include expansion, and ASCII art sentinels—also live in `conversion_core.py`, allowing alternate front-ends to reuse exactly the same behaviour without duplicating code. +The shared preprocessing helpers—YAML frontmatter parsing, recursive include expansion, and ASCII art sentinels—also live in `src/md2txt/conversion/core.py`, allowing alternate front-ends to reuse exactly the same behaviour without duplicating code. diff --git a/lorem.amb b/lorem.amb deleted file mode 100644 index 464377749cef8b1e1ff4673551bcb18267b4ccdb..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 14840 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 Salientia - - >> Structa hinc vetitus factum - - Lorem markdownum omnes tellus, in, Herculeis - `=reciprocalPower`= neque promissa est Latiae caput - umero talia fidissima, nymphae. Foedera et sagittas - tenetur adde matri esse: talia manibus. - - >> Fuit video longique dedisse - - Ille stupet, ultra `!incunabula agendum urbes`! mirer - `!aper`!, metu. Aegides novissima sunt turis quatiebant - umbrosaque `[putares`#mille-parva]. Metuunt eruiturque - maximus velit, ater Vesta vulnus rustica verba! Mei - vincite nulla, in Iovem: fama exurunt vernum! - - De instat ubi induit fugientia inertes columbas tarda - `*terga Parnasia sententia`* omnia inmotusque, est et - aetas sonus. Exhibuit coeperunt, canis. Suis minora, - tempore fateor neve moderamine erit. Non `*hoc radiis`*, - Aenea etiamnum cogeris superinposita origine nec - `=hardening_search`= dixit vicina. Heros subito - populifer adeunt quicquam cum `!ales`! Ixion angulus - placet reddere. - - >> Tractaque serpens arva quoque - - > Include - - <>=======() - (/\___ /|\\ ()==========<>_ - \_/ | \\ //|\ ______/ \) - \_| \\ // | \_/ - \|\/|\_ // /\/ - (oo)\ \_// / - //_/\_\/ / | - @@/ |=\ \ | - \_=\_ \ | - \==\ \|\_ snd - __(\===\( )\ - (((~) __(_/ | - (((~) \ / - ______/ / - '------' - <>=======() - (/\___ /|\\ ()==========<>_ - \_/ | \\ //|\ ______/ \) - \_| \\ // | \_/ - \|\/|\_ // /\/ - (oo)\ \_// / - //_/\_\/ / | - @@/ |=\ \ | - \_=\_ \ | - \==\ \|\_ snd - __(\===\( )\ - (((~) __(_/ | - (((~) \ / - ______/ / - '------' - <>=======() - (/\___ /|\\ ()==========<>_ - \_/ | \\ //|\ ______/ \) - \_| \\ // | \_/ - \|\/|\_ // /\/ - (oo)\ \_// / - //_/\_\/ / | - @@/ |=\ \ | - \_=\_ \ | - \==\ \|\_ snd - __(\===\( )\ - (((~) __(_/ | - (((~) \ / - ______/ / - '------' - - > h1 Heading 8-) - - >> h2 Heading - - >>> h3 Heading - - >>> h4 Heading - - >>> h5 Heading - - >>> h6 Heading - - >> Horizontal Rules - - - - - - - - - - - >> Emphasis - - `!This is bold text`! `!This is bold text`! `*This is - italic text`* `*This is italic text`* ~~Strikethrough~~ - - >> Blockquotes - - >>>>Blockquotes can also be nested... - - >>>>>>>>by using additional greater-than signs right - >>>>>>>>next to each other... - - >>>>>>>>>>>>or with spaces between arrows. - - >> Lists - - >>> Unordered - - + Create a list by starting a line with `=+`=, `=-`=, or - `=*`= - - Marker character change forces new list start: - * Ac tristique libero volutpat at - + Facilisis in pretium nisl aliquet - - Nulla volutpat aliquam velit - + Very easy - >>> Ordered - - 1. Lorem ipsum dolor sit amet - 2. Consectetur adipiscing elit - 3. Integer molestie lorem at massa - >> Code - - Inline `=code`= - - Indented code - - `= - // Some comments - line 1 of code - line 2 of code - line 3 of code - `= - - Block code "fences" - - `= - Sample text here... - `= - - Syntax highlighting - - `= - var foo = function (bar) { - return bar++; - }; - - console.log(foo(5)); - `= - - >> Links - - `[link text`http://dev.nodeca.com] - - `[link with title`http://nodeca.github.io/pica/demo/ - "title text!"] - - >> Images - - `[Minion`https://octodex.github.com/images/minion.png] ` - [Stormtroopocat`https://octodex.github.com/images/stormt - roopocat.jpg "The Stormtroopocat"] - - Inprudens dum memor alma, casses dedi Sinuessa iam quid - adicit `=gate_truncate_wave`=. `[Geratur de`#salientia- - iungit-contra-et], insolida temptat `=open`= carentia; - genitorem solet, potens. Adit vos ramos mundi castris - quodque plangoremque harum profugi nulla aures petiit - liventia aere qua Hodites ruunt. Achaidos dilexisse - `=redundancy`= praesagia tenuatus necis `!Capys - recordor`! tenebras actis signis, reliquit, placidissime - iram liquefactis longe nervi famem. Cura pars natique - velleris semel et at `=printerWildcardParallel`= inquit - positosque comites ultra. - - >> Quantaque augent - - Acuti ignotis ignis, ostendit nostras terras nondum, - `[petit utque utero`#structa-hinc-vetitus-factum] - sepulcrales, pluma reseratis Ismario. Tria silentia duae - `*nymphe`*: certe parva cernam et dixit. `[Verborum - sedisti`#mille-parva]. - - `= - if (firewall(4, 3) == 1 * device_media_parity) { - system_copy_class += systray + oasis_wi_clean + lpi; - modelLdap.search_secondary_socket *= bounce.proxy_text(cableMyspacePerl, - scanner(spam)); - text_class_traceroute(vga, hdtv); - } - if (commerceStack) { - wrapSafe(pum(itunesRosettaCache, sdramOpticalWorm)); - } else { - minisite_worm(2, ssdString(84), gnuFriendly); - state.qwerty(moduleJavascriptFloating + ugc, koffice, - mail_system.prebinding_service_teraflops(mediaIcmpBoot)); - } - memoryHexadecimalMultitasking += oop_android; - var passive_vdu = bar; - var ddrJava = leaf(unc_web); - `= - - >>> Mille parva - - `!Minetur et`! aurum! Charaxi sanguine quo; vires - subnectite `!pignora`!, omnique hostibus loquerentur - aquae Solis ferox? `!Cum ne`!, putat utar ex ad aede - mane. Vitalesque geminas exstimulat, iam vivunt dat - artes et quem! Iam violas molire ipse, spoliabitur dente - nervis regis, factos quae. - - >>>>Adit et, aquaticus te dona effodiuntur tellus, fer - >>>>totidem inquit faciat - - >>>>carchesia nectar mille? Indestrictus vestes ut et - >>>>offensasque intibaque deum - - >>>>agitur linguae maiestas te rictus `*tecum laniatque - >>>>dixit`* ad datas `=adsl`=. - - >>>>Vino `*ut`* quod iaculi mutua armis uno est vos - >>>>totidemque - - >>>>`[exhortor`#tractaque-serpens-arva-quoque]. Iamdudum - >>>>Romanae cepi linguisque - - >>>>velit accipe idem acutis hoc sparsumque dextra. - >>>>Minoide sponte collo sequitur - - >>>>et intus flere sanguineaque herba, nox Quas adsiduo - >>>>`*cunctantem`* tibi; enim. diff --git a/md2amb.py b/md2amb.py deleted file mode 100644 index 67589f0..0000000 --- a/md2amb.py +++ /dev/null @@ -1,282 +0,0 @@ -#!/usr/bin/env python3 -from __future__ import annotations - -import argparse -import re -import struct -from collections import deque -from dataclasses import dataclass -from pathlib import Path -from typing import Dict, Iterable, List, Tuple - -import ama_renderer # noqa: F401 - ensure AMA renderer plugin registration -from conversion_core import parse_frontmatter, run_conversion -from markdown_parser import MarkdownParser -from md_types import BlockStyle, FrontMatter -from plugins import get_parser_factory, get_renderer_factory, register_parser -from text_renderer import TextRenderer - -# Ensure markdown parser registered for standalone usage - - -def _markdown_parser_factory(*, base_style: BlockStyle, **_: object) -> MarkdownParser: - return MarkdownParser(base_style) - - -try: - register_parser("markdown", _markdown_parser_factory) -except ValueError: - pass - - -MARKDOWN_LINK_RE = re.compile(r"(\[[^\]]*\]\()([^)]+)(\))") -LOCAL_LINK_RE = re.compile(r"^[A-Za-z0-9_.~/\\-]+$") -EXT_MD = {".md", ".markdown", ".mkd", ".mkdn"} -AMA_MAX_BYTES = 65_535 -AMB_MAGIC = b"AMB1" -LINK_CONTINUE_LABEL = "Continue" - - -@dataclass -class Article: - source: Path - ama_name: str - - -def main(argv: Iterable[str] | None = None) -> int: - parser = argparse.ArgumentParser(description="Convert Markdown into an AMB archive.") - parser.add_argument("input", type=Path, help="Root Markdown file to convert.") - parser.add_argument("output", type=Path, help="Output AMB filename.") - parser.add_argument("--title", type=str, help="Optional book title.") - args = parser.parse_args(list(argv) if argv is not None else None) - - input_path = args.input.resolve() - if not input_path.exists(): - parser.error(f"Input file '{input_path}' does not exist.") - - amb_bytes = build_amb( - root_markdown=input_path, - title=args.title, - ) - args.output.parent.mkdir(parents=True, exist_ok=True) - args.output.write_bytes(amb_bytes) - print(str(args.output)) - return 0 - - -def build_amb(root_markdown: Path, title: str | None) -> bytes: - articles = collect_articles(root_markdown) - ama_contents = render_articles(articles) - files = assemble_files(ama_contents, title) - return pack_amb(files) - - -def collect_articles(root_markdown: Path) -> Dict[Path, Article]: - queue: deque[Path] = deque([root_markdown]) - visited: Dict[Path, Article] = {} - assigned_names: set[str] = set() - - while queue: - current = queue.popleft() - current = current.resolve() - if current in visited: - continue - if not current.exists(): - raise FileNotFoundError(f"Referenced file '{current}' was not found.") - if current == root_markdown: - ama_name = "INDEX.AMA" - else: - ama_name = assign_ama_name(current.stem, assigned_names) - assigned_names.add(ama_name) - visited[current] = Article(source=current, ama_name=ama_name) - - for linked in find_local_markdown_links(current): - queue.append(linked) - - return visited - - -def find_local_markdown_links(markdown_path: Path) -> List[Path]: - text = markdown_path.read_text(encoding="utf-8") - results: List[Path] = [] - - for _, target, _ in MARKDOWN_LINK_RE.findall(text): - cleaned = target.strip() - if not cleaned or cleaned.startswith("#"): - continue - if "://" in cleaned or cleaned.startswith(("mailto:", "ftp:", "gopher:", "tel:")): - continue - resolved = (markdown_path.parent / cleaned.split("#", 1)[0]).resolve() - if resolved.suffix.lower() in EXT_MD: - results.append(resolved) - return results - - -def assign_ama_name(stem: str, existing: set[str]) -> str: - base = "".join((c if c.isalnum() else "_") for c in stem.upper()) - if not base: - base = "ARTICLE" - if base[0].isdigit(): - base = f"_{base}" - base = base[:8] - - name = f"{base}.AMA" - counter = 1 - while name in existing: - suffix = f"{counter:02d}" - trimmed = base[: max(1, 8 - len(suffix))] - name = f"{trimmed}{suffix}.AMA" - counter += 1 - return name - - -def render_articles(articles: Dict[Path, Article]) -> Dict[str, List[str]]: - parser_factory = get_parser_factory("markdown") - renderer_factory = get_renderer_factory("ama") - rendered: Dict[str, List[str]] = {} - - for path, article in articles.items(): - content = path.read_text(encoding="utf-8") - rewritten = rewrite_links(content, path.parent, articles) - frontmatter, body_lines = parse_frontmatter(rewritten.splitlines(keepends=True)) - ama_lines = run_conversion( - body_lines, - frontmatter=frontmatter, - parser_factory=parser_factory, - renderer_factory=renderer_factory, - renderer_options={"width": 78}, - base_path=path.parent, - ) - split_articles = split_article(article.ama_name, ama_lines) - rendered.update(split_articles) - return rendered - - -def rewrite_links(markdown: str, base_dir: Path, articles: Dict[Path, Article]) -> str: - def replacer(match: re.Match[str]) -> str: - prefix, target, suffix = match.groups() - cleaned = target.strip() - candidate = (base_dir / cleaned.split("#", 1)[0]).resolve() - if candidate in articles: - mapped = articles[candidate].ama_name - return f"{prefix}{mapped}{suffix}" - return match.group(0) - - return MARKDOWN_LINK_RE.sub(replacer, markdown) - - -def split_article(filename: str, lines: List[str]) -> Dict[str, List[str]]: - encoded = "\n".join(lines).encode("utf-8") - if len(encoded) <= AMA_MAX_BYTES: - return {filename: lines} - - segments: List[List[str]] = [] - current: List[str] = [] - current_size = 0 - - def flush_segment() -> None: - nonlocal current, current_size - if current: - segments.append(current) - current = [] - current_size = 0 - - for line in lines: - candidate_size = current_size + len((line + "\n").encode("utf-8")) - if candidate_size > AMA_MAX_BYTES and current: - flush_segment() - current.append(line) - current_size += len((line + "\n").encode("utf-8")) - flush_segment() - - result: Dict[str, List[str]] = {} - stem = Path(filename).stem - generated_names = [filename] - - for idx in range(1, len(segments)): - suffix = f"{idx:02d}" - trimmed = stem[: max(1, 8 - len(suffix))] - new_name = f"{trimmed}{suffix}.AMA" - counter = 1 - while new_name in result or new_name in generated_names: - suffix = f"{idx:02d}{counter}" - trimmed = stem[: max(1, 8 - len(suffix))] - new_name = f"{trimmed}{suffix}.AMA" - counter += 1 - generated_names.append(new_name) - - for name, segment in zip(generated_names, segments, strict=False): - result[name] = segment[:] - - for idx, name in enumerate(generated_names[:-1]): - next_name = generated_names[idx + 1] - result[name].append("") - result[name].append(f"%l{next_name}:{LINK_CONTINUE_LABEL}%t") - return result - - -def assemble_files(ama_contents: Dict[str, List[str]], title: str | None) -> List[Tuple[str, bytes]]: - files: List[Tuple[str, bytes]] = [] - if title: - files.append(("TITLE", title.encode("ascii", "ignore")[:64])) - - index_bytes = encode_ama("INDEX.AMA", ama_contents.pop("INDEX.AMA")) - files.append(("INDEX.AMA", index_bytes)) - - for name, lines in sorted(ama_contents.items()): - files.append((name, encode_ama(name, lines))) - - return files - - -def encode_ama(name: str, lines: List[str]) -> bytes: - content = "\n".join(lines).rstrip("\n") + "\n" - data = content.encode("utf-8") - if len(data) > AMA_MAX_BYTES: - raise ValueError(f"Generated AMA article '{name}' exceeds {AMA_MAX_BYTES} bytes.") - if any("\t" in line for line in lines): - raise ValueError(f"Generated AMA article '{name}' contains tab characters.") - return data - - -def pack_amb(files: List[Tuple[str, bytes]]) -> bytes: - entries = [] - offset = 6 + 20 * len(files) - payloads = [] - - for filename, data in files: - canonical = filename.upper() - if len(canonical) > 12: - raise ValueError(f"Filename '{canonical}' does not fit 8.3 constraints.") - payloads.append(data) - checksum = bsd_checksum(data) - entries.append((canonical, offset, len(data), checksum)) - offset += len(data) - - output = bytearray() - output.extend(AMB_MAGIC) - output.extend(struct.pack(" int: - checksum = 0 - for byte in data: - checksum = (checksum >> 1) | ((checksum & 1) << 15) - checksum = (checksum + byte) & 0xFFFF - return checksum - - -if __name__ == "__main__": - raise SystemExit(main()) diff --git a/output.amb b/output.amb deleted file mode 100644 index 464377749cef8b1e1ff4673551bcb18267b4ccdb..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 14840 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.8.0", "pyphen>=0.17.2", ] + +[project.scripts] +md2txt = "md2txt.cli:main" + +[build-system] +requires = ["setuptools>=68", "wheel"] +build-backend = "setuptools.build_meta" + +[tool.setuptools.packages.find] +where = ["src"] diff --git a/src/md2txt/__init__.py b/src/md2txt/__init__.py new file mode 100644 index 0000000..a329d48 --- /dev/null +++ b/src/md2txt/__init__.py @@ -0,0 +1,5 @@ +"""Public package interface for md2txt.""" + +from .cli import convert_markdown, main + +__all__ = ["convert_markdown", "main"] diff --git a/src/md2txt/__main__.py b/src/md2txt/__main__.py new file mode 100644 index 0000000..1b6164e --- /dev/null +++ b/src/md2txt/__main__.py @@ -0,0 +1,7 @@ +"""Entry point for `python -m md2txt`.""" + +from .cli import main + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/md2txt.py b/src/md2txt/cli.py similarity index 92% rename from md2txt.py rename to src/md2txt/cli.py index b8a1a1d..c3e0bf5 100644 --- a/md2txt.py +++ b/src/md2txt/cli.py @@ -9,10 +9,10 @@ import sys from pathlib import Path from typing import Any, Dict, Iterable, List, Optional, Tuple -from conversion_core import parse_frontmatter, read_lines, run_conversion -from md_types import BlockStyle, FrontMatter -from markdown_parser import MarkdownParser -from plugins import ( +from .conversion.core import parse_frontmatter, read_lines, run_conversion +from .models import BlockStyle, FrontMatter +from .parsers.markdown import MarkdownParser +from .plugins import ( available_parsers, available_renderers, get_parser_factory, @@ -20,10 +20,10 @@ from plugins import ( register_parser, register_renderer, ) -from text_renderer import TextRenderer +from .renderers.text import TextRenderer -import micron_renderer # noqa: F401 # register micron renderer plugin -import ama_renderer # noqa: F401 # register AMA renderer plugin +from .renderers import micron # noqa: F401 # register micron renderer plugin +from .renderers import ama # noqa: F401 # register AMA renderer plugin def _split_option(token: str) -> Tuple[str, str]: diff --git a/src/md2txt/conversion/__init__.py b/src/md2txt/conversion/__init__.py new file mode 100644 index 0000000..696665e --- /dev/null +++ b/src/md2txt/conversion/__init__.py @@ -0,0 +1,19 @@ +"""Conversion pipeline helpers.""" + +from .core import ( + ParserFactory, + RendererFactory, + parse_frontmatter, + read_lines, + run_conversion, + run_pipeline, +) + +__all__ = [ + "ParserFactory", + "RendererFactory", + "parse_frontmatter", + "read_lines", + "run_conversion", + "run_pipeline", +] diff --git a/conversion_core.py b/src/md2txt/conversion/core.py similarity index 99% rename from conversion_core.py rename to src/md2txt/conversion/core.py index 7eea5e0..9d49517 100644 --- a/conversion_core.py +++ b/src/md2txt/conversion/core.py @@ -5,7 +5,7 @@ import re from pathlib import Path from typing import Any, Dict, Iterable, Iterator, List, Optional, Protocol, Set, Tuple, TypeVar -from md_types import BlockEvent, BlockStyle, FrontMatter, StyleUpdateEvent +from ..models import BlockEvent, BlockStyle, FrontMatter, StyleUpdateEvent FRONTMATTER_PATTERN = re.compile(r"^---\s*$") diff --git a/md_types.py b/src/md2txt/models.py similarity index 100% rename from md_types.py rename to src/md2txt/models.py diff --git a/src/md2txt/parsers/__init__.py b/src/md2txt/parsers/__init__.py new file mode 100644 index 0000000..11c9047 --- /dev/null +++ b/src/md2txt/parsers/__init__.py @@ -0,0 +1,5 @@ +"""Bundled parser implementations.""" + +from .markdown import MarkdownParser + +__all__ = ["MarkdownParser"] diff --git a/markdown_parser.py b/src/md2txt/parsers/markdown.py similarity index 99% rename from markdown_parser.py rename to src/md2txt/parsers/markdown.py index fe04876..23b4972 100644 --- a/markdown_parser.py +++ b/src/md2txt/parsers/markdown.py @@ -5,8 +5,8 @@ import re from pathlib import Path from typing import Dict, Iterable, Iterator, List, Optional, Union -from conversion_core import ASCII_SENTINEL_PREFIX -from md_types import ( +from ..conversion.core import ASCII_SENTINEL_PREFIX +from ..models import ( AsciiArtPayload, AsciiArtPiece, BlockEvent, diff --git a/plugins.py b/src/md2txt/plugins/__init__.py similarity index 73% rename from plugins.py rename to src/md2txt/plugins/__init__.py index 397918f..2d48eca 100644 --- a/plugins.py +++ b/src/md2txt/plugins/__init__.py @@ -2,8 +2,8 @@ from __future__ import annotations from typing import Any -from conversion_core import ParserFactory, RendererFactory -from plugin_registry import PluginRegistry +from ..conversion.core import ParserFactory, RendererFactory +from .registry import PluginRegistry parser_plugins = PluginRegistry[ParserFactory]() @@ -32,3 +32,13 @@ def available_parsers() -> list[str]: def available_renderers() -> list[str]: return renderer_plugins.names() + + +__all__ = [ + "available_parsers", + "available_renderers", + "get_parser_factory", + "get_renderer_factory", + "register_parser", + "register_renderer", +] diff --git a/plugin_registry.py b/src/md2txt/plugins/registry.py similarity index 100% rename from plugin_registry.py rename to src/md2txt/plugins/registry.py diff --git a/src/md2txt/renderers/__init__.py b/src/md2txt/renderers/__init__.py new file mode 100644 index 0000000..68c1c4f --- /dev/null +++ b/src/md2txt/renderers/__init__.py @@ -0,0 +1,7 @@ +"""Bundled renderer implementations.""" + +from .ama import AmaRenderer +from .micron import MicronRenderer +from .text import TextRenderer + +__all__ = ["AmaRenderer", "MicronRenderer", "TextRenderer"] diff --git a/ama_renderer.py b/src/md2txt/renderers/ama.py similarity index 99% rename from ama_renderer.py rename to src/md2txt/renderers/ama.py index e0e7172..185af07 100644 --- a/ama_renderer.py +++ b/src/md2txt/renderers/ama.py @@ -5,7 +5,7 @@ from functools import partial from pathlib import Path from typing import Any, Callable, List -from md_types import ( +from ..models import ( AsciiArtPayload, BlockQuotePayload, BlockStyle, @@ -16,8 +16,8 @@ from md_types import ( ParagraphPayload, StyleSpec, ) -from plugins import register_renderer -from text_renderer import ( +from ..plugins import register_renderer +from .text import ( BOLD_RE, CODE_STASH_RE, IMAGE_RE, diff --git a/micron_renderer.py b/src/md2txt/renderers/micron.py similarity index 95% rename from micron_renderer.py rename to src/md2txt/renderers/micron.py index a164e1e..38a1992 100644 --- a/micron_renderer.py +++ b/src/md2txt/renderers/micron.py @@ -3,9 +3,18 @@ from __future__ import annotations from functools import partial from typing import Any, List -from md_types import AsciiArtPayload, BlockQuotePayload, BlockStyle, CodeBlockPayload, FrontMatter, HeadingPayload, ListItemPayload, ParagraphPayload -from plugins import register_renderer -from text_renderer import ( +from ..models import ( + AsciiArtPayload, + BlockQuotePayload, + BlockStyle, + CodeBlockPayload, + FrontMatter, + HeadingPayload, + ListItemPayload, + ParagraphPayload, +) +from ..plugins import register_renderer +from .text import ( BOLD_RE, CODE_STASH_RE, IMAGE_RE, diff --git a/text_renderer.py b/src/md2txt/renderers/text.py similarity index 99% rename from text_renderer.py rename to src/md2txt/renderers/text.py index f6e1986..6a85ca4 100644 --- a/text_renderer.py +++ b/src/md2txt/renderers/text.py @@ -7,7 +7,7 @@ from dataclasses import dataclass from functools import partial from typing import Callable, Dict, List, Optional, Tuple -from md_types import ( +from ..models import ( AsciiArtPayload, AsciiArtPiece, BlockEvent, diff --git a/uv.lock b/uv.lock index 3b1bd70..344522f 100644 --- a/uv.lock +++ b/uv.lock @@ -3,9 +3,9 @@ revision = 3 requires-python = ">=3.13" [[package]] -name = "md2amb" +name = "md2txt" version = "0.1.0" -source = { virtual = "." } +source = { editable = "." } dependencies = [ { name = "pyfiglet" }, { name = "pyphen" },