├── .github ├── .gitignore └── workflows │ └── R-CMD-check.yaml ├── LICENSE ├── .gitignore ├── .Rbuildignore ├── NAMESPACE ├── tests └── test.R ├── postdoc.Rproj ├── README.md ├── NEWS ├── DESCRIPTION ├── LICENSE.md ├── man └── html_manual.Rd ├── inst └── help-template │ └── manual.html └── R └── manual.R /.github/.gitignore: -------------------------------------------------------------------------------- 1 | *.html 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | YEAR: 2022 2 | COPYRIGHT HOLDER: Jeroen Ooms 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .Rproj.user 3 | .Rhistory 4 | .RData 5 | -------------------------------------------------------------------------------- /.Rbuildignore: -------------------------------------------------------------------------------- 1 | ^.*\.Rproj$ 2 | ^\.Rproj\.user$ 3 | ^README.md$ 4 | ^LICENSE\.md$ 5 | ^\.github$ 6 | -------------------------------------------------------------------------------- /NAMESPACE: -------------------------------------------------------------------------------- 1 | # Generated by roxygen2: do not edit by hand 2 | 3 | export(r_universe_link) 4 | export(render_base_manuals) 5 | export(render_package_manual) 6 | -------------------------------------------------------------------------------- /tests/test.R: -------------------------------------------------------------------------------- 1 | # Same as examples, but it takes longer than 10s on Windows 2 | library(postdoc) 3 | out <- render_package_manual('parallel', tempfile()) 4 | unlink(out) 5 | -------------------------------------------------------------------------------- /postdoc.Rproj: -------------------------------------------------------------------------------- 1 | Version: 1.0 2 | ProjectId: 3448f54b-4a69-481c-8d89-6dbf6a3bb94f 3 | 4 | RestoreWorkspace: Default 5 | SaveWorkspace: Default 6 | AlwaysSaveHistory: Default 7 | 8 | EnableCodeIndexing: Yes 9 | UseSpacesForTab: Yes 10 | NumSpacesForTab: 2 11 | Encoding: UTF-8 12 | 13 | RnwWeave: Sweave 14 | LaTeX: pdfLaTeX 15 | 16 | AutoAppendNewline: Yes 17 | StripTrailingWhitespace: Yes 18 | 19 | BuildType: Package 20 | PackageUseDevtools: Yes 21 | PackageInstallArgs: --no-multiarch --with-keep.source 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # postdoc 2 | 3 | > Simple and Uncluttered Package Documentation 4 | 5 | ## Installation 6 | 7 | You can install the development version of postdoc from r-universe: 8 | 9 | ```r 10 | # Download and install postdoc in R 11 | install.packages('postdoc', 12 | repos = c('https://ropensci.r-universe.dev','https://cloud.r-project.org')) 13 | ``` 14 | 15 | ## Example 16 | 17 | Render the manual for the 'MASS' package: 18 | 19 | ```r 20 | library(postdoc) 21 | htmlfile <- render_package_manual(package = 'MASS') 22 | utils::browseURL(htmlfile) 23 | ``` 24 | -------------------------------------------------------------------------------- /NEWS: -------------------------------------------------------------------------------- 1 | 1.4.1 2 | - Give a warning instead of an error for unsupported image types 3 | 4 | 1.4.0 5 | - Use CDN for katex.min.css because it also needs font files 6 | - Use latest katex 7 | 8 | 1.3.0 9 | - Remove
tags from each page 10 | 11 | 1.2.2 12 | - Use https://api.cran.dev for faster link lookups 13 | 14 | 1.2.0 15 | - Small fixes for weird edge cases 16 | 17 | 1.1.0 18 | - Do not include 'internal' pages in the manual 19 | - Parse orcid links in author fields 20 | - Warn (instead of error) for broken topic links 21 | - Move help pages with name '*-package' to the top 22 | 23 | 1.0.0 24 | - Initial release 25 | -------------------------------------------------------------------------------- /DESCRIPTION: -------------------------------------------------------------------------------- 1 | Package: postdoc 2 | Type: Package 3 | Title: Minimal and Uncluttered Package Documentation 4 | Version: 1.4.1 5 | Authors@R: person("Jeroen", "Ooms", role = c("aut", "cre"), email = "jeroenooms@gmail.com", 6 | comment = c(ORCID = "0000-0002-4035-0289")) 7 | Description: Generates simple and beautiful one-page HTML reference manuals 8 | with package documentation. Math rendering and syntax highlighting are done 9 | server-side in R such that no JavaScript libraries are needed in the 10 | browser, which makes the documentation portable and fast to load. 11 | License: MIT + file LICENSE 12 | URL: https://ropensci.r-universe.dev/postdoc 13 | https://docs.ropensci.org/postdoc/ 14 | BugReports: https://github.com/ropensci/postdoc/issues 15 | Encoding: UTF-8 16 | Imports: 17 | curl, 18 | jsonlite, 19 | katex (>= 1.5.0), 20 | prismjs, 21 | xml2 22 | RoxygenNote: 7.3.2.9000 23 | Roxygen: list(markdown = TRUE) 24 | Language: en-US 25 | 26 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # MIT License 2 | 3 | Copyright (c) 2022 Jeroen Ooms 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /man/html_manual.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/manual.R 3 | \name{render_package_manual} 4 | \alias{render_package_manual} 5 | \alias{render_base_manuals} 6 | \alias{r_universe_link} 7 | \title{Generate HTML reference manual} 8 | \usage{ 9 | render_package_manual(package, outdir = ".", link_cb = r_universe_link) 10 | 11 | render_base_manuals(outdir = ".") 12 | 13 | r_universe_link(package) 14 | } 15 | \arguments{ 16 | \item{package}{name of the package} 17 | 18 | \item{outdir}{where to put the html file} 19 | 20 | \item{link_cb}{callback function which can be used to customize hyperlinks to 21 | other packages. This function gets invoked when a help file contains links to 22 | another package, and should return the URL to the html reference manual for 23 | this other package. Set to \code{NULL} to drop cross-package links.} 24 | } 25 | \value{ 26 | path to the generated html document 27 | } 28 | \description{ 29 | Renders complete package reference manual in HTML format. 30 | } 31 | \details{ 32 | Math rendering and syntax highlighting are done server-side in R such that no 33 | JavaScript libraries are needed in the browser, which makes the documents 34 | portable and fast to load. 35 | } 36 | \examples{ 37 | htmlfile <- render_package_manual('compiler', tempdir()) 38 | if(interactive()) utils::browseURL(htmlfile) 39 | } 40 | -------------------------------------------------------------------------------- /.github/workflows/R-CMD-check.yaml: -------------------------------------------------------------------------------- 1 | # Workflow derived from https://github.com/r-lib/actions/tree/v2/examples 2 | # Need help debugging build failures? Start at https://github.com/r-lib/actions#where-to-find-help 3 | on: 4 | push: 5 | pull_request: 6 | 7 | name: R-CMD-check 8 | 9 | jobs: 10 | R-CMD-check: 11 | runs-on: ${{ matrix.config.os }} 12 | 13 | name: ${{ matrix.config.os }} (${{ matrix.config.r }}) 14 | 15 | strategy: 16 | fail-fast: false 17 | matrix: 18 | config: 19 | - {os: macos-latest, r: 'release'} 20 | - {os: windows-latest, r: 'release'} 21 | - {os: ubuntu-latest, r: 'devel', http-user-agent: 'release'} 22 | - {os: ubuntu-latest, r: 'release'} 23 | - {os: ubuntu-latest, r: 'oldrel-1'} 24 | 25 | env: 26 | GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }} 27 | R_KEEP_PKG_SOURCE: yes 28 | 29 | steps: 30 | - uses: actions/checkout@v3 31 | 32 | - uses: r-lib/actions/setup-pandoc@v2 33 | 34 | - uses: r-lib/actions/setup-r@v2 35 | with: 36 | r-version: ${{ matrix.config.r }} 37 | http-user-agent: ${{ matrix.config.http-user-agent }} 38 | use-public-rspm: true 39 | 40 | - uses: r-lib/actions/setup-r-dependencies@v2 41 | with: 42 | extra-packages: any::rcmdcheck 43 | needs: check 44 | 45 | - uses: r-lib/actions/check-r-package@v2 46 | with: 47 | upload-snapshots: false 48 | -------------------------------------------------------------------------------- /inst/help-template/manual.html: -------------------------------------------------------------------------------- 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 |
Title:
Description:
Authors:
Maintainer:
License:
Version:
Built:
Source:
31 | 32 |

