Files
fmd26/build_docx.py
T

352 lines
11 KiB
Python

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