diff --git a/md2doc.py b/md2doc.py
index e6c62db..c3f6fbc 100644
--- a/md2doc.py
+++ b/md2doc.py
@@ -365,6 +365,71 @@ def _render_list(doc, node, style: dict, level: int = 0, num_state: dict = None)
_render_list(doc, nested, style=style, level=level + 1, num_state=num_state)
+def _render_table(doc, node, style: dict):
+ """Render
with header row shading."""
+ header_fill = style.get("table_header_fill", "1e3a8a")
+
+ rows = node.find_all("tr")
+ if not rows:
+ return
+
+ 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.
+
+ Preserves newlines via add_break() between lines (Word ignores literal \\n in runs).
+ Leading whitespace on each line is preserved as literal spaces (monospace font
+ keeps alignment). Trailing empty line from markdown's final \\n is dropped.
+ """
+ code_fill = style.get("code_fill", "f7f8fa")
+ text = node.get_text()
+ lines = text.split("\n")
+ while lines and lines[-1] == "":
+ lines.pop()
+
+ p = doc.add_paragraph()
+ for i, line in enumerate(lines):
+ if i > 0:
+ p.add_run().add_break()
+ run = p.add_run(line)
+ 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")
+
+
def convert_md_to_doc(md_file: Path, output_file: Path, style: str = "elegant") -> Path:
"""Convert a single markdown file to DOCX.
diff --git a/tests/test_md2doc_blocks.py b/tests/test_md2doc_blocks.py
new file mode 100644
index 0000000..9d37725
--- /dev/null
+++ b/tests/test_md2doc_blocks.py
@@ -0,0 +1,74 @@
+"""Tests for block-level element renderers."""
+import zipfile
+import xml.etree.ElementTree as ET
+from pathlib import Path
+
+from docx import Document
+from bs4 import BeautifulSoup
+
+from md2doc import STYLES, _render_table, _render_pre, _render_blockquote, _render_hr
+
+W_NS = "http://schemas.openxmlformats.org/wordprocessingml/2006/main"
+
+
+def get_doc_xml(doc):
+ tmp = Path(__file__).parent / "_tmp_blocks.docx"
+ doc.save(tmp)
+ try:
+ with zipfile.ZipFile(tmp) as z:
+ return ET.fromstring(z.read("word/document.xml"))
+ finally:
+ tmp.unlink()
+
+
+def test_table_renders_with_header_shading():
+ html = """
+
+ """
+ doc = Document()
+ node = BeautifulSoup(html, "html.parser").find("table")
+ _render_table(doc, node, style=STYLES["elegant"])
+ root = get_doc_xml(doc)
+ trs = list(root.iter(f"{{{W_NS}}}tr"))
+ assert len(trs) == 2
+ shds = list(root.iter(f"{{{W_NS}}}shd"))
+ assert len(shds) >= 2 # at least 2 header cells
+
+
+def test_pre_renders_with_shading():
+ html = 'print("hello")
'
+ doc = Document()
+ node = BeautifulSoup(html, "html.parser").find("pre")
+ _render_pre(doc, node, style=STYLES["elegant"])
+ root = get_doc_xml(doc)
+ shds = list(root.iter(f"{{{W_NS}}}shd"))
+ assert len(shds) >= 1
+ texts = [t.text for t in root.iter(f"{{{W_NS}}}t")]
+ assert any('print' in t for t in texts)
+
+
+def test_blockquote_renders_with_indent_and_italic():
+ html = "A quoted passage
"
+ doc = Document()
+ node = BeautifulSoup(html, "html.parser").find("blockquote")
+ _render_blockquote(doc, node, style=STYLES["elegant"])
+ root = get_doc_xml(doc)
+ italic_el = list(root.iter(f"{{{W_NS}}}i"))
+ assert len(italic_el) >= 1
+ indents = list(root.iter(f"{{{W_NS}}}ind"))
+ assert len(indents) >= 1
+
+
+def test_hr_renders_as_paragraph_with_bottom_border():
+ html = "
"
+ doc = Document()
+ node = BeautifulSoup(html, "html.parser").find("hr")
+ _render_hr(doc, node, style=STYLES["elegant"])
+ root = get_doc_xml(doc)
+ bottoms = list(root.iter(f"{{{W_NS}}}bottom"))
+ assert len(bottoms) >= 1