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.
This commit is contained in:
@@ -0,0 +1,212 @@
|
|||||||
|
# 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:
|
||||||
|
|
||||||
|
```python
|
||||||
|
#!/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)
|
||||||
|
|
||||||
|
### 6.1 B1: expunere `--footer`
|
||||||
|
|
||||||
|
Adauga argument argparse:
|
||||||
|
|
||||||
|
```python
|
||||||
|
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)
|
||||||
Reference in New Issue
Block a user