Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 20 additions & 8 deletions .github/scripts/check_output.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"""CI check of what `pixi run word` (and `pixi run pdf`) made.

python .github/scripts/check_output.py [concordia]
python .github/scripts/check_output.py [concordia|stanford]
"""
import glob
import re
Expand All @@ -12,18 +12,30 @@
xml = zipfile.ZipFile(docx[0]).read("word/document.xml").decode()
print("Word file:", docx[0])

if "concordia" in sys.argv[1:]:
sections = re.findall(r"<w:sectPr.*?</w:sectPr>", xml, re.S)
def sections():
found = []
for s in sections:
for s in re.findall(r"<w:sectPr.*?</w:sectPr>", xml, re.S):
fmt = re.search(r'w:fmt="(\w+)"', s)
start = re.search(r'w:start="(\d+)"', s)
found.append((fmt.group(1) if fmt else "decimal", start.group(1) if start else None, "footerReference" in s))
expected = [("lowerRoman", "1", False), ("lowerRoman", "3", True), ("decimal", "1", True)]
assert found == expected, f"sections {found}, expected {expected}"
left = re.search(r'w:left="(\d+)"', s)
found.append((fmt.group(1) if fmt else "decimal", start.group(1) if start else None,
"footerReference" in s, left.group(1) if left else None))
return found


if "concordia" in sys.argv[1:]:
expected = [("lowerRoman", "1", False, "1440"), ("lowerRoman", "3", True, "1440"), ("decimal", "1", True, "1440")]
assert sections() == expected, f"sections {sections()}, expected {expected}"
for text in ["SCHOOL OF GRADUATE STUDIES", "Abstract", "Table of Contents"]:
assert text in xml, f"front pages lack {text!r}"
print("Concordia sections: title/signature unnumbered, front pages from iii, chapters from 1")
print("Concordia: title/signature unnumbered, front pages from iii, chapters from 1")

if "stanford" in sys.argv[1:]:
expected = [("lowerRoman", "1", False, "2160"), ("lowerRoman", "4", True, "2160"), ("decimal", "1", True, "2160")]
assert sections() == expected, f"sections {sections()}, expected {expected}"
for text in ["AND THE COMMITTEE ON GRADUATE STUDIES", "Abstract", "Table of Contents", "List of Illustrations"]:
assert text in xml, f"front pages lack {text!r}"
print("Stanford: title page unnumbered, front pages from iv, chapters from 1, inner margin 1.5 in")

pdfs = glob.glob("_book/*.pdf")
if pdfs:
Expand Down
13 changes: 10 additions & 3 deletions .github/workflows/check.yml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
name: Check

# Builds the example thesis on Windows, macOS and Linux, plain and with the
# Concordia profile (doctoral everywhere, master's on Linux), and the PDF/A
# Concordia (doctoral everywhere, master's on Linux) and Stanford profiles, and the PDF/A
# on Linux and Windows. Runs only in the template itself, not in theses made
# from it.
on:
Expand All @@ -17,7 +17,7 @@ jobs:
fail-fast: false
matrix:
os: [windows-latest, macos-latest, ubuntu-latest]
thesis: [plain, concordia-doctoral, concordia-masters]
thesis: [plain, concordia-doctoral, concordia-masters, stanford]
exclude:
- { os: windows-latest, thesis: concordia-masters }
- { os: macos-latest, thesis: concordia-masters }
Expand All @@ -41,6 +41,13 @@ jobs:
pixi run concordia
echo "QUARTO_PROFILE=concordia" >> "$GITHUB_ENV"

- name: Stanford profile
if: matrix.thesis == 'stanford'
shell: bash
run: |
pixi run stanford
echo "QUARTO_PROFILE=stanford" >> "$GITHUB_ENV"

- name: Master's
if: matrix.thesis == 'concordia-masters'
shell: bash
Expand All @@ -65,7 +72,7 @@ jobs:

- name: Check what was made
shell: bash
run: pixi run python .github/scripts/check_output.py ${{ startsWith(matrix.thesis, 'concordia') && 'concordia' || '' }}
run: pixi run python .github/scripts/check_output.py ${{ startsWith(matrix.thesis, 'concordia') && 'concordia' || matrix.thesis == 'stanford' && 'stanford' || '' }}

