From 9c5796c1a13b1a92c66122c9ff7904c68c983c16 Mon Sep 17 00:00:00 2001 From: "Leandro G. Almeida" Date: Sat, 30 May 2026 16:17:47 -0700 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20template:=20add=20LaTeX=20paper=20s?= =?UTF-8?q?caffold?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/concepts/generated-project.md | 1 + docs/template-prompts.md | 6 +++ docs/template-structure.md | 2 + src/repoman/copier.yml | 5 +++ src/repoman/main_template/.gitignore.jinja | 4 ++ src/repoman/main_template/Makefile.jinja | 3 ++ src/repoman/main_template/README.md.jinja | 14 ++++++ .../docs/reference/architecture.md.jinja | 7 +++ ...if latex_paper %}paper.mk{% endif %}.jinja | 15 +++++++ .../.latexmkrc.jinja | 4 ++ .../Makefile.jinja | 24 +++++++++++ .../README.md.jinja | 22 ++++++++++ .../figures/.gitkeep.jinja | 1 + .../main.tex.jinja | 40 +++++++++++++++++ .../references.bib.jinja | 6 +++ .../sections/abstract.tex.jinja | 1 + .../sections/discussion.tex.jinja | 3 ++ .../sections/introduction.tex.jinja | 3 ++ .../sections/methods.tex.jinja | 3 ++ .../sections/results.tex.jinja | 3 ++ .../resources/copier_answers_template.yml | 3 ++ tests/fixtures/default_copier_answers.yml | 3 ++ tests/template_testing.py | 1 + tests/test_template/test_ci.py | 43 +++++++++++++++++++ 24 files changed, 217 insertions(+) create mode 100644 src/repoman/main_template/make_cmds/{% if latex_paper %}paper.mk{% endif %}.jinja create mode 100644 src/repoman/main_template/{% if latex_paper %}paper{% endif %}/.latexmkrc.jinja create mode 100644 src/repoman/main_template/{% if latex_paper %}paper{% endif %}/Makefile.jinja create mode 100644 src/repoman/main_template/{% if latex_paper %}paper{% endif %}/README.md.jinja create mode 100644 src/repoman/main_template/{% if latex_paper %}paper{% endif %}/figures/.gitkeep.jinja create mode 100644 src/repoman/main_template/{% if latex_paper %}paper{% endif %}/main.tex.jinja create mode 100644 src/repoman/main_template/{% if latex_paper %}paper{% endif %}/references.bib.jinja create mode 100644 src/repoman/main_template/{% if latex_paper %}paper{% endif %}/sections/abstract.tex.jinja create mode 100644 src/repoman/main_template/{% if latex_paper %}paper{% endif %}/sections/discussion.tex.jinja create mode 100644 src/repoman/main_template/{% if latex_paper %}paper{% endif %}/sections/introduction.tex.jinja create mode 100644 src/repoman/main_template/{% if latex_paper %}paper{% endif %}/sections/methods.tex.jinja create mode 100644 src/repoman/main_template/{% if latex_paper %}paper{% endif %}/sections/results.tex.jinja diff --git a/docs/concepts/generated-project.md b/docs/concepts/generated-project.md index 2235eac..7615585 100644 --- a/docs/concepts/generated-project.md +++ b/docs/concepts/generated-project.md @@ -18,3 +18,4 @@ Generated projects typically include: - **CLI:** If a CLI name is provided, the template generates a Typer-based CLI. - **FastAPI:** If FastAPI is enabled, the template generates `src//app/`. - **Datasets:** If dataset support is enabled, the template generates dataset config and the selected loaders under `src//datasets/`. +- **Paper:** If LaTeX paper support is enabled, the template generates a `paper/` manuscript scaffold with root `make paper` targets. diff --git a/docs/template-prompts.md b/docs/template-prompts.md index 06d73a3..3b3a6e5 100644 --- a/docs/template-prompts.md +++ b/docs/template-prompts.md @@ -33,6 +33,12 @@ Repoman prompts for: | ------------------ | --------------------------------- | ------- | | `python_notebooks` | Include Jupyter notebooks support | `false` | +### Paper + +| Prompt | Description | Default | +| ------------- | -------------------------------------------- | ------- | +| `latex_paper` | Include a LaTeX paper scaffold under `paper/` | `false` | + ### Datasets | Prompt | Description | Default | diff --git a/docs/template-structure.md b/docs/template-structure.md index d480ff6..4c6df75 100644 --- a/docs/template-structure.md +++ b/docs/template-structure.md @@ -8,6 +8,7 @@ - **Datasets:** If `dataset_enabled` is true, the template generates `config/dataset_config.json`, `src/{{ python_package_import_name }}/config/dataset_config.py`, and the selected dataset loaders/adapters under `src/{{ python_package_import_name }}/datasets/`. - **Docs-only:** If `docs_only` is true, source package and test files are omitted. - **Notebooks:** If `python_notebooks` is true, the template generates a `notebooks/` folder. +- **Paper:** If `latex_paper` is true, the template generates a `paper/` folder with a LaTeX manuscript scaffold and Make targets. - **C++:** The bundled `cpp_template` is a separate Meson-based template with `include/`, `src/`, `tests/`, `examples/`, Doxygen hooks, and the same repoman Make target names used by CI. ## High-level generated layout @@ -20,6 +21,7 @@ my_project/ ├── docs/ ├── make_cmds/ ├── notebooks/ +├── paper/ ├── scripts/ ├── src/ │ └── / diff --git a/src/repoman/copier.yml b/src/repoman/copier.yml index 1c22678..ddba8b2 100644 --- a/src/repoman/copier.yml +++ b/src/repoman/copier.yml @@ -170,6 +170,11 @@ python_notebooks: help: Include Jupyter notebooks support (notebooks folder and ipykernel) default: false +latex_paper: + type: bool + help: Include a LaTeX paper scaffold under paper/ + default: false + # FastAPI application (when fastapi_enabled) fastapi_enabled: type: bool diff --git a/src/repoman/main_template/.gitignore.jinja b/src/repoman/main_template/.gitignore.jinja index 0c66d07..a22e3f1 100644 --- a/src/repoman/main_template/.gitignore.jinja +++ b/src/repoman/main_template/.gitignore.jinja @@ -17,6 +17,10 @@ /htmlcov/ /site/ uv.lock +{% if latex_paper %} +/paper/build/ +/paper/main.pdf +{% endif %} # cache .cache/ diff --git a/src/repoman/main_template/Makefile.jinja b/src/repoman/main_template/Makefile.jinja index 6eb3613..9bf9e4b 100644 --- a/src/repoman/main_template/Makefile.jinja +++ b/src/repoman/main_template/Makefile.jinja @@ -66,6 +66,9 @@ include make_cmds/prod.mk {% if python_notebooks %} include make_cmds/notebooks.mk {% endif %} +{% if latex_paper %} +include make_cmds/paper.mk +{% endif %} ############## # Versioning # diff --git a/src/repoman/main_template/README.md.jinja b/src/repoman/main_template/README.md.jinja index 9354d01..74f198b 100644 --- a/src/repoman/main_template/README.md.jinja +++ b/src/repoman/main_template/README.md.jinja @@ -67,3 +67,17 @@ make prod `make prod` uses `docker compose up --build`, exposes the API on `http://localhost:8000`, and passes `OPENAI_API_KEY` into the container. Exporting `OPENAI_API_TOKEN` also works because the Make target normalizes it to `OPENAI_API_KEY` before Compose starts. {% endif %} +{% if latex_paper %} + +## Paper + +The LaTeX manuscript scaffold lives in `paper/`. + +Build it with: + +```bash +make paper +``` + +This target requires a TeX distribution with `latexmk`. +{% endif %} diff --git a/src/repoman/main_template/docs/reference/architecture.md.jinja b/src/repoman/main_template/docs/reference/architecture.md.jinja index 95e99c1..291ab7f 100644 --- a/src/repoman/main_template/docs/reference/architecture.md.jinja +++ b/src/repoman/main_template/docs/reference/architecture.md.jinja @@ -54,3 +54,10 @@ When dataset support is enabled, dataset loaders and types live in `src/{{ python_package_import_name }}/datasets/` and use `config/dataset_config.json` plus `DatasetConfig` for defaults and overrides. {% endif %} +{% if latex_paper %} +## Paper + +When the LaTeX paper scaffold is enabled, manuscript sources live under +`paper/`. The root `make paper`, `make paper-watch`, and `make paper-clean` +targets delegate to `paper/Makefile`. +{% endif %} diff --git a/src/repoman/main_template/make_cmds/{% if latex_paper %}paper.mk{% endif %}.jinja b/src/repoman/main_template/make_cmds/{% if latex_paper %}paper.mk{% endif %}.jinja new file mode 100644 index 0000000..cce42a6 --- /dev/null +++ b/src/repoman/main_template/make_cmds/{% if latex_paper %}paper.mk{% endif %}.jinja @@ -0,0 +1,15 @@ +####################### +# Paper # +####################### + +.PHONY: paper +paper: ## Build the LaTeX paper PDF. + $(MAKE) -C paper pdf + +.PHONY: paper-watch +paper-watch: ## Continuously rebuild the LaTeX paper PDF. + $(MAKE) -C paper watch + +.PHONY: paper-clean +paper-clean: ## Remove LaTeX build artifacts. + $(MAKE) -C paper clean diff --git a/src/repoman/main_template/{% if latex_paper %}paper{% endif %}/.latexmkrc.jinja b/src/repoman/main_template/{% if latex_paper %}paper{% endif %}/.latexmkrc.jinja new file mode 100644 index 0000000..b9794f2 --- /dev/null +++ b/src/repoman/main_template/{% if latex_paper %}paper{% endif %}/.latexmkrc.jinja @@ -0,0 +1,4 @@ +$pdf_mode = 1; +$bibtex_use = 2; +$interaction = "nonstopmode"; +$out_dir = "build"; diff --git a/src/repoman/main_template/{% if latex_paper %}paper{% endif %}/Makefile.jinja b/src/repoman/main_template/{% if latex_paper %}paper{% endif %}/Makefile.jinja new file mode 100644 index 0000000..03157f6 --- /dev/null +++ b/src/repoman/main_template/{% if latex_paper %}paper{% endif %}/Makefile.jinja @@ -0,0 +1,24 @@ +MAIN := main +BUILD_DIR := build +PDF := $(MAIN).pdf + +.PHONY: all +all: pdf + +.PHONY: pdf +pdf: ## Build $(PDF) with latexmk. + latexmk -pdf -interaction=nonstopmode -halt-on-error -outdir=$(BUILD_DIR) $(MAIN).tex + cp $(BUILD_DIR)/$(PDF) $(PDF) + +.PHONY: watch +watch: ## Rebuild $(PDF) whenever sources change. + latexmk -pdf -pvc -interaction=nonstopmode -halt-on-error -outdir=$(BUILD_DIR) $(MAIN).tex + +.PHONY: clean +clean: ## Remove LaTeX build artifacts. + latexmk -C -outdir=$(BUILD_DIR) $(MAIN).tex + rm -rf $(BUILD_DIR) + +.PHONY: distclean +distclean: clean ## Remove generated PDF. + rm -f $(PDF) diff --git a/src/repoman/main_template/{% if latex_paper %}paper{% endif %}/README.md.jinja b/src/repoman/main_template/{% if latex_paper %}paper{% endif %}/README.md.jinja new file mode 100644 index 0000000..352f5b9 --- /dev/null +++ b/src/repoman/main_template/{% if latex_paper %}paper{% endif %}/README.md.jinja @@ -0,0 +1,22 @@ +# Paper + +This folder contains the LaTeX manuscript scaffold for `{{ project_name }}`. + +## Build + +Install a TeX distribution with `latexmk`, then run: + +```bash +make paper +``` + +From this folder, you can also run: + +```bash +make pdf +make watch +make clean +``` + +The root `make paper` target delegates to `paper/Makefile` and writes +`paper/main.pdf`. diff --git a/src/repoman/main_template/{% if latex_paper %}paper{% endif %}/figures/.gitkeep.jinja b/src/repoman/main_template/{% if latex_paper %}paper{% endif %}/figures/.gitkeep.jinja new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/src/repoman/main_template/{% if latex_paper %}paper{% endif %}/figures/.gitkeep.jinja @@ -0,0 +1 @@ + diff --git a/src/repoman/main_template/{% if latex_paper %}paper{% endif %}/main.tex.jinja b/src/repoman/main_template/{% if latex_paper %}paper{% endif %}/main.tex.jinja new file mode 100644 index 0000000..8b43bf1 --- /dev/null +++ b/src/repoman/main_template/{% if latex_paper %}paper{% endif %}/main.tex.jinja @@ -0,0 +1,40 @@ +{% set paper_title = project_name | replace('-', ' ') | title -%} +\documentclass[11pt]{article} + +\usepackage[margin=1in]{geometry} +\usepackage[T1]{fontenc} +\usepackage[utf8]{inputenc} +\usepackage{lmodern} +\usepackage{microtype} +\usepackage{amsmath} +\usepackage{amssymb} +\usepackage{graphicx} +\usepackage{booktabs} +\usepackage[hidelinks]{hyperref} +\usepackage[backend=biber,style=numeric,sorting=none]{biblatex} + +\addbibresource{references.bib} +{% raw -%} +\graphicspath{{figures/}} +{% endraw %} + +\title{ {{- paper_title -}} } +\author{ {{- author_fullname -}} } +\date{\today} + +\begin{document} + +\maketitle + +\begin{abstract} +\input{sections/abstract} +\end{abstract} + +\input{sections/introduction} +\input{sections/methods} +\input{sections/results} +\input{sections/discussion} + +\printbibliography + +\end{document} diff --git a/src/repoman/main_template/{% if latex_paper %}paper{% endif %}/references.bib.jinja b/src/repoman/main_template/{% if latex_paper %}paper{% endif %}/references.bib.jinja new file mode 100644 index 0000000..c1f8720 --- /dev/null +++ b/src/repoman/main_template/{% if latex_paper %}paper{% endif %}/references.bib.jinja @@ -0,0 +1,6 @@ +@misc{project, + author = { {{- author_fullname -}} }, + title = { {{- project_name | replace('-', ' ') | title -}} }, + year = { {{- copyright_date -}} }, + note = {Project manuscript scaffold} +} diff --git a/src/repoman/main_template/{% if latex_paper %}paper{% endif %}/sections/abstract.tex.jinja b/src/repoman/main_template/{% if latex_paper %}paper{% endif %}/sections/abstract.tex.jinja new file mode 100644 index 0000000..7933e0d --- /dev/null +++ b/src/repoman/main_template/{% if latex_paper %}paper{% endif %}/sections/abstract.tex.jinja @@ -0,0 +1 @@ +Summarize the problem, approach, main result, and implication in one concise paragraph. diff --git a/src/repoman/main_template/{% if latex_paper %}paper{% endif %}/sections/discussion.tex.jinja b/src/repoman/main_template/{% if latex_paper %}paper{% endif %}/sections/discussion.tex.jinja new file mode 100644 index 0000000..623f84f --- /dev/null +++ b/src/repoman/main_template/{% if latex_paper %}paper{% endif %}/sections/discussion.tex.jinja @@ -0,0 +1,3 @@ +\section{Discussion} + +Interpret the results, identify limitations, and outline next steps. diff --git a/src/repoman/main_template/{% if latex_paper %}paper{% endif %}/sections/introduction.tex.jinja b/src/repoman/main_template/{% if latex_paper %}paper{% endif %}/sections/introduction.tex.jinja new file mode 100644 index 0000000..8ce2fdf --- /dev/null +++ b/src/repoman/main_template/{% if latex_paper %}paper{% endif %}/sections/introduction.tex.jinja @@ -0,0 +1,3 @@ +\section{Introduction} + +Introduce the problem, motivate why it matters, and define the contribution. diff --git a/src/repoman/main_template/{% if latex_paper %}paper{% endif %}/sections/methods.tex.jinja b/src/repoman/main_template/{% if latex_paper %}paper{% endif %}/sections/methods.tex.jinja new file mode 100644 index 0000000..08245cd --- /dev/null +++ b/src/repoman/main_template/{% if latex_paper %}paper{% endif %}/sections/methods.tex.jinja @@ -0,0 +1,3 @@ +\section{Methods} + +Describe the data, assumptions, model, implementation, and evaluation protocol. diff --git a/src/repoman/main_template/{% if latex_paper %}paper{% endif %}/sections/results.tex.jinja b/src/repoman/main_template/{% if latex_paper %}paper{% endif %}/sections/results.tex.jinja new file mode 100644 index 0000000..63e07ac --- /dev/null +++ b/src/repoman/main_template/{% if latex_paper %}paper{% endif %}/sections/results.tex.jinja @@ -0,0 +1,3 @@ +\section{Results} + +Present the key findings with tables, figures, and uncertainty where appropriate. diff --git a/src/repoman/resources/copier_answers_template.yml b/src/repoman/resources/copier_answers_template.yml index f5f659e..b9d4edd 100644 --- a/src/repoman/resources/copier_answers_template.yml +++ b/src/repoman/resources/copier_answers_template.yml @@ -37,6 +37,9 @@ container_registry: "" # Notebooks python_notebooks: false +# Paper +latex_paper: false + # FastAPI application fastapi_enabled: true fastapi_docs_url: "/docs" diff --git a/tests/fixtures/default_copier_answers.yml b/tests/fixtures/default_copier_answers.yml index 37bad4e..7ed20d3 100644 --- a/tests/fixtures/default_copier_answers.yml +++ b/tests/fixtures/default_copier_answers.yml @@ -37,6 +37,9 @@ container_registry: "" # Notebooks python_notebooks: true +# Paper +latex_paper: true + # FastAPI application fastapi_enabled: true fastapi_docs_url: "/docs" diff --git a/tests/template_testing.py b/tests/template_testing.py index 62754ef..c078d9e 100644 --- a/tests/template_testing.py +++ b/tests/template_testing.py @@ -145,6 +145,7 @@ def instantiate_template( "insiders": False, "public_release": False, "python_notebooks": False, + "latex_paper": False, } # Merge answers file data first (if provided), then provided copier_data (highest priority) diff --git a/tests/test_template/test_ci.py b/tests/test_template/test_ci.py index 21f2af9..b1af05a 100644 --- a/tests/test_template/test_ci.py +++ b/tests/test_template/test_ci.py @@ -113,6 +113,49 @@ def test_instantiated_template_notebook_executes(setup_template: Any) -> None: ) +def test_latex_paper_template_renders_paper_folder_only_when_enabled(tmp_path: Path) -> None: + """LaTeX paper scaffold files should be controlled by latex_paper.""" + answers_file = Path(__file__).parent.parent / "fixtures" / "default_copier_answers.yml" + + disabled_dir = instantiate_template( + output_dir=tmp_path / "disabled", + project_name="test-project", + answers_file=answers_file, + copier_data={"latex_paper": False}, + ) + assert not (disabled_dir / "paper").exists() + assert not (disabled_dir / "make_cmds" / "paper.mk").exists() + assert "include make_cmds/paper.mk" not in _read_text(disabled_dir / "Makefile") + assert "/paper/build/" not in _read_text(disabled_dir / ".gitignore") + + enabled_dir = instantiate_template( + output_dir=tmp_path / "enabled", + project_name="paper-project", + answers_file=answers_file, + copier_data={"latex_paper": True}, + ) + paper_dir = enabled_dir / "paper" + assert (paper_dir / "main.tex").exists() + assert (paper_dir / ".latexmkrc").exists() + assert (paper_dir / "references.bib").exists() + assert (paper_dir / "sections" / "introduction.tex").exists() + assert (paper_dir / "figures" / ".gitkeep").exists() + + makefile = _read_text(enabled_dir / "Makefile") + paper_makefile = _read_text(enabled_dir / "make_cmds" / "paper.mk") + paper_readme = _read_text(paper_dir / "README.md") + main_tex = _read_text(paper_dir / "main.tex") + references = _read_text(paper_dir / "references.bib") + + assert "include make_cmds/paper.mk" in makefile + assert "paper: ## Build the LaTeX paper PDF." in paper_makefile + assert "make paper" in paper_readme + assert r"\title{Paper Project}" in main_tex + assert r"\input{sections/introduction}" in main_tex + assert "title = {Paper Project}" in references + assert "/paper/build/" in _read_text(enabled_dir / ".gitignore") + + def test_instantiated_template_without_fastapi(tmp_path: Path) -> None: """When fastapi_enabled is false, no app folder is created and CI still passes.""" answers_file = Path(__file__).parent.parent / "fixtures" / "default_copier_answers.yml"