feat: добавлен скрипт генерации docx и итоговый документ svc-api-x.docx

This commit is contained in:
“Naeel”
2026-05-14 07:24:02 +04:00
parent 9327f22ef5
commit fda17c9cbf
2 changed files with 275 additions and 0 deletions
+275
View File
@@ -0,0 +1,275 @@
"""
Генерирует svc-api-x.docx из results/source-bundle.md
Что делает:
1. Читает results/source-bundle.md — все исходники склеены в один MD-файл.
2. Парсит его: разбивает по заголовкам ## (каждый заголовок = один исходный файл).
3. Строит DOCX:
- Титульная страница (название, подзаголовок, дата)
- Оглавление (поле Word TOC — пользователь жмёт правую кнопку → Обновить поле)
- 47 разделов с кодом (заголовок файла + код моноширинным шрифтом на сером фоне)
- Номер страницы в нижнем колонтитуле
Запуск:
/home/naeel/svc-api-x/.venv/bin/python build_docx.py
Как получить оглавление с номерами страниц (после открытия в Word):
1. Открыть svc-api-x.docx в Microsoft Word
2. Найти страницу с оглавлением (вторая страница, после титула)
3. Кликнуть ПРАВОЙ кнопкой мыши на текст оглавления
4. В контекстном меню выбрать «Обновить поле»
5. В диалоге выбрать «Обновить всё» → OK
6. Номера страниц проставятся автоматически
7. Сохранить (Ctrl+S)
В LibreOffice Writer: правая кнопка на оглавлении → «Обновить указатель»
Настройки — что и где менять:
BUNDLE_PATH / OUTPUT_PATH — пути к входному MD и выходному DOCX (ниже)
setup_styles() — шрифт и размер заголовков файлов (Heading 1) и базового текста
set_font() — шрифт и размер кода (по умолчанию Courier New 8pt)
build() — отступы/поля страницы (Cm), интервалы (Pt),
цвет заголовков (RGBColor), фон кода ('F2F2F2')
Титульная страница — текст, размер и цвет в блоке «Титульная страница» в build()
"""
import re
from docx import Document
from docx.shared import Pt, Cm, RGBColor
from docx.enum.text import WD_ALIGN_PARAGRAPH
from docx.oxml.ns import qn
from docx.oxml import OxmlElement
BUNDLE_PATH = '/home/naeel/svc-api-x/results/source-bundle.md' # входной файл
OUTPUT_PATH = '/home/naeel/svc-api-x/results/svc-api-x.docx' # выходной файл
# ---------------------------------------------------------------------------
# Парсинг
# ---------------------------------------------------------------------------
def parse_bundle(path):
with open(path, 'r', encoding='utf-8') as f:
content = f.read()
# Первый заголовок # — мета-блок
parts = re.split(r'^## (.+)$', content, flags=re.MULTILINE)
meta = parts[0].strip()
sections = []
for i in range(1, len(parts), 2):
filename = parts[i].strip()
body = parts[i + 1] if i + 1 < len(parts) else ''
code_match = re.search(r'```(?:\w+)?\n(.*?)```', body, re.DOTALL)
code = code_match.group(1).rstrip('\n') if code_match else body.strip()
sections.append((filename, code))
return sections
# ---------------------------------------------------------------------------
# Вспомогательные функции
# ---------------------------------------------------------------------------
def set_font(run, name='Courier New', size=9, bold=False, color=None):
run.font.name = name
run.font.size = Pt(size)
run.font.bold = bold
if color:
run.font.color.rgb = RGBColor(*color)
def add_toc(doc):
"""Вставляет поле TOC (обновляется при открытии в Word)."""
paragraph = doc.add_paragraph()
paragraph.paragraph_format.space_before = Pt(0)
paragraph.paragraph_format.space_after = Pt(0)
run = paragraph.add_run()
fld_char_begin = OxmlElement('w:fldChar')
fld_char_begin.set(qn('w:fldCharType'), 'begin')
run._r.append(fld_char_begin)
instr_text = OxmlElement('w:instrText')
instr_text.set(qn('xml:space'), 'preserve')
instr_text.text = ' TOC \\o "1-1" \\h \\z \\u '
run._r.append(instr_text)
fld_char_separate = OxmlElement('w:fldChar')
fld_char_separate.set(qn('w:fldCharType'), 'separate')
run._r.append(fld_char_separate)
no_display = OxmlElement('w:t')
no_display.text = '[Правая кнопка → Обновить поле]'
run._r.append(no_display)
fld_char_end = OxmlElement('w:fldChar')
fld_char_end.set(qn('w:fldCharType'), 'end')
run._r.append(fld_char_end)
return paragraph
def add_page_break(doc):
p = doc.add_paragraph()
run = p.add_run()
br = OxmlElement('w:br')
br.set(qn('w:type'), 'page')
run._r.append(br)
p.paragraph_format.space_before = Pt(0)
p.paragraph_format.space_after = Pt(0)
# ---------------------------------------------------------------------------
# Стили
# ---------------------------------------------------------------------------
def add_page_numbers_to_footer(doc):
"""Добавляет номер страницы в нижний колонтитул."""
for section in doc.sections:
footer = section.footer
footer.is_linked_to_previous = False
para = footer.paragraphs[0] if footer.paragraphs else footer.add_paragraph()
para.clear()
para.alignment = WD_ALIGN_PARAGRAPH.CENTER
run = para.add_run()
run.font.name = 'Arial'
run.font.size = Pt(9)
run.font.color.rgb = RGBColor(0x88, 0x88, 0x88)
# Поле PAGE
fld = OxmlElement('w:fldChar')
fld.set(qn('w:fldCharType'), 'begin')
run._r.append(fld)
instr = OxmlElement('w:instrText')
instr.set(qn('xml:space'), 'preserve')
instr.text = ' PAGE '
run._r.append(instr)
fld2 = OxmlElement('w:fldChar')
fld2.set(qn('w:fldCharType'), 'end')
run._r.append(fld2)
def setup_styles(doc):
styles = doc.styles
# Heading 1 — имя файла
h1 = styles['Heading 1']
h1.font.name = 'Arial'
h1.font.size = Pt(13)
h1.font.bold = True
h1.font.color.rgb = RGBColor(0x1F, 0x39, 0x64) # тёмно-синий
h1.paragraph_format.space_before = Pt(18)
h1.paragraph_format.space_after = Pt(6)
h1.paragraph_format.keep_with_next = True
# Normal — базовый
normal = styles['Normal']
normal.font.name = 'Arial'
normal.font.size = Pt(11)
# ---------------------------------------------------------------------------
# Построение документа
# ---------------------------------------------------------------------------
def build(sections):
doc = Document()
# Поля страницы
for section in doc.sections:
section.top_margin = Cm(2.5)
section.bottom_margin = Cm(2.5)
section.left_margin = Cm(2.5)
section.right_margin = Cm(2.0)
setup_styles(doc)
add_page_numbers_to_footer(doc)
# ── Титульная страница ──────────────────────────────────────────────────
title_p = doc.add_paragraph()
title_p.alignment = WD_ALIGN_PARAGRAPH.CENTER
title_p.paragraph_format.space_before = Pt(120)
title_p.paragraph_format.space_after = Pt(12)
tr = title_p.add_run('svc-api-x — Исходный код')
tr.font.name = 'Arial'
tr.font.size = Pt(22)
tr.font.bold = True
tr.font.color.rgb = RGBColor(0x1F, 0x39, 0x64)
sub_p = doc.add_paragraph()
sub_p.alignment = WD_ALIGN_PARAGRAPH.CENTER
sub_p.paragraph_format.space_after = Pt(6)
sr = sub_p.add_run('Прикладной слой · v1/ (без taffy)')
sr.font.name = 'Arial'
sr.font.size = Pt(13)
sr.font.color.rgb = RGBColor(0x55, 0x55, 0x55)
date_p = doc.add_paragraph()
date_p.alignment = WD_ALIGN_PARAGRAPH.CENTER
dr = date_p.add_run('Дата сборки: 2026-04-30')
dr.font.name = 'Arial'
dr.font.size = Pt(11)
dr.font.color.rgb = RGBColor(0x88, 0x88, 0x88)
add_page_break(doc)
# ── Оглавление ──────────────────────────────────────────────────────────
toc_title = doc.add_paragraph()
toc_title.alignment = WD_ALIGN_PARAGRAPH.LEFT
toc_title.paragraph_format.space_before = Pt(0)
toc_title.paragraph_format.space_after = Pt(12)
ttr = toc_title.add_run('ОГЛАВЛЕНИЕ')
ttr.font.name = 'Arial'
ttr.font.size = Pt(14)
ttr.font.bold = True
ttr.font.color.rgb = RGBColor(0x1F, 0x39, 0x64)
add_toc(doc)
add_page_break(doc)
# ── Исходники ────────────────────────────────────────────────────────────
for filename, code in sections:
# Заголовок файла (Heading 1 — попадает в TOC)
doc.add_heading(filename, level=1)
if not code:
p = doc.add_paragraph()
r = p.add_run('(файл пуст)')
r.font.name = 'Arial'
r.font.size = Pt(10)
r.font.italic = True
r.font.color.rgb = RGBColor(0x99, 0x99, 0x99)
continue
# Код — параграф с моноширинным шрифтом, закрашенный фон через XML
# Разбиваем по строкам, одним параграфом с переносами
lines = code.split('\n')
# Шапка блока
code_para = doc.add_paragraph()
code_para.paragraph_format.space_before = Pt(2)
code_para.paragraph_format.space_after = Pt(8)
code_para.paragraph_format.left_indent = Cm(0.5)
# Серый фон через shading
pPr = code_para._p.get_or_add_pPr()
shd = OxmlElement('w:shd')
shd.set(qn('w:val'), 'clear')
shd.set(qn('w:color'), 'auto')
shd.set(qn('w:fill'), 'F2F2F2')
pPr.append(shd)
for idx, line in enumerate(lines):
if idx > 0:
run = code_para.add_run()
br = OxmlElement('w:br')
run._r.append(br)
run = code_para.add_run(line)
set_font(run, name='Courier New', size=8)
doc.save(OUTPUT_PATH)
print(f'Готово: {OUTPUT_PATH}')
print(f'Файлов в документе: {len(sections)}')
if __name__ == '__main__':
sections = parse_bundle(BUNDLE_PATH)
build(sections)
Binary file not shown.