#!/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()