-
Notifications
You must be signed in to change notification settings - Fork 235
/
Copy pathmarkdown-link.R
308 lines (275 loc) · 10.3 KB
/
markdown-link.R
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
#' Add link reference definitions for functions to a markdown text.
#'
#' We find the `[text][ref]` and the `[ref]` forms. There must be no
#' spaces between the closing and opening bracket in the `[text][ref]`
#' form.
#'
#' Starting from R 4.0.2-ish, explicit cross-package links to topics are not
#' allowed, so for each such linked topic, we look up the linked file.
#'
#' These are the link references we add:
#' ```
#' MARKDOWN LINK TEXT CODE RD
#' -------- --------- ---- --
#' [fun()] fun() T \\link[=fun]{fun()} or
#' \\link[pkg:file]{pkg::fun()}
#' [obj] obj F \\link{obj} or
#' \\link[pkg:file]{pkg::obj}
#' [pkg::fun()] pkg::fun() T \\link[pkg:file]{pkg::fun()}
#' [pkg::obj] pkg::obj F \\link[pkg:file]{pkg::obj}
#' [text][fun()] text F \\link[=fun]{text} or
#' \\link[pkg:file]{text}
#' [text][obj] text F \\link[=obj]{text} or
#' \\link[pkg:file]{text}
#' [text][pkg::fun()] text F \\link[pkg:file]{text}
#' [text][pkg::obj] text F \\link[pkg:file]{text}
#' [s4-class] s4 F \\linkS4class{s4} or
#' \\link[pkg:file]{s4}
#' [pkg::s4-class] pkg::s4 F \\link[pkg:file]{pkg::s4}
#' ```
#'
#' For the links with two RD variants the first version is used for
#' within-package links, and the second version is used for cross-package
#' links.
#'
#' The reference links will always look like `R:ref` for `[ref]` and
#' `[text][ref]`. These are explicitly tested in `test-rd-markdown-links.R`.
#'
#' We add in a special `R:` marker to the URL. This way we don't
#' pick up other links, that were specified via `<url>` or
#' `[text](link)`. In the parsed XML tree these look the same as
#' our `[link]` and `[text][link]` links.
#'
#' In the link references, we need to URL encode the reference,
#' otherwise commonmark does not use it (see issue #518).
#'
#' @param text Input markdown text.
#' @return The input text and all dummy reference link definitions
#' appended.
#'
#' @noRd
get_md_linkrefs <- function(text) {
refs <- str_match_all(
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 {
"
)
)[[1]]
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])
refs3encoded <- map_chr(refs[,3], URLencode)
paste0("[", refs[, 3], "]: ", "R:", refs3encoded)
}
add_linkrefs_to_md <- function(text) {
ref_lines <- get_md_linkrefs(text)
if (length(ref_lines) == 0)
return(text)
ref_text <- paste0(ref_lines, collapse = "\n")
paste0(text, "\n\n", ref_text, "\n")
}
#' Parse a MarkDown link, to see if we should create an Rd link
#'
#' See the table above.
#'
#' @param destination string constant, the "url" of the link
#' @param contents An XML node, containing the contents of the link.
#'
#' @noRd
parse_link <- function(destination, contents, state) {
## Not a [] or [][] type link, remove prefix if it is
if (! grepl("^R:", destination)) return(NULL)
destination <- sub("^R:", "", URLdecode(destination))
## if contents is a `code tag`, then we need to move this outside
is_code <- FALSE
if (length(contents) == 1 && xml_name(contents) == "code") {
is_code <- TRUE
contents <- xml_contents(contents)
destination <- sub("`$", "", sub("^`", "", destination))
local_bindings(.env = state, in_link_code = TRUE)
}
if (!all(xml_name(contents) %in% c("text", "softbreak", "linebreak"))) {
incorrect <- setdiff(unique(xml_name(contents)), c("text", "softbreak", "linebreak"))
warn_roxy_tag(state$tag, c(
"markdown links must contain plain text",
i = "Problematic link: {destination}"
))
return("")
}
## If the supplied link text is the same as the reference text,
## then we assume that the link text was automatically generated and
## it was not specified explicitly. In this case `()` links are
## turned to `\\code{}`.
## We also assume link text if we see a non-text XML tag in contents.
has_link_text <- paste(xml_text(contents), collapse = "") != destination ||
any(xml_name(contents) != "text")
## if (is_code) then we'll need \\code
## `pkg` is package or NA
## `fun` is fun() or obj (fun is with parens)
## `is_fun` is TRUE for fun(), FALSE for obj
## `obj` is fun or obj (fun is without parens)
## `s4` is TRUE if we link to an S4 class (i.e. have -class suffix)
## `noclass` is fun with -class removed
## `file` is the file name of the linked topic.
thispkg <- roxy_meta_get("current_package") %||% ""
is_code <- is_code || (grepl("[(][)]$", destination) && ! has_link_text)
pkg <- str_match(destination, "^(.*)::")[1,2]
pkg <- gsub("%", "\\\\%", pkg)
fun <- utils::tail(strsplit(destination, "::", fixed = TRUE)[[1]], 1)
fun <- gsub("%", "\\\\%", fun)
is_fun <- grepl("[(][)]$", fun)
obj <- sub("[(][)]$", "", fun)
s4 <- str_detect(destination, "-class$")
noclass <- str_match(fun, "^(.*)-class$")[1,2]
if (is.na(pkg)) pkg <- resolve_link_package(obj, thispkg, state = state)
if (!is.na(pkg) && pkg == thispkg) pkg <- NA_character_
file <- find_topic_filename(pkg, obj, state$tag)
## To understand this, look at the RD column of the table above
if (!has_link_text) {
paste0(
if (is_code) "\\code{",
if (s4 && is.na(pkg)) "\\linkS4class" else "\\link",
if (is_fun || ! is.na(pkg)) "[",
if (is_fun && is.na(pkg)) "=",
if (! is.na(pkg)) paste0(pkg, ":"),
if (is_fun || ! is.na(pkg)) paste0(if (is.na(pkg)) obj else file, "]"),
"{",
if (!is.na(pkg)) paste0(pkg, "::"),
if (s4) noclass else fun,
"}",
if (is_code) "}" else ""
)
} else {
contents <- mdxml_link_text(contents, state)
list(
paste0(
if (is_code) "\\code{",
"\\link[",
if (is.na(pkg)) "=" else paste0(pkg, ":"),
if (is.na(pkg)) obj else file,
"]{"
),
contents,
"}",
if (is_code) "}" else ""
)
}
}
resolve_link_package <- function(topic, me = NULL, pkgdir = NULL, state = NULL) {
me <- me %||% roxy_meta_get("current_package")
# this is from the roxygen2 tests, should not happen on a real package
if (is.null(me) || is.na(me) || me == "") return(NA_character_)
# if it is in the current package, then no need for package name, right?
if (has_topic(topic, me)) return(NA_character_)
# try packages in depends, imports, suggests first, error on name clashes
pkgs <- local_pkg_deps(pkgdir)
pkg_has_topic <- pkgs[map_lgl(pkgs, has_topic, topic = topic)]
pkg_has_topic <- map_chr(pkg_has_topic, function(p) {
p0 <- find_reexport_source(topic, p)
if (is.na(p0)) p else p0
})
pkg_has_topic <- unique(pkg_has_topic)
base <- base_packages()
if (length(pkg_has_topic) == 0) {
# fall through to check base packages as well
} else if (length(pkg_has_topic) == 1) {
if (pkg_has_topic %in% base) {
return(NA_character_)
} else {
return(pkg_has_topic)
}
} else {
warn_roxy_tag(state$tag, c(
"Topic {.val {topic}} is available in multiple packages: {.pkg {pkg_has_topic}}",
i = "Qualify topic explicitly with a package name when linking to it."
))
return(NA_character_)
}
# try base packages as well, take the first hit,
# there should not be any name clashes, anyway
for (bp in base) {
if (has_topic(topic, bp)) return(NA_character_)
}
warn_roxy_tag(state$tag, c(
"Could not resolve link to topic {.val {topic}} in the dependencies or base packages",
"i" = paste(
"If you haven't documented {.val {topic}} yet, or just changed its name, this is normal.",
"Once {.val {topic}} is documented, this warning goes away."),
"i" = "Make sure that the name of the topic is spelled correctly.",
"i" = "Always list the linked package as a dependency.",
"i" = "Alternatively, you can fully qualify the link with a package name."
))
NA_character_
}
# this is mostly from downlit
is_exported <- function(name, package) {
name %in% getNamespaceExports(ns_env(package))
}
is_reexported <- function(name, package) {
if (package == "base") {
return(FALSE)
}
is_imported <- env_has(ns_imports_env(package), name)
is_imported && is_exported(name, package)
}
find_reexport_source <- function(topic, package) {
ns <- ns_env(package)
if (!env_has(ns, topic, inherit = TRUE)) {
return(NA_character_)
}
obj <- env_get(ns, topic, inherit = TRUE)
if (is.primitive(obj)) {
# primitive functions all live in base
"base"
} else if (is.function(obj)) {
## For functions, we can just take their environment.
ns_env_name(get_env(obj))
} else {
## For other objects, we need to check the import env of the package,
## to see where 'topic' is coming from. The import env has redundant
## information. It seems that we just need to find a named list
## entry that contains `topic`.
imp <- getNamespaceImports(ns)
imp <- imp[names(imp) != ""]
wpkgs <- vapply(imp, `%in%`, x = topic, FUN.VALUE = logical(1))
if (!any(wpkgs)) {
return(NA_character_)
}
pkgs <- names(wpkgs)[wpkgs]
# Take the last match, in case imports have name clashes.
pkgs[[length(pkgs)]]
}
}
#' Dummy page to test roxygen's markdown formatting
#'
#' Links are very tricky, so I'll put in some links here:
#' Link to a function: [roxygenize()].
#' Link to an object: [roxygenize] (we just treat it like an object here.
#'
#' Link to another package, function: [desc::desc()].
#' Link to another package, non-function: [desc::desc].
#'
#' Link with link text: [this great function][roxygenize()],
#' [`roxygenize`][roxygenize()], or [that great function][roxygenize].
#'
#' In another package: [and this one][desc::desc].
#'
#' This is a table:
#'
#' | __foo__ | __bar__ |
#' | :-- | --: |
#' | 1 | 2 |
#' | 100 | 200 |
#'
#' @name markdown-test
#' @keywords internal
NULL