# 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'') 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()