# File src/library/tools/R/news.R
# Part of the R package, http://www.R-project.org
#
# Copyright (C) 1995-2013 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
# http://www.r-project.org/Licenses/
## .build_news_db_from_R_NEWS <-
## function()
## {
## db <- readNEWS(chop = "keepAll")
## ## This currently is a list of x.y lists of x.y.z lists of
## ## categories list of entries.
## flatten <- function(e)
## cbind(rep.int(names(e), sapply(e, length)),
## unlist(lapply(e,
## function(s) {
## ## Also remove leading white space and
## ## trailing blank lines.
## lapply(s,
## function(e)
## sub("[[:space:]]*$", "",
## paste(sub("^ ", "", e),
## collapse = "\n")))
## }),
## use.names = FALSE))
## db <- lapply(Reduce(c, db), flatten)
## db <- do.call(rbind, Map(cbind, names(db), db))
## ## Squeeze in an empty date column.
## .make_news_db(cbind(db[, 1L], NA_character_, db[, -1L]),
## logical(nrow(db)))
## }
.build_news_db <-
function(package, lib.loc = NULL, format = NULL, reader = NULL)
{
dir <- system.file(package = package, lib.loc = lib.loc)
## Or maybe use find.package()?
##
## We had planned to eventually add support for DESCRIPTION
## News/File
## News/Format
## News/Reader
## News/Reader@R
## entries. But now that we're moving to NEWS.Rd, there seems
## little point in providing format/reader support ...
##
## Look for new-style inst/NEWS.Rd installed as NEWS.Rd.
## If not found, look for old-style
## NEWS inst/NEWS
## installed as NEWS (and ignore ChangeLog files).
nfile <- file.path(dir, "NEWS.Rd")
if(file_test("-f", nfile))
return(.build_news_db_from_package_NEWS_Rd(nfile))
nfile <- file.path(dir, "NEWS")
if(!file_test("-f", nfile))
return(invisible())
## Return NULL for now, no message that there is no NEWS or
## ChangeLog file.
if(!is.null(format))
.NotYetUsed("format", FALSE)
if(!is.null(reader))
.NotYetUsed("reader", FALSE)
reader <- .news_reader_default
reader(nfile)
}
.news_reader_default <-
function(file)
{
verbose <- getOption("verbose")
.collapse <- function(s) paste(s, collapse = "\n")
lines <- readLines(file, warn = FALSE)
## Re-encode if necessary.
if(any(ind <- is.na(nchar(lines, allowNA = TRUE)))) {
dir <- dirname(file)
if(basename(dir) == "inst")
dir <- dirname(file)
## This should now contain the DESCRIPTION file.
encoding <-
if(file.exists(dfile <- file.path(dir, "DESCRIPTION")))
.read_description(dfile)["Encoding"]
else
NA
if(!is.na(encoding))
lines[ind] <- iconv(lines[ind], encoding, "")
## Last resort.
if(any(is.na(nchar(lines[ind], allowNA = TRUE))))
lines[ind] <- iconv(lines[ind], "", "", sub = "byte")
}
## Save what we read in case we cannot figure out the news, in which
## case we simply return one entry with the whole text.
olines <- lines
## Get rid of underlines and friends.
lines <-
lines[!grepl("^[[:space:]]*[[:punct:]]*[[:space:]]*$", lines)]
## Determine lines containing version numbers, without being too
## liberal.
re_valid_package_name <- .standard_regexps()$valid_package_name
re_v <- sprintf("^([[:space:]]*(%s)|(%s))(%s).*$",
paste("CHANGES? *(IN|FOR).*VERSION *",
"CHANGES? *(IN|FOR|TO) *",
sep = "|"),
sprintf(paste(## TeachingDemos pomp ouch
"NEW IN .*",
## HyperbolicDist nls2 proto
"VERSION:? *",
"%s +",
## E.g., lattice:
## Changes in lattice 0.17
"CHANGES IN %s +",
## sv*
"== Changes in %s +",
## tcltk2
"== Version +",
## R2WinBUGS
"update *",
"v *",
"",
sep = "|"),
re_valid_package_name,
re_valid_package_name,
re_valid_package_name),
.standard_regexps()$valid_package_version
)
## Some people use
## $PACKAGE version $VERSION
## Let us try handling this later, or ask people to write their own
## readers.
ind <- grepl(re_v, lines, ignore.case = TRUE)
if(!any(ind))
return(.make_news_db(cbind(NA_character_,
NA_character_,
NA_character_,
.collapse(olines))))
## Could add an empty list of bad chunks (as none were found).
## Everything before the first version line is a header which will
## be dropped.
if(!ind[1L]) {
pos <- seq_len(which.max(ind) - 1L)
lines <- lines[-pos]
ind <- ind[-pos]
}
## Try catching date entries at the end of version lines as well.
re_d <- sprintf("^.*(%s)[[:punct:][:space:]]*$",
"[[:digit:]]{4}-[[:digit:]]{2}-[[:digit:]]{2}")
## Could try to allow for non ISO date specs ...
## Version lines determine the chunks, which after the version line
## should either start with a line tag (category) or an itemize
## "bullet".
chunks <- split(lines, cumsum(ind))
do_chunk <- function(chunk, header = NA_character_) {
## Process a single chunk.
## If there is no category header, the first line is the version
## line, after which the next non blank line should start with a
## line tag (category) or an itemize "bullet".
if(!is.na(header))
date <- NA_character_
else {
txt <- chunk[1L]
header <- sub(re_v, "\\6", txt, ignore.case = TRUE)
date <- if(grepl(re_d, txt, perl = TRUE))
sub(re_d, "\\1", txt, perl = TRUE)
else
NA_character_
}
lines <- chunk[-1L]
s <- .collapse(lines)
if(grepl("^[[:space:]]*([o*+-])", s)) {
sep <- sub("^[[:space:]]*([o*+-]).*$", "\\1", s)
ire <- sprintf("^[[:space:]]*([%s])[[:space:]]+", sep)
ind <- grepl(ire, lines)
list(entries =
sapply(split(lines, cumsum(ind)),
function(s)
sub(ire, "", .collapse(sub("^\t?", "", s)))
),
header = header,
chunk = chunk,
date = date)
} else {
## Categories should be non-empty starting in column 1.
re_c <- "^([[:alpha:]].*)[[:space:]]*$"
ind <- grepl(re_c, lines)
## If we detect neither bullet items nor categories, the
## chunk is in a different format than we can recognize.
## Return no entries, and have the finisher give the whole
## chunk and push it onto the bad chunk list.
if(!any(ind)) {
list(entries = character(),
header = header,
chunk = chunk,
date = date)
} else {
pos <- cumsum(ind) > 0
list(entries =
Map(do_chunk,
split(lines[pos], cumsum(ind)[pos]),
sub("[[:punct:]]*$", "",
sub(re_c, "\\1", lines[ind]))),
header = header,
chunk = chunk,
date = date)
}
}
}
out <- lapply(chunks, do_chunk)
## Now assemble pieces.
reporter <- function(x) {
if(verbose)
message(gettextf("Cannot process chunk/lines:\n%s",
.collapse(x)))
NULL
}
finisher <- function(x) {
entries <- x$entries
version <- x$header
date <- x$date
if(is.list(entries)) {
do.call(rbind,
lapply(entries,
function(x) {
entries <- x$entries
bad <- if(!length(entries)) {
reporter(x$chunk)
entries <-
sub("^[[:space:]]*", "",
.collapse(x$chunk[-1L]))
TRUE
}
else FALSE
cbind(version, date, x$header, entries,
bad)
}))
}
else {
bad <- if(!length(entries)) {
reporter(x$chunk)
entries <-
sub("^[[:space:]]*", "",
.collapse(x$chunk[-1L]))
TRUE
}
else FALSE
cbind(version, date, NA_character_, entries, bad)
}
}
out <- do.call(rbind, lapply(out, finisher))
## Try to remove a common 'exdent' from the entries.
entries <- out[, 4L]
exdent <-
unlist(lapply(gregexpr("\n *", entries), attr, "match.length"))
exdent <- exdent[exdent > 1L]
if(length(exdent)) {
out[, 4L] <-
gsub(sprintf("\n%s",
paste(rep.int(" ", min(exdent) - 1L),
collapse = "")),
"\n", entries)
}
.make_news_db(out[, -5L, drop = FALSE], as.logical(out[, 5L]))
}
.make_news_db <-
function(x, bad = NULL, classes = NULL)
{
## Expect x to be a 4 column
## version date category text
## character matrix.
## Could of course check for this using
## if(!is.character(x) || ncol(x) != 4L)
out <- data.frame(x, row.names = NULL, stringsAsFactors = FALSE)
## Note that we cannot do
## dimnames(out) <- list(NULL,
## c("Version", "Date", "Category", "Text"))
colnames(out) <- c("Version", "Date", "Category", "Text")
if(!is.null(bad))
attr(out, "bad") <- bad
class(out) <- unique(c(classes, "news_db", "data.frame"))
out
}
## Transform NEWS.Rd
Rd2txt_NEWS_in_Rd_options <-
list(sectionIndent = 0L, sectionExtra = 2L,
minIndent = 4L, code_quote = FALSE,
underline_titles = FALSE)
Rd2txt_NEWS_in_Rd <-
function(f, out = "") {
if (grepl("[.]rds$", f)) f <- readRDS(f)
Rd2txt(f, out,
stages = c("install", "render"),
outputEncoding = if(l10n_info()[["UTF-8"]]) "" else "ASCII//TRANSLIT",
options = Rd2txt_NEWS_in_Rd_options)
}
Rd2HTML_NEWS_in_Rd <-
function(f, out, ...) {
if (grepl("[.]rds$", f)) f <- readRDS(f)
Rd2HTML(f, out, stages = c("install", "render"), ...)
}
Rd2pdf_NEWS_in_Rd <-
function(f, pdf_file)
{
if (grepl("[.]rds$", f)) f <- readRDS(f)
f2 <- tempfile()
## See the comments in ?texi2dvi about spaces in paths
f3 <- if(grepl(" ", td <- Sys.getenv("TMPDIR")))
file.path("/tmp", "NEWS.tex")
else
file.path(tempdir(), "NEWS.tex")
out <- file(f3, "w")
Rd2latex(f, f2,
stages = c("install", "render"),
outputEncoding = "UTF-8", writeEncoding = FALSE)
cat("\\documentclass[", Sys.getenv("R_PAPERSIZE"), "paper]{book}\n",
"\\usepackage[ae,hyper]{Rd}\n",
"\\usepackage[utf8]{inputenc}\n",
"\\usepackage{graphicx}\n",
"\\setkeys{Gin}{width=0.7\\textwidth}\n",
"\\graphicspath{{", normalizePath(file.path(R.home("doc"), "html"), "/"),
"/}}\n",
"\\hypersetup{pdfpagemode=None,pdfstartview=FitH}\n",
"\\begin{document}\n",
"\\chapter*{}\\sloppy\n",
"\\begin{center}\n\\huge\n",
"NEWS for ", R.version$version.string, "\n",
"\\end{center}\n",
sep = "", file = out)
writeLines(readLines(f2), out)
writeLines("\\end{document}", out)
close(out)
od <- setwd(dirname(f3))
on.exit(setwd(od))
texi2pdf("NEWS.tex", quiet = TRUE)
setwd(od); on.exit()
invisible(file.copy(file.path(dirname(f3), "NEWS.pdf"),
pdf_file, overwrite = TRUE))
}
## Transform old-style plain text NEWS file to Rd.
news2Rd <-
function(file, out = stdout(), codify = FALSE)
{
## For add-on packages, the given NEWS file should be in the root
## package source directory or its 'inst' subdirectory, so that we
## can use the DESCRIPTION metadata to obtain the package name and
## encoding.
file <- file_path_as_absolute(file)
dir <- dirname(file)
format <- "default"
if(file_test("-f", dfile <- file.path(dir, "DESCRIPTION")))
meta <- .read_description(dfile)
else if(basename(dir) == "inst" &&
file_test("-f", dfile <- file.path(dirname(dir),
"DESCRIPTION")))
meta <- .read_description(dfile)
else
format <- "R"
wto <- function(x) writeLines(x, con = out, useBytes = TRUE)
cre <- "(\\W|^)(\"[[:alnum:]_.]*\"|[[:alnum:]_.:]+\\(\\))(\\W|$)"
if(is.character(out)) {
out <- file(out, "wt")
on.exit(close(out))
}
if(!isOpen(out, "wt")) {
open(out, "wt")
on.exit(close(out))
}
if(format == "R") {
news <- readNEWS(chop = "keepAll")
if(!length(news))
stop("No news found in given file using old-style R-like format.")
wto(c("\\newcommand{\\PR}{\\Sexpr[results=rd]{tools:::Rd_expr_PR(#1)}}",
"\\name{NEWS}",
"\\title{R News}",
"\\encoding{UTF-8}"))
for(y in news) {
for(i in seq_along(y)) {
wto(sprintf("\\section{CHANGES IN R VERSION %s}{",
names(y)[i]))
z <- y[[i]]
for(j in seq_along(z)) {
wto(c(sprintf(" \\subsection{%s}{", names(z)[j]),
" \\itemize{"))
for(chunk in z[[j]]) {
chunk <- toRd(paste(chunk, collapse = "\n "))
if(codify) {
chunk <- gsub(cre, "\\1\\\\code{\\2}\\3",
chunk)
}
chunk <- gsub("PR#([[:digit:]]+)", "\\\\PR{\\1}",
chunk)
wto(paste(" \\item", enc2utf8(chunk)))
}
wto(c(" }", " }"))
}
wto("}")
}
}
} else {
news <- .news_reader_default(file)
bad <- attr(news, "bad")
if(!length(bad))
stop("No news found in given file using package default format.")
if(any(bad)) {
bad <- news$Text[bad]
stop("Could not extract news from the following text chunks:\n",
paste(sprintf("\nChunk %s:\n%s",
format(seq_along(bad)), bad),
collapse = "\n"))
}
encoding <- meta["Encoding"]
package <- meta["Package"]
texts <- toRd(news$Text)
if(codify)
texts <- gsub(cre, "\\1\\\\code{\\2}\\3", texts)
## Note that .news_reader_default re-encodes ...
if(!is.na(encoding))
texts <- iconv(texts, to = encoding, sub = "byte", mark = FALSE)
news$Text <- texts
wto(c("\\name{NEWS}",
sprintf("\\title{News for Package '%s'}", package)))
if(!is.na(encoding))
wto(sprintf("\\encoding{%s}", encoding))
## Similar to print.news_db():
vchunks <- split(news, news$Version)
## Re-order according to decreasing version.
vchunks <- vchunks[order(as.numeric_version(names(vchunks)),
decreasing = TRUE)]
dates <- sapply(vchunks, function(v) v$Date[1L])
if(any(ind <- !is.na(dates)))
names(vchunks)[ind] <-
sprintf("%s (%s)", names(vchunks)[ind], dates[ind])
vheaders <- sprintf("\\section{Changes in %s version %s}{",
package, names(vchunks))
for(i in seq_along(vchunks)) {
wto(vheaders[i])
vchunk <- vchunks[[i]]
if(all(!is.na(category <- vchunk$Category)
& nzchar(category))) {
## need to preserve order of headings.
cchunks <-
split(vchunk,
factor(category, levels = unique(category)))
cheaders <- sprintf(" \\subsection{%s}{",
names(cchunks))
for(j in seq_along(cchunks)) {
wto(c(cheaders[j],
" \\itemize{",
paste(" \\item",
gsub("\n", "\n ",
cchunks[[j]]$Text)),
" }",
" }"))
}
} else {
wto(c(" \\itemize{",
paste(" \\item",
gsub("\n", "\n ", vchunk$Text)),
" }"))
}
wto("}")
}
}
}
Rd_expr_PR <-
function(x)
{
baseurl <- "https://bugs.R-project.org/bugzilla3/show_bug.cgi?id"
sprintf("\\href{%s=%s}{PR#%s}", baseurl, x, x)
}
.build_news_db_from_R_NEWS_Rd <-
function(file = NULL)
{
x <- if(is.null(file))
readRDS(file.path(R.home("doc"), "NEWS.rds"))
else {
## Expand \Sexpr et al now because this does not happen when using
## fragments.
prepare_Rd(parse_Rd(file), stages = "install")
}
db <- .extract_news_from_Rd(x)
db <- db[db[,1L] != "CHANGES in previous versions",,drop = FALSE]
## Squeeze in an empty date column.
.make_news_db(cbind(sub("^CHANGES IN (R )?(VERSION )?", "", db[, 1L]),
NA_character_,
db[, 2L],
sub("\n*$", "", db[, 3L])),
logical(nrow(db)),
"news_db_from_Rd")
}
.build_news_db_from_package_NEWS_Rd <-
function(file)
{
x <- prepare_Rd(parse_Rd(file), stages = "install")
db <- .extract_news_from_Rd(x)
## Post-process section names to extract versions and dates.
re_v <- sprintf(".*version[[:space:]]+(%s).*$",
.standard_regexps()$valid_package_version)
re_d <- sprintf("^.*(%s)[[:punct:][:space:]]*$",
"[[:digit:]]{4}-[[:digit:]]{2}-[[:digit:]]{2}")
nms <- db[, 1L]
ind <- grepl(re_v, nms, ignore.case = TRUE)
if(!all(ind))
warning("Cannot extract version info from the following section titles:\n",
sprintf(" %s", unique(nms[!ind])))
.make_news_db(cbind(ifelse(ind,
sub(re_v, "\\1", nms, ignore.case = TRUE),
NA_character_),
ifelse(grepl(re_d, nms, perl = TRUE),
sub(re_d, "\\1", nms, perl = TRUE),
NA_character_),
db[, 2L],
sub("\n*$", "", db[, 3L])),
logical(nrow(db)),
"news_db_from_Rd")
}
.extract_news_from_Rd <-
function(x)
{
.get_Rd_section_names <- function(x)
sapply(x, function(e) .Rd_get_text(e[[1L]]))
do_chunk <- function(x) {
## Currently, chunks should consist of a single \itemize list
## containing the news items. Notify if there is more than one
## such list, and stop if there is none.
pos <- which(RdTags(x) == "\\itemize")
if(!length(pos)) {
stop(gettextf("Malformed NEWS.Rd file:\nChunk starting\n %s\ncontains no \\itemize.",
substring(sub("^[[:space:]]*", "",
.Rd_deparse(x)),
1L, 60L)),
domain = NA)
} else if(length(pos) > 1L) {
warning(gettextf("Malformed NEWS.Rd file:\nChunk starting\n %s\ncontains more than one \\itemize.\nUsing the first one.",
substring(sub("^[[:space:]]*", "",
.Rd_deparse(x)),
1L, 60L)),
domain = NA)
pos <- pos[1L]
}
x <- x[pos]
out <- NULL
zz <- textConnection("out", "w", local = TRUE)
on.exit(close(zz))
Rd2txt(x, out = zz, fragment = TRUE,
options =
c(Rd2txt_NEWS_in_Rd_options,
list(itemBullet = "\036 ")))
## Try to find the column offset of the top-level bullets.
pat <- "^( *)\036.*"
off <- min(nchar(sub(pat, "\\1", out[grepl(pat, out)])))
pat <- sprintf("^%s\036 ",
paste(rep.int(" ", off), collapse = ""))
s <- sub(pat, "\036", out)
## Try to remove some indent for nested material.
pat <- sprintf("^%s",
paste(rep.int(" ", off + 2L), collapse = ""))
s <- sub(pat, "", s)
s <- paste(s, collapse = "\n")
s <- sub("^[[:space:]]*\036", "", s)
s <- sub("[[:space:]]*$", "", s)
##
## Could be more fancy and use \u2022 "if possible".
gsub("\036", "*", unlist(strsplit(s, "\n\036", fixed = TRUE)))
##
}
y <- x[RdTags(x) == "\\section"]
do.call(rbind,
Map(cbind,
.get_Rd_section_names(y),
lapply(y,
function(e) {
z <- e[[2L]]
ind <- RdTags(z) == "\\subsection"
if(any(ind)) {
z <- z[ind]
do.call(rbind,
Map(cbind,
.get_Rd_section_names(z),
lapply(z,
function(e)
do_chunk(e[[2L]]))))
} else {
cbind(NA_character_, do_chunk(z))
}
})))
}