Spaces:
Sleeping
Sleeping
File size: 5,746 Bytes
31fa536 2559985 31fa536 2559985 31fa536 2559985 31fa536 2559985 31fa536 2559985 31fa536 2559985 31fa536 2559985 31fa536 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 | """Step 8: assemble the Markdown post + images + captions into a .docx file."""
from __future__ import annotations
import re
from pathlib import Path
from typing import List
from docx import Document
from docx.enum.text import WD_ALIGN_PARAGRAPH
from docx.shared import Inches, Pt, RGBColor
_IMAGE_MARKER = re.compile(r"\[IMAGE:\s*.+?\]", re.DOTALL)
_INLINE = re.compile(r"(\*\*.+?\*\*|\*.+?\*|`.+?`)")
_SEP_CELL = re.compile(r"^:?-{2,}:?$")
_MAX_IMG_WIDTH = Inches(6.0)
def build_docx(markdown: str, images: List[dict], out_path: Path) -> Path:
"""Render `markdown` to a Word document, inserting `images` at [IMAGE:] markers in order."""
doc = Document()
img_iter = iter([im for im in images if im.get("path")])
blocks = _split_blocks(markdown)
i, n = 0, len(blocks)
while i < n:
block = blocks[i]
# Markdown pipe table: a header row followed by a separator row (| --- | --- |).
if _is_table_row(block) and i + 1 < n and _is_separator_row(blocks[i + 1]):
j = i + 2
rows = [block]
while j < n and _is_table_row(blocks[j]):
rows.append(blocks[j])
j += 1
_add_table(doc, rows)
i = j
continue
if _IMAGE_MARKER.fullmatch(block.strip()):
_insert_next_image(doc, img_iter)
i += 1
continue
# a block may still contain an inline marker mixed with text
if _IMAGE_MARKER.search(block):
for piece in _IMAGE_MARKER.split(block):
if piece.strip():
_render_line(doc, piece.strip())
_insert_next_image(doc, img_iter)
i += 1
continue
_render_line(doc, block)
i += 1
# any leftover images that never got placed → append at the end
for im in img_iter:
_add_picture(doc, im)
out_path.parent.mkdir(parents=True, exist_ok=True)
doc.save(out_path)
return out_path
def _is_table_row(line: str) -> bool:
s = line.strip()
return "|" in s and s.count("|") >= 2
def _is_separator_row(line: str) -> bool:
cells = _parse_row(line)
return bool(cells) and all(_SEP_CELL.match(c.replace(" ", "")) for c in cells)
def _parse_row(line: str) -> List[str]:
s = line.strip()
if s.startswith("|"):
s = s[1:]
if s.endswith("|"):
s = s[:-1]
return [c.strip() for c in s.split("|")]
def _add_table(doc: Document, rows: List[str]) -> None:
header = _parse_row(rows[0])
body = [_parse_row(r) for r in rows[1:]]
ncols = max([len(header)] + [len(r) for r in body]) if (header or body) else 0
if ncols == 0:
return
table = doc.add_table(rows=1, cols=ncols)
try:
table.style = "Table Grid"
except Exception:
pass
# header (cells start empty, so add_run yields runs[0])
for c in range(ncols):
run = table.rows[0].cells[c].paragraphs[0].add_run(header[c] if c < len(header) else "")
run.bold = True
# body
for r in body:
cells = table.add_row().cells
for c in range(ncols):
_add_runs(cells[c].paragraphs[0], r[c] if c < len(r) else "")
def _split_blocks(md: str) -> List[str]:
"""Split into logical blocks (headings/paragraphs/markers), preserving order."""
blocks: List[str] = []
for raw in md.split("\n"):
line = raw.rstrip()
if line.strip() == "":
continue
blocks.append(line)
return blocks
def _render_line(doc: Document, line: str) -> None:
stripped = line.strip()
if stripped.startswith("#"):
level = len(stripped) - len(stripped.lstrip("#"))
text = stripped[level:].strip()
doc.add_heading(text, level=min(max(level, 1), 4))
elif re.match(r"^[-*]\s+", stripped):
text = re.sub(r"^[-*]\s+", "", stripped)
p = doc.add_paragraph(style="List Bullet")
_add_runs(p, text)
elif re.match(r"^\d+\.\s+", stripped):
text = re.sub(r"^\d+\.\s+", "", stripped)
p = doc.add_paragraph(style="List Number")
_add_runs(p, text)
elif stripped.startswith(">"):
p = doc.add_paragraph(style="Intense Quote")
_add_runs(p, stripped.lstrip("> ").strip())
else:
p = doc.add_paragraph()
_add_runs(p, stripped)
def _add_runs(paragraph, text: str) -> None:
"""Add text to a paragraph, honoring **bold**, *italic*, and `code`."""
for token in _INLINE.split(text):
if not token:
continue
if token.startswith("**") and token.endswith("**"):
paragraph.add_run(token[2:-2]).bold = True
elif token.startswith("*") and token.endswith("*"):
paragraph.add_run(token[1:-1]).italic = True
elif token.startswith("`") and token.endswith("`"):
run = paragraph.add_run(token[1:-1])
run.font.name = "Consolas"
else:
paragraph.add_run(token)
def _insert_next_image(doc: Document, img_iter) -> None:
im = next(img_iter, None)
if im is not None:
_add_picture(doc, im)
def _add_picture(doc: Document, im: dict) -> None:
path = im.get("path")
if not path or not Path(path).exists():
return
try:
p = doc.add_paragraph()
p.alignment = WD_ALIGN_PARAGRAPH.CENTER
p.add_run().add_picture(str(path), width=_MAX_IMG_WIDTH)
except Exception:
return
caption = (im.get("caption") or "").strip()
if caption:
cap = doc.add_paragraph()
cap.alignment = WD_ALIGN_PARAGRAPH.CENTER
run = cap.add_run(caption)
run.italic = True
run.font.size = Pt(9)
run.font.color.rgb = RGBColor(0x66, 0x66, 0x66)
|