Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
799a52f
Replace `str_trim()` with `trimws()`
hadley Mar 20, 2026
b6adcb2
Replace `str_detect()` with `grepl()`
hadley Mar 20, 2026
84d69b9
Replace `str_replace()` with `sub()`
hadley Mar 20, 2026
f743b22
Replace `str_replace_all()` with `gsub()` and `re_replace_all()`
hadley Mar 20, 2026
345d5eb
Replace `str_remove()` with `sub()`
hadley Mar 20, 2026
cecae83
Replace `str_split()` with `strsplit()`
hadley Mar 20, 2026
c626bb4
Add local `str_count()` helper to replace stringr's
hadley Mar 20, 2026
3da0b9f
Replace `str_extract()` with `regexpr()`
hadley Mar 20, 2026
fbf66be
Replace `str_sub()` with `substr()` and `substring()`
hadley Mar 20, 2026
841be6a
Add local `str_split_fixed()` helper to replace stringr's
hadley Mar 20, 2026
698d825
Replace `str_match()` and `str_match_all()` with base equivalents
hadley Mar 20, 2026
09cdb18
Replace `str_locate_all()` with `gregexpr()`
hadley Mar 20, 2026
a8ca9ca
Drop stringr dependency
hadley Mar 20, 2026
71e8b02
Switch to installed package
hadley Mar 23, 2026
be07779
Merge commit '988a96be8d5f50bd4bccd2dc6c4ad10604b14e7b'
hadley Mar 23, 2026
be89975
Switch to `str_split_half()`
hadley Mar 23, 2026
a8d12f1
Fix a logic error revealed by close inspection of `str_count()`
hadley Mar 23, 2026
2bb9d4e
Add missing test case
hadley Mar 23, 2026
e54731e
Merge commit 'c35dacc31dd32b6f865be010653483c4d5cc7f1b'
hadley Apr 7, 2026
9934b24
Restore comments
hadley Apr 7, 2026
e881f6d
Add news bullet
hadley Apr 7, 2026
14516df
Consistently use `re_` prefix
hadley Apr 7, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion DESCRIPTION
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@ Imports:
pkgload (>= 1.0.2),
R6 (>= 2.1.2),
rlang (>= 1.1.0),
stringr (>= 1.0.0),
utils,
withr,
xml2
Expand Down
1 change: 0 additions & 1 deletion NAMESPACE
Original file line number Diff line number Diff line change
Expand Up @@ -274,7 +274,6 @@ export(update_collate)
export(vignette_roclet)
export(warn_roxy_tag)
import(rlang)
import(stringr)
importFrom(R6,R6Class)
importFrom(knitr,knit)
importFrom(knitr,opts_chunk)
Expand Down
1 change: 1 addition & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
# roxygen2 (development version)
* roxygen2 no longer depends on stringr/stringi. This means that no package in the devtools constellation depends on stringr, which in turn means you no longer need stringi, making it a bit easier to install in constrained Linux environments.
* roxygen2 options can now be set using `Config/roxygen2/` fields in DESCRIPTION (e.g. `Config/roxygen2/markdown: TRUE`) instead of the `Roxygen` field. The old `Roxygen` field is still supported. Similarly, the roxygen2 version is now stored in `Config/roxygen2/version` instead of `RoxygenNote` (#1328).
* Tags that expect single-line input now warn when they span multiple lines, catching common mistakes. Affected tags: `@aliases`, `@concept`, `@encoding`, `@exportClass`, `@exportMethod`, `@exportPattern`, `@exportS3Method`, `@importFrom`, `@importClassesFrom`, `@importMethodsFrom`, `@include`, `@inheritParams`, `@keywords`, `@method`, `@name`, `@order`, `@rdname`, `@S3method`, `@template`, and `@useDynLib` (#1642, #1688). This may break some existing usage, but it prevents a wide class of otherwise silent errors.
* `@examplesIf` now warns when there is no example code after the condition (#1695).
Expand Down
6 changes: 3 additions & 3 deletions R/block.R
Original file line number Diff line number Diff line change
Expand Up @@ -240,16 +240,16 @@ parse_description <- function(tags) {
}

intro <- tags[[1]]
intro$val <- str_trim(intro$raw)
intro$val <- trimws(intro$raw)
if (intro$val == "") {
return(tags[-1])
}

tags <- tags[-1]
tag_names <- tag_names[-1]

paragraphs <- str_split(intro$val, fixed('\n\n'))[[1]]
lines <- str_count(paragraphs, "\n") + rep(2, length(paragraphs))
paragraphs <- strsplit(intro$val, '\n\n', fixed = TRUE)[[1]]
lines <- re_count(paragraphs, "\n") + rep(2, length(paragraphs))
offsets <- c(0, cumsum(lines))

# 1st paragraph = title (unless has @title)
Expand Down
2 changes: 1 addition & 1 deletion R/collate.R
Original file line number Diff line number Diff line change
Expand Up @@ -103,5 +103,5 @@ base_path <- function(path, base) {
path <- normalizePath(path, winslash = "/")
base <- normalizePath(base, winslash = "/")

str_replace(path, fixed(paste0(base, "/")), "")
sub(paste0(base, "/"), "", path, fixed = TRUE)
}
19 changes: 10 additions & 9 deletions R/markdown-code.R
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ markdown_evaluate <- function(text) {
rcode_pos <- parse_md_pos(map_chr(rcode_nodes, xml_attr, "sourcepos"))
rcode_pos <- work_around_cmark_sourcepos_bug(text, rcode_pos)
out <- eval_code_nodes(rcode_nodes)
str_set_all_pos(text, rcode_pos, out, rcode_nodes)
re_set_all_pos(text, rcode_pos, out, rcode_nodes)
}

# Work around commonmark sourcepos bug for inline R code
Expand All @@ -68,7 +68,7 @@ work_around_cmark_sourcepos_bug <- function(text, rcode_pos) {
return(rcode_pos)
}

lines <- str_split(text, fixed("\n"))[[1]]
lines <- strsplit(text, "\n", fixed = TRUE)[[1]]

for (l in seq_len(nrow(rcode_pos))) {
# Do not try to fix multi-line code, we error for that (below)
Expand All @@ -79,7 +79,7 @@ work_around_cmark_sourcepos_bug <- function(text, rcode_pos) {
start <- rcode_pos$start_column[l]

# Maybe correct? At some point this will be fixed upstream, hopefully.
if (str_sub(line, start - 1, start + 1) == "`r ") {
if (substr(line, start - 1, start + 1) == "`r ") {
next
}

Expand All @@ -91,10 +91,11 @@ work_around_cmark_sourcepos_bug <- function(text, rcode_pos) {
# the real "`r " left by six characters, there happens to be another
# "`r " there.

indent <- nchar(str_extract(line, "^[ ]+"))
m <- regexpr("^[ ]+", line)
indent <- attr(m, "match.length")
if (
!is.na(indent) &&
str_sub(line, start - 1 + indent, start + 1 + indent) == "`r "
m > 0L &&
substr(line, start - 1 + indent, start + 1 + indent) == "`r "
) {
rcode_pos$start_column[l] <- rcode_pos$start_column[l] + indent
rcode_pos$end_column[l] <- rcode_pos$end_column[l] + indent
Expand All @@ -106,7 +107,7 @@ work_around_cmark_sourcepos_bug <- function(text, rcode_pos) {

is_markdown_code_node <- function(x) {
info <- xml_attr(x, "info")
str_sub(xml_text(x), 1, 2) == "r " ||
substr(xml_text(x), 1, 2) == "r " ||
(!is.na(info) && grepl("^[{][a-zA-z]+[}, ]", info))
}

Expand Down Expand Up @@ -158,7 +159,7 @@ knitr_chunk_defaults <- function() {
)
}

str_set_all_pos <- function(text, pos, value, nodes) {
re_set_all_pos <- function(text, pos, value, nodes) {
# Cmark has a bug when reporting source positions for multi-line
# code tags, and it does not count the indenting space in the
# continuation lines: https://github.com/commonmark/cmark/issues/296
Expand All @@ -169,7 +170,7 @@ str_set_all_pos <- function(text, pos, value, nodes) {

# Need to split the string, because of the potential multi-line
# code tags, and then also recode the positions
lens <- nchar(str_split(text, fixed("\n"))[[1]])
lens <- nchar(strsplit(text, "\n", fixed = TRUE)[[1]])
shifts <- c(0, cumsum(lens + 1L))
shifts <- shifts[-length(shifts)]
start <- shifts[pos$start_line] + pos$start_column
Expand Down
40 changes: 30 additions & 10 deletions R/markdown-escaping.R
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ find_fragile_rd_tags <- function(text, fragile) {
ftags <- ftags[keep, ]

if (nrow(ftags)) {
ftags$text <- str_sub(text, ftags$start, ftags$argend)
ftags$text <- substring(text, ftags$start, ftags$argend)
}

ftags
Expand All @@ -142,7 +142,7 @@ find_all_rd_tags <- function(text) {
## Find the end of the argument list for each tag. Note that
## tags might be embedded into the arguments of other tags.
tags$argend <- map_int(seq_len(nrow(tags)), function(i) {
tag_plus <- str_sub(text, tags$end[i], text_len)
tag_plus <- substr(text, tags$end[i], text_len)
findEndOfTag(tag_plus, is_code = FALSE, start = 0L) + tags$end[i]
})

Expand All @@ -163,12 +163,28 @@ find_all_rd_tags <- function(text) {

find_all_tag_names <- function(text) {
## Find the tags without arguments first
tag_pos <- str_locate_all(text, r"(\\[a-zA-Z][a-zA-Z0-9]*)")[[1]]
m <- gregexpr(r"(\\[a-zA-Z][a-zA-Z0-9]*)", text)[[1]]
if (m[[1]] == -1L) {
tag_pos <- matrix(
integer(),
ncol = 2,
dimnames = list(NULL, c("start", "end"))
)
} else {
tag_pos <- cbind(
start = as.integer(m),
end = as.integer(m) + attr(m, "match.length") - 1L
)
}

data.frame(
tag = str_sub(text, tag_pos[, "start"], tag_pos[, "end"]),
as.data.frame(tag_pos)
)
if (nrow(tag_pos) == 0) {
data.frame(tag = character(), start = integer(), end = integer())
} else {
data.frame(
tag = substring(text, tag_pos[, "start"], tag_pos[, "end"]),
as.data.frame(tag_pos)
)
}
}

#' Replace fragile Rd tags with placeholders
Expand All @@ -184,7 +200,7 @@ find_all_tag_names <- function(text) {
protect_rd_tags <- function(text, rd_tags) {
id <- make_random_string()

text <- str_sub_same(text, rd_tags, id)
text <- re_sub_same(text, rd_tags, id)

attr(text, "roxygen-markdown-subst") <-
list(tags = rd_tags, id = id)
Expand All @@ -207,7 +223,7 @@ protect_rd_tags <- function(text, rd_tags) {
#'
#' @noRd

str_sub_same <- function(str, repl, id) {
re_sub_same <- function(str, repl, id) {
repl <- repl[order(repl$start), ]

if (is.unsorted(repl$end) || is.unsorted(repl$argend)) {
Expand All @@ -217,7 +233,11 @@ str_sub_same <- function(str, repl, id) {
for (i in seq_len(nrow(repl))) {
## The trailing - is needed, to distinguish between -1 and -10
new_text <- paste0(id, "-", i, "-")
str_sub(str, repl$start[i], repl$argend[i]) <- new_text
str <- paste0(
substr(str, 1, repl$start[i] - 1),
new_text,
substr(str, repl$argend[i] + 1, nchar(str))
)

## Need to shift other coordinates (we shift everything,
## it is just simpler).
Expand Down
32 changes: 18 additions & 14 deletions R/markdown-link.R
Original file line number Diff line number Diff line change
Expand Up @@ -64,26 +64,30 @@ add_linkrefs_to_md <- function(text) {
}

get_md_linkrefs <- function(text) {
refs <- str_match_all(
refs <- regmatches(
text,
regex(
comments = TRUE,
"
(?<=[^\\]\\\\]|^) # must not be preceded by ] or \
\\[([^\\]\\[]+)\\] # match anything inside of []
(?:\\[([^\\]\\[]+)\\])? # match optional second pair of []
(?=[^\\[{]|$) # must not be followed by [ or {
"
gregexec(
paste0(
"(?x)",
"(?<=[^\\]\\\\]|^)", # must not be preceded by ] or \
"\\[([^\\]\\[]+)\\]", # match anything inside of []
"(?:\\[([^\\]\\[]+)\\])?", # match optional second pair of []
"(?=[^\\[{]|$)" # must not be followed by [ or {
),
text,
perl = TRUE
)
)[[1]]
if (length(refs) > 0) {
refs <- t(refs)
}

if (length(refs) == 0) {
return(character())
}

## For the [fun] form the link text is the same as the destination.
# Need to check both NA and "" for different versions of stringr
refs[, 3] <- ifelse(is.na(refs[, 3]) | refs[, 3] == "", refs[, 2], refs[, 3])
refs[, 3] <- ifelse(refs[, 3] == "", refs[, 2], refs[, 3])

refs3encoded <- map_chr(refs[, 3], URLencode)
paste0("[", refs[, 3], "]: ", "R:", refs3encoded)
Expand Down Expand Up @@ -126,12 +130,12 @@ parse_link <- function(destination, contents, state) {

is_code <- is_code || (grepl("[(][)]$", destination) && !has_link_text)

pkg <- str_match(destination, "^(.*)::")[1, 2]
pkg <- regmatches(destination, regexec("^(.*)::", destination))[[1]][2]
explicit_pkg <- !is.na(pkg)
fun <- utils::tail(strsplit(destination, "::", fixed = TRUE)[[1]], 1)
topic <- sub("[(][)]$", "", fun)
if (!has_link_text && str_detect(destination, "-class$")) {
fun <- str_match(fun, "^(.*)-class$")[1, 2]
if (!has_link_text && grepl("-class$", destination)) {
fun <- regmatches(fun, regexec("^(.*)-class$", fun))[[1]][2]
}

# Standardise links: cross-packagae always get prefix;
Expand Down
23 changes: 12 additions & 11 deletions R/markdown.R
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ markdown_pass1 <- function(text) {
rcode_pos <- parse_md_pos(map_chr(rcode_nodes, xml_attr, "sourcepos"))
rcode_pos <- work_around_cmark_sourcepos_bug(text, rcode_pos)
out <- eval_code_nodes(rcode_nodes)
str_set_all_pos(text, rcode_pos, out, rcode_nodes)
re_set_all_pos(text, rcode_pos, out, rcode_nodes)
}

# Work around commonmark sourcepos bug for inline R code
Expand All @@ -88,7 +88,7 @@ work_around_cmark_sourcepos_bug <- function(text, rcode_pos) {
return(rcode_pos)
}

lines <- str_split(text, fixed("\n"))[[1]]
lines <- strsplit(text, "\n", fixed = TRUE)[[1]]

for (l in seq_len(nrow(rcode_pos))) {
# Do not try to fix multi-line code, we error for that (below)
Expand All @@ -99,7 +99,7 @@ work_around_cmark_sourcepos_bug <- function(text, rcode_pos) {
start <- rcode_pos$start_column[l]

# Maybe correct? At some point this will be fixed upstream, hopefully.
if (str_sub(line, start - 1, start + 1) == "`r ") {
if (substr(line, start - 1, start + 1) == "`r ") {
next
}

Expand All @@ -111,10 +111,11 @@ work_around_cmark_sourcepos_bug <- function(text, rcode_pos) {
# the real "`r " left by six characters, there happens to be another
# "`r " there.

indent <- nchar(str_extract(line, "^[ ]+"))
m <- regexpr("^[ ]+", line)
indent <- attr(m, "match.length")
if (
!is.na(indent) &&
str_sub(line, start - 1 + indent, start + 1 + indent) == "`r "
m > 0L &&
substr(line, start - 1 + indent, start + 1 + indent) == "`r "
) {
rcode_pos$start_column[l] <- rcode_pos$start_column[l] + indent
rcode_pos$end_column[l] <- rcode_pos$end_column[l] + indent
Expand All @@ -126,7 +127,7 @@ work_around_cmark_sourcepos_bug <- function(text, rcode_pos) {

is_markdown_code_node <- function(x) {
info <- xml_attr(x, "info")
str_sub(xml_text(x), 1, 2) == "r " ||
substr(xml_text(x), 1, 2) == "r " ||
(!is.na(info) && grepl("^[{][a-zA-z]+[}, ]", info))
}

Expand Down Expand Up @@ -178,7 +179,7 @@ knitr_chunk_defaults <- function() {
)
}

str_set_all_pos <- function(text, pos, value, nodes) {
re_set_all_pos <- function(text, pos, value, nodes) {
# Cmark has a bug when reporting source positions for multi-line
# code tags, and it does not count the indenting space in the
# continuation lines: https://github.com/commonmark/cmark/issues/296
Expand All @@ -189,7 +190,7 @@ str_set_all_pos <- function(text, pos, value, nodes) {

# Need to split the string, because of the potential multi-line
# code tags, and then also recode the positions
lens <- nchar(str_split(text, fixed("\n"))[[1]])
lens <- nchar(strsplit(text, "\n", fixed = TRUE)[[1]])
shifts <- c(0, cumsum(lens + 1L))
shifts <- shifts[-length(shifts)]
start <- shifts[pos$start_line] + pos$start_column
Expand Down Expand Up @@ -235,13 +236,13 @@ mdxml_children_to_rd_top <- function(xml, state) {
state$section_tag <- uuid()
out <- map_chr(xml_children(xml), mdxml_node_to_rd, state)
out <- c(out, mdxml_close_sections(state))
rd <- str_trim(paste0(out, collapse = ""))
rd <- trimws(paste0(out, collapse = ""))
if (state$has_sections) {
secs <- strsplit(rd, state$section_tag, fixed = TRUE)[[1]] %||% ""
titles <- c("", state$titles)
# strsplit drops trailing empty strings, so pad to match titles length
secs <- c(secs, rep("", length(titles) - length(secs)))
rd <- structure(str_trim(secs), names = titles)
rd <- structure(trimws(secs), names = titles)
}
rd
}
Expand Down
8 changes: 4 additions & 4 deletions R/namespace.R
Original file line number Diff line number Diff line change
Expand Up @@ -224,23 +224,23 @@ roxy_tag_ns.roxy_tag_exportS3Method <- function(x, block, env) {
return()
}

if (!str_detect(x$val, "::")) {
if (!grepl("::", x$val, fixed = TRUE)) {
warn_roxy_tag(x, "must have form package::generic")
return()
}

generic <- str_split(x$val, "::")[[1]]
generic <- re_split_half(x$val, "::")
generic_re <- paste0("^", generic[[2]], "\\.")

if (!str_detect(obj$alias, generic_re)) {
if (!grepl(generic_re, obj$alias)) {
warn_roxy_tag(
x,
"generic ({.str {generic[[2]]}}) doesn't match function ({.str {obj$alias}})",
)
return()
}

class <- str_remove(obj$alias, generic_re)
class <- sub(generic_re, "", obj$alias)
method <- c(x$val, class)
} else {
method <- x$val
Expand Down
4 changes: 2 additions & 2 deletions R/object-from-call.R
Original file line number Diff line number Diff line change
Expand Up @@ -235,8 +235,8 @@ add_s3_metadata <- function(val, name, env, block) {

if (block_has_tags(block, "exportS3Method")) {
method <- block_get_tag_value(block, "exportS3Method")
if (length(method) == 1 && str_detect(method, "::")) {
generic <- strsplit(method, "::")[[1]][[2]]
if (length(method) == 1 && grepl("::", method, fixed = TRUE)) {
generic <- re_split_half(method, "::")[[2]]
class <- gsub(paste0("^", generic, "\\."), "", name)
return(s3_method(val, c(generic, class)))
}
Expand Down
Loading
Loading