Files
svc-api-x/build_docx.py
T

276 lines
11 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
Генерирует 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)