Files
md2pdf/docs/specs/2026-07-27-md2doc-design.md
T
sebastian 23deb7b3f5 Add md2doc design spec
Design for new md2doc.py (Markdown to DOCX converter) plus two bug-fixes
for md2pdf.py (--footer dead in CLI, --forms produces no AcroForm).

Spec includes 5 empirical hypotheses (H1-H5) about python-docx capabilities
that must be validated before implementation.
2026-07-27 19:34:57 +03:00

7.3 KiB

md2doc — design spec

Data: 2026-07-27 Autor: Sebastian Petrescu Stare: draft (pending empirical review)


1. Obiectiv

Construieste md2doc.py — un utilitar single-file Python care converteste Markdown la .docx (Microsoft Word OOXML), simetric cu md2pdf.py existent. In acelasi plan: bug-fixes pentru md2pdf.py identificate in timpul brainstorming-ului.

2. Context

md2pdf.py exista, functioneaza (verified: rularea pe pluxee-todo.md produce PDF byte-identic cu cel committed), dar are 2 bug-uri:

  • B1. convert_md_to_pdf() accepta parametru footer, dar argparse nu expune flag-ul --footer. Functionalitate moarta in CLI.
  • B2. --forms ruleaza fara eroare, dar nu produce campuri AcroForm. Verificare: PDF-ul rezultat nu contine /AcroForm si nici anotari /Widget. weasyprint nu genereaza campuri de formular din <input> HTML in mod automat.

Pentru md2doc: niciun work anterior in AW pe conversie md→docx. Research extern (web-search-prime) arata 3 abordari posibile: Pandoc (CLI/subprocess), pypandoc (wrapper), sau python-docx cu parsare manuala. Am ales a treia pentru a pastra controlul fin asupra stilurilor si consistenta cu md2pdf.py.

3. Arhitectura

3.1 Layout repository

tools/
├── md2pdf.py     (existing — bug fixes only, scope-limited)
├── md2doc.py     (new — sibling, mirrors md2pdf structure)
└── docs/specs/2026-07-27-md2doc-design.md

md2doc.py este single-file, simetric cu md2pdf.py. Nu se face refactor spre package comun — YAGNI pentru 2 fisiere.

3.2 Structura md2doc.py

Pattern duplicat din md2pdf.py:

#!/usr/bin/env python3
import argparse, sys
from pathlib import Path
import markdown
from bs4 import BeautifulSoup
from docx import Document
from docx.shared import Pt, RGBColor, Cm

STYLES = {
    "elegant": {...},  # default
    "report":  {...},
    "default": {...},
    "dark":    {...},
    "mono":    {...},
}

COMMON = {...}  # reguli comune, ca si COMMON_CSS in md2pdf

def convert_md_to_doc(md_file, output_file, style="elegant", footer=""):
    ...

def main():
    parser = argparse.ArgumentParser(...)
    ...

3.3 Pipeline conversie

md text
  ↓ _bulletize()             (reutilizat din md2pdf — scos ca util comun)
  ↓ markdown.markdown()       (extensii: tables, fenced_code, toc, attr_list, nl2br)
  ↓ BeautifulSoup HTML
  ↓ walk DOM, emit docx elements
.docx file

_bulletize() e duplicated logic between md2pdf and md2doc. Pentru scope-ul curent (2 fisiere), se copiaza. Daca pe viitor se adauga un al 3-lea converter, se refactor la un md_utils.py comun.

4. Mapare stiluri (md → docx)

Fiecare stil din STYLES defineste echivalentele Word ale CSS-ului md2pdf. Mapping-ul e manual, element cu element.

Element MD md2pdf (CSS) md2doc (Word)
h1 font-size + border-bottom Heading 1 style + paragraph bottom border
h2/h3/h4 font-size + color Heading 2/3/4 style
p line-height + color paragraph format
strong font-weight 700 run.bold = True
em italic run.italic = True
code (inline) monospace + bg run.font.name = mono + run highlight
pre block + border-left paragraph + indent + shading
blockquote border-left + bg paragraph left indent + italic + shading
table borders + header bg docx Table + cell shading + borders
ul/ol list markers List Bullet / List Number paragraph styles
a color + underline run.color + run.underline
hr border-top paragraph bottom border

