Files
2025-09-15 07:48:37 +03:00

284 lines
11 KiB
Python

# md_to_docx_gost_final.py
from docx import Document
from docx.shared import Pt, Mm, RGBColor, Inches
from docx.enum.text import WD_ALIGN_PARAGRAPH, WD_LINE_SPACING
from docx.oxml.ns import qn
from docx.oxml import OxmlElement
from docx.enum.table import WD_TABLE_ALIGNMENT
import json
import re
import requests
from io import BytesIO
import parser
# Счётчик листингов
listing_counter = 0
def setup_gost_style(doc):
style = doc.styles['Normal']
font = style.font
font.name = 'Times New Roman'
font.size = Pt(14)
font.color.rgb = RGBColor(0, 0, 0) # Чёрный цвет
rFonts = style.element.rPr.get_or_add_rFonts()
rFonts.set(qn('w:eastAsia'), 'Times New Roman')
rFonts.set(qn('w:cs'), 'Times New Roman')
pf = style.paragraph_format
pf.alignment = WD_ALIGN_PARAGRAPH.JUSTIFY
pf.line_spacing_rule = WD_LINE_SPACING.ONE_POINT_FIVE
pf.first_line_indent = Mm(1.25)
pf.space_before = Pt(0)
pf.space_after = Pt(0)
section = doc.sections[0]
section.top_margin = Mm(20)
section.bottom_margin = Mm(20)
section.left_margin = Mm(30)
section.right_margin = Mm(15)
def add_heading(doc, text, level=1):
heading = doc.add_heading(text, level=level)
if level <= 3:
heading.alignment = WD_ALIGN_PARAGRAPH.CENTER
for run in heading.runs:
run.font.bold = True
run.font.size = Pt(16) if level == 1 else Pt(14)
run.font.color.rgb = RGBColor(0, 0, 0)
def add_listing_caption(doc, language="", is_continuation=False):
global listing_counter
p = doc.add_paragraph(style='Caption')
p.alignment = WD_ALIGN_PARAGRAPH.CENTER
run = p.add_run()
run.font.color.rgb = RGBColor(0, 0, 0)
run.font.name = 'Times New Roman'
run._element.rPr.rFonts.set(qn('w:cs'), 'Times New Roman')
if is_continuation:
run.text = f"Продолжение листинга {listing_counter} — Код на {language}"
else:
listing_counter += 1
run.text = f"Листинг {listing_counter} — Код на {language}"
def add_code_block(doc, code_lines, language=""):
global listing_counter
add_listing_caption(doc, language, is_continuation=False)
# Создаём таблицу для кода с выравниванием по левому краю
table = doc.add_table(rows=0, cols=1)
table.style = 'Table Grid'
table.alignment = WD_TABLE_ALIGNMENT.LEFT # Важно: выравнивание по левому краю
# Добавляем каждую строку кода как отдельную строку таблицы
for line in code_lines:
row = table.add_row()
cell = row.cells[0]
cell.text = line.strip()
# Серый фон для ячейки
shading_elm = parse_xml(f'<w:shd {nsdecls("w")} w:fill="D9D9D9"/>')
cell._tc.get_or_add_tcPr().append(shading_elm)
# Настройка параграфа внутри ячейки
for paragraph in cell.paragraphs:
paragraph_format = paragraph.paragraph_format
paragraph_format.left_indent = Mm(0) # Убираем левый отступ
paragraph_format.right_indent = Mm(0)
paragraph_format.first_line_indent = Mm(0)
paragraph_format.alignment = WD_ALIGN_PARAGRAPH.LEFT # Выравнивание по левому краю
for run in paragraph.runs:
run.font.name = 'Courier New'
run._element.rPr.rFonts.set(qn('w:eastAsia'), 'Courier New')
run.font.size = Pt(12)
run.font.color.rgb = RGBColor(0, 0, 0)
# Добавляем подпись о продолжении, если код длинный
if len(code_lines) > 25:
p = doc.add_paragraph()
run = p.add_run(f"Продолжение листинга {listing_counter} — Код на {language}")
run.italic = True
run.font.size = Pt(12)
run.font.color.rgb = RGBColor(80, 80, 80)
p.alignment = WD_ALIGN_PARAGRAPH.CENTER
p.space_before = Pt(6)
p.space_after = Pt(6)
def add_image_from_url(doc, url, caption_text):
try:
response = requests.get(url.strip(), timeout=5)
image_stream = BytesIO(response.content)
p = doc.add_picture(image_stream, width=Inches(5))
p.alignment = WD_ALIGN_PARAGRAPH.CENTER
caption = doc.add_paragraph(f"Рисунок — {caption_text}", style='Caption')
caption.alignment = WD_ALIGN_PARAGRAPH.CENTER
for run in caption.runs:
run.font.color.rgb = RGBColor(0, 0, 0)
except Exception as e:
print(f"⚠️ Ошибка загрузки изображения: {url}{e}")
doc.add_paragraph(f"[Изображение не загружено: {caption_text}]").alignment = WD_ALIGN_PARAGRAPH.CENTER
def parse_xml(xml):
from docx.oxml import parse_xml as ox_parse
return ox_parse(xml)
def nsdecls(*namespaces):
return ' '.join(f'xmlns:{ns}="http://schemas.openxmlformats.org/wordprocessingml/2006/main"' for ns in namespaces)
def process_element(doc, element, depth=0):
"""Рекурсивная обработка элемента с учётом вложенности"""
global listing_counter
if element['type'] == 'blank_line':
return
elif element['type'] == 'heading':
level = element['attrs']['level']
text = ''.join(extract_text(element['children']))
add_heading(doc, text, level=level)
elif element['type'] == 'paragraph':
p = doc.add_paragraph()
p.style = 'Normal'
# Обработка вложенных элементов в параграфе
for child in element['children']:
if child['type'] == 'text':
p.add_run(child['raw'])
elif child['type'] == 'strong':
text = ''.join(extract_text(child['children']))
run = p.add_run(text)
run.bold = True
elif child['type'] == 'codespan':
run = p.add_run(child['raw'])
run.font.name = 'Courier New'
run.font.size = Pt(12)
elif child['type'] == 'linebreak':
p.add_run().add_break()
elif child['type'] == 'image':
# Изображение обрабатывается отдельно
pass
# Установка отступов в зависимости от глубины
pf = p.paragraph_format
pf.left_indent = Mm(5 * depth) if depth > 0 else Mm(0)
elif element['type'] == 'thematic_break':
doc.add_paragraph().add_run().add_break()
elif element['type'] == 'list':
# Обработка списка с учётом глубины вложенности
for item in element['children']:
if item['type'] == 'list_item':
# Создаём маркированный пункт
p = doc.add_paragraph(style='List Bullet')
p.paragraph_format.left_indent = Mm(5 * depth)
p.paragraph_format.first_line_indent = Mm(-3.5) # Отступ для маркера
# Обрабатываем содержимое пункта списка
for child in item['children']:
if child['type'] == 'block_text':
for subchild in child['children']:
if subchild['type'] == 'text':
p.add_run(subchild['raw'])
elif subchild['type'] == 'strong':
text = ''.join(extract_text(subchild['children']))
run = p.add_run(text)
run.bold = True
elif subchild['type'] == 'codespan':
run = p.add_run(subchild['raw'])
run.font.name = 'Courier New'
run.font.size = Pt(12)
# Рекурсивная обработка вложенных списков
for child in item['children']:
if child['type'] == 'list':
process_element(doc, child, depth=depth + 1)
elif element['type'] == 'block_code':
code_lines = element['raw'].split('\n')
language = element['attrs'].get('info', 'plain')
add_code_block(doc, code_lines, language)
elif element['type'] == 'image':
url = element['attrs']['url']
caption = ''.join(extract_text(element['children']))
add_image_from_url(doc, url, caption)
# Обработка других типов элементов
elif element['type'] == 'list_item':
# Обработка отдельных пунктов списка (для рекурсии)
p = doc.add_paragraph(style='List Bullet')
p.paragraph_format.left_indent = Mm(5 * depth)
p.paragraph_format.first_line_indent = Mm(-3.5)
for child in element['children']:
if child['type'] == 'block_text':
for subchild in child['children']:
if subchild['type'] == 'text':
p.add_run(subchild['raw'])
elif subchild['type'] == 'strong':
text = ''.join(extract_text(subchild['children']))
run = p.add_run(text)
run.bold = True
elif subchild['type'] == 'codespan':
run = p.add_run(subchild['raw'])
run.font.name = 'Courier New'
run.font.size = Pt(12)
# Обработка вложенных списков
for child in element['children']:
if child['type'] == 'list':
process_element(doc, child, depth=depth + 1)
def extract_text(elements):
"""Извлекает текст из структурированных элементов"""
texts = []
for el in elements:
if el['type'] == 'text':
texts.append(el['raw'])
elif 'children' in el:
texts.extend(extract_text(el['children']))
return texts
def parse_structured_data_and_fill(doc, structured_data):
"""Основная функция обработки структурированных данных"""
global listing_counter
listing_counter = 0 # Сброс счётчика листингов
for element in structured_data:
process_element(doc, element)
def main():
structured_data = parser.mdToraw()
# Создаём документ
doc = Document()
setup_gost_style(doc)
# Обрабатываем структурированные данные
parse_structured_data_and_fill(doc, structured_data)
# Сохраняем результат
output_filename = "Практическая_работа_6.docx"
doc.save(output_filename)
print(f"✅ Документ сохранён как '{output_filename}'")
print(f"📎 Обработано {len(structured_data)} элементов структурированных данных")
if __name__ == '__main__':
main()