with header row shading."""
header_fill = style.get("table_header_fill", "1e3a8a")
rows = node.find_all("tr")
if not rows:
return
# Determine column count
cols = max(len(row.find_all(["th", "td"])) for row in rows)
table = doc.add_table(rows=len(rows), cols=cols)
table.style = 'Table Grid'
for i, row in enumerate(rows):
cells = row.find_all(["th", "td"])
is_header = row.parent.name == "thead" or any(c.name == "th" for c in cells)
for j, cell in enumerate(cells):
if j >= cols:
break
tc = table.rows[i].cells[j]
tc.text = cell.get_text(strip=True)
if is_header:
add_cell_shading(tc, fill=header_fill)
def _render_pre(doc, node, style: dict):
"""Render (code block) — shaded background + monospace font."""
code_fill = style.get("code_fill", "f7f8fa")
text = node.get_text()
p = doc.add_paragraph()
run = p.add_run(text)
run.font.name = "Menlo"
run.font.size = Pt(8)
add_paragraph_shading(p, fill=code_fill)
def _render_blockquote(doc, node, style: dict):
"""Render — left indent + italic + shading."""
quote_fill = style.get("blockquote_fill", "f7f8fa")
text = node.get_text(strip=True)
p = doc.add_paragraph()
p.paragraph_format.left_indent = Cm(1)
run = p.add_run(text)
run.italic = True
add_paragraph_shading(p, fill=quote_fill)
def _render_hr(doc, node, style: dict):
"""Render as empty paragraph with bottom border."""
p = doc.add_paragraph()
add_paragraph_bottom_border(p, color="d8d8d8", size="6")
```
- [ ] **Step 4: Run tests**
```bash
pytest tests/test_md2doc_blocks.py -v
```
Expected: PASS (4 tests).
- [ ] **Step 5: Commit**
```bash
git add md2doc.py tests/test_md2doc_blocks.py
git commit -m "feat(md2doc): table, pre, blockquote, hr renderers"
```
---
## Task 7: Image skip-with-warning + converter orchestration
**Files:**
- Modify: `md2doc.py` (add `_warn_skip_image`, replace `convert_md_to_doc` stub)
- Test: `tests/test_md2doc_integration.py`
- [ ] **Step 1: Write failing tests**
Creează `tests/test_md2doc_integration.py`:
```python
"""Integration test — full md → docx conversion on pluxee-todo.md fixture."""
import subprocess
import sys
import zipfile
import xml.etree.ElementTree as ET
from pathlib import Path
REPO = Path(__file__).parent.parent
FIXTURE = REPO / "pluxee-todo.md"
W_NS = "http://schemas.openxmlformats.org/wordprocessingml/2006/main"
def test_convert_pluxee_todo_creates_valid_docx(tmp_path):
out = tmp_path / "pluxee.docx"
r = subprocess.run(
[sys.executable, "md2doc.py", str(FIXTURE), "-o", str(out)],
cwd=REPO, capture_output=True, text=True
)
assert r.returncode == 0, f"Conversion failed: {r.stderr}"
assert out.exists()
assert out.stat().st_size > 0
def test_converted_docx_has_headings_and_paragraphs(tmp_path):
out = tmp_path / "pluxee.docx"
subprocess.run(
[sys.executable, "md2doc.py", str(FIXTURE), "-o", str(out), "-q"],
cwd=REPO, capture_output=True, text=True
)
with zipfile.ZipFile(out) as z:
root = ET.fromstring(z.read("word/document.xml"))
# pluxee-todo.md has multiple ## headings and paragraphs
headings = list(root.iter(f"{{{W_NS}}}pStyle"))
heading_vals = [h.get(f"{{{W_NS}}}val") for h in headings]
assert any(v.startswith("Heading") for v in heading_vals), \
f"expected at least one Heading style, got {heading_vals}"
def test_image_in_markdown_warns_to_stderr(tmp_path):
md_with_image = tmp_path / "img.md"
md_with_image.write_text("# Test\n\n\n")
out = tmp_path / "img.docx"
r = subprocess.run(
[sys.executable, "md2doc.py", str(md_with_image), "-o", str(out)],
cwd=REPO, capture_output=True, text=True
)
assert r.returncode == 0 # warning, not error
assert "image" in r.stderr.lower() or "skip" in r.stderr.lower()
def test_directory_mode_converts_all_md(tmp_path):
# Create 2 markdown files
(tmp_path / "a.md").write_text("# A\n")
(tmp_path / "b.md").write_text("# B\n")
out_dir = tmp_path / "out"
r = subprocess.run(
[sys.executable, "md2doc.py", str(tmp_path), "-o", str(out_dir)],
cwd=REPO, capture_output=True, text=True
)
assert r.returncode == 0
assert (out_dir / "a.docx").exists()
assert (out_dir / "b.docx").exists()
```
- [ ] **Step 2: Run test to verify it fails**
```bash
pytest tests/test_md2doc_integration.py -v
```
Expected: FAIL — `NotImplementedError` from stub.
- [ ] **Step 3: Implement converter**
Adaugă warning helper și înlocuiește stub-ul `convert_md_to_doc`:
```python
def _warn_skip_image(node):
"""Warn to stderr when an is encountered (out of scope per spec §10)."""
src = node.get("src", "(no src)")
print(f"Warning: image skipped (out of scope): {src}", file=sys.stderr)
def convert_md_to_doc(md_file: Path, output_file: Path, style: str = "elegant") -> Path:
"""Convert a single markdown file to DOCX."""
import re
style_cfg = STYLES.get(style, STYLES["elegant"])
with open(md_file, 'r', encoding='utf-8') as f:
md_content = f.read()
# Reuse _bulletize and BLANK_MARKER logic from md2pdf (copied, not imported)
md_content = _bulletize(md_content)
BLANK_MARKER = ''
md_content = re.sub(
r'\n{3,}',
lambda m: '\n\n' + (BLANK_MARKER + '\n\n') * (len(m.group(0)) - 2),
md_content
)
html_content = markdown.markdown(
md_content,
extensions=[
'markdown.extensions.tables',
'markdown.extensions.fenced_code',
'markdown.extensions.toc',
'markdown.extensions.attr_list',
'markdown.extensions.md_in_html',
'markdown.extensions.nl2br',
]
)
html_content = html_content.replace(
BLANK_MARKER, '
'
)
soup = BeautifulSoup(html_content, "lxml")
doc = Document()
# Walk top-level elements in
body = soup.find("body") or soup
for element in body.children:
if not hasattr(element, "name") or element.name is None:
continue
name = element.name
if name in ("h1", "h2", "h3", "h4"):
_render_heading(doc, element, level=int(name[1]), style=style_cfg)
elif name == "p":
_render_paragraph(doc, element, style=style_cfg)
elif name in ("ul", "ol"):
_render_list(doc, element, style=style_cfg)
elif name == "table":
_render_table(doc, element, style=style_cfg)
elif name == "pre":
_render_pre(doc, element, style=style_cfg)
elif name == "blockquote":
_render_blockquote(doc, element, style=style_cfg)
elif name == "hr":
_render_hr(doc, element, style=style_cfg)
elif name == "img":
_warn_skip_image(element)
elif name == "figure":
# Check for inner img
img = element.find("img")
if img:
_warn_skip_image(img)
else:
# Render figure text content as paragraph
_render_paragraph(doc, element, style=style_cfg)
# else: silently skip unknown elements (divs, spans, etc.)
doc.save(output_file)
return output_file
```
- [ ] **Step 4: Run tests**
```bash
pytest tests/test_md2doc_integration.py -v
```
Expected: PASS (4 tests).
- [ ] **Step 5: Manual visual check**
```bash
python3 md2doc.py pluxee-todo.md -o /tmp/pluxee-todo.docx
open /tmp/pluxee-todo.docx
```
Inspect visually in Word/Pages:
- Headings styled + H1 has bottom border
- Bullet lists are nested where markdown had nesting
- Tables have header row shading
- Code blocks have shading
- No images (should have stderr warnings)
- [ ] **Step 6: Commit**
```bash
git add md2doc.py tests/test_md2doc_integration.py
git commit -m "feat(md2doc): full converter — walk BeautifulSoup DOM, emit docx elements"
```
---
## Task 8: B1 fix — expose `--footer` in md2pdf.py CLI
**Files:**
- Modify: `md2pdf.py` (lines around 670-676 for argparse, 728 for convert call)
- Test: `tests/test_md2pdf_footer.py`
- [ ] **Step 1: Write failing test**
Creează `tests/test_md2pdf_footer.py`:
```python
"""B1 fix: --footer flag should be accepted and passed to convert_md_to_pdf."""
import subprocess
import sys
from pathlib import Path
REPO = Path(__file__).parent.parent
def test_footer_flag_accepted():
"""--footer should be in --help output."""
r = subprocess.run(
[sys.executable, "md2pdf.py", "--help"],
cwd=REPO, capture_output=True, text=True
)
assert "--footer" in r.stdout
def test_footer_produces_pdf(tmp_path):
"""End-to-end: --footer flag should produce a PDF successfully."""
md = tmp_path / "test.md"
md.write_text("# Test\nHello\n")
out = tmp_path / "out.pdf"
r = subprocess.run(
[sys.executable, "md2pdf.py", str(md), "-o", str(out), "--footer", "Confidential v1.0"],
cwd=REPO, capture_output=True, text=True
)
assert r.returncode == 0, f"stderr: {r.stderr}"
assert out.exists()
```
- [ ] **Step 2: Run test to verify it fails**
```bash
pytest tests/test_md2pdf_footer.py -v
```
Expected: FAIL — `--footer` not in help output.
- [ ] **Step 3: Modify md2pdf.py argparse**
În `md2pdf.py` (curent 747 linii):
1. La linia **674-676** — șterge cele 2 linii cu `--forms` (B2 le va face redundant; se suprapune cu Task 9, darTask 8 poate ține pasul doar cu adăugarea `--footer`). Alternativ, dacă Task 9 se face înainte, sări peste acest sub-punct.
2. La linia **676** (după `parser.add_argument('-q', '--quiet', ..., help='Suppress output')`) — adaugă imediat sub:
```python
parser.add_argument('--footer',
help='Custom footer text (right-aligned, appears in @bottom-right)')
```
3. La linia **728** — înlocuiește apelul:
```python
convert_md_to_pdf(md_file, pdf_file, args.style, forms=args.forms)
```
cu:
```python
convert_md_to_pdf(md_file, pdf_file, args.style,
forms=args.forms, footer=args.footer or "")
```
(Notă: `forms=args.forms` rămâne până când Task 9 șterge `--forms`.)
- [ ] **Step 4: Run tests**
```bash
pytest tests/test_md2pdf_footer.py -v
```
Expected: PASS (2 tests).
- [ ] **Step 5: Commit**
```bash
git add md2pdf.py tests/test_md2pdf_footer.py
git commit -m "fix(md2pdf): expose --footer in CLI (was dead code)"
```
---
## Task 9: B2 fix — remove `--forms` from md2pdf.py
**Files:**
- Modify: `md2pdf.py` (remove `--forms` arg, remove `forms` param, remove `options={'pdf_forms': forms}`)
- Test: `tests/test_md2pdf_no_forms.py`
- [ ] **Step 1: Write failing test (characterization — proves forms are gone)**
Creează `tests/test_md2pdf_no_forms.py`:
```python
"""B2 fix: --forms is removed. Verified empirically that weasyprint 68.1 silently
drops /