- uses: actions/upload-artifact@v4
with:
Expand Down
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,7 @@ _book/
# Built on your computer from Concordia's official template by `pixi run concordia`.
universities/concordia/reference.docx
universities/concordia/footer.txt

# Built on your computer from Stanford's format rules by `pixi run stanford`.
universities/stanford/reference.docx
universities/stanford/footer.txt
10 changes: 10 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ template.
| `pixi run word` | The whole thesis as one Word file in `template.docx`, in `_book/` |
| `pixi run preview` | A live preview in the browser |
| `pixi run pdf` | The Word file as PDF/A, contents and lists filled in (needs LibreOffice) |
| `pixi run stanford` | Once, for a Stanford dissertation: builds `universities/stanford/reference.docx` from Stanford's format rules (`-- 1.5` for one-and-a-half spacing) |
| `pixi run concordia` | Once, for a Concordia thesis: builds `universities/concordia/reference.docx` from Concordia's official template |

## Moving their chapters in
Expand Down Expand Up @@ -55,6 +56,15 @@ template.
4. `pixi run word`, then `pixi run pdf`. Check the page numbers with them:
title and signature pages unnumbered, abstract iii, first chapter 1.

## A Stanford University dissertation

1. `pixi run stanford`; uncomment the profile in `_quarto.yml` with `stanford`.
2. Fill in `_quarto-stanford.yml` with the author: title, `submitted-to` (their
department, program or school), degree, name, month and year of
electronic submission, abstract, optional pages. Ask; don't invent.
3. `pixi run word`, then `pixi run pdf`. The file must not contain a copyright
or signature page (Axess adds them): the abstract is page iv.

## Known limits, to say plainly

- The template gives the Word file its styles, margins, headers and page
Expand Down
23 changes: 22 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ and the chapters from 1.