4.1 Ipoteze de verificat (HARD CLAIMS — astea trebuie validate empiric inainte de implementare)

Urmatoarele afirmatii despre python-docx sunt ipoteze, nu fapte, pana cand reviewer-ul empiric le rupe sau le confirma cu comenzi reale:

  • H1. python-docx poate adauga bottom border pe un paragraph Heading 1 (necesita manipulare XML — Issue python-docx #105 sugereaza ca nu exista API direct).
  • H2. python-docx poate aplica shading pe un paragraph (pentru code/blockquote) — documentatia oficiala nu listeaza API;StackTrace-ul sugereaza XML manipulation cu OxmlElement('w:shd').
  • H3. python-docx poate crea liste nested (Level 2, Level 3) care se randeaza corect in Word SI Google Docs. Issue #122 si thread-urile Latenate/Reddit sugereaza probleme cunoscute aici.
  • H4. Cell shading (w:shd pe w:tcPr) functioneaza pentru background de header row intr-un tabel.
  • H5. style = document.styles['List Bullet'] exista intr-un Document nou creat de python-docx (sau trebuie adaugat manual).

Pana nu se valideaza H1-H5 cu comenzi reale (run python-docx, deschide output in Word/Pages, verifica vizual), mapping-ul din tabel este plan, nu fapt.

5. CLI interface

Identica cu md2pdf.py:

md2doc.py input.md                      # → input.docx
md2doc.py input.md -o output.docx
md2doc.py docs/                         # converteste toate .md din docs/
md2doc.py input.md --style mono
md2doc.py input.md -q                   # quiet

6. Bug-fixes md2pdf.py (scope-limited)

Adauga argument argparse:

parser.add_argument('--footer', help='Custom footer text (right-aligned)')

Si paseaza-l in apelul convert_md_to_pdf(md_file, pdf_file, args.style, forms=args.forms, footer=args.footer).

6.2 B2: --forms nu produce AcroForm

Fapt stabilit (verified): weasyprint 68.1 nu genereaza campuri de formular din <input type="text"> sau <textarea> din HTML-ul stringify-uit. Markul HTML e prezent (verified: markdown.markdown() produce <input ...> corect), dar weasyprint il randeaza ca text static.

Ipoteza (de verificat): weasyprint necesita ca HTML-ul sa fie incarcat ca string cu base_url setat, sau tag-urile <form> sa fie wrappate explicit, sau exista un flag documentat presentational_hints=True.

Alternativ: daca weasyprint 68.1 nu suporta forms deloc, atunci flag-ul --forms trebuie eliminat din md2pdf (YAGNI) sau documentat ca no-op cu eroare loud.

Inainte de implementare, reviewer-ul empiric verifica care varianta e adevarata.

7. Dependencies

md2doc.py:

  • python-docx (NOU — pip install python-docx)
  • markdown (deja instalat, v3.10.2)
  • beautifulsoup4 (trebuie verificat daca e instalat)
  • lxml (parser pentru BeautifulSoup, mai rapid decat html.parser)

md2pdf.py:

  • existente, niciun nou dep.

8. Error handling

Fail-loud, niciodata silent. Pe orice eroare de parsare/conversie:

  • print la stderr cu context (file, line if available)
  • sys.exit(1) cu mesaj
  • nu se scrie fisier de output partial

9. Testing

  • md2doc: PT urmeaza pattern-ul md2pdf — nu exista test suite inca. Pentru MVP: convertim pluxee-todo.md si un edge-case file, verificam ca output .docx se deschide in Word/Pages fara erori si ca structura (headings, lists, tables, code blocks) e corecta vizual.
  • md2pdf bug-fixes: dupa B1, rulam md2pdf.py input.md --footer "Test" si verificam ca footer-ul apare in PDF. Pentru B2, depinde de verdictul empiric.

10. Out of scope

  • Refactor md2pdf + md2doc la un package comun (YAGNI pentru 2 fisiere)
  • Stiluri noi peste cele 5 existente
  • Suport pentru images embedded in markdown (de tratat separat)
  • Suport pentru syntax highlighting in code blocks (Pygments integration)
  • .doc (format vechi binar) — doar .docx
  • Integrare cu reference.docx templates (pandoc-style)