284 lines
11 KiB
Python
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() |