| Title: | Cite Zotero References in Quarto Documents |
|---|---|
| Description: | Scans Quarto and R Markdown documents for citation keys, pulls the matching items from the Zotero web API, and writes a bibliography file. Also provides dry-run-by-default helpers to find, add, and update Zotero items. |
| Authors: | Joel Nitta [aut, cre, cph] |
| Maintainer: | Joel Nitta <[email protected]> |
| License: | MIT + file LICENSE |
| Version: | 0.0.0.9000 |
| Built: | 2026-11-06 00:30:21 UTC |
| Source: | https://github.com/joelnitta/citezot |
Looks up the metadata for a DOI (via the public Crossref API), checks that
the item is not already in your library, and creates it. The default is a
dry run: the library is read and Crossref is queried, but nothing is
written to Zotero. Use dry_run = FALSE to create the item.
cz_add( id, key = NULL, collection = NULL, dry_run = TRUE, auth = cz_auth(), cache = getOption("citezot.cache", FALSE) )cz_add( id, key = NULL, collection = NULL, dry_run = TRUE, auth = cz_auth(), cache = getOption("citezot.cache", FALSE) )
id |
A DOI, with or without a |
key |
Optional citekey to pin on the new item, e.g. |
collection |
Optional collection to add the item to: its 8-character key, or its exact name. |
dry_run |
If |
auth |
Credentials from |
cache |
Passed to |
The new item is built from Zotero's own template for its type, so it only
contains fields valid for that type. The citekey, if you give one, is pinned
in the same request (the native citationKey field), so there is no moment
where the item exists without it. Only DOIs registered with Crossref are
supported for now; ISBN, arXiv and PMID lookups are planned.
Creating is retry-safe: each request carries a random Zotero-Write-Token,
so a retried request after a network hiccup cannot create the item twice.
Existing items are never changed or deleted.
Crossref asks clients to identify themselves with a contact address to use
its faster pool. citezot never sends an address by itself; to opt in, set
options(citezot.mailto = "[email protected]").
Invisibly, a list with item (the payload), key (the new Zotero
item ID, or NULL), existing (matching rows of cz_find() if the item
was already in the library) and written.
## Not run: cz_add("10.3767/000651906X622210", key = "Ebihara2006") # preview cz_add("10.3767/000651906X622210", key = "Ebihara2006", dry_run = FALSE) ## End(Not run)## Not run: cz_add("10.3767/000651906X622210", key = "Ebihara2006") # preview cz_add("10.3767/000651906X622210", key = "Ebihara2006", dry_run = FALSE) ## End(Not run)
Resolves the library ID and API key, by default from the environment
variables ZOTERO_LIBRARY_ID and ZOTERO_API_KEY. Set them once with
cz_set_auth(), or load them yourself (e.g. from a gitignored .Renviron
or .env.local). citezot never reads credential files; only cz_set_auth()
writes the key, and only when you ask it to. The key is never printed.
cz_auth( library_id = NULL, api_key = Sys.getenv("ZOTERO_API_KEY"), library_type = c("user", "group") )cz_auth( library_id = NULL, api_key = Sys.getenv("ZOTERO_API_KEY"), library_type = c("user", "group") )
library_id |
Numeric user ID (or group ID), as a string or number.
If |
api_key |
Zotero API key. |
library_type |
|
Create a key at https://www.zotero.org/settings/keys; the numeric user ID
is on the same page. A read-only key is enough for cz_fetch_items(); only
functions that change the library need a key with write access.
A cz_auth object, for the auth argument of other functions.
# Explicit values (normally these come from environment variables). The key # is never shown when the object is printed. cz_auth(library_id = "1234567", api_key = "not-a-real-key")# Explicit values (normally these come from environment variables). The key # is never shown when the object is printed. cz_auth(library_id = "1234567", api_key = "not-a-real-key")
@) citekey-shaped tokens from a text/data fileFor files that reference sources informally (e.g. a CSV "comments" column
saying "defined in Dubuisson2013a"). Matches an author-shaped run of letters
(hyphens allowed) immediately followed by a 19xx/20xx year and an optional
lowercase disambiguator, e.g. Ebihara2006, Boucheron-Dubuisson2024.
Deliberately conservative: Ebihara 2006 or Hsu7949 do not match.
cz_bare_keys(path)cz_bare_keys(path)
path |
Path to the file to scan. |
Sorted character vector of unique citekey-shaped tokens.
notes <- tempfile(fileext = ".csv") writeLines( c("site,comment", "A,defined in Darwin1859", "B,see Ebihara 2006"), notes ) cz_bare_keys(notes) # "Ebihara 2006" does not matchnotes <- tempfile(fileext = ".csv") writeLines( c("site,comment", "A,defined in Darwin1859", "B,see Ebihara 2006"), notes ) cz_bare_keys(notes) # "Ebihara 2006" does not match
Removes cache files created by cz_fetch_items() when caching is enabled.
Only citezot's own cache files are removed; nothing in Zotero is touched.
cz_clear_cache(auth = NULL)cz_clear_cache(auth = NULL)
auth |
Credentials from |
Invisibly, the number of files removed.
# Use an empty temporary cache directory so nothing real is deleted old <- options(citezot.cache_dir = tempfile("citezot-cache")) cz_clear_cache() options(old)# Use an empty temporary cache directory so nothing real is deleted old <- options(citezot.cache_dir = tempfile("citezot-cache")) cz_clear_cache() options(old)
Uses the /items/top endpoint (excludes notes, attachments and other child
items) and pages through the whole library using the Total-Results
header. After the first page, the remaining pages are requested in parallel
(at most getOption("citezot.max_active", 4) at a time, and throttled) and
the library version is checked so that a library edited mid-download is
re-fetched rather than returned inconsistent.
cz_fetch_items( auth = cz_auth(), page_size = 100, cache = getOption("citezot.cache", FALSE), refresh = FALSE )cz_fetch_items( auth = cz_auth(), page_size = 100, cache = getOption("citezot.cache", FALSE), refresh = FALSE )
auth |
Credentials from |
page_size |
Items per request (Zotero's maximum is 100). |
cache |
Use and update the local cache? Defaults to
|
refresh |
If |
A full download of a large library takes a while. With cache = TRUE the
items are stored in a local file (in tools::R_user_dir("citezot", "cache"),
or getOption("citezot.cache_dir")), and later calls download only what
changed since: one request if nothing changed, otherwise the changed items
plus the lists of deleted and trashed items. The cache holds the library's
bibliographic metadata, never your API key. It is opt-in: nothing is
written to disk unless you ask, either here or with
options(citezot.cache = TRUE). Use refresh = TRUE to ignore the cache
and download everything again, and cz_clear_cache() to delete it.
A list of CSL-JSON items (plain lists, simplifyVector = FALSE).
The order is not guaranteed.
## Not run: items <- cz_fetch_items() length(items) # Keep a local copy, so later calls download only what changed items <- cz_fetch_items(cache = TRUE) ## End(Not run)## Not run: items <- cz_fetch_items() length(items) # Keep a local copy, so later calls download only what changed items <- cz_fetch_items(cache = TRUE) ## End(Not run)
Searches the library for items matching all the criteria given. Matching
is done locally on the library's CSL-JSON (see cz_fetch_items()), because
Zotero's server-side search cannot look items up by DOI or citekey. Use
cache = TRUE (or options(citezot.cache = TRUE)) to make this fast on
repeat calls.
cz_find( query = NULL, doi = NULL, isbn = NULL, citekey = NULL, auth = cz_auth(), cache = getOption("citezot.cache", FALSE), items = NULL )cz_find( query = NULL, doi = NULL, isbn = NULL, citekey = NULL, auth = cz_auth(), cache = getOption("citezot.cache", FALSE), items = NULL )
query |
Free text. Every word must appear (case-insensitively) in the title, authors, year, journal or citekey. |
doi |
A DOI, with or without a |
isbn |
An ISBN (hyphens and spaces are ignored). |
citekey |
An exact pinned citekey. |
auth |
Credentials from |
cache |
Passed to |
items |
Items already fetched with |
A data frame with one row per match and columns item (Zotero item
ID), citekey, author, year, title, doi and type. It can be
passed as the item argument of cz_pin_key() when it has one row.
## Not run: cz_find(doi = "10.3767/000651906X622210") cz_find("Ebihara Hymenophyllaceae revision") cz_find(citekey = "Ebihara2006") ## End(Not run)## Not run: cz_find(doi = "10.3767/000651906X622210") cz_find("Ebihara Hymenophyllaceae revision") cz_find(citekey = "Ebihara2006") ## End(Not run)
Returns the item as Zotero stores it (not CSL-JSON): writable fields are in
$data, and $version is the item version used for safe updates.
cz_get_item(item, auth = cz_auth(), cache = getOption("citezot.cache", FALSE))cz_get_item(item, auth = cz_auth(), cache = getOption("citezot.cache", FALSE))
item |
The item: a DOI, Zotero item ID, citekey, or one-row result of
|
auth |
Credentials from |
cache |
Passed to |
A list with key, version, data and other API fields.
## Not run: item <- cz_get_item("Darwin1859") item$data$title item$version ## End(Not run)## Not run: item <- cz_get_item("Darwin1859") item$data$title item$version ## End(Not run)
Checks the native citation-key property first, then falls back to a
Citation Key: line in note.
cz_item_key(item)cz_item_key(item)
item |
A single CSL-JSON item (a list). |
The citekey, or NA_character_.
cz_item_key(list(`citation-key` = "Darwin1859")) cz_item_key(list(note = "Citation Key: Darwin1859")) cz_item_key(list(title = "No key pinned"))cz_item_key(list(`citation-key` = "Darwin1859")) cz_item_key(list(note = "Citation Key: Darwin1859")) cz_item_key(list(title = "No key pinned"))
In order: getOption("citezot.key_format"); the Better BibTeX pattern in
your Zotero profile (see Details); Better BibTeX's default pattern.
cz_key_format()cz_key_format()
Detection of the Zotero profile reads one line of its prefs.js and only
happens when a key is generated, never at load. Set
options(citezot.key_format = ...) to choose a formula yourself and skip
the detection, and options(citezot.zotero_profile = "<folder>") if Zotero
keeps its profile somewhere unusual. The source in use is reported once per
session.
A list with formula and source ("option", "zotero profile"
or "default").
options(citezot.key_format = "auth.lower + year") cz_key_format()options(citezot.key_format = "auth.lower + year") cz_key_format()
Sets the item's native citationKey field via the Web API (the same field
a desktop Better BibTeX install manages), so cz_item_key() and
cz_fetch_items() see it. Requires a key with write access.
cz_pin_key( item, key, dry_run = TRUE, overwrite = FALSE, auth = cz_auth(), cache = getOption("citezot.cache", FALSE) )cz_pin_key( item, key, dry_run = TRUE, overwrite = FALSE, auth = cz_auth(), cache = getOption("citezot.cache", FALSE) )
item |
The item to pin: a DOI, a Zotero item ID (bare, |
key |
The citekey to pin. |
dry_run |
If |
overwrite |
Replace a different key that is already pinned. Defaults
to |
auth |
Credentials from |
cache |
Passed to |
The default is a dry run: the item is read (to get its current version and
key) but nothing is written. Set dry_run = FALSE to write. The write
sends If-Unmodified-Since-Version, so if the item changed in the meantime
Zotero rejects it (HTTP 412) and nothing is overwritten.
Invisibly, a list with item, before, after and written.
## Not run: cz_pin_key("10.1016/0022-5193(64)90038-4", "Hamilton1964") # dry run cz_pin_key("10.1016/0022-5193(64)90038-4", "Hamilton1964", dry_run = FALSE) ## End(Not run)## Not run: cz_pin_key("10.1016/0022-5193(64)90038-4", "Hamilton1964") # dry run cz_pin_key("10.1016/0022-5193(64)90038-4", "Hamilton1964", dry_run = FALSE) ## End(Not run)
Strips fenced code blocks first (so @ in R code is never mistaken for a
citation), finds @key tokens, then drops Quarto cross-references
(@fig-x, @tbl-x, @eq-x, ...), which use the same syntax but are not
bibliography citations.
cz_qmd_keys( path, crossref_prefixes = c("fig", "tbl", "eq", "sec", "lst", "thm", "lem", "cor", "prp", "cnj", "def", "exm", "exr", "rem", "sol", "nte") )cz_qmd_keys( path, crossref_prefixes = c("fig", "tbl", "eq", "sec", "lst", "thm", "lem", "cor", "prp", "cnj", "def", "exm", "exr", "rem", "sol", "nte") )
path |
Path to a .qmd or .Rmd file. |
crossref_prefixes |
Cross-reference prefixes to drop (without the trailing hyphen). |
Sorted character vector of unique citation keys (no leading @).
qmd <- tempfile(fileext = ".qmd") writeLines(c( "Species evolve by natural selection [@Darwin1859]; see @fig-map.", "```{r}", "x@slot # not a citation", "```" ), qmd) cz_qmd_keys(qmd)qmd <- tempfile(fileext = ".qmd") writeLines(c( "Species evolve by natural selection [@Darwin1859]; see @fig-map.", "```{r}", "x@slot # not a citation", "```" ), qmd) cz_qmd_keys(qmd)
.Renviron
Prompts for your Zotero user (or group) ID and API key and saves them as
ZOTERO_LIBRARY_ID and ZOTERO_API_KEY in .Renviron, so you do not have
to edit that file by hand. They also take effect in the current R session
right away.
cz_set_auth(scope = c("user", "project"))cz_set_auth(scope = c("user", "project"))
scope |
|
The key is deliberately not an argument: values typed as arguments end up in your command history and scripts. Instead, you are prompted. This only works in an interactive session. Note that the prompt is not masked, so the key is visible on screen while you paste it. The key is never printed back. If either variable is already set in the file, you are asked before it is replaced; other lines are left alone.
The path of the file written, invisibly.
The default, "user", writes to your user-level ~/.Renviron, which is
available in every project and cannot be committed by accident (use
R_ENVIRON_USER to point R at a different file).
"project" is for advanced use, such as a different key per project. It
writes .Renviron in the current working directory, and refuses unless that
file is listed in .gitignore. R reads only one .Renviron, so a project
file replaces (it is not merged with) your user-level one in that project,
hiding any other variables defined there (for example GITHUB_PAT).
Restart R after saving. citezot does not write .Rprofile, which is code,
not configuration.
cz_auth() to check the saved credentials.
# Interactive only: prompts for the ID and key. cz_set_auth()# Interactive only: prompts for the ID and key. cz_set_auth()
Builds a key from a Better BibTeX-style formula, e.g.
"auth.lower + year + shorttitle(1).lower". If the key is already taken, a
letter (a, b, ...) is appended, as Better BibTeX does.
cz_suggest_key(item, formula = NULL, existing = character())cz_suggest_key(item, formula = NULL, existing = character())
item |
A Zotero native item (as from |
formula |
The key formula. See Details. |
existing |
Keys already in use, so the result can be made unique. |
The formula is, in order of preference, the formula argument, then the one
found by cz_key_format() (the citezot.key_format option, your Zotero
profile's Better BibTeX pattern, or Better BibTeX's default
auth.lower + shorttitle(3,3) + year). Supported functions are auth
(first author's last name), year, shorttitle(n, m) (first n words of
the title, skipping short function words, with the first m capitalised),
and title; filters are lower, upper, fold, transliterate, clean,
condense and nopunct; terms are joined with + and quoted text is kept
as is. Anything else is an error rather than a silently different key. The
result is ASCII and safe to cite in pandoc.
A single string.
item <- list( itemType = "journalArticle", title = "A taxonomic revision of Hymenophyllaceae", date = "2006", creators = list( list(creatorType = "author", firstName = "A", lastName = "Ebihara") ) ) cz_suggest_key(item, "auth.lower + year + shorttitle(1).lower") cz_suggest_key(item, existing = "ebihara2006taxonomic", formula = "auth.lower + year + shorttitle(1).lower" )item <- list( itemType = "journalArticle", title = "A taxonomic revision of Hymenophyllaceae", date = "2006", creators = list( list(creatorType = "author", firstName = "A", lastName = "Ebihara") ) ) cz_suggest_key(item, "auth.lower + year + shorttitle(1).lower") cz_suggest_key(item, existing = "ebihara2006taxonomic", formula = "auth.lower + year + shorttitle(1).lower" )
Scans Quarto / R Markdown documents for @key citations, looks the keys up
among the pinned citekeys in your Zotero library, and writes a bibliography
file containing only the cited items. Zotero is the source of truth for the
content of each reference; the documents decide which references are needed.
cz_update_bib( qmd = NULL, out = NULL, format = NULL, bare = NULL, allow_incomplete = FALSE, auth = cz_auth(), cache = getOption("citezot.cache", FALSE), quiet = FALSE )cz_update_bib( qmd = NULL, out = NULL, format = NULL, bare = NULL, allow_incomplete = FALSE, auth = cz_auth(), cache = getOption("citezot.cache", FALSE), quiet = FALSE )
qmd |
Documents to scan for |
out |
Bibliography file to write. Defaults to the file named by
|
format |
|
bare |
Optional files (e.g. CSVs) scanned for bare citekey-shaped
tokens such as |
allow_incomplete |
Write the file even if keys cited in |
auth |
Credentials from |
cache |
Passed to |
quiet |
Suppress the summary message. |
Safety: if a key cited in a document is not found in Zotero, the function
stops and leaves the existing output file untouched (a bibliography that
silently loses entries breaks the render). Set allow_incomplete = TRUE to
write what was found anyway. Keys found only in bare files, and
duplicate citekeys in Zotero (the first match is used), give warnings. The
file is only rewritten when its content would change, so unchanged
bibliographies keep their modification time.
out, invisibly (handy for targets::tar_target(format = "file")).
"bib" (default) writes BibLaTeX using Zotero's own exporter, with each
entry keyed by its pinned citekey; it works with every Quarto output format,
including Typst. "yaml" writes CSL-YAML (a references: list with id
and citation-key set to the citekey). The format is inferred from the
extension of out (.bib or .yaml/.yml).
## Not run: # reads ZOTERO_LIBRARY_ID / ZOTERO_API_KEY from the environment cz_update_bib("manuscript.qmd") cz_update_bib("manuscript.qmd", out = "references.yaml") ## End(Not run)## Not run: # reads ZOTERO_LIBRARY_ID / ZOTERO_API_KEY from the environment cz_update_bib("manuscript.qmd") cz_update_bib("manuscript.qmd", out = "references.yaml") ## End(Not run)
Changes selected fields of one item and shows exactly what will change. The
default is a dry run: the item is read and a before/after diff is shown, but
nothing is written. Use dry_run = FALSE to write.
cz_update_item( item, fields, dry_run = TRUE, auth = cz_auth(), cache = getOption("citezot.cache", FALSE) )cz_update_item( item, fields, dry_run = TRUE, auth = cz_auth(), cache = getOption("citezot.cache", FALSE) )
item |
The item: a DOI, Zotero item ID, citekey, or one-row result of
|
fields |
A named list of new values, e.g.
|
dry_run |
If |
auth |
Credentials from |
cache |
Passed to |
Only fields whose value actually changes are sent (a PATCH), together with
If-Unmodified-Since-Version: if the item was edited elsewhere since it was
read, Zotero rejects the write (HTTP 412) and nothing is overwritten.
Plain fields take a single string (use "" to clear one). creators,
tags, collections and relations take lists and are replaced as a
whole, so to add one tag pass the full list of tags. Identity and
bookkeeping fields (key, version, itemType, dates, parentItem,
deleted) can't be changed, and the citekey is changed with cz_pin_key().
Invisibly, a list with item, changes (for each changed field,
its before and after) and written.
## Not run: cz_update_item("Regnier2024", list(date = "in press")) # preview cz_update_item("Regnier2024", list(date = "in press"), dry_run = FALSE) ## End(Not run)## Not run: cz_update_item("Regnier2024", list(date = "in press")) # preview cz_update_item("Regnier2024", list(date = "in press"), dry_run = FALSE) ## End(Not run)