From a29b2af43d86b93f881bc198bea0fe4e8a97c674 Mon Sep 17 00:00:00 2001 From: Michael DeWitt Date: Tue, 31 Mar 2026 19:49:01 -0400 Subject: [PATCH 1/3] update latest --- .gitignore | 2 +- Cargo.lock | 2 +- Cargo.toml | 2 +- src/templates/Taskfile.yml | 5 +++++ 4 files changed, 8 insertions(+), 3 deletions(-) diff --git a/.gitignore b/.gitignore index 23f5f27..98d9fd1 100644 --- a/.gitignore +++ b/.gitignore @@ -2,7 +2,7 @@ # will have compiled files and executables debug target - +md-notes.md # These are backup files generated by rustfmt **/*.rs.bk diff --git a/Cargo.lock b/Cargo.lock index 9cee2e9..7bc5444 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -773,7 +773,7 @@ dependencies = [ [[package]] name = "ptmp" -version = "0.1.3" +version = "1.1.2" dependencies = [ "anyhow", "chrono", diff --git a/Cargo.toml b/Cargo.toml index adfee8b..fc74b8c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "ptmp" -version = "0.1.3" +version = "1.1.2" edition = "2021" authors = ["Michael DeWitt "] description = "Copy a template directory and render Tera placeholders via a TUI." diff --git a/src/templates/Taskfile.yml b/src/templates/Taskfile.yml index 51a8ee4..da340de 100644 --- a/src/templates/Taskfile.yml +++ b/src/templates/Taskfile.yml @@ -90,4 +90,9 @@ tasks: deps: [latex] cmds: - open {{.OUTDIR}}/{{.MAIN}} + + bib: + desc: Format references.bib + cmds: + - bibfmt references.bib {% endraw %} From 71162e2b0a1b1b6221ddc53023972b6e748b4ca7 Mon Sep 17 00:00:00 2001 From: Michael DeWitt Date: Thu, 21 May 2026 11:19:49 -0400 Subject: [PATCH 2/3] updates --- README.Md | 13 ++ src/cli.rs | 12 ++ src/components.rs | 75 ++++++- src/config.rs | 115 +++++++++++ src/main.rs | 36 ++++ src/templates.rs | 5 + src/templates/Taskfile.yml | 11 + src/templates/example.R | 3 +- src/templates/example.slurm | 4 + src/templates/justfile | 24 +++ src/templates/nature.csl | 189 ++++++++++++++++++ .../pandoc-reference-arial11-0p5in.docx | Bin 0 -> 26430 bytes src/templates/review-taskfile.yml | 53 +++++ 13 files changed, 534 insertions(+), 6 deletions(-) create mode 100644 src/config.rs create mode 100644 src/templates/justfile create mode 100644 src/templates/nature.csl create mode 100644 src/templates/pandoc-reference-arial11-0p5in.docx create mode 100644 src/templates/review-taskfile.yml diff --git a/README.Md b/README.Md index c8e262f..ab49a74 100644 --- a/README.Md +++ b/README.Md @@ -29,6 +29,17 @@ ptmp new \ --destination ~/projects/WNV-modeling ``` +### Set personal defaults + +Run once to save your name, email, ORCID, and other fields. +These become the pre-filled defaults whenever ptmp prompts you. + +```bash +ptmp config +``` + +Defaults are stored in `~/.config/ptmp/defaults.toml`. + ### Emit a single component Write one file into the current working directory, prompting only for the fields that file needs. @@ -48,6 +59,8 @@ ptmp citation # CITATION.cff ptmp license # LICENSE (MIT) ptmp quarto # report.qmd ptmp slides # slides.qmd +ptmp docx # templates/ (pandoc reference docx + CSL) +ptmp justfile # justfile (pandoc rendering tasks) ``` ## Template rendering diff --git a/src/cli.rs b/src/cli.rs index 4ccd78b..0eb833e 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -79,4 +79,16 @@ pub enum Commands { /// Emit a Quarto revealjs slide deck (slides.qmd) into the current directory Slides, + + /// Emit a review Taskfile.yml for academic peer review + Review, + + /// Emit pandoc reference docx and CSL into templates/ + Docx, + + /// Emit a justfile with pandoc rendering tasks + Justfile, + + /// Set personal defaults (author, email, ORCID, etc.) + Config, } diff --git a/src/components.rs b/src/components.rs index eb74fa0..022a234 100644 --- a/src/components.rs +++ b/src/components.rs @@ -45,21 +45,39 @@ pub static KNOWN_FIELDS: &[FieldDef] = &[ FieldDef { name: "slurm_account", prompt: "SLURM account/group", - default: "", + default: "kortessisGrp", required: true, }, FieldDef { name: "slurm_email", prompt: "Email for SLURM notifications", - default: "", + default: "dewime23@wfu.edu", required: true, }, + FieldDef { + name: "email", + prompt: "Email address", + default: "michael.dewitt@wfusm.edu", + required: false, + }, FieldDef { name: "orcid", - prompt: "ORCID identifier (e.g., 0000-0001-2345-6789)", - default: "", + prompt: "ORCID identifier (e.g., 0000-0001-8940-1967)", + default: "0000-0001-8940-1967", required: false, }, + FieldDef { + name: "pdf_path", + prompt: "Path to the PDF to review", + default: "", + required: true, + }, + FieldDef { + name: "reviewer_name", + prompt: "Reviewer name", + default: "", + required: true, + }, ]; pub struct Component { @@ -184,6 +202,33 @@ pub static COMPONENTS: &[Component] = &[ extra_text: &[("reveal-theme.scss", templates::REVEAL_THEME)], extra_binary: &[("assets/logo-wide.png", templates::LOGO_WIDE)], }, + Component { + name: "review", + output_path: "Taskfile.yml", + template: templates::REVIEW_TASKFILE, + fields: &["pdf_path", "reviewer_name"], + extra_text: &[], + extra_binary: &[], + }, + Component { + name: "docx", + output_path: "templates/nature.csl", + template: templates::NATURE_CSL, + fields: &["title"], + extra_text: &[], + extra_binary: &[( + "templates/pandoc-reference-arial11-0p5in.docx", + templates::PANDOC_REFERENCE_DOCX, + )], + }, + Component { + name: "justfile", + output_path: "justfile", + template: templates::JUSTFILE, + fields: &["title"], + extra_text: &[], + extra_binary: &[], + }, ]; /// Map a CLI command variant to its registered [`Component`], if applicable. @@ -205,13 +250,28 @@ pub fn component_for_command(cmd: &Commands) -> Option<&'static Component> { Commands::License => "license", Commands::Quarto => "quarto", Commands::Slides => "slides", + Commands::Review => "review", + Commands::Docx => "docx", + Commands::Justfile => "justfile", _ => return None, }; COMPONENTS.iter().find(|c| c.name == name) } /// Prompt only for the fields required by a component. +/// +/// User defaults from `~/.config/ptmp/defaults.toml` override +/// hardcoded defaults so the user can just press Enter. pub fn collect_fields_for_component(component: &Component) -> Result> { + collect_fields_with_defaults(component, &crate::config::load_user_defaults()?) +} + +/// Inner implementation that accepts explicit user defaults +/// (makes unit-testing possible without touching the filesystem). +pub fn collect_fields_with_defaults( + component: &Component, + user_defaults: &HashMap, +) -> Result> { let mut values = HashMap::new(); for &needed in component.fields { let def = KNOWN_FIELDS @@ -219,7 +279,9 @@ pub fn collect_fields_for_component(component: &Component) -> Result PathBuf { + dirs_fallback().join("defaults.toml") +} + +/// Best-effort XDG / macOS config directory. +fn dirs_fallback() -> PathBuf { + if let Ok(xdg) = std::env::var("XDG_CONFIG_HOME") { + PathBuf::from(xdg).join("ptmp") + } else if let Some(home) = home_dir() { + home.join(".config").join("ptmp") + } else { + PathBuf::from(".config").join("ptmp") + } +} + +fn home_dir() -> Option { + std::env::var("HOME").ok().map(PathBuf::from) +} + +/// Load user defaults from `~/.config/ptmp/defaults.toml`. +/// +/// Returns an empty map if the file does not exist. +pub fn load_user_defaults() -> Result> { + load_defaults_from(&config_path()) +} + +/// Load defaults from an arbitrary path (used by tests). +pub fn load_defaults_from(path: &std::path::Path) -> Result> { + if !path.exists() { + return Ok(HashMap::new()); + } + let content = fs::read_to_string(path) + .with_context(|| format!("Failed to read config: {}", path.display()))?; + let table: toml::Table = content + .parse() + .with_context(|| format!("Failed to parse config: {}", path.display()))?; + let mut map = HashMap::new(); + for (key, val) in table { + if let toml::Value::String(s) = val { + if !s.is_empty() { + map.insert(key, s); + } + } + } + Ok(map) +} + +/// Save user defaults to `~/.config/ptmp/defaults.toml`. +pub fn save_user_defaults(values: &HashMap) -> Result<()> { + save_defaults_to(values, &config_path()) +} + +/// Save defaults to an arbitrary path (used by tests). +pub fn save_defaults_to(values: &HashMap, path: &std::path::Path) -> Result<()> { + if let Some(parent) = path.parent() { + fs::create_dir_all(parent)?; + } + let mut table = toml::Table::new(); + let mut keys: Vec<&String> = values.keys().collect(); + keys.sort(); + for key in keys { + table.insert( + key.clone(), + toml::Value::String(values.get(key).cloned().unwrap_or_default()), + ); + } + let content = toml::to_string_pretty(&table).context("Failed to serialise config")?; + fs::write(path, content) + .with_context(|| format!("Failed to write config: {}", path.display()))?; + println!("Saved config to {}", path.display()); + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + use tempfile::tempdir; + + #[test] + fn load_missing_file_returns_empty() { + let dir = tempdir().unwrap(); + let path = dir.path().join("nope.toml"); + let defaults = load_defaults_from(&path).unwrap(); + assert!(defaults.is_empty()); + } + + #[test] + fn round_trip_save_load() { + let dir = tempdir().unwrap(); + let path = dir.path().join("defaults.toml"); + + let mut vals = HashMap::new(); + vals.insert("author".to_string(), "Test Author".to_string()); + vals.insert("email".to_string(), "test@example.com".to_string()); + save_defaults_to(&vals, &path).unwrap(); + + assert!(path.exists()); + + let loaded = load_defaults_from(&path).unwrap(); + assert_eq!( + loaded.get("author").map(|s| s.as_str()), + Some("Test Author") + ); + assert_eq!( + loaded.get("email").map(|s| s.as_str()), + Some("test@example.com") + ); + } +} diff --git a/src/main.rs b/src/main.rs index a8da712..89be46a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -32,6 +32,7 @@ use std::collections::HashMap; mod cli; mod commands; mod components; +mod config; mod manifest; mod render; mod templates; @@ -102,6 +103,9 @@ fn main() -> Result<()> { destination.display() ); } + Commands::Config => { + run_config()?; + } cmd => { let component = component_for_command(cmd).expect("Unhandled command variant"); emit_component(component)?; @@ -110,3 +114,35 @@ fn main() -> Result<()> { Ok(()) } + +/// Interactive config: prompt for every known field and save +/// to `~/.config/ptmp/defaults.toml`. +fn run_config() -> Result<()> { + use components::KNOWN_FIELDS; + use config::{load_user_defaults, save_user_defaults}; + use inquire::Text; + + let existing = load_user_defaults()?; + let mut values = HashMap::new(); + + println!( + "Set your personal defaults (press Enter to keep \ + current value).\n" + ); + + for field in KNOWN_FIELDS { + let current = existing + .get(field.name) + .map(|s| s.as_str()) + .unwrap_or(field.default); + let mut prompt = Text::new(field.prompt); + if !current.is_empty() { + prompt = prompt.with_default(current); + } + let val = prompt.prompt().unwrap_or_default(); + values.insert(field.name.to_string(), val); + } + + save_user_defaults(&values)?; + Ok(()) +} diff --git a/src/templates.rs b/src/templates.rs index b9abcc8..f04e234 100644 --- a/src/templates.rs +++ b/src/templates.rs @@ -16,3 +16,8 @@ pub const QUARTO: &str = include_str!("templates/report.qmd"); pub const SLIDES: &str = include_str!("templates/slides.qmd"); pub const REVEAL_THEME: &str = include_str!("templates/reveal-theme.scss"); pub const LOGO_WIDE: &[u8] = include_bytes!("templates/logo-wide.png"); +pub const REVIEW_TASKFILE: &str = include_str!("templates/review-taskfile.yml"); +pub const NATURE_CSL: &str = include_str!("templates/nature.csl"); +pub const PANDOC_REFERENCE_DOCX: &[u8] = + include_bytes!("templates/pandoc-reference-arial11-0p5in.docx"); +pub const JUSTFILE: &str = include_str!("templates/justfile"); diff --git a/src/templates/Taskfile.yml b/src/templates/Taskfile.yml index da340de..00650b3 100644 --- a/src/templates/Taskfile.yml +++ b/src/templates/Taskfile.yml @@ -95,4 +95,15 @@ tasks: desc: Format references.bib cmds: - bibfmt references.bib + + docx: + desc: Render Markdown to Word via pandoc + cmds: + - mkdir -p {{.OUTDIR}} + - >- + pandoc report.md + --reference-doc=templates/pandoc-reference-arial11-0p5in.docx + --citeproc --csl=templates/nature.csl + --bibliography=references.bib + -o {{.OUTDIR}}/report.docx {% endraw %} diff --git a/src/templates/example.R b/src/templates/example.R index 68940d8..c40ad5c 100644 --- a/src/templates/example.R +++ b/src/templates/example.R @@ -1,4 +1,3 @@ -# Example R script # Project: {{ title }} # Author: {{ author }} # Date: {{ today }} @@ -11,3 +10,5 @@ library(here) # library(officer) message(paste("Project root is:", here::here())) + +# Data import ----------------------------------------------------------------- diff --git a/src/templates/example.slurm b/src/templates/example.slurm index 3f9ba74..2fbdfe6 100644 --- a/src/templates/example.slurm +++ b/src/templates/example.slurm @@ -10,3 +10,7 @@ #SBATCH --time=00-14:00:00 echo "Running {{ title }}" + +# module load apps/python/3.11.8 +# module load apps/julia/1.10.2 +# module load apps/r/4.3.2 \ No newline at end of file diff --git a/src/templates/justfile b/src/templates/justfile new file mode 100644 index 0000000..7572f31 --- /dev/null +++ b/src/templates/justfile @@ -0,0 +1,24 @@ +# Justfile for {{ title }} + +main_file := "report.md" +outdir := "output" +template := "templates/pandoc-reference-arial11-0p5in.docx" +csl := "templates/nature.csl" +bib := "references.bib" + +{% raw %} +default: render + +# Render Markdown to Word via pandoc +render: + mkdir -p {{outdir}} + pandoc {{main_file}} \ + --reference-doc={{template}} \ + --citeproc --csl={{csl}} \ + --bibliography={{bib}} \ + -o {{outdir}}/report.docx + +# Clean generated output +clean: + rm -rf {{outdir}} +{% endraw %} diff --git a/src/templates/nature.csl b/src/templates/nature.csl new file mode 100644 index 0000000..8e7c5f4 --- /dev/null +++ b/src/templates/nature.csl @@ -0,0 +1,189 @@ + + \ No newline at end of file diff --git a/src/templates/pandoc-reference-arial11-0p5in.docx b/src/templates/pandoc-reference-arial11-0p5in.docx new file mode 100644 index 0000000000000000000000000000000000000000..2695afbadfcc6d49d13600fc754ee750673d4e5e GIT binary patch literal 26430 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 s{{#N(9Adqx=HYczC4>NBtXaE2J literal 0 HcmV?d00001 diff --git a/src/templates/review-taskfile.yml b/src/templates/review-taskfile.yml new file mode 100644 index 0000000..c2102ac --- /dev/null +++ b/src/templates/review-taskfile.yml @@ -0,0 +1,53 @@ +# Taskfile for academic peer review +# Reviewer: {{ reviewer_name }} +# https://taskfile.dev +version: "3" + +vars: + PDF: "{{ pdf_path }}" + REVIEWER: "{{ reviewer_name }}" + OUTDIR: . + +{% raw %} +tasks: + default: + desc: Run the full review pipeline + deps: [review] + + review: + desc: Extract annotations and generate review template + cmds: + - >- + academic-review {{.PDF}} + -f markdown + -o {{.OUTDIR}} + + review-all: + desc: Generate review in all formats (md, tex, docx) + cmds: + - >- + academic-review {{.PDF}} + -f markdown latex docx + -o {{.OUTDIR}} + + review-zotero: + desc: Generate review with live Zotero lookup + cmds: + - >- + academic-review {{.PDF}} + -f markdown + -o {{.OUTDIR}} + --no-zotero-stub + + clean: + desc: Remove generated review files + vars: + STEM: + sh: basename "{{.PDF}}" .pdf + cmds: + - rm -f {{.OUTDIR}}/{{.STEM}}_review.md + - rm -f {{.OUTDIR}}/{{.STEM}}_review.tex + - rm -f {{.OUTDIR}}/{{.STEM}}_review.docx + - rm -f {{.OUTDIR}}/{{.STEM}}_citations.md + - rm -f {{.OUTDIR}}/{{.STEM}}_context.json +{% endraw %} From 15a12547be1e3101dad4ff541cc320836fe1c3e6 Mon Sep 17 00:00:00 2001 From: Michael DeWitt Date: Sun, 5 Jul 2026 15:47:54 -0400 Subject: [PATCH 3/3] tweaks and updates --- README.Md | 2 +- src/components.rs | 4 +- src/render.rs | 36 ++++++++++++++ src/templates/main.tex | 92 +++++++++++++++++++++++++++++------- src/templates/references.bib | 15 +++++- 5 files changed, 128 insertions(+), 21 deletions(-) diff --git a/README.Md b/README.Md index ab49a74..fbc30d5 100644 --- a/README.Md +++ b/README.Md @@ -46,7 +46,7 @@ Write one file into the current working directory, prompting only for the fields ```bash ptmp taskfile # Taskfile.yml -ptmp latex # main.tex +ptmp latex # main.tex + references.bib (Nature-style manuscript) ptmp makefile # Makefile ptmp readme # README.md ptmp r # R/example.R diff --git a/src/components.rs b/src/components.rs index 022a234..c94e936 100644 --- a/src/components.rs +++ b/src/components.rs @@ -102,8 +102,8 @@ pub static COMPONENTS: &[Component] = &[ name: "latex", output_path: "main.tex", template: templates::MAIN_TEX, - fields: &["title", "author", "affiliation"], - extra_text: &[], + fields: &["title", "author", "affiliation", "email"], + extra_text: &[("references.bib", templates::REFERENCES_BIB)], extra_binary: &[], }, Component { diff --git a/src/render.rs b/src/render.rs index 6eb78c2..7d99ec9 100644 --- a/src/render.rs +++ b/src/render.rs @@ -217,6 +217,42 @@ mod tests { assert_eq!(copied, bytes); } + // --- MAIN_TEX template --- + + #[test] + fn main_tex_renders_with_all_fields() { + let mut vals = HashMap::new(); + vals.insert("title".to_string(), "My Paper".to_string()); + vals.insert("author".to_string(), "Jane Doe".to_string()); + vals.insert("affiliation".to_string(), "Some Lab".to_string()); + vals.insert("email".to_string(), "jane@example.com".to_string()); + let ctx = make_tera_context(vals, "proj"); + + let out = Tera::one_off(crate::templates::MAIN_TEX, &ctx, false).unwrap(); + + assert!(out.contains("\\documentclass[11pt,letterpaper]{article}")); + assert!(out.contains("My Paper")); + assert!(out.contains("\\thanks{Correspondence:")); + assert!(out.contains("\\href{mailto:jane@example.com}{jane@example.com}")); + assert!(out.contains("\\itshape Some Lab")); + assert!(out.contains("\\bibliographystyle{unsrtnat}")); + } + + #[test] + fn main_tex_omits_optional_fields_when_empty() { + let mut vals = HashMap::new(); + vals.insert("title".to_string(), "My Paper".to_string()); + vals.insert("author".to_string(), "Jane Doe".to_string()); + vals.insert("affiliation".to_string(), String::new()); + vals.insert("email".to_string(), String::new()); + let ctx = make_tera_context(vals, "proj"); + + let out = Tera::one_off(crate::templates::MAIN_TEX, &ctx, false).unwrap(); + + assert!(!out.contains("\\thanks{")); + assert!(!out.contains("\\itshape")); + } + // --- copy_template --- #[test] diff --git a/src/templates/main.tex b/src/templates/main.tex index da22d03..764f83e 100644 --- a/src/templates/main.tex +++ b/src/templates/main.tex @@ -1,34 +1,94 @@ -\documentclass[12pt,letterpaper]{article} -\usepackage{setspace} -% Packages -\usepackage[margin=1in]{geometry} -\usepackage{graphicx} % Required for inserting images -\usepackage{comment} +% !TEX program = pdflatex +% Modern single-column manuscript in a Nature-like style. +% Citations render as sorted, superscript numbers ordered by appearance. +\documentclass[11pt,letterpaper]{article} + +% --- Encoding and fonts ---------------------------------------------------- \usepackage[T1]{fontenc} \usepackage[utf8]{inputenc} +% lmodern is universal; for a Times look swap in: newtxtext,newtxmath \usepackage{lmodern} -\usepackage{natbib} +\usepackage{microtype} % subtle protrusion/expansion for cleaner justification + +% --- Page layout ----------------------------------------------------------- +\usepackage[margin=1in]{geometry} +\usepackage{setspace} +\onehalfspacing +\usepackage{parskip} % vertical space between paragraphs, no first-line indent + +% --- Figures, tables, and math --------------------------------------------- +\usepackage{graphicx} +{% raw %}\graphicspath{{figures/}}{% endraw %} +\usepackage[font=small,labelfont=bf]{caption} +\usepackage{booktabs} % \toprule \midrule \bottomrule for clean tables +\usepackage{siunitx} % \num{1.2e3}, \qty{5}{\micro\liter} +\usepackage{amsmath} +\usepackage{amssymb} + +% --- Colour and bibliography ----------------------------------------------- +\usepackage{xcolor} +% Nature-style: numeric, sorted, compressed, superscript citations. +\usepackage[numbers,sort&compress,super]{natbib} +% unsrtnat ships with natbib and always compiles; if you have naturemag.bst, +% swap the next line for \bibliographystyle{naturemag}. +\bibliographystyle{unsrtnat} -\title{ {{ title }} } -\author{ {{ author }} \\ {{ affiliation }} } +% --- Hyperlinks and smart cross-references (load these last) --------------- +\usepackage[colorlinks=true,allcolors=blue!55!black]{hyperref} +\usepackage{cleveref} % \cref{fig:x} -> "Fig. 1", \Cref{...} -> "Figure 1" + +% --- Metadata -------------------------------------------------------------- +\title{\bfseries {{ title }}} +\author{ +{{- author -}} +{%- if email -%} +\thanks{Correspondence: \href{mailto:{{ email }}}{ {{- email -}} }} +{%- endif -%} +{%- if affiliation -%} +\\[0.4em]\normalsize\itshape {{ affiliation }} +{%- endif -%} +} \date{ {{ today }} } \begin{document} \maketitle -\section{Introduction} -Write your introduction here. +\begin{abstract} +\noindent +State the question, what you did, and the main result in a few sentences. +Keep the abstract self-contained and free of citations. +\end{abstract} -\section{Methods} -Describe your methods here. +\section{Introduction} +Motivate the problem and state your contribution. +Cite prior work with \verb|\cite{key}|, which renders as a superscript +number\cite{example2024}. \section{Results} -Present results here. Figures can be stored in `figures/`. +Present the findings. +Reference figures with \verb|\cref{fig:label}| and keep sources in +\texttt{figures/}, one figure per file. \section{Discussion} -Discuss findings here. +Interpret the results, state the limitations, and give the outlook. + +\section*{Methods} +Describe the data, models, and analysis in enough detail to reproduce. +Report uncertainty, not just point estimates. + +% --- Back matter ----------------------------------------------------------- +\section*{Data and code availability} +State where the data and analysis code can be found. + +\section*{Acknowledgements} +Funding sources and thanks. + +\section*{Author contributions} +Summarise each author's contribution by initials. + +\section*{Competing interests} +The authors declare no competing interests. -\bibliographystyle{plain} \bibliography{references} \end{document} diff --git a/src/templates/references.bib b/src/templates/references.bib index d31bb9b..1ca0389 100644 --- a/src/templates/references.bib +++ b/src/templates/references.bib @@ -1,2 +1,13 @@ -% Bibliography for {{ title }} -% Add BibTeX entries below +% Bibliography. Add BibTeX entries below. +% Format with `bibfmt` before committing. + +@article{example2024, + author = {Doe, Jane and Roe, Richard}, + title = {An example reference}, + journal = {Journal of Examples}, + year = {2024}, + volume = {1}, + number = {1}, + pages = {1--10}, + doi = {10.1000/example}, +}