blob: 9e339b3f7ddc6ce0b3952a87a5d0e2097f439951 [file] [log] [blame]
# File src/library/utils/R/citation.R
# Part of the R package, https://www.R-project.org
#
# Copyright (C) 1995-2018 The R Core Team
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# A copy of the GNU General Public License is available at
# https://www.R-project.org/Licenses/
## What a silly name ...
.is_not_nonempty_text <-
function(x)
is.null(x) || anyNA(x) || all(grepl("^[[:space:]]*$", x))
person <-
function(given = NULL, family = NULL, middle = NULL,
email = NULL, role = NULL, comment = NULL,
first = NULL, last = NULL)
{
## Arrange all arguments in lists of equal length.
args <- list(given = given, family = family, middle = middle,
email = email, role = role, comment = comment,
first = first, last = last)
if(all(vapply(args, is.null, NA)))
return(structure(list(), class = "person"))
args <- lapply(args, .listify)
args_length <- lengths(args)
if(!all(args_length_ok <- args_length %in% c(1L, max(args_length))))
warning(gettextf("Not all arguments are of the same length, the following need to be recycled: %s",
paste(names(args)[!args_length_ok],
collapse = ", ")),
domain = NA)
args <- lapply(args, function(x) rep_len(x, max(args_length)))
## <COMMENT Z>
## We could do this more elegantly, but let's just go through the
## list processing person by person. I'm just recycling the old
## person() code for this.
## </COMMENT>
person1 <-
function(given = NULL, family = NULL, middle = NULL,
email = NULL, role = NULL, comment = NULL,
first = NULL, last = NULL)
{
if(!.is_not_nonempty_text(first)) {
if(!.is_not_nonempty_text(given))
stop(gettextf("Use either %s or %s/%s but not both.",
sQuote("given"),
sQuote("first"), sQuote("middle")),
domain = NA)
## <FIXME>
## Start warning eventually ... maybe use message() for now?
message(gettextf("It is recommended to use %s instead of %s.",
sQuote("given"), sQuote("first")),
domain = NA)
## </FIXME>
given <- first
}
if(!.is_not_nonempty_text(middle)) {
## <FIXME>
## Start warning eventually ... maybe use message() for now?
message(gettextf("It is recommended to use %s instead of %s.",
sQuote("given"), sQuote("middle")),
domain = NA)
## </FIXME>
given <- c(given, unlist(strsplit(middle, "[[:space:]]+")))
}
if(!.is_not_nonempty_text(last)) {
if(!.is_not_nonempty_text(family))
stop(gettextf("Use either %s or %s but not both.",
sQuote("family"), sQuote("last")),
domain = NA)
## <FIXME>
## Start warning eventually ... maybe use message() for now?
message(gettextf("It is recommended to use %s instead of %s.",
sQuote("family"), sQuote("last")),
domain = NA)
## </FIXME>
family <- last
}
## Set all empty arguments to NULL.
if(.is_not_nonempty_text(given)) given <- NULL
if(.is_not_nonempty_text(family)) family <- NULL
if(.is_not_nonempty_text(email)) email <- NULL
if(.is_not_nonempty_text(role)) {
if(!is.null(role))
warning(sprintf(ngettext(length(role),
"Invalid role specification: %s.",
"Invalid role specifications: %s."),
paste(sQuote(role), collapse = ", ")),
domain = NA)
role <- NULL
}
if(.is_not_nonempty_text(comment)) comment <- NULL
## <FIXME>
## Use something along the lines of
## tools:::.valid_maintainer_field_regexp
## to validate given email addresses.
## </FIXME>
if(length(role))
role <- .canonicalize_person_role(role)
if(length(comment)) {
## Be nice and recognize ORCID identifiers given as URLs
## but perhaps without an ORCID name.
ind <- grepl(paste0("^https?://orcid.org/",
"([[:digit:]]{4}[-]){3}[[:digit:]]{3}[[:alnum:]]$"),
comment)
if(any(ind)) {
if(is.null(names(comment)))
names(comment) <- ifelse(ind, "ORCID", "")
else
names(comment)[ind] <- "ORCID"
}
}
rval <- list(given = given, family = family, role = role,
email = email, comment = comment)
## Canonicalize 0-length character arguments to NULL.
if(any(ind <- (lengths(rval) == 0L)))
rval[ind] <- vector("list", length = sum(ind))
## Give nothing if there is nothing.
if(all(vapply(rval, is.null, NA)))
NULL
else
rval
} ## end{ person1 }
force(person1)# {codetools}
rval <-
lapply(seq_along(args$given),
function(i)
with(args,
person1(given = given[[i]], family = family[[i]],
middle = middle[[i]], email = email[[i]],
role = role[[i]], comment = comment[[i]],
first = first[[i]], last = last[[i]])))
## <COMMENT Z>
## Should we check that for each person there is at least one
## non-NULL entry?
## </COMMENT>
## Yes!
structure(rval[!vapply(rval, is.null, NA)],
class = "person")
}
.canonicalize_person_role <-
function(role)
{
## Be nice. Given roles must either exactly match the role code,
## or be uniquely pmatchable modulo case against the role terms.
pos <- which(is.na(match(role, MARC_relator_db$code)))
if(length(pos)) {
ind <- pmatch(tolower(role[pos]),
tolower(MARC_relator_db$name),
0L)
role[pos[ind > 0L]] <- MARC_relator_db$code[ind]
if(any(ind <- (ind == 0L))) {
warning(sprintf(ngettext(length(pos[ind]),
"Invalid role specification: %s.",
"Invalid role specifications: %s."),
paste(sQuote(role[pos[ind]]), collapse = ", ")),
domain = NA)
role <- role[-pos[ind]]
}
}
role
}
`[[.person` <-
`[.person` <-
function(x, i)
{
rval <- unclass(x)[i]
class(rval) <- class(x)
return(rval)
}
print.person <-
function(x, ...)
{
if(length(x)) print(format(x, ...))
invisible(x)
}
`$.person` <-
function(x, name)
{
## <COMMENT Z>
## extract internal list elements, return list if length > 1, vector
## otherwise (to mirror the behaviur of the input format for
## person())
## </COMMENT>
name <- match.arg(name,
c("given", "family", "role", "email", "comment",
"first", "last", "middle")) # for now ...
## <COMMENT Z>
## Let's be nice and support first/middle/last for now.
## </COMMENT>
if(name %in% c("first", "last", "middle")) {
message(gettextf("It is recommended to use %s/%s instead of %s/%s/%s.",
sQuote("given"), sQuote("family"),
sQuote("first"), sQuote("middle"), sQuote("last")),
domain = NA)
oname <- name
name <- switch(name,
"first" = "given",
"middle" = "given",
"last" = "family"
)
} else {
oname <- name
}
rval <- lapply(unclass(x), function(p) p[[name]])
if(oname == "first") rval <- lapply(rval, head, 1L)
if(oname == "middle") {
rval <- lapply(rval, tail, -1L)
if(any(ind <- (lengths(rval) == 0L)))
rval[ind] <- vector("list", length = sum(ind))
}
if(length(rval) == 1L) rval <- rval[[1L]]
rval
}
`$<-.person` <-
function(x, name, value)
{
name <- match.arg(name, c("given", "family", "role", "email", "comment"))
x <- .listify(unclass(x))
value <- rep_len(value, length(x))
if(name == "role")
value <- lapply(value, .canonicalize_person_role)
for(i in seq_along(x)) {
x[[i]][[name]] <- if(.is_not_nonempty_text(value[[i]]))
NULL
else as.character(value[[i]])
}
class(x) <- "person"
x
}
c.person <-
function(..., recursive = FALSE)
{
args <- list(...)
if(!all(vapply(args, inherits, NA, "person")))
warning(gettextf("method is only applicable to %s objects",
sQuote("person")),
domain = NA)
args <- lapply(args, unclass)
rval <- do.call("c", args)
class(rval) <- "person"
rval
}
as.person <-
function(x)
UseMethod("as.person")
as.person.default <-
function(x)
{
if(inherits(x, "person")) return(x)
x <- trimws(as.character(x))
if(!length(x)) return(person())
## Need to split the strings into individual person components.
## We used to split at ',' and 'and', but of course these could be
## contained in roles or comments as well.
## Hence, try the following.
## A. Replace all comment, role and email substrings by all-z
## substrings of the same length.
## B. Tokenize the strings according to the split regexp matches in
## the corresponding z-ified strings.
## C. Extract the persons from the thus obtained tokens.
## Create strings consisting of a given character c with given
## numbers n of characters.
strings <- function(n, c = "z") {
vapply(Map(rep.int, rep.int(c, length(n)), n,
USE.NAMES = FALSE),
paste, "", collapse = "")
}
## Replace matches of pattern in x by all-z substrings of the same
## length.
zify <- function(pattern, x) {
if(!length(x)) return(character())
m <- gregexpr(pattern, x)
regmatches(x, m) <-
Map(strings, lapply(regmatches(x, m), nchar))
x
}
## Step A.
y <- zify("\\([^)]*\\)", x)
y <- zify("\\[[^]]*\\]", y)
y <- zify("<[^>]*>", y)
## Step B.
pattern <- "[[:space:]]?(,|,?[[:space:]]and)[[:space:]]+"
x <- do.call("c",
regmatches(x, gregexpr(pattern, y), invert = TRUE))
x <- x[!vapply(x, .is_not_nonempty_text, NA)]
## don't expect Jr. to be a person
jr <- which(!is.na(match(x, c("Jr", "Jr.", "jr", "jr."))))
if(length(jr)) {
jr <- jr[jr > 1L]
x[jr - 1L] <- paste(x[jr - 1L], x[jr], sep = ", ")
x <- x[-jr]
}
if(!length(x)) return(person())
## Step C.
as_person1 <- function(x) {
comment <- if(grepl("\\(.*\\)", x))
sub(".*\\(([^)]*)\\).*", "\\1", x)
else NULL
x <- sub("[[:space:]]*\\([^)]*\\)", "", x)
email <- if(grepl("<.*>", x))
unlist(strsplit(gsub("[[:space:]]*", "",
sub(".*<([^>]*)>.*", "\\1", x)),
",", fixed = TRUE))
else NULL
x <- sub("[[:space:]]*<[^>]*>", "", x)
role <- if(grepl("\\[.*\\]", x))
unlist(strsplit(gsub("[[:space:]]*", "",
sub(".*\\[([^]]*)\\].*", "\\1", x)),
",", fixed = TRUE))
else NULL
x <- sub("[[:space:]]*\\[[^)]*\\]", "", x)
x <- unlist(strsplit(x, "[[:space:]]+"))
## try to correctly guess von/van/de, Jr., etc.
jr <- c("Jr", "Jr.")
von <- c("De", "Den", "Der", "La", "Le", "Ten", "Van", "Von")
family <- x[length(x)]
given <- x[-length(x)]
if(length(family) &&
!is.na(match(family, c(jr, tolower(jr))))) {
family <- paste(given[length(given)], family)
given <- given[-length(given)]
}
if((ng <- length(given)) &&
!is.na(match(gng <- given[ng], c(von, tolower(von))))) {
family <- paste(gng, family)
given <- given[-ng]
}
if((ng <- length(given)) &&
!is.na(match(gng <- given[ng], c(von, tolower(von))))) {
family <- paste(gng, family)
given <- given[-ng]
}
z <- person(given = given, family = family,
email = email, role = role, comment = comment)
return(z)
}
as.list(do.call("c", lapply(x, as_person1)))
}
personList <-
function(...)
{
z <- list(...)
if(!all(vapply(z, inherits, NA, "person")))
stop(gettextf("all arguments must be of class %s",
dQuote("person")),
domain = NA)
do.call("c", z)
}
as.personList <-
function(x)
UseMethod("as.personList")
as.personList.person <-
function(x)
x
as.personList.default <-
function(x)
{
if(inherits(x, "person")) return(x)
do.call("c", lapply(x, as.person))
}
format.person <-
function(x,
include = c("given", "family", "email", "role", "comment"),
braces =
list(given = "", family = "", email = c("<", ">"),
role = c("[", "]"), comment = c("(", ")")),
collapse =
list(given = " ", family = " ", email = ", ",
role = ", ", comment = ", "),
...,
style = c("text", "R")
)
{
if(!length(x)) return(character())
style <- match.arg(style)
if(style == "R") return(.format_person_as_R_code(x))
args <- c("given", "family", "email", "role", "comment")
include <- sapply(include, match.arg, args)
## process defaults
braces <- braces[args]
collapse <- collapse[args]
names(braces) <- names(collapse) <- args
if(is.null(braces$given)) braces$given <- ""
if(is.null(braces$family)) braces$family <- ""
if(is.null(braces$email)) braces$email <- c("<", ">")
if(is.null(braces$role)) braces$role <- c("[", "]")
if(is.null(braces$comment)) braces$comment <- c("(", ")")
braces <- lapply(braces, rep_len, 2L)
if(is.null(collapse$given)) collapse$given <- " "
if(is.null(collapse$family)) collapse$family <- " "
if(is.null(collapse$email)) collapse$email <- ", "
if(is.null(collapse$role)) collapse$role <- ", "
if(is.null(collapse$comment)) collapse$comment <- ", "
collapse <- lapply(collapse, rep_len, 1L)
## extract selected elements
x <- lapply(unclass(x), "[", include)
braces <- braces[include]
collapse <- collapse[include]
if(any(include == "comment"))
x <- lapply(x,
function(e) {
e$comment <-
.expand_ORCID_identifier(e$comment)
e
})
paste_collapse <- function(x, collapse) {
if(is.na(collapse) || isFALSE(collapse)) {
x[1L]
} else {
paste(x, collapse = collapse)
}
}
## format 1 person
format_person1 <- function(p) {
rval <- lapply(seq_along(p),
function(i) {
if(is.null(p[[i]]))
NULL
else
paste0(braces[[i]][1L],
paste_collapse(p[[i]],
collapse[[i]]),
braces[[i]][2L])
})
paste(do.call("c", rval), collapse = " ")
}
sapply(x, format_person1)
}
as.character.person <-
function(x, ...)
format(x, ...)
toBibtex.person <-
function(object, ...)
{
object <- sapply(object, function(p) {
br <- if(is.null(p$family)) c("{", "}") else c("", "")
format(p, include = c("family", "given"),
braces = list(given = br, family = c("", ",")))
})
paste(object[nzchar(object)], collapse = " and ")
}
.canonicalize_ORCID_identifier <-
function(x)
{
paste0("https://orcid.org/", sub(".*/", "", x))
}
.expand_ORCID_identifier <-
function(x)
{
if(any(ind <- (names(x) == "ORCID")))
x[ind] <- paste0("<",
.canonicalize_ORCID_identifier(x[ind]),
">")
x
}
######################################################################
bibentry <-
function(bibtype, textVersion = NULL, header = NULL, footer = NULL, key = NULL,
...,
other = list(), mheader = NULL, mfooter = NULL)
{
BibTeX_names <- names(tools:::BibTeX_entry_field_db)
args <- c(list(...), other)
if(!length(args))
return(structure(list(), class = "bibentry"))
if(any(vapply(names(args), .is_not_nonempty_text, NA)))
stop("all fields have to be named")
## arrange all arguments in lists of equal length
args <- c(list(bibtype = bibtype, textVersion = textVersion,
header = header, footer = footer, key = key), list(...))
args <- lapply(args, .listify)
other <- lapply(other, .listify)
max_length <- max(lengths(c(args, other)))
args_length <- lengths(args)
if(!all(args_length_ok <- args_length %in% c(1L, max_length)))
warning(gettextf("Not all arguments are of the same length, the following need to be recycled: %s",
paste(names(args)[!args_length_ok],
collapse = ", ")),
domain = NA)
args <- lapply(args, function(x) rep_len(x, max_length))
other_length <- lengths(other)
if(!all(other_length_ok <- other_length %in% c(1L, max_length)))
warning(gettextf("Not all arguments are of the same length, the following need to be recycled: %s",
paste(names(other)[!other_length_ok],
collapse = ", ")),
domain = NA)
other <- lapply(other, function(x) rep_len(x, max_length))
bibentry1 <-
function(bibtype, textVersion, header = NULL, footer = NULL, key = NULL, ..., other = list())
{
## process bibtype
bibtype <- as.character(bibtype)
stopifnot(length(bibtype) == 1L)
pos <- match(tolower(bibtype), tolower(BibTeX_names))
if(is.na(pos))
stop(gettextf("%s has to be one of %s",
sQuote("bibtype"),
paste(BibTeX_names, collapse = ", ")),
domain = NA)
bibtype <- BibTeX_names[pos]
## process fields
rval <- c(list(...), other)
rval <- rval[!vapply(rval, .is_not_nonempty_text, NA)]
fields <- tolower(names(rval))
names(rval) <- fields
attr(rval, "bibtype") <- bibtype
## check required fields
.bibentry_check_bibentry1(rval)
## canonicalize
pos <- fields %in% c("author", "editor")
if(any(pos)) {
for(i in which(pos)) rval[[i]] <- as.person(rval[[i]])
}
if(any(!pos)) {
for(i in which(!pos)) rval[[i]] <- as.character(rval[[i]])
}
## set attributes
attr(rval, "key") <-
if(is.null(key)) NULL else as.character(key)
if(!is.null(textVersion))
attr(rval, "textVersion") <- as.character(textVersion)
if(!.is_not_nonempty_text(header))
attr(rval, "header") <- paste(header, collapse = "\n")
if(!.is_not_nonempty_text(footer))
attr(rval, "footer") <- paste(footer, collapse = "\n")
return(rval)
}
rval <- lapply(seq_along(args$bibtype),
function(i)
do.call(bibentry1,
c(lapply(args, "[[", i),
list(other = lapply(other, "[[", i)))))
## add main header/footer for overall bibentry vector
if(!.is_not_nonempty_text(mheader))
attr(rval, "mheader") <- paste(mheader, collapse = "\n")
if(!.is_not_nonempty_text(mfooter))
attr(rval, "mfooter") <- paste(mfooter, collapse = "\n")
class(rval) <- "bibentry"
rval
}
.bibentry_check_bibentry1 <-
function(x, force = FALSE)
{
fields <- names(x)
if(!force && !.is_not_nonempty_text(x$crossref)) return(NULL)
bibtype <- attr(x, "bibtype")
rfields <-
strsplit(tools:::BibTeX_entry_field_db[[bibtype]], "|",
fixed = TRUE)
if(length(rfields) > 0L) {
ok <- vapply(rfields, function(f) any(f %in% fields), NA)
if(any(!ok))
stop(sprintf(ngettext(sum(!ok),
"A bibentry of bibtype %s has to specify the field: %s",
"A bibentry of bibtype %s has to specify the fields: %s"),
sQuote(bibtype), paste(rfields[!ok], collapse = ", ")),
domain = NA)
}
}
bibentry_attribute_names <-
c("bibtype", "textVersion", "header", "footer", "key")
bibentry_list_attribute_names <-
c("mheader", "mfooter")
.bibentry_get_key <-
function(x)
{
if(!length(x)) return(character())
keys <- lapply(unclass(x), attr, "key")
keys[!vapply(keys, length, 0L)] <- ""
unlist(keys)
}
`[[.bibentry` <-
`[.bibentry` <-
function(x, i, drop = TRUE)
{
if(!length(x)) return(x)
cl <- class(x)
class(x) <- NULL
## For character subscripting, use keys if there are no names.
## Note that creating bibentries does not add the keys as names:
## assuming that both can independently be set, we would need to
## track whether names were auto-generated or not.
## (We could consider providing a names() getter which returns given
## names or keys as used for character subscripting, though).
if(is.character(i) && is.null(names(x)))
names(x) <- .bibentry_get_key(x)
y <- x[i]
if(!all(ok <- lengths(y) > 0L)) {
warning("subscript out of bounds")
y <- y[ok]
}
if(!drop)
attributes(y) <- attributes(x)[bibentry_list_attribute_names]
class(y) <- cl
y
}
bibentry_format_styles <-
c("text", "Bibtex", "citation", "html", "latex", "textVersion", "R")
.bibentry_match_format_style <-
function(style)
{
ind <- pmatch(tolower(style), tolower(bibentry_format_styles),
nomatch = 0L)
if(all(ind == 0L))
stop(gettextf("%s should be one of %s",
sQuote("style"),
paste(dQuote(bibentry_format_styles),
collapse = ", ")),
domain = NA)
bibentry_format_styles[ind]
}
format.bibentry <-
function(x, style = "text", .bibstyle = NULL,
citation.bibtex.max = getOption("citation.bibtex.max", 1),
bibtex = length(x) <= citation.bibtex.max,
sort = FALSE,
macros = NULL,
...)
{
if(!length(x)) return(character())
style <- .bibentry_match_format_style(style)
if(sort) x <- sort(x, .bibstyle = .bibstyle)
x$.index <- as.list(seq_along(x))
if(!missing(citation.bibtex.max))
warning(gettextf("Argument '%s' is deprecated; rather set '%s' instead.",
"citation.bibtex.max", "bibtex=*"),
domain = NA)
format_via_Rd <- function(f) {
out <- file()
saveopt <- tools::Rd2txt_options(width = getOption("width"))
on.exit({tools::Rd2txt_options(saveopt); close(out)})
permissive <-
Sys.getenv("_R_UTILS_FORMAT_BIBENTRY_VIA_RD_PERMISSIVE_",
"TRUE")
permissive <- tools:::config_val_to_logical(permissive)
macros <- if(is.null(macros))
tools:::initialRdMacros()
else if(is.character(macros))
tools::loadRdMacros(macros,
tools:::initialRdMacros())
sapply(.bibentry_expand_crossrefs(x),
function(y) {
txt <- tools::toRd(y, style = .bibstyle)
## <FIXME>
## Ensure a closing </p> via a final empty line for
## now (PR #15692).
if(style == "html") txt <- paste(txt, "\n")
## </FIXME>
con <- textConnection(txt)
on.exit(close(con))
rd <- tools::parse_Rd(con,
fragment = TRUE,
permissive = permissive,
macros = macros)
rd <- tools:::processRdSexprs(rd,
"build",
macros = attr(rd, "macros"))
f(rd, fragment = TRUE, out = out, ...)
paste(readLines(out), collapse = "\n")
})
}
format_as_citation <- function(x, msg) {
c(paste(strwrap(attr(x, "mheader")), collapse = "\n"),
unlist(lapply(x, function(y) {
paste(c(if(!is.null(y$header))
c(strwrap(y$header), ""),
if(!is.null(y$textVersion)) {
strwrap(y$textVersion, prefix = " ")
} else {
format(y)
},
if(bibtex) {
c(gettext("\nA BibTeX entry for LaTeX users is\n"),
paste0(" ", unclass(toBibtex(y))))
},
if(!is.null(y$footer))
c("", strwrap(y$footer))),
collapse = "\n")
})),
paste(strwrap(c(attr(x, "mfooter"),
if(!bibtex && msg)
"To see these entries in BibTeX format, use 'print(<citation>, bibtex=TRUE)',
'toBibtex(.)', or set 'options(citation.bibtex.max=999)'."
)), collapse = "\n")
)
}
out <-
switch(style,
"text" = format_via_Rd(tools::Rd2txt),
"html" = format_via_Rd(tools::Rd2HTML),
"latex" = format_via_Rd(tools::Rd2latex),
"Bibtex" = {
unlist(lapply(x,
function(y)
paste(toBibtex(y), collapse = "\n")))
},
"textVersion" = {
out <- lapply(unclass(x), attr, "textVersion")
out[!lengths(out)] <- ""
unlist(out)
},
"citation" = format_as_citation(x,
msg = missing(bibtex) &&
missing(citation.bibtex.max)),
"R" = .format_bibentry_as_R_code(x, ...)
)
as.character(out)
}
.bibentry_expand_crossrefs <-
function(x, more = list())
{
y <- if(length(more))
do.call(c, c(list(x), more))
else
x
x <- unclass(x)
y <- unclass(y)
crossrefs <- lapply(x, `[[`, "crossref")
pc <- which(vapply(crossrefs, length, 0L) > 0L)
if(length(pc)) {
pk <- match(unlist(crossrefs[pc]), .bibentry_get_key(y))
## If an entry has a crossref we cannot resolve it might still
## be complete: we could warn about the bad crossref ...
ok <- !is.na(pk)
## Merge entries: note that InCollection and InProceedings need
## to remap title to booktitle as needed.
x[pc[ok]] <-
Map(function(u, v) {
add <- setdiff(names(v), names(u))
u[add] <- v[add]
if(!is.na(match(tolower(attr(u, "bibtype")),
c("incollection", "inproceedings"))) &&
is.null(u$booktitle))
u$booktitle <- v$title
u
},
x[pc[ok]],
y[pk[ok]])
## Now check entries with crossrefs for completeness.
## Ignore bad entries with a warning.
status <- lapply(x[pc],
function(e)
tryCatch(.bibentry_check_bibentry1(e, TRUE),
error = identity))
bad <- which(vapply(status, inherits, NA, "error"))
if(length(bad)) {
for(b in bad) {
warning(gettextf("Dropping invalid entry %d:\n%s",
pc[b],
conditionMessage(status[[b]])))
}
x[pc[bad]] <- NULL
}
}
class(x) <- "bibentry"
x
}
print.bibentry <-
function(x, style = "text", .bibstyle = NULL, ...)
{
style <- .bibentry_match_format_style(style)
if(style == "R") {
writeLines(format(x, "R", collapse = TRUE, ...))
} else if(length(x)) {
y <- format(x, style, .bibstyle, ...)
if(style == "citation") {
## Printing in citation style does extra headers/footers
## (which however may be empty), so it is handled
## differently.
n <- length(y)
if(nzchar(header <- y[1L]))
header <- c("", header, "")
if(nzchar(footer <- y[n]))
footer <- c("", footer, "")
writeLines(c(header,
paste(y[-c(1L, n)], collapse = "\n\n"),
footer))
} else {
writeLines(paste(y, collapse = "\n\n"))
}
}
invisible(x)
}
.format_call_RR <-
function(cname, cargs)
{
## Format call with ragged right argument list (one arg per line).
cargs <- as.list(cargs)
n <- length(cargs)
lens <- lengths(cargs)
sums <- cumsum(lens)
starters <- c(sprintf("%s(", cname),
rep.int(strrep(" ", nchar(cname) + 1L), sums[n] - 1L))
trailers <- c(rep.int("", sums[n] - 1L), ")")
trailers[sums[-n]] <- ","
sprintf("%s%s%s", starters, unlist(cargs), trailers)
}
.format_bibentry_as_R_code <-
function(x, collapse = FALSE)
{
if(!length(x)) return("bibentry()")
x$.index <- NULL
## There are two subleties for constructing R calls giving a given
## bibentry object.
## * There can be mheader and mfooter entries.
## If there are, we put them into the first bibentry.
## * There could be field names which clash with the names of the
## bibentry() formals: these would need to be put as a list into
## the 'other' formal.
## The following make it into the attributes of an entry.
anames <- bibentry_attribute_names
## The following make it into the attributes of the object.
manames <- c("mheader", "mfooter")
## Format a single element (person or string, at least for now).
f <- function(e) {
if(inherits(e, "person"))
.format_person_as_R_code(e)
else
deparse(e)
}
g <- function(u, v) {
prefix <- sprintf("%s = ", u)
n <- length(v)
if(n > 1L)
prefix <- c(prefix,
rep.int(strrep(" ", nchar(prefix)), n - 1L))
sprintf("%s%s", prefix, v)
}
s <- lapply(unclass(x),
function(e) {
a <- Filter(length, attributes(e)[anames])
e <- e[!vapply(e, is.null, NA)]
ind <- !is.na(match(names(e),
c(anames, manames, "other")))
if(any(ind)) {
other <- paste(names(e[ind]),
sapply(e[ind], f),
sep = " = ")
other <- Map(g,
names(e[ind]),
sapply(e[ind], f))
other <- .format_call_RR("list", other)
e <- e[!ind]
} else {
other <- NULL
}
c(Map(g, names(a), sapply(a, deparse)),
Map(g, names(e), sapply(e, f)),
if(length(other)) list(g("other", other)))
})
if(!is.null(mheader <- attr(x, "mheader")))
s[[1L]] <- c(s[[1L]],
paste("mheader =", deparse(mheader)))
if(!is.null(mfooter <- attr(x, "mfooter")))
s[[1L]] <- c(s[[1L]],
paste("mfooter =", deparse(mfooter)))
s <- Map(.format_call_RR, "bibentry", s)
if(collapse && (length(s) > 1L))
paste(.format_call_RR("c", s), collapse = "\n")
else
unlist(lapply(s, paste, collapse = "\n"), use.names = FALSE)
}
.format_person_as_R_code <-
function(x)
{
s <- lapply(unclass(x),
function(e) {
e <- e[!vapply(e, is.null, NA)]
cargs <-
sprintf("%s = %s", names(e), sapply(e, deparse))
.format_call_RR("person", cargs)
})
if(length(s) > 1L)
.format_call_RR("c", s)
else
unlist(s, use.names = FALSE)
}
`$.bibentry` <-
function(x, name)
{
if(!length(x)) return(NULL)
## <COMMENT Z>
## Extract internal list elements, return list if length > 1, vector
## otherwise (to mirror the behaviour of the input format for
## bibentry())
## </COMMENT>
is_attribute <- name %in% bibentry_attribute_names
rval <- if(is_attribute) lapply(unclass(x), attr, name)
else lapply(unclass(x), "[[", name)
if(length(rval) == 1L) rval <- rval[[1L]]
rval
}
`$<-.bibentry` <-
function(x, name, value)
{
is_attribute <- name %in% bibentry_attribute_names
x <- unclass(x)
name <- tolower(name)
## recycle value
value <- rep_len(.listify(value), length(x))
## check bibtype
if(name == "bibtype") {
stopifnot(all(lengths(value) == 1L))
BibTeX_names <- names(tools:::BibTeX_entry_field_db)
value <- unlist(value)
pos <- match(tolower(value), tolower(BibTeX_names))
if(anyNA(pos))
stop(gettextf("%s has to be one of %s",
sQuote("bibtype"),
paste(BibTeX_names, collapse = ", ")),
domain = NA)
value <- as.list(BibTeX_names[pos])
}
## replace all values
for(i in seq_along(x)) {
if(is_attribute) {
attr(x[[i]], name) <-
if(is.null(value[[i]])) NULL else paste(value[[i]])
} else {
x[[i]][[name]] <-
if(is.null(value[[i]])) NULL else {
if(name %in% c("author", "editor"))
as.person(value[[i]])
else paste(value[[i]])
}
}
}
## check whether all elements still have their required fields
for(i in seq_along(x)) .bibentry_check_bibentry1(x[[i]])
class(x) <- "bibentry"
x
}
c.bibentry <-
function(..., recursive = FALSE)
{
args <- list(...)
if(!all(vapply(args, inherits, NA, "bibentry")))
warning(gettextf("method is only applicable to %s objects",
sQuote("bibentry")),
domain = NA)
args <- lapply(args, unclass)
rval <- do.call("c", args)
class(rval) <- "bibentry"
rval
}
toBibtex.bibentry <-
function(object, ...)
{
format_author <- function(author) paste(sapply(author, function(p) {
fnms <- p$family
only_given_or_family <-
(is.null(fnms) || is.null(p$given)) &&
!(identical(fnms, "others") || identical(p$given, "others"))
fbrc <- if(length(fnms) > 1L ||
any(grepl("[[:space:]]", fnms)) ||
only_given_or_family) c("{", "}") else ""
gbrc <- if(only_given_or_family) c("{", "}") else ""
format(p, include = c("given", "family"),
braces = list(given = gbrc, family = fbrc))
}), collapse = " and ")
format_bibentry1 <- function(object) {
object <- unclass(object)[[1L]]
rval <- paste0("@", attr(object, "bibtype"), "{", attr(object, "key"), ",")
if("author" %in% names(object))
object$author <- format_author(object$author)
if("editor" %in% names(object))
object$editor <- format_author(object$editor)
rval <- c(rval,
sapply(names(object), function (n)
paste0(" ", n, " = {", object[[n]], "},")),
"}", "")
return(rval)
}
if(length(object)) {
object$.index <- NULL
rval <- head(unlist(lapply(object, format_bibentry1)), -1L)
} else
rval <- character()
class(rval) <- "Bibtex"
rval
}
sort.bibentry <-
function(x, decreasing = FALSE, .bibstyle = NULL, drop = FALSE, ...)
{
x[order(tools::bibstyle(.bibstyle, .default = FALSE)$sortKeys(x),
decreasing = decreasing),
drop = drop]
}
rep.bibentry <-
function(x, ...)
{
y <- NextMethod("rep")
class(y) <- class(x)
y
}
unique.bibentry <-
function(x, ...)
{
y <- NextMethod("unique")
class(y) <- class(x)
y
}
######################################################################
citEntry <-
function(entry, textVersion, header = NULL, footer = NULL, ...)
bibentry(bibtype = entry, textVersion = textVersion,
header = header, footer = footer, ...)
citHeader <-
function(...)
{
rval <- paste(...)
class(rval) <- "citationHeader"
rval
}
citFooter <-
function(...)
{
rval <- paste(...)
class(rval) <- "citationFooter"
rval
}
readCitationFile <-
function(file, meta = NULL)
{
meta <- as.list(meta)
exprs <- tools:::.parse_CITATION_file(file, meta$Encoding)
rval <- list()
mheader <- NULL
mfooter <- NULL
envir <- new.env(hash = TRUE)
## Make the package metadata available to the citation entries.
assign("meta", meta, envir = envir)
for(expr in exprs) {
x <- eval(expr, envir = envir)
if(inherits(x, "bibentry"))
rval <- c(rval, list(x))
else if(identical(class(x), "citationHeader"))
mheader <- c(mheader, x)
else if(identical(class(x), "citationFooter"))
mfooter <- c(mfooter, x)
}
rval <- if(length(rval) == 1L)
rval[[1L]]
else
do.call("c", rval)
if(!.is_not_nonempty_text(mheader))
attr(rval, "mheader") <- paste(mheader, collapse = "\n")
if(!.is_not_nonempty_text(mfooter))
attr(rval, "mfooter") <- paste(mfooter, collapse = "\n")
.citation(rval, meta$Package)
}
######################################################################
citation <-
function(package = "base", lib.loc = NULL, auto = NULL)
{
## Allow citation(auto = meta) in CITATION files to include
## auto-generated package citation.
if(!is.null(auto) &&
!is.logical(auto) &&
!any(is.na(match(c("Package", "Version", "Title"),
names(meta <- as.list(auto))))) &&
!all(is.na(match(c("Authors@R", "Author"),
names(meta))))
) {
auto_was_meta <- TRUE
package <- meta$Package
} else {
auto_was_meta <- FALSE
dir <- system.file(package = package, lib.loc = lib.loc)
if(dir == "")
stop(packageNotFoundError(package, lib.loc, sys.call()))
meta <- packageDescription(pkg = package,
lib.loc = dirname(dir))
## if(is.null(auto)): Use default auto-citation if no CITATION
## available.
citfile <- file.path(dir, "CITATION")
test <- file_test("-f", citfile)
if(!test) { # allow package source
citfile <- file.path(dir, "inst", "CITATION")
test <- file_test("-f", citfile)
}
if(is.null(auto)) auto <- !test
## if CITATION is available
if(!auto) {
return(readCitationFile(citfile, meta))
}
}
## Auto-generate citation info.
## Base packages without a CITATION file use the base citation.
if((!is.null(meta$Priority)) && (meta$Priority == "base")) {
cit <- citation("base", auto = FALSE)
attr(cit, "mheader")[1L] <-
paste0("The ", sQuote(package), " package is part of R. ",
attr(cit, "mheader")[1L])
return(.citation(cit, package))
}
year <- sub("-.*", "", meta$`Date/Publication`)
if(!length(year)) {
if(is.null(meta$Date)) {
warning(gettextf("no date field in DESCRIPTION file of package %s",
sQuote(package)),
domain = NA)
} else {
date <- trimws(as.vector(meta$Date))[1L]
date <- strptime(date, "%Y-%m-%d", tz = "GMT")
if(!is.na(date)) year <- format(date, "%Y")
}
}
## If neither Date/Publication nor Date work, try Packaged (build
## time stamp): if this fails too, use NA (PR #16550).
if(!length(year)) {
date <- as.POSIXlt(sub(";.*", "", trimws(meta$Packaged)[1L]))
if(!is.na(date)) year <- format(date, "%Y")
}
if(!length(year)) {
warning(gettextf("could not determine year for %s from package DESCRIPTION file",
sQuote(package)),
domain = NA)
year <- NA_character_
}
author <- meta$`Authors@R`
## <NOTE>
## Older versions took persons with no roles as "implied" authors.
## Now we only use persons with a name and a 'aut' role. If there
## are none, we use persons with a name and a 'cre' role.
## If this still gives nothing (which really should not happen), we
## fall back to the plain text Author field.
## Checking will at least note the cases where there are no persons
## with names and 'aut' or 'cre' roles.
if(length(author)) {
aar <- .read_authors_at_R_field(author)
author <- Filter(function(e) {
!(is.null(e$given) &&
is.null(e$family)) &&
!is.na(match("aut", e$role))
},
aar)
if(!length(author))
author <- Filter(function(e) {
!(is.null(e$given) &&
is.null(e$family)) &&
!is.na(match("cre", e$role))
},
aar)
}
if(length(author)) {
has_authors_at_R_field <- TRUE
} else {
has_authors_at_R_field <- FALSE
author <- as.personList(meta$Author)
}
## </NOTE>
z <- list(title = paste0(package, ": ", meta$Title),
author = author,
year = year,
note = paste("R package version", meta$Version)
)
if(identical(meta$Repository, "CRAN"))
z$url <-
sprintf("https://CRAN.R-project.org/package=%s", package)
if(identical(meta$Repository, "R-Forge")) {
z$url <- if(!is.null(rfp <- meta$"Repository/R-Forge/Project"))
sprintf("https://R-Forge.R-project.org/projects/%s/", rfp)
else
"https://R-Forge.R-project.org/"
if(!is.null(rfr <- meta$"Repository/R-Forge/Revision"))
z$note <- paste(z$note, rfr, sep = "/r")
}
if(!length(z$url) && !is.null(url <- meta$URL)) {
## Cannot have several URLs in BibTeX and bibentry object URL
## fields (PR #16240).
if(grepl("[, ]", url))
z$note <- url
else
z$url <- url
}
header <- if(!auto_was_meta) {
gettextf("To cite package %s in publications use:",
sQuote(package))
} else NULL
## No auto-generation message for auto was meta so that maintainers
## can safely use citation(auto = meta) in their CITATION without
## getting notified about possible needs for editing.
footer <- if(!has_authors_at_R_field && !auto_was_meta) {
gettextf("ATTENTION: This citation information has been auto-generated from the package DESCRIPTION file and may need manual editing, see %s.",
sQuote("help(\"citation\")"))
} else NULL
author <- format(z$author, include = c("given", "family"))
if(length(author) > 1L)
author <- paste(paste(head(author, -1L), collapse = ", "),
tail(author, 1L), sep = " and ")
rval <- bibentry(bibtype = "Manual",
textVersion =
paste0(author, " (", z$year, "). ", z$title, ". ",
z$note, ". ", z$url),
header = header,
footer = footer,
other = z
)
.citation(rval, package)
}
.citation <-
function(x, package = NULL)
structure(x, package = package, class = c("citation", "bibentry"))
.read_authors_at_R_field <-
function(x)
{
out <- if((Encoding(x) == "UTF-8") && !l10n_info()$"UTF-8") {
con <- file()
on.exit(close(con))
writeLines(x, con, useBytes = TRUE)
eval(parse(con, encoding = "UTF-8"))
} else {
eval(parse(text = x))
}
## Let's by nice ...
## Alternatively, we could throw an error.
if(!inherits(out, "person"))
out <- do.call("c", lapply(x, as.person))
out
}
.person_has_author_role <-
function(x)
{
## <NOTE>
## Earlier versions used
## is.null(r <- x$role) || "aut" %in% r
## using author roles by default.
## </NOTE>
"aut" %in% x$role
}
format.citation <-
function(x, style = "citation", ...) format.bibentry(x, style = style, ...)
print.citation <-
function(x, style = "citation", ...) print.bibentry(x, style = style, ...)
as.bibentry <-
function(x)
UseMethod("as.bibentry")
as.bibentry.bibentry <- identity
as.bibentry.citation <-
function(x)
{
class(x) <- "bibentry"
x
}
.listify <-
function(x)
if(inherits(x, "list")) x else list(x)
.format_person_for_plain_author_spec <-
function(x)
{
## Single person only.
## Give empty if person has no name or no role.
if((is.null(x$given) && is.null(x$family)) || is.null(x$role))
return("")
format(x, include = c("given", "family", "role", "comment"))
}
.format_authors_at_R_field_for_author <-
function(x)
{
if(is.character(x))
x <- .read_authors_at_R_field(x)
header <- attr(x, "header")
footer <- attr(x, "footer")
x <- sapply(x, .format_person_for_plain_author_spec)
## Drop persons with irrelevant roles.
x <- x[nzchar(x)]
## And format.
if(!length(x)) return("")
## We need to ensure that the first line has no indentation, whereas
## all subsequent lines are indented (as .write_description avoids
## folding for Author fields). We use a common indentation of 2,
## with an extra indentation of 2 within single author descriptions.
out <- paste(lapply(strwrap(x, indent = 0L, exdent = 4L,
simplify = FALSE),
paste, collapse = "\n"),
collapse = ",\n ")
if(!is.null(header)) {
header <- paste(strwrap(header, indent = 0L, exdent = 2L),
collapse = "\n")
out <- paste(header, out, sep = "\n ")
}
if(!is.null(footer)) {
footer <- paste(strwrap(footer, indent = 2L, exdent = 2L),
collapse = "\n")
out <- paste(out, footer, sep = ".\n")
}
out
}
## preserves encoding if marked.
.format_authors_at_R_field_for_maintainer <-
function(x)
{
if(is.character(x))
x <- .read_authors_at_R_field(x)
## Maintainers need cre roles, valid email addresses and non-empty
## names.
## <FIXME>
## Check validity of email addresses.
x <- Filter(function(e)
(!is.null(e$given) || !is.null(e$family)) && !is.null(e$email) && ("cre" %in% e$role),
x)
## </FIXME>
## If this leaves nothing or more than one ...
if(length(x) != 1L) return("")
display <- format(x, include = c("given", "family"))
address <- format(x, include = c("email"),
collapse = list(email = FALSE))
## Need to quote display names at least when they contain commas
## (RFC 5322 <https://tools.ietf.org/html/rfc5322>).
if(any(ind <- grepl(",", display))) {
display[ind] <- sprintf("\"%s\"",
gsub("\"", "\\\\\"", display[ind]))
}
paste(display, address)
}
## Cite using the default style (which is usually citeNatbib)
cite <-
function(keys, bib, ...)
{
fn <- tools::bibstyle()$cite
if (is.null(fn))
fn <- citeNatbib
fn(keys, bib, ...)
}
## Cite using natbib-like options. A bibstyle would normally
## choose some of these options and just have a cite(keys, bib, previous)
## function within it.
citeNatbib <-
local({
cited <- c()
function(keys, bib, textual = FALSE, before = NULL, after = NULL,
mode = c("authoryear", "numbers", "super"),
abbreviate = TRUE, longnamesfirst = TRUE,
bibpunct = c("(", ")", ";", "a", "", ","),
previous) {
shortName <- function(person) {
if (length(person$family))
paste(tools:::cleanupLatex(person$family), collapse = " ")
else
paste(tools:::cleanupLatex(person$given), collapse = " ")
}
authorList <- function(paper)
sapply(paper$author, shortName)
if (!missing(previous))
cited <<- previous
if (!missing(mode))
mode <- match.arg(mode)
else
mode <- switch(bibpunct[4L],
n = "numbers",
s = "super",
"authoryear")
numeric <- mode %in% c('numbers', 'super')
if (numeric)
bib <- sort(bib)
keys <- unlist(strsplit(keys, " *, *"))
if (!length(keys)) return("")
n <- length(keys)
first <- !(keys %in% cited)
cited <<- unique(c(cited, keys))
bibkeys <- unlist(bib$key)
# Use year to hold numeric entry; makes things
# simpler below
year <- match(keys, bibkeys)
papers <- bib[year]
if (textual || !numeric) {
auth <- character(n)
if (!numeric)
year <- unlist(papers$year)
authorLists <- lapply(papers, authorList)
lastAuthors <- NULL
for (i in seq_along(keys)) {
authors <- authorLists[[i]]
if (identical(lastAuthors, authors))
auth[i] <- ""
else {
if (length(authors) > 1L)
authors[length(authors)] <- paste("and", authors[length(authors)])
if (length(authors) > 2L) {
if (!abbreviate || (first[i] && longnamesfirst))
auth[i] <- paste(authors, collapse=", ")
else
auth[i] <- paste(authors[1L], "et al.")
} else
auth[i] <- paste(authors, collapse=" ")
}
lastAuthors <- authors
}
suppressauth <- which(!nzchar(auth))
if (length(suppressauth)) {
for (i in suppressauth)
year[i - 1L] <-
paste0(year[i - 1L], bibpunct[6L], " ", year[i])
auth <- auth[-suppressauth]
year <- year[-suppressauth]
}
}
if (!is.null(before))
before <- paste0(before, " ")
if (!is.null(after))
after <- paste0(" ", after)
if (textual) {
result <- paste0(bibpunct[1L], before, year, after, bibpunct[2L])
if (mode == "super")
result <- paste0(auth, "^{", result, "}")
else
result <- paste0(auth, " ", result)
result <- paste(result, collapse = paste0(bibpunct[3L], " "))
} else if (numeric) {
result <- paste(year, collapse=paste0(bibpunct[3L], " "))
result <- paste0(bibpunct[1L], before, result, after, bibpunct[2L])
if (mode == "super")
result <- paste0("^{", result, "}")
} else {
result <- paste0(auth, bibpunct[5L], " ", year)
result <- paste(result, collapse = paste0(bibpunct[3L], " "))
result <- paste0(bibpunct[1L], before, result, after, bibpunct[2L])
}
result
}
})