diff --git a/docs/specs/2026-07-27-md2doc-design.md b/docs/specs/2026-07-27-md2doc-design.md
index 361ec50..d50d145 100644
--- a/docs/specs/2026-07-27-md2doc-design.md
+++ b/docs/specs/2026-07-27-md2doc-design.md
@@ -2,35 +2,35 @@
**Data:** 2026-07-27
**Autor:** Sebastian Petrescu
-**Stare:** draft (pending empirical review)
+**Stare:** draft (pending empirical review, post Stage 1 review fixes)
---
## 1. Obiectiv
-Construieste `md2doc.py` — un utilitar single-file Python care converteste
+Construiește `md2doc.py` — un utilitar single-file Python care convertește
Markdown la `.docx` (Microsoft Word OOXML), simetric cu `md2pdf.py` existent.
-In acelasi plan: bug-fixes pentru `md2pdf.py` identificate in timpul
+În același plan: bug-fixes pentru `md2pdf.py` identificate în timpul
brainstorming-ului.
## 2. Context
-`md2pdf.py` exista, functioneaza (verified: rularea pe `pluxee-todo.md`
+`md2pdf.py` există, funcționează (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 ``
- HTML in mod automat.
+- **B1.** `convert_md_to_pdf()` acceptă parametru `footer`, dar argparse nu
+ expune flag-ul `--footer`. Funcționalitate moartă în CLI.
+- **B2.** `--forms` rulează fără eroare, dar nu produce câmpuri AcroForm.
+ Verificare: PDF-ul rezultat nu conține `/AcroForm` și nici adnotări
+ `/Widget`. `weasyprint` nu generează câmpuri de formular din ``
+ HTML în 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.
+Pentru md2doc: niciun work anterior în AW pe conversie md→docx. Research
+extern (web-search-prime) arată 3 abordări posibile: Pandoc (CLI/subprocess),
+pypandoc (wrapper), sau python-docx cu parsare manuală. Am ales a treia
+pentru a păstra controlul fin asupra stilurilor și consistența cu md2pdf.py.
-## 3. Arhitectura
+## 3. Arhitectură
### 3.1 Layout repository
@@ -42,7 +42,7 @@ tools/
```
md2doc.py este single-file, simetric cu md2pdf.py. Nu se face refactor spre
-package comun — YAGNI pentru 2 fisiere.
+package comun — YAGNI pentru 2 fișiere.
### 3.2 Structura md2doc.py
@@ -65,9 +65,19 @@ STYLES = {
"mono": {...},
}
-COMMON = {...} # reguli comune, ca si COMMON_CSS in md2pdf
+# Reguli comune — echivalentul COMMON_CSS din md2pdf, tradus în Word.
+# Conține proprietăți care nu țin de un stil anume. Override-uibile per stil:
+# la inițializare, STYLES[style] face merge peste COMMON (deep merge per key),
+# deci un stil poate suprascrie orice valoare de aici.
+COMMON = {
+ "page_break_before_h1": False, # H1 pe pagină nouă (definabil per stil)
+ "keep_with_next": True, # headings nu se despart de următorul paragraf
+ "table_cell_valign": "top", # vertical-align pe celule
+ "orphans": 3, # minimum linii orphan
+ "widows": 3, # minimum linii widow
+}
-def convert_md_to_doc(md_file, output_file, style="elegant", footer=""):
+def convert_md_to_doc(md_file, output_file, style="elegant"):
...
def main():
@@ -79,20 +89,28 @@ def main():
```
md text
- ↓ _bulletize() (reutilizat din md2pdf — scos ca util comun)
- ↓ markdown.markdown() (extensii: tables, fenced_code, toc, attr_list, nl2br)
+ ↓ _bulletize() (copiat din md2pdf.py — YAGNI refactor comun)
+ ↓ BLANK_MARKER injection (copiată din md2pdf — preservă multi-blank-lines)
+ ↓ markdown.markdown() (extensii: tables, fenced_code, toc, attr_list,
+ ↓ md_in_html, nl2br)
+ ↓ replace BLANK_MARKER cu
(same logic ca md2pdf)
↓ 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.
+`_bulletize()` și logica BLANK_MARKER sunt duplicate între md2pdf și md2doc
+pentru scope-ul curent (2 fișiere). Dacă pe viitor se adaugă un al 3-lea
+converter, se refactor la un `md_utils.py` comun.
+
+**Extensii markdown** — md2doc folosește `md_in_html` (pentru a procesa
+corect markdown embedded în blocuri HTML raw, inclusiv celule de tabel cu
+formatting). `codehilite` este omis pentru că syntax highlighting cu Pygments
+e out-of-scope (§10).
## 4. Mapare stiluri (md → docx)
-Fiecare stil din `STYLES` defineste echivalentele Word ale CSS-ului md2pdf.
+Fiecare stil din `STYLES` definește echivalentele Word ale CSS-ului md2pdf.
Mapping-ul e manual, element cu element.
| Element MD | md2pdf (CSS) | md2doc (Word) |
@@ -102,111 +120,161 @@ Mapping-ul e manual, element cu element.
| 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 |
+| code (inline) | monospace + bg | run.font.name = mono + run shading (background color) |
| 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 |
+| ul/ol | list markers | **Vezi §4.2 — implementare numerotare XML manuală (H3 verified critical)** |
| a | color + underline | run.color + run.underline |
| hr | border-top | paragraph bottom border |
+| `` | figure/img | **skip cu warning la stderr** (out of scope per §10) |
-### 4.1 Ipoteze de verificat (HARD CLAIMS — astea trebuie validate empiric inainte de implementare)
+### 4.1 IPOTEZE — verdicturi empirice (Stage 2 reviewer, 2026-07-27)
-Urmatoarele afirmatii despre python-docx sunt **ipoteze**, nu fapte, pana
-cand reviewer-ul empiric le rupe sau le confirma cu comenzi reale:
+Review empiric cu python-docx 1.2.0 rulat contra Script-uri care generează
+.docx și inspectează XML-ul intern.
-- **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).
+| Ipoteză | Verdict | Mecanism validat |
+|---|---|---|
+| **H1.** paragraph bottom border pe Heading 1 | ✅ SURVIVED | API oficial refuzat (`'ParagraphFormat' object has no attribute 'borders'`); XML route cu `OxmlElement('w:pBdr')` + `w:bottom` atașat la `paragraph._p.get_or_add_pPr()` produce XML conform schemei. |
+| **H2.** paragraph shading (code, blockquote) | ✅ SURVIVED | API oficial refuzat; XML route cu `OxmlElement('w:shd')` sub `w:pPr` merge. |
+| **H3.** liste nested L2/L3 în Word + Google Docs | ❌ **PARTIALLY-REFUTED — CRITICAL** | Vezi §4.2. |
+| **H4.** cell shading pe header row tabel | ✅ SURVIVED | `OxmlElement('w:shd')` sub `w:tcPr` merge curat. |
+| **H5.** `styles['List Bullet']` există în Document() nou | ✅ SURVIVED | Există (împreună cu 164 alte stiluri). |
+| **B2.** weasyprint 68.1 nu generează AcroForm din ``/`