ckb/.agents/skills/clouddrift-docx/scripts/build_reference.py
2026-07-15 09:54:12 +02:00

250 lines
8.5 KiB
Python

#!/usr/bin/env python3
"""Build the Cloud Drift pandoc reference.docx from pandoc's default reference doc."""
import copy
from docx import Document
from docx.shared import Pt, Inches, RGBColor, Emu
from docx.enum.text import WD_ALIGN_PARAGRAPH
from docx.oxml.ns import qn
from docx.oxml import OxmlElement
SRC = "pandoc-default-reference.docx"
OUT = "clouddrift-reference.docx"
LOGO = "clouddrift-logo.png"
FIRE_OPAL = RGBColor(0xE4, 0x52, 0x49)
WHITE_COFFEE = RGBColor(0xE8, 0xDC, 0xD0)
RAISIN = RGBColor(0x25, 0x25, 0x25)
MUTED = RGBColor(0x59, 0x59, 0x59)
LIGHT = "Open Sans Light"
REGULAR = "Open Sans"
def strip_theme_attrs(rpr_or_font_element):
"""Remove theme-based color/font attrs so explicit values always win."""
if rpr_or_font_element is None:
return
for tag in ("color",):
el = rpr_or_font_element.find(qn(f"w:{tag}"))
if el is not None:
for attr in ("themeColor", "themeTint", "themeShade"):
if el.get(qn(f"w:{attr}")) is not None:
del el.attrib[qn(f"w:{attr}")]
rfonts = rpr_or_font_element.find(qn("w:rFonts"))
if rfonts is not None:
for attr in ("asciiTheme", "hAnsiTheme", "eastAsiaTheme", "cstheme"):
if rfonts.get(qn(f"w:{attr}")) is not None:
del rfonts.attrib[qn(f"w:{attr}")]
def set_style(doc, name, font_name=None, size=None, color=None, bold=None, italic=None):
style = doc.styles[name]
f = style.font
if font_name:
f.name = font_name
rpr = style.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"), font_name)
rfonts.set(qn("w:hAnsi"), font_name)
rfonts.set(qn("w:cs"), font_name)
rfonts.set(qn("w:eastAsia"), font_name)
if size:
f.size = Pt(size)
if color:
f.color.rgb = color
if bold is not None:
f.bold = bold
if italic is not None:
f.italic = italic
strip_theme_attrs(style.element.get_or_add_rPr())
def disable_keep_with_next(doc, names):
"""Remove keepNext/keepLines from these styles' pPr.
Pandoc's default reference doc sets keepNext+keepLines on every Heading
style (standard Word behavior: never orphan a heading alone at the
bottom of a page). But when the very next block is a large table, this
can backfire badly under some renderers (observed in Pages): the
heading gets stranded alone on a page and the entire table is pushed to
the following page, leaving a mostly-blank page in between. Dropping
keepNext/keepLines lets pagination flow naturally instead - worst case
a heading ends up as the last line on a page, which is a far smaller
cosmetic cost than a near-empty page.
"""
for name in names:
style = doc.styles[name]
pPr = style.element.get_or_add_pPr()
for tag in ("w:keepNext", "w:keepLines"):
el = pPr.find(qn(tag))
if el is not None:
pPr.remove(el)
def add_field(paragraph, field_code):
run = paragraph.add_run()
r = run._r
fld_begin = OxmlElement("w:fldChar")
fld_begin.set(qn("w:fldCharType"), "begin")
instr = OxmlElement("w:instrText")
instr.set(qn("xml:space"), "preserve")
instr.text = f" {field_code} "
fld_sep = OxmlElement("w:fldChar")
fld_sep.set(qn("w:fldCharType"), "separate")
fld_end = OxmlElement("w:fldChar")
fld_end.set(qn("w:fldCharType"), "end")
r.append(fld_begin)
r2 = paragraph.add_run()._r
r2.append(instr)
r3 = paragraph.add_run()._r
r3.append(fld_sep)
r4 = paragraph.add_run()._r
r4.append(fld_end)
def main():
doc = Document(SRC)
# --- Base body text ---
set_style(doc, "Normal", LIGHT, 11, RAISIN)
set_style(doc, "Body Text", LIGHT, 11, RAISIN)
set_style(doc, "Compact", LIGHT, 11, RAISIN)
set_style(doc, "First Paragraph", LIGHT, 11, RAISIN)
set_style(doc, "Default Paragraph Font", LIGHT, 11, RAISIN)
# --- Title / subtitle ---
set_style(doc, "Title", LIGHT, 30, FIRE_OPAL, bold=False)
doc.styles["Title"].paragraph_format.space_after = Pt(4)
set_style(doc, "Subtitle", LIGHT, 14, MUTED, bold=False, italic=False)
# --- Headings ---
set_style(doc, "Heading 1", LIGHT, 22, RAISIN, bold=False)
doc.styles["Heading 1"].paragraph_format.space_before = Pt(20)
doc.styles["Heading 1"].paragraph_format.space_after = Pt(8)
set_style(doc, "Heading 2", REGULAR, 16, FIRE_OPAL, bold=True)
doc.styles["Heading 2"].paragraph_format.space_before = Pt(16)
doc.styles["Heading 2"].paragraph_format.space_after = Pt(6)
set_style(doc, "Heading 3", REGULAR, 13, RAISIN, bold=True)
doc.styles["Heading 3"].paragraph_format.space_before = Pt(12)
for lvl, sz in ((4, 11.5), (5, 11), (6, 11)):
name = f"Heading {lvl}"
set_style(doc, name, REGULAR, sz, MUTED, bold=True, italic=(lvl == 6))
# Avoid huge empty-page gaps when a heading is immediately followed by
# a large table (see disable_keep_with_next docstring).
disable_keep_with_next(doc, [f"Heading {n}" for n in range(1, 10)])
# --- Quotes / block text ---
set_style(doc, "Block Text", LIGHT, 11, MUTED, italic=True)
bt_pPr = doc.styles["Block Text"].element.get_or_add_pPr()
pbdr = OxmlElement("w:pBdr")
left = OxmlElement("w:left")
left.set(qn("w:val"), "single")
left.set(qn("w:sz"), "18")
left.set(qn("w:space"), "8")
left.set(qn("w:color"), "E45249")
pbdr.append(left)
bt_pPr.append(pbdr)
# --- Hyperlinks ---
set_style(doc, "Hyperlink", LIGHT, None, FIRE_OPAL)
hl_rpr = doc.styles["Hyperlink"].element.get_or_add_rPr()
u = OxmlElement("w:u")
u.set(qn("w:val"), "single")
hl_rpr.append(u)
# --- Table: shaded header row, light borders ---
table_style = doc.styles["Table"]
tbl_pr = table_style.element.find(qn("w:tblPr"))
if tbl_pr is None:
tbl_pr = OxmlElement("w:tblPr")
table_style.element.append(tbl_pr)
borders = OxmlElement("w:tblBorders")
for edge in ("top", "left", "bottom", "right", "insideH", "insideV"):
el = OxmlElement(f"w:{edge}")
el.set(qn("w:val"), "single")
el.set(qn("w:sz"), "4")
el.set(qn("w:space"), "0")
el.set(qn("w:color"), "E8DCD0")
borders.append(el)
tbl_pr.append(borders)
style_pr = table_style.element.find(qn("w:tblStylePr"))
if style_pr is None:
style_pr = OxmlElement("w:tblStylePr")
style_pr.set(qn("w:type"), "firstRow")
table_style.element.append(style_pr)
tc_pr = style_pr.find(qn("w:tcPr"))
if tc_pr is None:
tc_pr = OxmlElement("w:tcPr")
style_pr.append(tc_pr)
shd = OxmlElement("w:shd")
shd.set(qn("w:val"), "clear")
shd.set(qn("w:color"), "auto")
shd.set(qn("w:fill"), "E8DCD0")
tc_pr.append(shd)
rpr_fr = style_pr.find(qn("w:rPr"))
if rpr_fr is None:
rpr_fr = OxmlElement("w:rPr")
style_pr.append(rpr_fr)
b_el = OxmlElement("w:b")
rpr_fr.append(b_el)
color_el = OxmlElement("w:color")
color_el.set(qn("w:val"), "252525")
rpr_fr.append(color_el)
# --- Verbatim / code ---
try:
set_style(doc, "Verbatim Char", None, 10, RAISIN)
except KeyError:
pass
# --- Page setup: A4, 1 inch margins ---
section = doc.sections[0]
section.page_width = Inches(8.27)
section.page_height = Inches(11.69)
section.left_margin = Inches(1)
section.right_margin = Inches(1)
section.top_margin = Inches(1)
section.bottom_margin = Inches(1)
section.header_distance = Inches(0.4)
section.footer_distance = Inches(0.4)
# --- Header: Cloud Drift logo ---
header = section.header
header.is_linked_to_previous = False
hp = header.paragraphs[0]
hp.text = ""
hp.alignment = WD_ALIGN_PARAGRAPH.LEFT
run = hp.add_run()
run.add_picture(LOGO, width=Inches(0.85))
# --- Footer: page number, right aligned, muted ---
footer = section.footer
footer.is_linked_to_previous = False
fp = footer.paragraphs[0]
fp.text = ""
fp.alignment = WD_ALIGN_PARAGRAPH.RIGHT
run = fp.add_run("Page ")
run.font.name = REGULAR
run.font.size = Pt(9)
run.font.color.rgb = MUTED
add_field(fp, "PAGE")
run2 = fp.add_run(" of ")
run2.font.name = REGULAR
run2.font.size = Pt(9)
run2.font.color.rgb = MUTED
add_field(fp, "NUMPAGES")
doc.save(OUT)
print("Saved", OUT)
if __name__ == "__main__":
main()