# -*- coding: utf-8 -*- import re from docx import Document from docx.shared import Pt, Cm, RGBColor from docx.enum.text import WD_ALIGN_PARAGRAPH from docx.enum.section import WD_ORIENT from docx.oxml.ns import qn from docx.oxml import OxmlElement SRC = 'Метод_Дегтярева_концепция_трека.md' OUT = 'Метод_Дегтярева_концепция_трека.docx' GREEN = RGBColor(0x2E, 0x7D, 0x5B) DARK = RGBColor(0x1A, 0x1A, 0x1A) GRAY = RGBColor(0x5F, 0x66, 0x6B) LIGHT_GRAY = RGBColor(0x8A, 0x90, 0x95) ROSE = RGBColor(0xB0, 0x3A, 0x55) def set_run(run, name='Arial', size=10.5, bold=False, italic=False, color=DARK): run.font.name = name run.font.size = Pt(size) run.font.bold = bold run.font.italic = italic run.font.color.rgb = color rPr = run._element.get_or_add_rPr() rFonts = rPr.find(qn('w:rFonts')) if rFonts is None: rFonts = OxmlElement('w:rFonts') rPr.append(rFonts) rFonts.set(qn('w:ascii'), name) rFonts.set(qn('w:hAnsi'), name) rFonts.set(qn('w:eastAsia'), name) rFonts.set(qn('w:cs'), name) def shade(cell, hex_color): tcPr = cell._tc.get_or_add_tcPr() shd = OxmlElement('w:shd') shd.set(qn('w:val'), 'clear') shd.set(qn('w:color'), 'auto') shd.set(qn('w:fill'), hex_color) tcPr.append(shd) def cell_margins(table, top=45, bottom=45, left=80, right=80): tblPr = table._tbl.tblPr mar = OxmlElement('w:tblCellMar') for tag, val in (('top', top), ('left', left), ('bottom', bottom), ('right', right)): el = OxmlElement('w:' + tag) el.set(qn('w:w'), str(val)) el.set(qn('w:type'), 'dxa') mar.append(el) tblPr.append(mar) def table_borders(table, color='D9E2DD', size=4): tblPr = table._tbl.tblPr borders = OxmlElement('w:tblBorders') for edge in ('top', 'left', 'bottom', 'right', 'insideH', 'insideV'): el = OxmlElement('w:' + edge) el.set(qn('w:val'), 'single') el.set(qn('w:sz'), str(size)) el.set(qn('w:space'), '0') el.set(qn('w:color'), color) borders.append(el) tblPr.append(borders) def para_bottom_border(paragraph, color='2E7D5B', size=8): pPr = paragraph._p.get_or_add_pPr() pBdr = OxmlElement('w:pBdr') bottom = OxmlElement('w:bottom') bottom.set(qn('w:val'), 'single') bottom.set(qn('w:sz'), str(size)) bottom.set(qn('w:space'), '4') bottom.set(qn('w:color'), color) pBdr.append(bottom) pPr.append(pBdr) def para_left_border(paragraph, color='B03A55', size=18): pPr = paragraph._p.get_or_add_pPr() pBdr = OxmlElement('w:pBdr') left = OxmlElement('w:left') left.set(qn('w:val'), 'single') left.set(qn('w:sz'), str(size)) left.set(qn('w:space'), '8') left.set(qn('w:color'), color) pBdr.append(left) pPr.append(pBdr) def add_page_number(paragraph): run = paragraph.add_run() fld1 = OxmlElement('w:fldChar') fld1.set(qn('w:fldCharType'), 'begin') instr = OxmlElement('w:instrText') instr.set(qn('xml:space'), 'preserve') instr.text = 'PAGE' fld2 = OxmlElement('w:fldChar') fld2.set(qn('w:fldCharType'), 'end') run._r.append(fld1) run._r.append(instr) run._r.append(fld2) set_run(run, size=8, color=LIGHT_GRAY) def parse_inline(text): parts = re.split(r'(\*\*.*?\*\*|_.*?_)', text) out = [] for p in parts: if not p: continue if p.startswith('**') and p.endswith('**') and len(p) > 4: out.append((p[2:-2], True, False)) elif p.startswith('_') and p.endswith('_') and len(p) > 2: out.append((p[1:-1], False, True)) else: out.append((p, False, False)) return out def add_rich_paragraph(doc, text, style=None, size=10.5, color=DARK, bold=False, italic=False, align=None, space_before=0, space_after=4, left_indent=None, first_line_indent=None, line_spacing=1.15): p = doc.add_paragraph(style=style) if style else doc.add_paragraph() pf = p.paragraph_format pf.space_before = Pt(space_before) pf.space_after = Pt(space_after) pf.line_spacing = line_spacing if align is not None: p.alignment = align if left_indent is not None: pf.left_indent = Cm(left_indent) if first_line_indent is not None: pf.first_line_indent = Cm(first_line_indent) for chunk, b, i in parse_inline(text): r = p.add_run(chunk) set_run(r, size=size, bold=(bold or b), italic=(italic or i), color=color) return p def add_heading(doc, text, level): p = doc.add_paragraph() pf = p.paragraph_format pf.keep_with_next = True if level == 1: pf.space_before = Pt(14) pf.space_after = Pt(5) r = p.add_run(text) set_run(r, name='Georgia', size=15, bold=True, color=GREEN) para_bottom_border(p, color='CFE3D9', size=6) else: pf.space_before = Pt(10) pf.space_after = Pt(3) r = p.add_run(text) set_run(r, name='Arial', size=11.5, bold=True, color=DARK) return p def add_table(doc, rows, keep_together=True): ncols = len(rows[0]) table = doc.add_table(rows=len(rows), cols=ncols) table.alignment = 1 table.autofit = True table_borders(table) cell_margins(table) for ri, row in enumerate(rows): tr = table.rows[ri]._tr trPr = tr.get_or_add_trPr() cantSplit = OxmlElement('w:cantSplit') trPr.append(cantSplit) if ri == 0: tblHeader = OxmlElement('w:tblHeader') trPr.append(tblHeader) for ci, cell_text in enumerate(row): cell = table.cell(ri, ci) cell.text = '' p = cell.paragraphs[0] p.paragraph_format.space_before = Pt(0) p.paragraph_format.space_after = Pt(0) p.paragraph_format.line_spacing = 1.0 if keep_together and ri < len(rows) - 1: p.paragraph_format.keep_with_next = True for chunk, b, i in parse_inline(cell_text): r = p.add_run(chunk) if ri == 0: set_run(r, size=8.8, bold=True, color=GREEN) else: set_run(r, size=8.8, bold=b, italic=i, color=DARK) if ri == 0: shade(cell, 'EAF4EF') elif ri % 2 == 0: shade(cell, 'F7FAF8') doc.add_paragraph().paragraph_format.space_after = Pt(2) return table def build(): doc = Document() # Page setup section = doc.sections[0] section.page_width = Cm(21.0) section.page_height = Cm(29.7) section.top_margin = Cm(2.0) section.bottom_margin = Cm(2.0) section.left_margin = Cm(2.2) section.right_margin = Cm(2.2) # Base styles normal = doc.styles['Normal'] normal.font.name = 'Arial' normal.font.size = Pt(10.5) normal.font.color.rgb = DARK normal.paragraph_format.line_spacing = 1.15 normal.paragraph_format.space_after = Pt(4) # Footer footer_p = section.footer.paragraphs[0] footer_p.alignment = WD_ALIGN_PARAGRAPH.RIGHT r = footer_p.add_run('Метод Дегтярева • Форум молодёжи Донбасса • ') set_run(r, size=8, color=LIGHT_GRAY) add_page_number(footer_p) # Cover block p = doc.add_paragraph() p.paragraph_format.space_after = Pt(2) r = p.add_run('ФОРУМ МОЛОДЁЖИ ДОНБАССА «ФМД: У НАС ТАК ПРИНЯТО»') set_run(r, size=8.5, bold=True, color=GRAY) p = doc.add_paragraph() p.paragraph_format.space_before = Pt(10) p.paragraph_format.space_after = Pt(2) r = p.add_run('ТРЕК «МЕТОД ДЕГТЯРЕВА»') set_run(r, name='Georgia', size=26, bold=True, color=DARK) p = doc.add_paragraph() p.paragraph_format.space_after = Pt(8) r = p.add_run('Цифра, у которой есть хозяин') set_run(r, name='Georgia', size=15, italic=True, color=GREEN) p = doc.add_paragraph() p.paragraph_format.space_after = Pt(1) r = p.add_run('Смена 1 «ПСБ Патриот» • 10–13 ноября 2026') set_run(r, size=9.5, color=GRAY) p = doc.add_paragraph() p.paragraph_format.space_after = Pt(10) r = p.add_run('Эксперт и наставник трека: Бойко К.С.') set_run(r, size=9.5, color=GRAY) para_bottom_border(p, color='2E7D5B', size=10) # Parse markdown from the first content heading with open(SRC, 'r', encoding='utf-8') as f: lines = f.read().splitlines() start = 0 for i, line in enumerate(lines): if line.strip().startswith('## Короткая версия'): start = i break i = start while i < len(lines): line = lines[i] stripped = line.strip() if not stripped: i += 1 continue if stripped == '---': p = doc.add_paragraph() p.paragraph_format.space_before = Pt(2) p.paragraph_format.space_after = Pt(2) para_bottom_border(p, color='E3E8E5', size=4) i += 1 continue if stripped.startswith('### '): add_heading(doc, stripped[4:].strip(), 2) i += 1 continue if stripped.startswith('## '): add_heading(doc, stripped[3:].strip(), 1) i += 1 continue if stripped.startswith('# '): add_heading(doc, stripped[2:].strip(), 1) i += 1 continue # Table if stripped.startswith('|') and stripped.endswith('|'): rows = [] while i < len(lines): s = lines[i].strip() if not (s.startswith('|') and s.endswith('|')): break cells = [c.strip() for c in s.strip('|').split('|')] if all(re.fullmatch(r':?-{2,}:?', c) for c in cells): i += 1 continue rows.append(cells) i += 1 if rows: add_table(doc, rows, keep_together=(len(rows) <= 7)) continue # Blockquote if stripped.startswith('> '): text = stripped[2:].strip() p = add_rich_paragraph(doc, text, size=10.5, italic=True, color=GRAY, left_indent=0.8, space_before=2, space_after=6) para_left_border(p, color='B03A55', size=14) i += 1 continue # Bullet if stripped.startswith('- '): text = stripped[2:].strip() p = add_rich_paragraph(doc, '• ' + text, size=10.5, left_indent=0.55, first_line_indent=-0.55, space_after=2) i += 1 continue # Numbered list m = re.match(r'^(\d+)\.\s+(.*)$', stripped) if m: num, text = m.group(1), m.group(2) p = add_rich_paragraph(doc, f'{num}. ' + text, size=10.5, left_indent=0.6, first_line_indent=-0.6, space_after=2) i += 1 continue # Regular paragraph add_rich_paragraph(doc, stripped, size=10.5, space_after=4) i += 1 doc.save(OUT) print('saved', OUT) if __name__ == '__main__': build()