Update spec after Stage 1 + Stage 2 review
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).
This commit is contained in:
@@ -2,35 +2,35 @@
|
|||||||
|
|
||||||
**Data:** 2026-07-27
|
**Data:** 2026-07-27
|
||||||
**Autor:** Sebastian Petrescu
|
**Autor:** Sebastian Petrescu
|
||||||
**Stare:** draft (pending empirical review)
|
**Stare:** draft (pending empirical review, post Stage 1 review fixes)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 1. Obiectiv
|
## 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.
|
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.
|
brainstorming-ului.
|
||||||
|
|
||||||
## 2. Context
|
## 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:
|
produce PDF byte-identic cu cel committed), dar are 2 bug-uri:
|
||||||
|
|
||||||
- **B1.** `convert_md_to_pdf()` accepta parametru `footer`, dar argparse nu
|
- **B1.** `convert_md_to_pdf()` acceptă parametru `footer`, dar argparse nu
|
||||||
expune flag-ul `--footer`. Functionalitate moarta in CLI.
|
expune flag-ul `--footer`. Funcționalitate moartă în CLI.
|
||||||
- **B2.** `--forms` ruleaza fara eroare, dar nu produce campuri AcroForm.
|
- **B2.** `--forms` rulează fără eroare, dar nu produce câmpuri AcroForm.
|
||||||
Verificare: PDF-ul rezultat nu contine `/AcroForm` si nici anotari
|
Verificare: PDF-ul rezultat nu conține `/AcroForm` și nici adnotări
|
||||||
`/Widget`. `weasyprint` nu genereaza campuri de formular din `<input>`
|
`/Widget`. `weasyprint` nu generează câmpuri de formular din `<input>`
|
||||||
HTML in mod automat.
|
HTML în mod automat.
|
||||||
|
|
||||||
Pentru md2doc: niciun work anterior in AW pe conversie md→docx. Research
|
Pentru md2doc: niciun work anterior în AW pe conversie md→docx. Research
|
||||||
extern (web-search-prime) arata 3 abordari posibile: Pandoc (CLI/subprocess),
|
extern (web-search-prime) arată 3 abordări posibile: Pandoc (CLI/subprocess),
|
||||||
pypandoc (wrapper), sau python-docx cu parsare manuala. Am ales a treia
|
pypandoc (wrapper), sau python-docx cu parsare manuală. Am ales a treia
|
||||||
pentru a pastra controlul fin asupra stilurilor si consistenta cu md2pdf.py.
|
pentru a păstra controlul fin asupra stilurilor și consistența cu md2pdf.py.
|
||||||
|
|
||||||
## 3. Arhitectura
|
## 3. Arhitectură
|
||||||
|
|
||||||
### 3.1 Layout repository
|
### 3.1 Layout repository
|
||||||
|
|
||||||
@@ -42,7 +42,7 @@ tools/
|
|||||||
```
|
```
|
||||||
|
|
||||||
md2doc.py este single-file, simetric cu md2pdf.py. Nu se face refactor spre
|
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
|
### 3.2 Structura md2doc.py
|
||||||
|
|
||||||
@@ -65,9 +65,19 @@ STYLES = {
|
|||||||
"mono": {...},
|
"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():
|
def main():
|
||||||
@@ -79,20 +89,28 @@ def main():
|
|||||||
|
|
||||||
```
|
```
|
||||||
md text
|
md text
|
||||||
↓ _bulletize() (reutilizat din md2pdf — scos ca util comun)
|
↓ _bulletize() (copiat din md2pdf.py — YAGNI refactor comun)
|
||||||
↓ markdown.markdown() (extensii: tables, fenced_code, toc, attr_list, nl2br)
|
↓ 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 <div> (same logic ca md2pdf)
|
||||||
↓ BeautifulSoup HTML
|
↓ BeautifulSoup HTML
|
||||||
↓ walk DOM, emit docx elements
|
↓ walk DOM, emit docx elements
|
||||||
.docx file
|
.docx file
|
||||||
```
|
```
|
||||||
|
|
||||||
`_bulletize()` e duplicated logic between md2pdf and md2doc. Pentru scope-ul
|
`_bulletize()` și logica BLANK_MARKER sunt duplicate între md2pdf și md2doc
|
||||||
curent (2 fisiere), se copiaza. Daca pe viitor se adauga un al 3-lea converter,
|
pentru scope-ul curent (2 fișiere). Dacă pe viitor se adaugă un al 3-lea
|
||||||
se refactor la un `md_utils.py` comun.
|
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)
|
## 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.
|
Mapping-ul e manual, element cu element.
|
||||||
|
|
||||||
| Element MD | md2pdf (CSS) | md2doc (Word) |
|
| Element MD | md2pdf (CSS) | md2doc (Word) |
|
||||||
@@ -102,111 +120,161 @@ Mapping-ul e manual, element cu element.
|
|||||||
| p | line-height + color | paragraph format |
|
| p | line-height + color | paragraph format |
|
||||||
| strong | font-weight 700 | run.bold = True |
|
| strong | font-weight 700 | run.bold = True |
|
||||||
| em | italic | run.italic = 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 |
|
| pre | block + border-left | paragraph + indent + shading |
|
||||||
| blockquote | border-left + bg | paragraph left indent + italic + shading |
|
| blockquote | border-left + bg | paragraph left indent + italic + shading |
|
||||||
| table | borders + header bg | docx Table + cell shading + borders |
|
| 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 |
|
| a | color + underline | run.color + run.underline |
|
||||||
| hr | border-top | paragraph bottom border |
|
| 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
|
Review empiric cu python-docx 1.2.0 rulat contra Script-uri care generează
|
||||||
cand reviewer-ul empiric le rupe sau le confirma cu comenzi reale:
|
.docx și inspectează XML-ul intern.
|
||||||
|
|
||||||
- **H1.** python-docx poate adauga bottom border pe un paragraph Heading 1
|
| Ipoteză | Verdict | Mecanism validat |
|
||||||
(necesita manipulare XML — Issue python-docx #105 sugereaza ca nu exista
|
|---|---|---|
|
||||||
API direct).
|
| **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.** python-docx poate aplica shading pe un paragraph (pentru code/blockquote)
|
| **H2.** paragraph shading (code, blockquote) | ✅ SURVIVED | API oficial refuzat; XML route cu `OxmlElement('w:shd')` sub `w:pPr` merge. |
|
||||||
— documentatia oficiala nu listeaza API;StackTrace-ul sugereaza XML
|
| **H3.** liste nested L2/L3 în Word + Google Docs | ❌ **PARTIALLY-REFUTED — CRITICAL** | Vezi §4.2. |
|
||||||
manipulation cu `OxmlElement('w:shd')`.
|
| **H4.** cell shading pe header row tabel | ✅ SURVIVED | `OxmlElement('w:shd')` sub `w:tcPr` merge curat. |
|
||||||
- **H3.** python-docx poate crea liste nested (Level 2, Level 3) care se
|
| **H5.** `styles['List Bullet']` există în Document() nou | ✅ SURVIVED | Există (împreună cu 164 alte stiluri). |
|
||||||
randeaza corect in Word SI Google Docs. Issue #122 si thread-urile
|
| **B2.** weasyprint 68.1 nu generează AcroForm din `<input>`/`<textarea>` | ✅ SURVIVED (claim originală) / ❌ REFUTED (ipoteza `presentational_hints=True`) | `presentational_hints=True` nu ajută. weasyprint renunță complet la tag-urile `<input>`/`<textarea>` — nici măcar nu le randează ca text static. |
|
||||||
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
|
Limitare comună: fără validare vizuală în Word/Google Docs (reviewer-ul
|
||||||
in Word/Pages, verifica vizual), mapping-ul din tabel este plan, nu fapt.
|
rulează headless). Pentru H3 route B, autorul trebuie să deschidă manual
|
||||||
|
`scratchpad/h3_route_b.docx` în Word și să verifice vizual înainte de
|
||||||
|
implementare (vezi §4.2).
|
||||||
|
|
||||||
|
### 4.2 Implementare liste (H3 CRITICAL — design schimbat)
|
||||||
|
|
||||||
|
Review-ul empiric a infirmat mapping-ul original "List Bullet 2/3" pentru
|
||||||
|
liste nested. Situația reală:
|
||||||
|
|
||||||
|
- **Stilurile `'List Bullet 2'` / `'List Bullet 3'` EXISTĂ** în Document() nou,
|
||||||
|
DAR fiecare referențiază un `numId` diferit, care se rezolvă la un
|
||||||
|
`abstractNum` **cu un singur nivel (doar `ilvl=0`)**.
|
||||||
|
- Rezultat: Word randează 3 liste plat neimbricate, nu listă ierarhică.
|
||||||
|
- Din cele 9 abstractNums din template-ul default python-docx, **0 sunt
|
||||||
|
multi-level**.
|
||||||
|
|
||||||
|
**Implementare corectă (mandatorie):** md2doc.py trebuie să construiască
|
||||||
|
propriul numbering definition XML:
|
||||||
|
|
||||||
|
1. La inițializare document, injectează un `w:abstractNum` cu 3 sau mai
|
||||||
|
multe elemente `<w:lvl w:ilvl="0">`, `<w:lvl w:ilvl="1">`, `<w:lvl w:ilvl="2">`
|
||||||
|
(pentru bullet sau decimal sau mixt, în funcție de stil).
|
||||||
|
2. Injectează un `w:num` care referențiază acel `abstractNumId`.
|
||||||
|
3. Pentru fiecare paragraph de listă, atașează `w:numPr` cu `w:ilvl` explicit
|
||||||
|
(0 pentru top-level, 1 pentru nested, 2 pentru double-nested) și `w:numId`
|
||||||
|
care referențiază numbering-ul custom.
|
||||||
|
|
||||||
|
Pattern verificat în XML de reviewer — output-ul parsează corect prin
|
||||||
|
python-docx și produce OOXML conform schemei. **Verificare vizuală umană
|
||||||
|
încă necesară** (deschide `scratchpad/h3_route_b.docx` în Word) înainte de
|
||||||
|
implementare.
|
||||||
|
|
||||||
|
**Cost estimat:** ~50-80 linii cod pentru helper-ul de numbering, pe lângă
|
||||||
|
restul converter-ului. Intră în scope-ul implementării.
|
||||||
|
|
||||||
## 5. CLI interface
|
## 5. CLI interface
|
||||||
|
|
||||||
Identica cu md2pdf.py:
|
md2doc.py expune aceeași interfață de bază ca md2pdf.py:
|
||||||
|
|
||||||
```
|
```
|
||||||
md2doc.py input.md # → input.docx
|
md2doc.py input.md # → input.docx
|
||||||
md2doc.py input.md -o output.docx
|
md2doc.py input.md -o output.docx
|
||||||
md2doc.py docs/ # converteste toate .md din docs/
|
md2doc.py docs/ # convertește toate .md din docs/
|
||||||
md2doc.py input.md --style mono
|
md2doc.py input.md --style mono
|
||||||
md2doc.py input.md -q # quiet
|
md2doc.py input.md -q # quiet
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**Diferențe față de md2pdf.py (intenționate):**
|
||||||
|
- md2doc **nu** are `--footer` (footer-ele PDF sunt specifice paginării — în
|
||||||
|
docx se adaugă prin Word's header/footer API diferit, out of scope aici)
|
||||||
|
- md2doc **nu** are `--forms` (câmpuri de formular AcroForm sunt PDF-specific)
|
||||||
|
|
||||||
|
După B1 (md2pdf primește `--footer` functional), diferența CLI între cele 2
|
||||||
|
tool-uri devine explicită și intenționată — nu "identice", ci "simetrice pe
|
||||||
|
overlap-ul comun".
|
||||||
|
|
||||||
## 6. Bug-fixes md2pdf.py (scope-limited)
|
## 6. Bug-fixes md2pdf.py (scope-limited)
|
||||||
|
|
||||||
### 6.1 B1: expunere `--footer`
|
### 6.1 B1: expunere `--footer`
|
||||||
|
|
||||||
Adauga argument argparse:
|
Adaugă argument argparse:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
parser.add_argument('--footer', help='Custom footer text (right-aligned)')
|
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,
|
Și pasează-l în apelul `convert_md_to_pdf(md_file, pdf_file, args.style,
|
||||||
forms=args.forms, footer=args.footer)`.
|
forms=args.forms, footer=args.footer)`.
|
||||||
|
|
||||||
### 6.2 B2: `--forms` nu produce AcroForm
|
### 6.2 B2: `--forms` nu produce AcroForm — elimină flag-ul
|
||||||
|
|
||||||
Fapt stabilit (verified): weasyprint 68.1 nu genereaza campuri de formular
|
Fapt stabilit (verified 2x — prima dată în brainstorming, apoi re-verificat
|
||||||
din `<input type="text">` sau `<textarea>` din HTML-ul stringify-uit. Markul
|
în Stage 2 review): weasyprint 68.1 nu generează câmpuri de formular din
|
||||||
HTML e prezent (verified: `markdown.markdown()` produce `<input ...>` corect),
|
`<input type="text">` sau `<textarea>` HTML, indiferent de:
|
||||||
dar weasyprint il randeaza ca text static.
|
|
||||||
|
|
||||||
**Ipoteza (de verificat):** weasyprint necesita ca HTML-ul sa fie incarcat
|
- wrapping în `<form>` tag
|
||||||
ca string cu base_url setat, sau tag-urile `<form>` sa fie wrappate explicit,
|
- flag-ul `presentational_hints=True` (ipoteza anterioară — REFUTED empiric)
|
||||||
sau exista un flag documentat `presentational_hints=True`.
|
- dimensiuni explicite în CSS
|
||||||
|
|
||||||
**Alternativ:** daca weasyprint 68.1 nu suporta forms deloc, atunci flag-ul
|
weasyprint renunță complet la tag-urile `<input>`/`<textarea>` — nici măcar
|
||||||
`--forms` trebuie eliminat din md2pdf (YAGNI) sau documentat ca no-op cu
|
nu le randează ca text static în PDF.
|
||||||
eroare loud.
|
|
||||||
|
|
||||||
Inainte de implementare, reviewer-ul empiric verifica care varianta e adevarata.
|
**Decizie:** flag-ul `--forms` se **elimină** din md2pdf.py (YAGNI —
|
||||||
|
funcționalitatea nu a funcționat niciodată și nu există cale simplă s-o
|
||||||
|
reparăm fără schimbare library). Removing includes:
|
||||||
|
|
||||||
|
- Șterge argumentul `--forms` din argparse
|
||||||
|
- Șterge parametrul `forms` din semnătura `convert_md_to_pdf()`
|
||||||
|
- Șterge parametrul `options={'pdf_forms': forms}` din apelul `write_pdf()`
|
||||||
|
|
||||||
|
Documentația din argparse epilog se actualizează corespunzător.
|
||||||
|
|
||||||
## 7. Dependencies
|
## 7. Dependencies
|
||||||
|
|
||||||
**md2doc.py:**
|
**md2doc.py:**
|
||||||
- `python-docx` (NOU — `pip install python-docx`)
|
- `python-docx` (NOU — `pip install python-docx`)
|
||||||
- `markdown` (deja instalat, v3.10.2)
|
- `markdown` (deja instalat, v3.10.2)
|
||||||
- `beautifulsoup4` (trebuie verificat daca e instalat)
|
- `beautifulsoup4` (trebuie verificat dacă e instalat)
|
||||||
- `lxml` (parser pentru BeautifulSoup, mai rapid decat html.parser)
|
- `lxml` (parser pentru BeautifulSoup, mai rapid decât html.parser)
|
||||||
|
|
||||||
**md2pdf.py:**
|
**md2pdf.py:**
|
||||||
- existente, niciun nou dep.
|
- existente, niciun nou dep.
|
||||||
|
|
||||||
## 8. Error handling
|
## 8. Error handling
|
||||||
|
|
||||||
Fail-loud, niciodata silent. Pe orice eroare de parsare/conversie:
|
Fail-loud, niciodată silent. Pe orice eroare de parsare/conversie:
|
||||||
- print la stderr cu context (file, line if available)
|
- print la stderr cu context (file, line if available)
|
||||||
- sys.exit(1) cu mesaj
|
- sys.exit(1) cu mesaj
|
||||||
- nu se scrie fisier de output partial
|
- nu se scrie fișier de output partial
|
||||||
|
|
||||||
|
**Comportament specific elementelor out-of-scope (ex. images):** md2doc
|
||||||
|
ridică warning la stderr (continuă execuția), nu eșuează. Eșecul e rezervat
|
||||||
|
pentru erorile care împiedică output-ul (parsare, scriere disk). Diferența:
|
||||||
|
skip+warn = "am procesat dar am omis ceva"; fail = "nu pot produce output".
|
||||||
|
|
||||||
## 9. Testing
|
## 9. Testing
|
||||||
|
|
||||||
- **md2doc:** PT urmeaza pattern-ul md2pdf — nu exista test suite inca.
|
- **md2doc:** Pentru testare urmează pattern-ul md2pdf — nu există test suite încă.
|
||||||
Pentru MVP: convertim `pluxee-todo.md` si un edge-case file, verificam
|
Pentru MVP: convertim `pluxee-todo.md` și un edge-case file, verificăm
|
||||||
ca output .docx se deschide in Word/Pages fara erori si ca structura
|
că output .docx se deschide în Word/Pages fără erori și că structura
|
||||||
(headings, lists, tables, code blocks) e corecta vizual.
|
(headings, lists, tables, code blocks) e corectă vizual.
|
||||||
- **md2pdf bug-fixes:** dupa B1, rulam `md2pdf.py input.md --footer "Test"`
|
- **md2pdf bug-fixes:** după B1, rulăm `md2pdf.py input.md --footer "Test"`
|
||||||
si verificam ca footer-ul apare in PDF. Pentru B2, depinde de verdictul
|
și verificăm că footer-ul apare în PDF. Pentru B2, depinde de verdictul
|
||||||
empiric.
|
empiric.
|
||||||
|
|
||||||
## 10. Out of scope
|
## 10. Out of scope
|
||||||
|
|
||||||
- Refactor md2pdf + md2doc la un package comun (YAGNI pentru 2 fisiere)
|
- Refactor md2pdf + md2doc la un package comun (YAGNI pentru 2 fișiere)
|
||||||
- Stiluri noi peste cele 5 existente
|
- Stiluri noi peste cele 5 existente
|
||||||
- Suport pentru images embedded in markdown (de tratat separat)
|
- Suport pentru images embedded in markdown (de tratat separat)
|
||||||
- Suport pentru syntax highlighting in code blocks (Pygments integration)
|
- Suport pentru syntax highlighting in code blocks (Pygments integration)
|
||||||
- `.doc` (format vechi binar) — doar `.docx`
|
- `.doc` (format vechi binar) — doar `.docx`
|
||||||
- Integrare cu reference.docx templates (pandoc-style)
|
- Integrare cu reference.docx templates (pandoc-style)
|
||||||
|
- `--footer` și `--forms` pentru md2doc (specifice md2pdf)
|
||||||
|
|||||||
Reference in New Issue
Block a user