Help Index

33 | 34 | 35 |
36 |
37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /R/manual.R: -------------------------------------------------------------------------------- 1 | #' Generate HTML reference manual 2 | #' 3 | #' Renders complete package reference manual in HTML format. 4 | #' 5 | #' Math rendering and syntax highlighting are done server-side in R such that no 6 | #' JavaScript libraries are needed in the browser, which makes the documents 7 | #' portable and fast to load. 8 | #' 9 | #' @rdname html_manual 10 | #' @param package name of the package 11 | #' @param outdir where to put the html file 12 | #' @param link_cb callback function which can be used to customize hyperlinks to 13 | #' other packages. This function gets invoked when a help file contains links to 14 | #' another package, and should return the URL to the html reference manual for 15 | #' this other package. Set to `NULL` to drop cross-package links. 16 | #' @export 17 | #' @return path to the generated html document 18 | #' @examples 19 | #' htmlfile <- render_package_manual('compiler', tempdir()) 20 | #' if(interactive()) utils::browseURL(htmlfile) 21 | render_package_manual <- function(package, outdir = '.', link_cb = r_universe_link){ 22 | dir.create(outdir, recursive = TRUE, showWarnings = FALSE) 23 | get_link <- if(is.function(link_cb)){ 24 | simple_cache(link_cb) 25 | } 26 | sapply(package, render_package_manual_one, outdir = outdir, get_link = get_link) 27 | } 28 | 29 | render_package_manual_one <- function(package, outdir, get_link){ 30 | desc <- package_desc(package) 31 | #Sys.setenv("_R_RD_MACROS_PACKAGE_DIR_" = installdir) 32 | manfiles <- load_rd_env(package) 33 | doc <- xml2::read_html(system.file(package = 'postdoc', 'help-template/manual.html'), options = c("RECOVER", "NOERROR")) 34 | body <- xml2::xml_find_first(doc, '//body') 35 | xml2::xml_set_attr(body, 'class', 'postdoc macintosh') 36 | xml2::xml_set_text(xml2::xml_find_first(doc, '//title'), sprintf("Package '%s' reference manual", desc$package)) 37 | xml2::xml_set_text(xml2::xml_find_first(body, '//h1'), sprintf("Package '%s'", desc$package)) 38 | lapply(xml2::xml_find_all(doc, "//td[starts-with(@class,'description')]"), function(node){ 39 | field <- substring(xml2::xml_attr(node, 'class'), 13) 40 | if(field == 'author' && nchar(desc$author)){ 41 | xml2::xml_add_child(node, make_author_node(desc[[field]])) 42 | } else if(length(desc[[field]])){ 43 | xml2::xml_set_text(node, desc[[field]]) 44 | } 45 | }) 46 | write_footer(doc) 47 | rlinkdb <- tools::findHTMLlinks(system.file(package = package)) # can be expensive if many pkgs installed 48 | nodes <- lapply(ls(manfiles), function(page_id){ 49 | render_one_page(page_id, rd = manfiles[[page_id]], package = package, links = rlinkdb) 50 | }) 51 | nodes <- sort_chapters(nodes) 52 | pagediv <- xml2::xml_find_first(doc, "//div[@class='manual-pages-content']") 53 | lapply(nodes, xml2::xml_add_child, .x = pagediv) 54 | fix_links(doc, package, get_link) 55 | fix_images(doc, package) 56 | prismjs::prism_process_xmldoc(doc) 57 | render_math(doc) 58 | make_index(doc, nodes) 59 | outfile <- file.path(outdir, paste0(package, '.html')) 60 | xml2::write_html(doc, outfile) 61 | return(outfile) 62 | } 63 | 64 | #' @rdname html_manual 65 | #' @export 66 | render_base_manuals <- function(outdir = '.'){ 67 | render_package_manual(basepkgs, outdir = outdir) 68 | } 69 | 70 | #' @export 71 | #' @rdname html_manual 72 | r_universe_link <- function(package){ 73 | if(package %in% basepkgs){ 74 | return(sprintf('https://r-universe.dev/manuals/%s.html', package)) 75 | } 76 | link <- tryCatch(lookup_docs_link(package), error = message) 77 | if(length(link)){ 78 | message(sprintf("Using link for package '%s' -> %s", package, link)) 79 | link 80 | } else { 81 | message(sprintf("Did not find suitable link for package '%s'", package)) 82 | } 83 | } 84 | 85 | #TODO: maybe use tools::Rd_db() instead ? 86 | load_rd_env <- function(package){ 87 | manfiles <- new.env(parent = emptyenv()) 88 | installdir <- system.file(package = package, mustWork = TRUE) 89 | lazyLoad(file.path(installdir, 'help', package), envir = manfiles) 90 | # cf https://github.com/wch/r-source/blob/b12ffba7584825d6b11bba8b7dbad084a74c1c20/src/library/tools/R/Rd2pdf.R#L109 91 | Filter(function(x){ 92 | is.na(match("internal", get_rd_keywords(x))) 93 | }, as.list(manfiles)) 94 | } 95 | 96 | sort_chapters <- function(nodes){ 97 | mannames <- vapply(nodes, attr, character(1), 'name') 98 | sortnames <- sub("^(.*-package)$", '___\\1', mannames) 99 | nodes[order(sortnames)] 100 | } 101 | 102 | render_one_page <- function(page_id, rd, package, links){ 103 | out <- tempfile(fileext = '.html') 104 | page_name <- get_rd_name(rd) 105 | html <- tools::Rd2HTML(rd, package = package, out = out, stages=c("build", "install", "render"), 106 | Links = links, Links2 = character(), stylesheet="", dynamic = FALSE) 107 | doc <- xml2::read_html(html) 108 | container <- xml2::xml_find_first(doc, "//div[@class = 'container']") 109 | main <- xml2::xml_find_first(doc, "//main") 110 | if(xml2::xml_length(main)){ 111 | xml2::xml_name(main) <- "div" 112 | xml2::xml_set_attr(main, 'class', 'page-main') 113 | } 114 | xml2::xml_set_attr(container, 'id', page_id) 115 | xml2::xml_set_attr(container, 'class', "container manual-page") 116 | xml2::xml_remove(xml2::xml_find_first(doc, "//div[a[@href = '00Index.html']]")) # Remove footer 117 | headertable <- xml2::xml_find_first(doc, "//table[.//td[text() = 'R Documentation']]") 118 | xml2::xml_remove(headertable) 119 | titlenode <- xml2::xml_find_first(doc, '//h2') 120 | page_title <- xml2::xml_text(titlenode) 121 | titlelink <- xml2::xml_replace(titlenode, 'a') 122 | xml2::xml_set_attr(titlelink, 'href', paste0("#", page_id)) 123 | xml2::xml_set_attr(titlelink, 'class', 'help-page-title') 124 | xml2::xml_add_child(titlelink, titlenode) 125 | xml2::xml_set_attr(xml2::xml_find_first(doc, "//h3[text() = 'Arguments']"), 'class', 'r-arguments-title') 126 | structure(container, id = page_id, name = page_name, title = page_title) 127 | } 128 | 129 | fix_images <- function(doc, package){ 130 | images <- xml2::xml_find_all(doc, "//img[starts-with(@src,'../help/')]") 131 | lapply(images, function(x){ 132 | helpdir <- system.file(package = package, 'help', mustWork = TRUE) 133 | img <- file.path(helpdir, xml2::xml_attr(x, 'src')) 134 | if(!file.exists(img)){ 135 | warning("Document references non-existing image: ", xml2::xml_attr(x, 'src')) 136 | } else { 137 | # TODO: maybe better just remove these images, because they seem mostly 138 | # intended for pkgdown, and don't show up in the PDF manual either... 139 | tryCatch(xml2::xml_set_attr(x, 'src', image_base64(img)), error = function(e){ 140 | warning(e) 141 | xml2::xml_remove(x) 142 | }) 143 | } 144 | }) 145 | } 146 | 147 | make_index <- function(doc, nodes){ 148 | index <- xml2::xml_find_first(doc, "//ul[@id='help-index-list']") 149 | lapply(nodes, function(x){ 150 | id <- attr(x, 'id') 151 | title <- attr(x, 'title') 152 | li <- xml2::xml_add_child(index, 'li') 153 | xml2::xml_set_attr(li, 'class', 'help-index-item') 154 | a <- xml2::xml_add_child(li, 'a') 155 | xml2::xml_set_attr(a, 'href', paste0("#", id)) 156 | xml2::xml_set_text(a, title) 157 | }) 158 | } 159 | 160 | image_base64 <- function(path){ 161 | ext <- tolower(utils::tail(strsplit(path, '.', fixed = TRUE)[[1]], 1)) 162 | type <- switch(ext, 163 | svg = 'image/svg+xml', 164 | png = 'image/png', 165 | jpeg = 'image/jpeg', 166 | jpg = 'image/jpeg', 167 | stop("Unknown image extension: ", path)) 168 | content <- readBin(path, raw(), file.info(path)$size) 169 | b64 <- gsub('\n', '', jsonlite::base64_enc(content), fixed = TRUE) 170 | sprintf('data:%s;base64,%s', type, b64) 171 | } 172 | 173 | # Simulate what happens in R katex-config.js script 174 | # https://github.com/r-devel/r-svn/blob/HEAD/doc/html/katex-config.js 175 | render_math <- function(doc){ 176 | macros = list("\\R"= "\\textsf{R}", "\\mbox"= "\\text", "\\code"= "\\texttt") 177 | lapply(xml2::xml_find_all(doc, "//code[@class = 'reqn']"), function(x){ 178 | input <- trimws(xml2::xml_text(x)) 179 | output <- katex::katex_html(input, preview = FALSE, macros = macros, displayMode = FALSE, throwOnError = FALSE) 180 | newnode <- parse_html_node(paste0('', trimws(output), '')) 181 | xml2::xml_replace(x, newnode) 182 | }) 183 | } 184 | 185 | package_desc <- function(pkg){ 186 | desc <- unclass(utils::packageDescription(pkg)) 187 | if(!is.list(desc)) stop("Package not installed: ", pkg, call. = FALSE) 188 | names(desc) <- tolower(names(desc)) 189 | desc$date <- trimws(strsplit(desc$built, ';')[[1]][3]) 190 | desc$source <- if(length(desc$remoteurl)){ 191 | desc$remoteurl 192 | } else if(length(desc$repository)){ 193 | desc$repository 194 | } else { 195 | desc$priority # should be base only 196 | } 197 | desc 198 | } 199 | 200 | escape_txt <- function(txt){ 201 | doc <- xml2::read_xml(charToRaw("")) 202 | node <- xml2::xml_root(doc) 203 | xml2::xml_set_text(node, txt) 204 | as.character(xml2::xml_contents(node)) 205 | } 206 | 207 | make_author_node <- function(author){ 208 | snippet <- gsub("\\(<(https://orcid.org/[0-9X-]{19})>\\)", 209 | '', 210 | escape_txt(author), perl=TRUE) 211 | parse_html_node(sprintf('%s', snippet)) 212 | } 213 | 214 | # Try to mimic tools:::.Rd_get_name(rd) 215 | get_rd_name <- function(rd){ 216 | nametag <- Filter(function(x){identical("\\name", attr(x, 'Rd_tag'))}, rd) 217 | if(length(nametag)){ 218 | # Should dispatch tools:::as.character.Rd() 219 | val <- structure(nametag[[1]], names = 'Rd') 220 | paste(as.character(val), collapse = "") 221 | } else { 222 | stop("Failed to find \\name in Rd") 223 | } 224 | } 225 | 226 | get_rd_keywords <- function(rd){ 227 | # Mimic: tools:::.Rd_get_metadata 228 | keywords <- Filter(function(x){identical("\\keyword", attr(x, 'Rd_tag'))}, rd) 229 | unique(trimws(vapply(keywords, paste, "", collapse = "\n"))) 230 | } 231 | 232 | fix_links <- function(doc, package, get_link){ 233 | # Open true external links in a new page 234 | xml2::xml_set_attr(xml2::xml_find_all(doc, "//a[starts-with(@href,'http://')]"), 'target', '_blank') 235 | xml2::xml_set_attr(xml2::xml_find_all(doc, "//a[starts-with(@href,'https://')]"), 'target', '_blank') 236 | 237 | # Normalize local hyperlinks 238 | locallinks <- xml2::xml_find_all(doc, "//a[starts-with(@href,'../help/')]") 239 | xml2::xml_set_attr(locallinks, 'href', sub("^../", sprintf("../../%s/", package), xml2::xml_attr(locallinks, 'href'))) 240 | 241 | # Find and replace x-package links 242 | links <- xml2::xml_find_all(doc, "//a[starts-with(@href,'../../')]") 243 | xml2::xml_set_attr(links, 'href', sub("00Index.html$", './', xml2::xml_attr(links, 'href'))) 244 | linkvalues <- substring(xml2::xml_attr(links, 'href'), 7) 245 | matches <- gregexec("^([^/]+)/(html|help)/([^/]+)\\.html", linkvalues, perl = TRUE) 246 | parsedlinks <- regmatches(linkvalues, matches) 247 | aliases <- readRDS(system.file("help", "aliases.rds", package = package, mustWork = TRUE)) 248 | newlinks <- vapply(parsedlinks, function(x){ 249 | if(length(x) == 4){ 250 | linkpkg <- x[2] 251 | topic <- utils::URLdecode(gsub("+", "%", x[4], fixed = TRUE)) 252 | if(linkpkg == package){ 253 | target <- aliases[topic] 254 | if(!is.na(target)){ 255 | return(paste0("#", target)) 256 | } else { 257 | message("Failed to resolve internal help alias to: ", linkpkg, "::", topic) 258 | } 259 | } else if(is.function(get_link)){ 260 | res <- get_link(linkpkg) 261 | if(length(res)){ 262 | return(sprintf('%s#%s', res, topic)) 263 | } 264 | } 265 | } 266 | return("#") 267 | }, character(1)) 268 | xml2::xml_set_attr(links, 'href', newlinks) 269 | 270 | # Remove dead links produced above 271 | xml2::xml_set_attr(xml2::xml_find_all(doc, "//a[@href = '#']"), 'href', NULL) 272 | 273 | # Check remaining links 274 | doclinks <- xml2::xml_attr(xml2::xml_find_all(doc, "//a[@href]"), 'href') 275 | badlinks <- grep('^(http|mailto|#)', doclinks, invert = TRUE, value = TRUE) 276 | if(length(badlinks)){ 277 | message("Found unresolved local links:") 278 | lapply(badlinks, message) 279 | } 280 | } 281 | 282 | write_footer <- function(doc){ 283 | footer <- xml2::xml_find_first(doc, '//footer') 284 | p <- xml2::xml_add_child(footer, 'p') 285 | xml2::xml_set_text(p, sprintf('Rendered with postdoc %s', utils::packageVersion('postdoc'))) 286 | } 287 | 288 | lookup_docs_link <- function(package){ 289 | res <- curl::curl_fetch_memory(sprintf('https://api.cran.dev/%s', package)) 290 | if(res$status_code == 200){ 291 | out <- jsonlite::fromJSON(rawToChar(res$content)) 292 | if(length(out$devel$docs)){ 293 | return(out$devel$docs) 294 | } 295 | if(length(out$release$docs)){ 296 | return(out$release$docs) 297 | } 298 | stop('Unexpected response from cran.dev for: ', package) 299 | } 300 | my_universe <- Sys.getenv("MY_UNIVERSE") 301 | if(package %in% universe_list(my_universe)){ 302 | sprintf("%s/%s/doc/manual.html", my_universe, package) 303 | } 304 | } 305 | 306 | list_universe_packages_internal <- function(universe){ 307 | if(length(universe) && nchar(universe)){ 308 | message("Quering packages in: ", universe) 309 | if(nchar(universe)){ 310 | jsonlite::fromJSON(sprintf('%s/api/ls', universe)) 311 | } 312 | } 313 | } 314 | 315 | simple_cache <- function(fun){ 316 | cache <- new.env(parent = emptyenv()) 317 | function(arg){ 318 | key <- gsub("\n", "", jsonlite::base64_enc(serialize(arg, NULL)), fixed = TRUE) 319 | if(!exists(key, cache)){ 320 | assign(key, fun(arg), envir = cache) 321 | } 322 | get0(key, envir = cache) 323 | } 324 | } 325 | 326 | universe_list <- simple_cache(list_universe_packages_internal) 327 | 328 | basepkgs <- c("base", "boot", "class", "cluster", "codetools", "compiler", 329 | "datasets", "foreign", "graphics", "grDevices", "grid", "KernSmooth", 330 | "lattice", "MASS", "Matrix", "methods", "mgcv", "nlme", "nnet", 331 | "parallel", "rpart", "spatial", "splines", "stats", 332 | "stats4", "survival", "tcltk", "tools", "utils") 333 | 334 | parse_html_node <- function(html){ 335 | xml2::xml_child(xml2::xml_child(xml2::read_html(html))) 336 | } 337 | --------------------------------------------------------------------------------