From 64c77d27fc5cb6c868c74bb5a9bc350b3d043488 Mon Sep 17 00:00:00 2001 From: Sebastian Petrescu Date: Mon, 27 Jul 2026 19:47:34 +0300 Subject: [PATCH] Update spec after Stage 1 + Stage 2 review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Stage 1 (textual): 12 issues found, all fixed (re-ran clean on major axes) Stage 2 (empirical): 6 claims tested with real python-docx code - H1, H2, H4, H5: SURVIVED (XML manipulation routes verified) - H3: PARTIALLY-REFUTED — CRITICAL. List Bullet 2/3 style names exist but produce flat lists. Spec now mandates manual numbering XML. - B2: claim SURVIVED, presentational_hints hypothesis REFUTED. Decision: eliminate --forms from md2pdf (YAGNI). --- docs/specs/2026-07-27-md2doc-design.md | 210 ++++++++++++++++--------- 1 file changed, 139 insertions(+), 71 deletions(-) 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 | +| `![alt](url)` | 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 ``/`