1. `pixi run concordia`, once. It downloads Concordia's official template from
concordia.ca and builds the styles from it on your computer.
2. In `_quarto.yml`, uncomment `profile:` and `default: concordia`.
2. In `_quarto.yml`, uncomment `profile:` and `default:`, with `concordia`.
3. In `_quarto-concordia.yml`, fill in your degree (`doctoral` or `masters`),
title, department, committee, abstract and the optional pages.
4. `pixi run word`, then `pixi run pdf` for the PDF/A you submit.
Expand All @@ -65,6 +65,27 @@ Check the result against the [Thesis Preparation Guide](https://www.concordia.ca
this starter follows Concordia's templates but isn't made or checked by
Concordia.

## A Stanford University dissertation

Stanford publishes [format rules](https://studentservices.stanford.edu/my-academics/earn-my-degree/graduate-degree-progress/dissertations-and-theses/prepare-your-work-0)
rather than a template. The Stanford profile follows them: the title page laid
out as Stanford's specimen pages (counts as i, not numbered), the abstract
and other front pages in roman numerals from iv (Axess adds the copyright
page ii and signature page iii itself, so your file must not contain them),
a table of contents and lists of tables and illustrations, chapters from 1;
a 1.5-inch inner margin and one inch elsewhere, Times New Roman 12 in black,
main text double-spaced.

1. `pixi run stanford`, once (`pixi run stanford -- 1.5` for one-and-a-half
spacing).
2. In `_quarto.yml`, uncomment `profile:` and `default:`, with `stanford`.
3. In `_quarto-stanford.yml`, fill in your title, department or program,
degree, name, month and year, abstract and the optional pages.
4. `pixi run word`, then `pixi run pdf`.

This follows Stanford's published rules but isn't made or checked by
Stanford; check the result against them before you submit.

## The PDF you submit

`pixi run pdf` turns the Word file into PDF/A, the archival PDF most
Expand Down
27 changes: 27 additions & 0 deletions _quarto-stanford.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# Stanford University dissertation: Stanford's format rules (title page,
# roman front pages from iv, chapters from 1; Axess adds the copyright and
# signature pages). Run `pixi run stanford` once, switch this profile on in
# _quarto.yml (profile: default: stanford), and fill in yours.
format:
docx:
reference-doc: universities/stanford/reference.docx
toc: false # the front pages carry the table of contents

filters:
- universities/stanford/front-pages.lua

stanford:
title: "Archives and Their Silences"
document: "dissertation" # D.M.A.: "final project"
submitted-to: "Department of History" # or "Graduate School of Business", "Program in ..."
degree: "Doctor of Philosophy"
author: "A. Researcher"
date: "April 2027" # month and year you submit electronically
abstract: |
Colonial archives were not neutral collections. This dissertation reads
two of them along and against their grain, and asks what their silences
can tell.
acknowledgments: |
To my advisor, and to the archivists who found the boxes.
list-of-tables: true
list-of-illustrations: true
5 changes: 3 additions & 2 deletions _quarto.yml
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
project:
type: book

# A Concordia University thesis: run `pixi run concordia` once, uncomment the
# two lines below, and fill in _quarto-concordia.yml.
# A university's own format: run `pixi run concordia` or `pixi run stanford`
# once, uncomment the two lines below with its name, and fill in
# _quarto-concordia.yml or _quarto-stanford.yml.
# profile:
# default: concordia

Expand Down
1 change: 1 addition & 0 deletions pixi.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,5 @@ word = { cmd = "quarto render --to docx", description = "The whole thesis as one
html = { cmd = "quarto render --to html", description = "The thesis as a website, in _book/" }
preview = { cmd = "quarto preview", description = "A live preview that updates as you save" }
pdf = { cmd = "python tools/pdf.py", description = "The Word file as PDF/A, contents filled in (needs LibreOffice)" }
stanford = { cmd = "python tools/stanford_template.py", description = "Once, for a Stanford dissertation: build the reference document from Stanford's format rules (add -- 1.5 for one-and-a-half spacing)" }
concordia = { cmd = "python tools/concordia_template.py", description = "Once, for a Concordia thesis: build the reference document from Concordia's official template" }
143 changes: 143 additions & 0 deletions tools/stanford_template.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
"""Build the Stanford reference document from Stanford's format rules.

pixi run stanford (main text double-spaced)
pixi run stanford -- 1.5 (one-and-a-half spacing)

Stanford publishes rules rather than a template:
https://studentservices.stanford.edu/my-academics/earn-my-degree/graduate-degree-progress/dissertations-and-theses/prepare-your-work-0
This starts from pandoc's own reference document and applies them:

letter paper; inner margin 1.5 in (left, single-sided), all others 1 in
page numbers centred at the foot, half an inch from the edge
Times New Roman 12 pt, black throughout (pandoc's headings are blue)
main text double or one-and-a-half spaced; footnotes, quotations,
tables, captions and the bibliography single-spaced
each chapter on a new page, chapters numbered from 1

It writes universities/stanford/reference.docx and footer.txt (which footer
carries the page number). The front pages are written by front-pages.lua
from _quarto-stanford.yml.
"""

import subprocess
import sys
from io import BytesIO
from pathlib import Path

from docx import Document
from docx.enum.style import WD_STYLE_TYPE
from docx.enum.text import WD_ALIGN_PARAGRAPH
from docx.oxml import OxmlElement
from docx.oxml.ns import qn
from docx.shared import Inches, Pt

ROOT = Path(__file__).resolve().parent.parent
OUT = ROOT / "universities" / "stanford"
FONT = "Times New Roman"
SINGLE = ["Footnote Text", "Block Text", "Compact", "Image Caption", "Table Caption",
"Captioned Figure", "Bibliography", "Figure", "Table"]


def pandoc_reference() -> Document:
data = subprocess.run(["pandoc", "--print-default-data-file", "reference.docx"],
capture_output=True, check=True).stdout
return Document(BytesIO(data))


def set_font(rpr) -> None:
"""Times New Roman in black, replacing theme fonts and colours."""
fonts = rpr.find(qn("w:rFonts"))
if fonts is None:
fonts = OxmlElement("w:rFonts")
rpr.insert(0, fonts)
for attr in list(fonts.attrib):
if attr.endswith("Theme"):
del fonts.attrib[attr]
for attr in ("w:ascii", "w:hAnsi", "w:cs", "w:eastAsia"):
fonts.set(qn(attr), FONT)
for color in rpr.findall(qn("w:color")):
rpr.remove(color)


def build(spacing: float) -> str:
doc = pandoc_reference()
styles = doc.styles

# Document defaults, then every style: one font, black.
defaults = styles.element.find(qn("w:docDefaults")).find(qn("w:rPrDefault")).find(qn("w:rPr"))
set_font(defaults)
for style in styles:
if style.type in (WD_STYLE_TYPE.PARAGRAPH, WD_STYLE_TYPE.CHARACTER, WD_STYLE_TYPE.TABLE):
set_font(style.element.get_or_add_rPr())

styles["Normal"].font.size = Pt(12)
for name in ["Body Text", "First Paragraph"]:
styles[name].paragraph_format.line_spacing = spacing
names = {s.name for s in styles}
for name in SINGLE:
if name in names:
styles[name].paragraph_format.line_spacing = 1.0

headings = {"Heading 1": (14, True, False), "Heading 2": (12, True, False), "Heading 3": (12, True, True)}
for name, (size, bold, italic) in headings.items():
s = styles[name]
s.font.size, s.font.bold, s.font.italic = Pt(size), bold, italic
s.paragraph_format.space_before, s.paragraph_format.space_after = Pt(18), Pt(12)
styles["Heading 1"].paragraph_format.alignment = WD_ALIGN_PARAGRAPH.CENTER
styles["Heading 1"].paragraph_format.page_break_before = True

def add(name, align, bold=False, size=12, after=12, line=None):
s = styles[name] if name in names else styles.add_style(name, WD_STYLE_TYPE.PARAGRAPH)
s.base_style = styles["Normal"]
s.quick_style = True
s.font.bold, s.font.size = bold, Pt(size)
s.paragraph_format.alignment = align
s.paragraph_format.space_before, s.paragraph_format.space_after = Pt(0), Pt(after)
s.paragraph_format.line_spacing = line or 1.0

# Front pages (front-pages.lua). The title page's lines are about 27 pt
# apart, as in Stanford's specimen title pages.
add("Title Page Line", WD_ALIGN_PARAGRAPH.CENTER, after=0, line=Pt(27))
add("Front Heading", WD_ALIGN_PARAGRAPH.CENTER, bold=True, size=14, after=18)

section = doc.sections[0]
section.page_width, section.page_height = Inches(8.5), Inches(11)
section.left_margin = Inches(1.5)
section.right_margin = section.top_margin = section.bottom_margin = Inches(1)
section.header_distance = section.footer_distance = Inches(0.5)

# A centred page number in the footer, counted from 1 in the chapters.
section.footer.is_linked_to_previous = False
paragraph = section.footer.paragraphs[0]
paragraph.alignment = WD_ALIGN_PARAGRAPH.CENTER
page = OxmlElement("w:fldSimple")
page.set(qn("w:instr"), "PAGE")
run = OxmlElement("w:r")
text = OxmlElement("w:t")
text.text = "1"
run.append(text)
page.append(run)
paragraph._p.append(page)
sect = section._sectPr
numbering = sect.find(qn("w:pgNumType"))
if numbering is None:
numbering = OxmlElement("w:pgNumType")
sect.append(numbering)
numbering.set(qn("w:start"), "1")

OUT.mkdir(parents=True, exist_ok=True)
doc.save(OUT / "reference.docx")
return sect.find(qn("w:footerReference")).get(qn("r:id"))


def main() -> None:
spacing = 1.5 if "1.5" in sys.argv[1:] else 2.0
rid = build(spacing)
(OUT / "footer.txt").write_text(rid + "\n", encoding="utf-8")
words = "one-and-a-half" if spacing == 1.5 else "double"
print(f"universities/stanford/reference.docx: Stanford's format rules, {words}-spaced (page numbers in {rid})")
print("Next: switch on the profile in _quarto.yml, fill in _quarto-stanford.yml, then `pixi run word`.")


if __name__ == "__main__":
main()
Loading
Loading