Files
svc-api-x/build_docx.py

339 lines
13 KiB
Python
Raw Permalink 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 — пользователь жмёт правую кнопку → Обновить поле)
- разделы с кодом (заголовок файла + код с подсветкой синтаксиса на сером фоне)
- Номер страницы в нижнем колонтитуле
Подсветка синтаксиса CFML (4 цвета):
синий — CF-теги (<cfif>, <cfset>, </cffunction>, ...)
зелёный — строковые значения ("...", '...')
фиолетовый — выражения в # (#variables#, #func()#)
тёмно-голубой — SQL-ключевые слова (SELECT, FROM, WHERE, JOIN, ...)
чёрный — всё остальное
Запуск:
/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 = 'results/source-bundle.md' # входной файл
OUTPUT_PATH = '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
# ---------------------------------------------------------------------------
# Вспомогательные функции
# ---------------------------------------------------------------------------
# ---------------------------------------------------------------------------
# Токенизатор CFML
# ---------------------------------------------------------------------------
# Цвета подсветки (RGB)
COL_TAG = (0x1A, 0x5F, 0xB4) # синий — CF-теги
COL_STR = (0x1E, 0x7E, 0x34) # зелёный — строки
COL_VAR = (0x6F, 0x31, 0x78) # фиолетовый — #переменные#
COL_SQL = (0x0D, 0x5C, 0x73) # тёмно-голубой — SQL
COL_DEF = None # чёрный (default)
_PATTERNS = [
('tag', re.compile(
r'</cf\w+|<cf\w+|</cfscript>|<cfscript>', re.IGNORECASE)),
('var', re.compile(
r'#[^#\n]+#')),
('str', re.compile(
r'"[^"\n]*"|' + r"'[^'\n]*'")),
('sql', re.compile(
r'\b(SELECT|FROM|WHERE|JOIN|LEFT|RIGHT|INNER|OUTER|ON|AND|OR|NOT|'
r'INSERT|INTO|UPDATE|DELETE|SET|GROUP|ORDER|BY|HAVING|LIMIT|OFFSET|'
r'AS|DISTINCT|COUNT|SUM|AVG|MAX|MIN|CAST|COALESCE|CASE|WHEN|THEN|'
r'ELSE|END|IS|NULL|IN|EXISTS|UNION|ALL)\b', re.IGNORECASE)),
]
_COLOR_MAP = {'tag': COL_TAG, 'str': COL_STR, 'var': COL_VAR,
'sql': COL_SQL, None: COL_DEF}
def tokenize_cfml(line):
"""Разбивает строку кода на список (text, color) с подсветкой CFML."""
tokens = []
pos = 0
length = len(line)
while pos < length:
best_match = None
best_kind = None
best_start = length
for kind, pat in _PATTERNS:
m = pat.search(line, pos)
if m and m.start() < best_start:
best_match = m
best_kind = kind
best_start = m.start()
if best_match is None:
tokens.append((line[pos:], COL_DEF))
break
if best_start > pos:
tokens.append((line[pos:best_start], COL_DEF))
tokens.append((best_match.group(0), _COLOR_MAP[best_kind]))
pos = best_match.end()
return tokens
def set_font(run, name='Courier New', size=8, 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:
br_run = code_para.add_run()
br = OxmlElement('w:br')
br_run._r.append(br)
set_font(br_run)
for text, color in tokenize_cfml(line):
run = code_para.add_run(text)
set_font(run, color=color)
doc.save(OUTPUT_PATH)
print(f'Готово: {OUTPUT_PATH}')
print(f'Файлов в документе: {len(sections)}')
if __name__ == '__main__':
sections = parse_bundle(BUNDLE_PATH)
build(sections)