250 lines
8.5 KiB
Python
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()
|