Spaces:
Running
Running
Upload 7 files
Browse files- app.py +40 -15
- converter.py +49 -34
app.py
CHANGED
@@ -2,20 +2,39 @@ import gradio as gr
|
|
2 |
from converter import MarkdownToDocxConverter
|
3 |
from datetime import datetime
|
4 |
import os
|
|
|
5 |
|
6 |
converter = MarkdownToDocxConverter()
|
7 |
|
8 |
-
def convert_markdown_to_docx(markdown_text: str, file_input: str):
|
9 |
-
|
10 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
11 |
markdown_text = file.read()
|
12 |
|
13 |
-
|
|
|
|
|
14 |
output_dir = "output"
|
15 |
os.makedirs(output_dir, exist_ok=True)
|
16 |
output_filename = f"output_{datetime.now().strftime('%Y%m%d_%H%M%S')}.docx"
|
17 |
-
|
18 |
-
|
|
|
|
|
|
|
19 |
|
20 |
demo = gr.Blocks(title="Markdown to DOCX Converter")
|
21 |
|
@@ -24,15 +43,21 @@ with demo:
|
|
24 |
with gr.Row():
|
25 |
with gr.Column():
|
26 |
with gr.Tab("Текст"):
|
27 |
-
markdown_input = gr.TextArea(label="Markdown Input", value="")
|
28 |
-
with gr.Tab("Файл
|
29 |
-
file_input = gr.File(label="Markdown
|
|
|
|
|
30 |
with gr.Column():
|
31 |
-
gr.Markdown("
|
32 |
-
docx_output = gr.File(label="DOCX
|
33 |
-
|
34 |
-
convert_button
|
35 |
-
|
|
|
|
|
|
|
|
|
36 |
|
37 |
if __name__ == "__main__":
|
38 |
-
demo.launch()
|
|
|
2 |
from converter import MarkdownToDocxConverter
|
3 |
from datetime import datetime
|
4 |
import os
|
5 |
+
import requests
|
6 |
|
7 |
converter = MarkdownToDocxConverter()
|
8 |
|
9 |
+
def convert_markdown_to_docx(markdown_text: str, file_input: str, url_input: str):
|
10 |
+
# Приоритет: URL > Файл > Текст
|
11 |
+
if url_input:
|
12 |
+
try:
|
13 |
+
response = requests.get(url_input)
|
14 |
+
response.raise_for_status()
|
15 |
+
# Пытаемся получить "сырое" содержимое для таких сайтов, как GitHub Gist
|
16 |
+
if "gist.github.com" in url_input and not url_input.endswith("/raw"):
|
17 |
+
raw_url = url_input + "/raw"
|
18 |
+
response = requests.get(raw_url)
|
19 |
+
response.raise_for_status()
|
20 |
+
markdown_text = response.text
|
21 |
+
except requests.exceptions.RequestException as e:
|
22 |
+
raise gr.Error(f"Ошибка при скачивании файла по URL: {e}")
|
23 |
+
elif file_input:
|
24 |
+
with open(file_input.name, "r", encoding='utf-8') as file:
|
25 |
markdown_text = file.read()
|
26 |
|
27 |
+
if not markdown_text.strip():
|
28 |
+
raise gr.Error("Нет входных данных для конвертации. Введите текст, загрузите файл или укажите URL.")
|
29 |
+
|
30 |
output_dir = "output"
|
31 |
os.makedirs(output_dir, exist_ok=True)
|
32 |
output_filename = f"output_{datetime.now().strftime('%Y%m%d_%H%M%S')}.docx"
|
33 |
+
output_path = os.path.join(output_dir, output_filename)
|
34 |
+
|
35 |
+
converter.convert(markdown_text, output_path)
|
36 |
+
|
37 |
+
return output_path
|
38 |
|
39 |
demo = gr.Blocks(title="Markdown to DOCX Converter")
|
40 |
|
|
|
43 |
with gr.Row():
|
44 |
with gr.Column():
|
45 |
with gr.Tab("Текст"):
|
46 |
+
markdown_input = gr.TextArea(label="Markdown Input", value="", lines=15)
|
47 |
+
with gr.Tab("Файл"):
|
48 |
+
file_input = gr.File(label="Загрузите Markdown файл")
|
49 |
+
with gr.Tab("URL"):
|
50 |
+
url_input = gr.Textbox(label="Введите URL Markdown файла", placeholder="https://gist.github.com/...")
|
51 |
with gr.Column():
|
52 |
+
gr.Markdown("### Результат")
|
53 |
+
docx_output = gr.File(label="Скачать DOCX")
|
54 |
+
|
55 |
+
convert_button = gr.Button("Конвертировать", variant="primary")
|
56 |
+
convert_button.click(
|
57 |
+
convert_markdown_to_docx,
|
58 |
+
inputs=[markdown_input, file_input, url_input],
|
59 |
+
outputs=docx_output
|
60 |
+
)
|
61 |
|
62 |
if __name__ == "__main__":
|
63 |
+
demo.launch(server_name="0.0.0.0", server_port=8010)
|
converter.py
CHANGED
@@ -1,8 +1,8 @@
|
|
1 |
import re
|
2 |
import json
|
3 |
import io
|
4 |
-
import requests
|
5 |
import base64
|
|
|
6 |
from pathlib import Path
|
7 |
from typing import List, Dict, Any, Optional, Tuple
|
8 |
import markdown
|
@@ -21,7 +21,6 @@ class MarkdownToDocxConverter:
|
|
21 |
|
22 |
def __init__(self):
|
23 |
self.doc = None
|
24 |
-
self.styles_created = False
|
25 |
|
26 |
# Цветовая схема для подсветки кода
|
27 |
self.code_colors = {
|
@@ -35,9 +34,6 @@ class MarkdownToDocxConverter:
|
|
35 |
|
36 |
def create_styles(self):
|
37 |
"""Создание пользовательских стилей для документа"""
|
38 |
-
if self.styles_created:
|
39 |
-
return
|
40 |
-
|
41 |
# Стиль для блоков кода
|
42 |
code_style = self.doc.styles.add_style('CodeBlock', WD_STYLE_TYPE.PARAGRAPH)
|
43 |
code_style.font.name = 'Consolas'
|
@@ -65,8 +61,6 @@ class MarkdownToDocxConverter:
|
|
65 |
heading_style.font.color.rgb = RGBColor(0, 0, 0)
|
66 |
heading_style.font.bold = True
|
67 |
heading_style.font.size = Pt(26 - i * 3)
|
68 |
-
|
69 |
-
self.styles_created = True
|
70 |
|
71 |
def parse_code_block(self, code_text: str, language: str = '') -> List[Tuple[str, str]]:
|
72 |
"""Простая подсветка синтаксиса для кода"""
|
@@ -198,17 +192,19 @@ class MarkdownToDocxConverter:
|
|
198 |
elif element.name == 'pre':
|
199 |
code_element = element.find('code')
|
200 |
if code_element:
|
201 |
-
# Извлекаем язык из класса
|
202 |
classes = code_element.get('class', [])
|
203 |
-
language = ''
|
204 |
-
for cls in classes:
|
205 |
-
if cls.startswith('language-'):
|
206 |
-
language = cls.replace('language-', '')
|
207 |
-
break
|
208 |
code_text = code_element.get_text()
|
209 |
-
|
|
|
|
|
210 |
self.add_mermaid_diagram(code_text)
|
211 |
else:
|
|
|
|
|
|
|
|
|
|
|
|
|
212 |
self.add_code_block(code_text, language)
|
213 |
else:
|
214 |
self.add_code_block(element.get_text())
|
@@ -248,11 +244,11 @@ class MarkdownToDocxConverter:
|
|
248 |
self.process_html_element(child, parent_paragraph)
|
249 |
|
250 |
def add_mermaid_diagram(self, code: str):
|
251 |
-
"""Рендеринг и вставка диаграммы Mermaid"""
|
252 |
try:
|
253 |
-
# Кодируем код диаграммы в base64
|
254 |
-
graphbytes = code.encode("
|
255 |
-
base64_bytes = base64.
|
256 |
base64_string = base64_bytes.decode("ascii")
|
257 |
|
258 |
# Формируем URL для запроса
|
@@ -327,30 +323,49 @@ class MarkdownToDocxConverter:
|
|
327 |
|
328 |
def convert(self, markdown_text: str, output_path: str):
|
329 |
"""Конвертация Markdown текста в DOCX файл"""
|
330 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
331 |
self.doc = Document()
|
332 |
self.create_styles()
|
333 |
-
|
334 |
-
# Конвертируем Markdown в HTML
|
335 |
md = markdown.Markdown(extensions=[
|
336 |
-
'fenced_code',
|
337 |
-
'codehilite',
|
338 |
-
'tables',
|
339 |
-
'toc',
|
340 |
-
'nl2br',
|
341 |
-
'sane_lists'
|
342 |
])
|
343 |
-
|
344 |
html = md.convert(markdown_text)
|
345 |
-
|
346 |
-
# Парсим HTML
|
347 |
soup = BeautifulSoup(html, 'html.parser')
|
348 |
-
|
349 |
-
# Обрабатываем каждый элемент
|
350 |
for element in soup.children:
|
351 |
-
|
|
|
|
|
|
|
|
|
352 |
|
353 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
354 |
self.doc.save(output_path)
|
355 |
print(f"Документ успешно сохранен: {output_path}")
|
356 |
|
|
|
1 |
import re
|
2 |
import json
|
3 |
import io
|
|
|
4 |
import base64
|
5 |
+
import requests
|
6 |
from pathlib import Path
|
7 |
from typing import List, Dict, Any, Optional, Tuple
|
8 |
import markdown
|
|
|
21 |
|
22 |
def __init__(self):
|
23 |
self.doc = None
|
|
|
24 |
|
25 |
# Цветовая схема для подсветки кода
|
26 |
self.code_colors = {
|
|
|
34 |
|
35 |
def create_styles(self):
|
36 |
"""Создание пользовательских стилей для документа"""
|
|
|
|
|
|
|
37 |
# Стиль для блоков кода
|
38 |
code_style = self.doc.styles.add_style('CodeBlock', WD_STYLE_TYPE.PARAGRAPH)
|
39 |
code_style.font.name = 'Consolas'
|
|
|
61 |
heading_style.font.color.rgb = RGBColor(0, 0, 0)
|
62 |
heading_style.font.bold = True
|
63 |
heading_style.font.size = Pt(26 - i * 3)
|
|
|
|
|
64 |
|
65 |
def parse_code_block(self, code_text: str, language: str = '') -> List[Tuple[str, str]]:
|
66 |
"""Простая подсветка синтаксиса для кода"""
|
|
|
192 |
elif element.name == 'pre':
|
193 |
code_element = element.find('code')
|
194 |
if code_element:
|
|
|
195 |
classes = code_element.get('class', [])
|
|
|
|
|
|
|
|
|
|
|
196 |
code_text = code_element.get_text()
|
197 |
+
|
198 |
+
# Check if it's a mermaid diagram
|
199 |
+
if 'mermaid' in classes or 'language-mermaid' in classes:
|
200 |
self.add_mermaid_diagram(code_text)
|
201 |
else:
|
202 |
+
# It's a regular code block, find the language
|
203 |
+
language = ''
|
204 |
+
for cls in classes:
|
205 |
+
if cls.startswith('language-'):
|
206 |
+
language = cls.replace('language-', '')
|
207 |
+
break
|
208 |
self.add_code_block(code_text, language)
|
209 |
else:
|
210 |
self.add_code_block(element.get_text())
|
|
|
244 |
self.process_html_element(child, parent_paragraph)
|
245 |
|
246 |
def add_mermaid_diagram(self, code: str):
|
247 |
+
"""Рендеринг и вставка диаграммы Mermaid через mermaid.ink"""
|
248 |
try:
|
249 |
+
# Кодируем код диаграммы в URL-безопасный base64
|
250 |
+
graphbytes = code.encode("utf-8")
|
251 |
+
base64_bytes = base64.urlsafe_b64encode(graphbytes)
|
252 |
base64_string = base64_bytes.decode("ascii")
|
253 |
|
254 |
# Формируем URL для запроса
|
|
|
323 |
|
324 |
def convert(self, markdown_text: str, output_path: str):
|
325 |
"""Конвертация Markdown текста в DOCX файл"""
|
326 |
+
# 1. Извлекаем диаграммы Mermaid и заменяем их плейсхолдерами
|
327 |
+
mermaid_diagrams = {}
|
328 |
+
def replace_mermaid(match):
|
329 |
+
key = f"%%MERMAID_DIAGRAM_{len(mermaid_diagrams)}%%"
|
330 |
+
# Сохраняем только код диаграммы
|
331 |
+
mermaid_diagrams[key] = match.group(1).strip()
|
332 |
+
# Возвращаем плейсхолдер в виде параграфа, чтобы он не был удален
|
333 |
+
return f"\n<p>{key}</p>\n"
|
334 |
+
|
335 |
+
# Регулярное выражение для поиска блоков ```mermaid ... ```
|
336 |
+
markdown_text = re.sub(r'```mermaid\n(.*?)\n```', replace_mermaid, markdown_text, flags=re.DOTALL)
|
337 |
+
|
338 |
+
# 2. Создаем новый документ и стили
|
339 |
self.doc = Document()
|
340 |
self.create_styles()
|
341 |
+
|
342 |
+
# 3. Конвертируем оставшийся Markdown в HTML
|
343 |
md = markdown.Markdown(extensions=[
|
344 |
+
'fenced_code', 'codehilite', 'tables', 'toc', 'nl2br', 'sane_lists'
|
|
|
|
|
|
|
|
|
|
|
345 |
])
|
|
|
346 |
html = md.convert(markdown_text)
|
347 |
+
|
348 |
+
# 4. Парсим HTML и обрабатываем элементы
|
349 |
soup = BeautifulSoup(html, 'html.parser')
|
|
|
|
|
350 |
for element in soup.children:
|
351 |
+
# Проверяем, содержит ли элемент плейсхолдер Mermaid
|
352 |
+
if isinstance(element, NavigableString):
|
353 |
+
# Пропускаем пустые строки
|
354 |
+
if not str(element).strip():
|
355 |
+
continue
|
356 |
|
357 |
+
text_content = element.get_text().strip()
|
358 |
+
if "%%MERMAID_DIAGRAM_" in text_content:
|
359 |
+
key = text_content
|
360 |
+
if key in mermaid_diagrams:
|
361 |
+
self.add_mermaid_diagram(mermaid_diagrams[key])
|
362 |
+
else:
|
363 |
+
# Если плейсхолдер найден, но нет диаграммы, просто пропускаем
|
364 |
+
continue
|
365 |
+
else:
|
366 |
+
self.process_html_element(element)
|
367 |
+
|
368 |
+
# 5. Сохраняем документ
|
369 |
self.doc.save(output_path)
|
370 |
print(f"Документ успешно сохранен: {output_path}")
|
371 |
|