From c5b1d43da1e413f56c19e2e8eba3873bf9dcb459 Mon Sep 17 00:00:00 2001 From: Behzad Samadi Date: Sat, 3 Oct 2026 11:19:35 -0400 Subject: [PATCH] A Stanford University profile; shared front-page helpers Stanford publishes format rules, not a template. `pixi run stanford` builds the reference document from pandoc's own and those rules: letter paper, 1.5 in inner margin and 1 in elsewhere, page numbers centred half an inch from the edge, Times New Roman 12 in black (pandoc's headings are blue), main text double-spaced (or 1.5 with `-- 1.5`), footnotes, quotations, tables and captions single. universities/stanford/front-pages.lua writes the title page as Stanford's specimen pages lay it out (uppercase, centred, not bold, each line within 2 pt of the specimen), then the abstract from iv, since Axess adds the copyright page ii and signature page iii itself, the optional pages, the table of contents, the lists of tables and illustrations, and chapters from 1. universities/common.lua now holds what both universities share (section and page breaks, fields, optional pages, contents and lists); Concordia's output is unchanged but for two placeholder words that fields replace. CI builds the Stanford profile on all three systems and checks its sections and margin. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01AmzgbBq9swiYXP9W9tgbS2 --- .github/scripts/check_output.py | 28 +++-- .github/workflows/check.yml | 13 ++- .gitignore | 4 + AGENTS.md | 10 ++ README.md | 23 +++- _quarto-stanford.yml | 27 +++++ _quarto.yml | 5 +- pixi.toml | 1 + tools/stanford_template.py | 143 +++++++++++++++++++++++++ universities/common.lua | 97 +++++++++++++++++ universities/concordia/front-pages.lua | 110 ++++--------------- universities/stanford/front-pages.lua | 67 ++++++++++++ 12 files changed, 425 insertions(+), 103 deletions(-) create mode 100644 _quarto-stanford.yml create mode 100644 tools/stanford_template.py create mode 100644 universities/common.lua create mode 100644 universities/stanford/front-pages.lua diff --git a/.github/scripts/check_output.py b/.github/scripts/check_output.py index 129330e..91bcfb4 100644 --- a/.github/scripts/check_output.py +++ b/.github/scripts/check_output.py @@ -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 @@ -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"", xml, re.S) +def sections(): found = [] - for s in sections: + for s in re.findall(r"", 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: diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index b14bfa6..4b03e26 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -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: @@ -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 } @@ -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 @@ -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: diff --git a/.gitignore b/.gitignore index f1b5292..1685bc4 100644 --- a/.gitignore +++ b/.gitignore @@ -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 diff --git a/AGENTS.md b/AGENTS.md index c7afbad..8693a3b 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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 @@ -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 diff --git a/README.md b/README.md index ad6055b..5c6db6a 100644 --- a/README.md +++ b/README.md @@ -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. @@ -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 diff --git a/_quarto-stanford.yml b/_quarto-stanford.yml new file mode 100644 index 0000000..bc1ffa1 --- /dev/null +++ b/_quarto-stanford.yml @@ -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 diff --git a/_quarto.yml b/_quarto.yml index e7594eb..3b11fe6 100644 --- a/_quarto.yml +++ b/_quarto.yml @@ -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 diff --git a/pixi.toml b/pixi.toml index 2fb659a..c35188c 100644 --- a/pixi.toml +++ b/pixi.toml @@ -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" } diff --git a/tools/stanford_template.py b/tools/stanford_template.py new file mode 100644 index 0000000..7b0d2fb --- /dev/null +++ b/tools/stanford_template.py @@ -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() diff --git a/universities/common.lua b/universities/common.lua new file mode 100644 index 0000000..52a0848 --- /dev/null +++ b/universities/common.lua @@ -0,0 +1,97 @@ +--[[ +Shared by the universities' front-page filters (concordia/, stanford/). + +The front pages are built as pandoc blocks and, where pandoc has no +equivalent, raw Word XML: page breaks, section breaks (each carrying its own +page numbering) and fields (table of contents, lists of figures and tables). +Load with: local common = dofile(quarto.utils.resolve_path("../common.lua")) +]] + +local M = {} + +M.stringify = pandoc.utils.stringify + +function M.esc(s) + return (s:gsub("&", "&"):gsub("<", "<"):gsub(">", ">")) +end + +function M.raw(xml) return pandoc.RawBlock("openxml", xml) end + +M.PAGE_BREAK = M.raw('') + +-- reference.docx's page-number footer. pandoc keeps the reference's +-- relationship ids; `pixi run ` writes this one to footer.txt +-- beside the filter when it builds reference.docx. +function M.footer_rid(path, task) + local f = io.open(path, "r") + if not f then + error(path .. " is missing: run `pixi run " .. task .. "` first.") + end + local rid = f:read("*l") + f:close() + return rid +end + +-- Ends a section. `page` is its /, `numbering` its +-- , and `footer` the footer's relationship id, or nil for a +-- section whose pages show no number. +function M.section_break(page, numbering, footer) + return M.raw('' + .. (footer and ('') or '') + .. page .. numbering .. '') +end + +-- A paragraph in one of reference.docx's front-page styles. +function M.styled(style, inlines) + return pandoc.Div({ pandoc.Para(inlines) }, pandoc.Attr("", {}, { { "custom-style", style } })) +end + +function M.line(style, text) return M.styled(style, { pandoc.Str(text) }) end + +-- A Word field (table of contents, list of figures): Word fills it when the +-- document is opened; tools/pdf.py updates it before making the PDF. +function M.field(instruction, placeholder) + return M.raw('' + .. ' ' .. instruction .. ' ' + .. '' + .. '' .. M.esc(placeholder) .. '' + .. '') +end + +-- Settings text (a string, inlines or Markdown blocks) as blocks. +function M.blocks_of(value) + if value == nil then return nil end + if pandoc.utils.type(value) == "Blocks" then return value end + if pandoc.utils.type(value) == "Inlines" then return pandoc.Blocks({ pandoc.Para(value) }) end + return pandoc.Blocks({ pandoc.Para({ pandoc.Str(M.stringify(value)) }) }) +end + +-- A page that appears only when its setting is filled in. +function M.optional_page(out, heading_style, heading, value) + local content = M.blocks_of(value) + if not content then return end + out:insert(M.PAGE_BREAK) + out:insert(M.line(heading_style, heading)) + out:extend(content) +end + +-- The table of contents, then each list, every one on its own page. `lists` +-- is in the order the university wants, each { title, style } with style the +-- caption style it collects: pandoc writes "Image Caption" and "Table Caption". +function M.contents_and_lists(out, heading_style, contents_title, lists) + out:insert(M.PAGE_BREAK) + out:insert(M.line(heading_style, contents_title)) + out:insert(M.field('TOC \\o "1-3" \\h \\z \\u', "Table of contents: update fields to fill")) + for _, list in ipairs(lists) do + out:insert(M.PAGE_BREAK) + out:insert(M.line(heading_style, list.title)) + out:insert(M.field('TOC \\h \\z \\t "' .. list.style .. ',1"', list.title .. ": update fields to fill")) + end +end + +-- The front pages replace Quarto's title block. +function M.drop_title_block(meta) + for _, k in ipairs({ "title", "subtitle", "author", "date", "abstract" }) do meta[k] = nil end +end + +return M diff --git a/universities/concordia/front-pages.lua b/universities/concordia/front-pages.lua index d0d2f03..3b70cfd 100644 --- a/universities/concordia/front-pages.lua +++ b/universities/concordia/front-pages.lua @@ -10,48 +10,16 @@ _quarto-concordia.yml: table of contents, lists chapters arabic from 1 (reference.docx's own section) -The wording follows the official templates. Word sections do the numbering: -each front block ends with a section break carrying its own page setup. +The wording follows the official templates. ]] --- reference.docx's page-number footer: pandoc keeps the reference's --- relationship ids, and `pixi run concordia` writes this one to footer.txt --- when it builds reference.docx from Concordia's template. -local function footer_rid() - local f = io.open(quarto.utils.resolve_path("footer.txt"), "r") - if not f then - error("universities/concordia/footer.txt is missing: run `pixi run concordia` first.") - end - local rid = f:read("*l"); f:close() - return rid -end -local FOOTER_RID = footer_rid() +local common = dofile(quarto.utils.resolve_path("../common.lua")) +local stringify, esc, raw, line = common.stringify, common.esc, common.raw, common.line + +-- Concordia: letter paper, one-inch margins. local PAGE = '' .. '' -local stringify = pandoc.utils.stringify - -local function esc(s) - return (s:gsub("&", "&"):gsub("<", "<"):gsub(">", ">")) -end - -local function raw(xml) return pandoc.RawBlock("openxml", xml) end - -local PAGE_BREAK = raw('') - -local function section_break(numbering, footer) - return raw('' - .. (footer and ('') or '') - .. PAGE .. numbering .. '') -end - --- A paragraph in one of reference.docx's front-page styles. -local function styled(style, inlines) - return pandoc.Div({ pandoc.Para(inlines) }, pandoc.Attr("", {}, { { "custom-style", style } })) -end - -local function line(style, text) return styled(style, { pandoc.Str(text) }) end - -- One paragraph of text and tabs, as raw Word XML: "By:name". `stops` -- are tab positions in twentieths of a point (1440 = one inch); `before` is -- space above the paragraph, in the same unit. @@ -92,38 +60,13 @@ local function committee_table(members) .. table.concat(rows) .. '') end --- A Word field (table of contents, list of figures): Word fills it when --- the document is opened; tools/pdf.py updates it before making the PDF. -local function field(instruction, placeholder) - return raw('' - .. ' ' .. instruction .. ' ' - .. '' - .. '' .. esc(placeholder) .. '' - .. '') -end - -local function blocks_of(value) - if value == nil then return nil end - if pandoc.utils.type(value) == "Blocks" then return value end - if pandoc.utils.type(value) == "Inlines" then return pandoc.Blocks({ pandoc.Para(value) }) end - return pandoc.Blocks({ pandoc.Para({ pandoc.Str(stringify(value)) }) }) -end - -local function optional_page(out, heading, value) - local content = blocks_of(value) - if not content then return end - out:insert(PAGE_BREAK) - out:insert(line("Front Heading", heading)) - out:extend(content) -end - function Pandoc(doc) if not FORMAT:match("docx") then return nil end local c = doc.meta.concordia if not c then return nil end + local footer = common.footer_rid(quarto.utils.resolve_path("footer.txt"), "concordia") - local level = stringify(c["degree-level"] or "doctoral") - local doctoral = level ~= "masters" + local doctoral = stringify(c["degree-level"] or "doctoral") ~= "masters" local title, author = stringify(c.title or ""), stringify(c.author or "") local department, degree = stringify(c.department or ""), stringify(c.degree or "") local date, year = stringify(c.date or ""), stringify(c.year or "") @@ -149,7 +92,7 @@ function Pandoc(doc) out:insert(line("Front Centered", "© " .. author .. ", " .. year)) -- Signature page (ii, unnumbered). - out:insert(PAGE_BREAK) + out:insert(common.PAGE_BREAK) out:insert(line("Front Heading", "CONCORDIA UNIVERSITY")) out:insert(line("Front Heading", "SCHOOL OF GRADUATE STUDIES")) out:insert(line("Front Text", "This is to certify that the thesis prepared")) @@ -168,36 +111,25 @@ function Pandoc(doc) out:insert(tabbed({ "", director }, { 2160 })) out:insert(tabbed({ "__________________", "_______________________________________________" }, { 2880 }, 480)) out:insert(tabbed({ "Date", dean }, { 2880 })) - out:insert(section_break('', false)) + out:insert(common.section_break(PAGE, '', nil)) -- Abstract (iii) and the optional pages. out:insert(line("Front Heading", "Abstract")) out:insert(line("Front Centered", title)) out:insert(line("Front Centered", doctoral and (author .. ", " .. stringify(c["degree-short"] or "Ph.D.")) or author)) if doctoral then out:insert(line("Front Centered", "Concordia University, " .. year)) end - out:extend(blocks_of(c.abstract) or pandoc.Blocks({})) - optional_page(out, "Summary", c.summary) - optional_page(out, "Acknowledgments", c.acknowledgments) - optional_page(out, "Dedication", c.dedication) - optional_page(out, "Contribution of Authors", c["contribution-of-authors"]) - - out:insert(PAGE_BREAK) - out:insert(line("Front Heading", "Table of Contents")) - out:insert(field('TOC \\o "1-3" \\h \\z \\u', "Table of contents: update fields to fill")) - if c["list-of-figures"] ~= false then - out:insert(PAGE_BREAK) - out:insert(line("Front Heading", "List of Figures")) - out:insert(field('TOC \\h \\z \\t "Image Caption,1"', "List of figures: update fields to fill")) - end - if c["list-of-tables"] ~= false then - out:insert(PAGE_BREAK) - out:insert(line("Front Heading", "List of Tables")) - out:insert(field('TOC \\h \\z \\t "Table Caption,1"', "List of tables: update fields to fill")) - end - out:insert(section_break('', true)) - - -- The title page replaces Quarto's title block. - for _, k in ipairs({ "title", "subtitle", "author", "date", "abstract" }) do doc.meta[k] = nil end + out:extend(common.blocks_of(c.abstract) or pandoc.Blocks({})) + common.optional_page(out, "Front Heading", "Summary", c.summary) + common.optional_page(out, "Front Heading", "Acknowledgments", c.acknowledgments) + common.optional_page(out, "Front Heading", "Dedication", c.dedication) + common.optional_page(out, "Front Heading", "Contribution of Authors", c["contribution-of-authors"]) + local lists = {} + if c["list-of-figures"] ~= false then lists[#lists + 1] = { title = "List of Figures", style = "Image Caption" } end + if c["list-of-tables"] ~= false then lists[#lists + 1] = { title = "List of Tables", style = "Table Caption" } end + common.contents_and_lists(out, "Front Heading", "Table of Contents", lists) + out:insert(common.section_break(PAGE, '', footer)) + + common.drop_title_block(doc.meta) doc.blocks = out .. doc.blocks return doc end diff --git a/universities/stanford/front-pages.lua b/universities/stanford/front-pages.lua new file mode 100644 index 0000000..9b66c5f --- /dev/null +++ b/universities/stanford/front-pages.lua @@ -0,0 +1,67 @@ +--[[ +Stanford University front pages, for Word output. + +Builds the preliminary pages Stanford's format rules ask for, from the +`stanford:` settings in _quarto-stanford.yml: + + title page counts as i, not numbered + (copyright ii, signature iii) not in your file: Axess adds them + abstract, optional pages, lower-case roman from iv + table of contents, lists + chapters arabic from 1 (reference.docx's own section) + +The title page follows Stanford's specimen pages: uppercase, centred, not +bold, no page number. +]] + +local common = dofile(quarto.utils.resolve_path("../common.lua")) +local stringify, esc, raw, line = common.stringify, common.esc, common.raw, common.line + +-- Stanford: letter paper, inner (left) margin 1.5 in, all others 1 in, page +-- numbers half an inch from the edge. +local PAGE = '' + .. '' + +-- One line of the title page, `before` twentieths of a point below the last. +local function title_line(text, before) + return raw('' + .. '' .. esc(text:upper()) .. '') +end + +function Pandoc(doc) + if not FORMAT:match("docx") then return nil end + local s = doc.meta.stanford + if not s then return nil end + local footer = common.footer_rid(quarto.utils.resolve_path("footer.txt"), "stanford") + + local out = pandoc.Blocks({}) + + -- Title page (i, unnumbered), laid out as Stanford's specimens. + out:insert(title_line(stringify(s.title or ""))) + out:insert(title_line("A " .. stringify(s.document or "dissertation"), 2075)) + out:insert(title_line("Submitted to the " .. stringify(s["submitted-to"] or ""))) + out:insert(title_line("and the Committee on Graduate Studies")) + out:insert(title_line("of Stanford University")) + out:insert(title_line("in Partial Fulfillment of the Requirements")) + out:insert(title_line("for the Degree of")) + out:insert(title_line(stringify(s.degree or "Doctor of Philosophy"))) + out:insert(title_line(stringify(s.author or ""), 3185)) + out:insert(title_line(stringify(s.date or ""))) + out:insert(common.section_break(PAGE, '', nil)) + + -- Abstract (iv: Axess adds the copyright page ii and signature page iii). + out:insert(line("Front Heading", "Abstract")) + out:extend(common.blocks_of(s.abstract) or pandoc.Blocks({})) + common.optional_page(out, "Front Heading", "Preface", s.preface) + common.optional_page(out, "Front Heading", "Acknowledgments", s.acknowledgments) + common.optional_page(out, "Front Heading", "Dedication", s.dedication) + local lists = {} + if s["list-of-tables"] ~= false then lists[#lists + 1] = { title = "List of Tables", style = "Table Caption" } end + if s["list-of-illustrations"] ~= false then lists[#lists + 1] = { title = "List of Illustrations", style = "Image Caption" } end + common.contents_and_lists(out, "Front Heading", "Table of Contents", lists) + out:insert(common.section_break(PAGE, '', footer)) + + common.drop_title_block(doc.meta) + doc.blocks = out .. doc.blocks + return doc +end