Spaces:
Running
Running
File size: 5,134 Bytes
3913f6f 6ce52dc 939527c 3913f6f 6ce52dc 3913f6f 67cd891 3913f6f 939527c e0a8d80 3913f6f e0a8d80 3913f6f e0a8d80 3913f6f 939527c e0a8d80 3913f6f 939527c 3913f6f 2e3717c e0a8d80 3913f6f e0a8d80 2e3717c 69fee5e e0a8d80 3913f6f e0a8d80 67cd891 3913f6f e0a8d80 3913f6f e0a8d80 3913f6f 67cd891 939527c 3913f6f e0a8d80 3913f6f e0a8d80 3913f6f e0a8d80 3913f6f e0a8d80 3913f6f e0a8d80 3913f6f 67cd891 e0a8d80 3913f6f e0a8d80 3913f6f e0a8d80 3913f6f 6ce52dc 3913f6f 6ce52dc 3913f6f 6ce52dc | 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 | # -*- coding: utf-8 -*-
import io
import mistune
from bs4 import BeautifulSoup
from docx import Document
from docx.shared import Pt
from config import APP_CONFIG
class DocxBuildError(RuntimeError):
"""Raised when markdown cannot be rendered into a valid DOCX document."""
class DocxRenderer(mistune.HTMLRenderer):
def __init__(self):
super().__init__()
self.doc = Document()
self.h_counters = [0] * 6
self.is_first_h1 = True
try:
style = self.doc.styles['Normal']
style.font.name = APP_CONFIG["docx_styles"]["font_name"]
style.font.size = Pt(APP_CONFIG["docx_styles"]["font_size_pt"])
except:
pass
def _render_rich_text(self, p, html_text):
if not html_text.strip():
return
soup = BeautifulSoup(html_text, 'html.parser')
for element in soup.children:
if isinstance(element, str):
p.add_run(element)
elif element.name in ['strong', 'b']:
p.add_run(element.get_text()).bold = True
elif element.name in ['em', 'i']:
p.add_run(element.get_text()).italic = True
elif element.name == 'code':
run = p.add_run(element.get_text())
run.font.name = APP_CONFIG["docx_styles"]["code_font"]
run.font.size = Pt(APP_CONFIG["docx_styles"]["code_size_pt"])
else:
p.add_run(element.get_text())
def heading(self, text, level):
clean_text = BeautifulSoup(text, 'html.parser').get_text().strip()
lower_text = clean_text.lower()
skip_keywords = APP_CONFIG["blacklists"]["meta_sections"]
if any(kw in lower_text for kw in skip_keywords) or (level == 1 and self.is_first_h1):
if level == 1:
self.is_first_h1 = False
self.doc.add_heading(clean_text, level=1)
return ''
if level == 1:
self.is_first_h1 = False
idx = level - 1
if 0 <= idx < 6:
for j in range(idx):
if self.h_counters[j] == 0:
self.h_counters[j] = 1
self.h_counters[idx] += 1
for i in range(idx + 1, 6):
self.h_counters[i] = 0
num_str = ".".join(map(str, self.h_counters[0:idx+1]))
self.doc.add_heading(f"{num_str} {clean_text}", level=level)
else:
self.doc.add_heading(clean_text, level=level)
return ''
def paragraph(self, text):
if text.strip():
self._render_rich_text(self.doc.add_paragraph(), text)
return ''
def block_code(self, code, info=None):
run = self.doc.add_paragraph().add_run(code)
run.font.name = APP_CONFIG["docx_styles"]["code_font"]
run.font.size = Pt(APP_CONFIG["docx_styles"]["code_size_pt"])
return ''
def block_quote(self, text):
p = self.doc.add_paragraph()
self._render_rich_text(p, text)
try:
p.style = 'Intense Quote' if 'Intense Quote' in self.doc.styles else 'Normal'
except:
pass
return ''
def list(self, text, ordered, **attrs):
return text
def list_item(self, text):
if text.strip():
p = self.doc.add_paragraph()
p.style = 'List Bullet'
self._render_rich_text(p, text)
return ''
def table(self, text):
soup = BeautifulSoup(text, 'html.parser')
rows = soup.find_all('tr')
if not rows:
return ''
max_cols = max(len(row.find_all(['td', 'th'])) for row in rows)
if max_cols == 0:
return ''
table = self.doc.add_table(rows=len(rows), cols=max_cols)
try:
table.style = 'Table Grid'
except:
pass
for i, row in enumerate(rows):
for j, cell in enumerate(row.find_all(['td', 'th'])):
if j < max_cols:
table.cell(i, j).text = cell.get_text(strip=True)
self.doc.add_paragraph()
return ''
def thematic_break(self):
self.doc.add_paragraph('_' * 50)
return ''
def image(self, src, alt="", title=None):
self.doc.add_paragraph(f"[Image: {alt or src}]")
return ''
def text_to_docx(markdown_content):
"""Render markdown strictly; never disguise renderer failures as success."""
try:
renderer = DocxRenderer()
mistune.create_markdown(renderer=renderer, plugins=['table'])(str(markdown_content or ''))
doc = renderer.doc
if not doc.paragraphs and not doc.tables:
raise DocxBuildError("DOCX renderer produced no document content.")
stream = io.BytesIO()
doc.save(stream)
stream.seek(0)
return stream
except DocxBuildError:
raise
except Exception as exc:
raise DocxBuildError(f"DOCX rendering failed: {exc}") from exc
|