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