R/link.R

Defines functions is_base_package href_package_ref href_package find_vignette_package is_bioc_pkg href_article find_reexport_source href_topic_reexported is_exported is_reexported href_topic_remote href_topic_local is_package_local href_topic is_help_literal href_expr href_expr_ autolink_curly autolink_url autolink

Documented in autolink autolink_url href_article href_package href_topic

#' Automatically link inline code
#'
#' @param text String of code to highlight and link.
#' @return
#'   If `text` is linkable, an HTML link for `autolink()`, and or just
#'   the URL for `autolink_url()`. Both return `NA` if the text is not
#'   linkable.
#' @inheritSection highlight Options
#' @export
#' @examples
#' autolink("stats::median()")
#' autolink("vignette('grid', package = 'grid')")
#'
#' autolink_url("stats::median()")
autolink <- function(text) {
  href <- autolink_url(text)
  if (identical(href, NA_character_)) {
    return(NA_character_)
  }

  paste0("<a href='", href, "'>", escape_html(text), "</a>")
}

#' @export
#' @rdname autolink
autolink_url <- function(text) {
  expr <- safe_parse(text)
  if (length(expr) == 0) {
    return(NA_character_)
  }

  href_expr(expr[[1]])
}

autolink_curly <- function(text) {
  package_name <- extract_curly_package(text)
  if (is.na(package_name)) {
    return(NA_character_)
  }

  href <- href_package(package_name)
  if (is.na(href)) {
    return(NA_character_)
  }

  paste0("<a href='", href, "'>", package_name, "</a>")
}


# Helper for testing
href_expr_ <- function(expr, ...) {
  href_expr(substitute(expr), ...)
}

href_expr <- function(expr) {
  if (!is_call(expr)) {
    return(NA_character_)
  }

  fun <- expr[[1]]
  if (is_call(fun, "::", n = 2)) {
    pkg <- as.character(fun[[2]])
    fun <- fun[[3]]
  } else {
    pkg <- NULL
  }

  if (!is_symbol(fun))
    return(NA_character_)

  fun_name <- as.character(fun)
  n_args <- length(expr) - 1

  if (n_args == 0) {
    href_topic(fun_name, pkg, is_fun = TRUE)
  } else if (fun_name %in% c("library", "require", "requireNamespace")) {
    simple_call <- n_args == 1 &&
      is.null(names(expr)) &&
      (is_string(expr[[2]]) || (fun_name != "requireNamespace") && is_symbol(expr[[2]]))

    if (simple_call) {
      pkg <- as.character(expr[[2]])
      topic <- href_package(pkg)
      if (is.na(topic)) {
        href_topic(fun_name)
      } else {
        topic
      }
    } else {
      href_topic(fun_name, is_fun = TRUE)
    }
  } else if (fun_name == "vignette" && n_args >= 1) {
    # vignette("foo", "package")
    expr <- match.call(utils::vignette, expr)
    topic_ok <- is.character(expr$topic)
    package_ok <- is.character(expr$package) || is.null(expr$package)
    if (topic_ok && package_ok) {
      href_article(expr$topic, expr$package)
    } else {
      NA_character_
    }
  } else if (fun_name == "?" && n_args == 1) {
    topic <- expr[[2]]
    if (is_call(topic, "::")) {
      # ?pkg::x
      href_topic(as.character(topic[[3]]), as.character(topic[[2]]))
    } else if (is_symbol(topic) || is_string(topic)) {
      # ?x
      href_topic(as.character(expr[[2]]))
    } else {
      NA_character_
    }
  } else if (fun_name == "?" && n_args == 2) {
    # package?x
    href_topic(paste0(expr[[3]], "-", expr[[2]]))
  } else if (fun_name == "help" && n_args >= 1) {
    expr <- match.call(utils::help, expr)
    if (is_help_literal(expr$topic) && is_help_literal(expr$package)) {
      href_topic(as.character(expr$topic), as.character(expr$package))
    } else if (is_help_literal(expr$topic) && is.null(expr$package)) {
      href_topic(as.character(expr$topic))
    } else if (is.null(expr$topic) && is_help_literal(expr$package)) {
      href_package_ref(as.character(expr$package))
    } else {
      NA_character_
    }
  } else if (fun_name == "::" && n_args == 2) {
    href_topic(as.character(expr[[3]]), as.character(expr[[2]]))
  } else {
    NA_character_
  }
}

is_help_literal <- function(x) is_string(x) || is_symbol(x)

# Topics ------------------------------------------------------------------

#' Generate url for topic/article/package
#'
#' @param topic,article Topic/article name
#' @param package Optional package name. If not supplied, will search
#'   in all attached packages.
#' @param is_fun Only return topics that are (probably) for functions.
#' @keywords internal
#' @export
#' @return URL topic or article; `NA` if can't find one.
#' @examples
#' href_topic("t")
#' href_topic("DOESN'T EXIST")
#' href_topic("href_topic", "downlit")
#'
#' href_package("downlit")
href_topic <- function(topic, package = NULL, is_fun = FALSE) {
  if (length(topic) != 1L) {
    return(NA_character_)
  }
  if (is_package_local(package)) {
    href_topic_local(topic, is_fun = is_fun)
  } else {
    href_topic_remote(topic, package)
  }
}

is_package_local <- function(package) {
  if (is.null(package)) {
    return(TRUE)
  }
  cur <- getOption("downlit.package")
  if (is.null(cur)) {
    return(FALSE)
  }

  package == cur
}

href_topic_local <- function(topic, is_fun = FALSE) {
  rdname <- find_rdname(NULL, topic)
  if (is.null(rdname)) {
    # Check attached packages
    loc <- find_rdname_attached(topic, is_fun = is_fun)
    if (is.null(loc)) {
      return(NA_character_)
    } else {
      return(href_topic_remote(topic, loc$package))
    }
  }

  if (rdname == "reexports") {
    return(href_topic_reexported(topic, getOption("downlit.package")))
  }

  cur_rdname <- getOption("downlit.rdname", "")
  if (rdname == cur_rdname) {
    return(NA_character_)
  }

  if (cur_rdname != "") {
    paste0(rdname, ".html")
  } else {
    paste0(getOption("downlit.topic_path"), rdname, ".html")
  }
}

href_topic_remote <- function(topic, package) {
  rdname <- find_rdname(package, topic)
  if (is.null(rdname)) {
    return(NA_character_)
  }

  if (is_reexported(topic, package)) {
    href_topic_reexported(topic, package)
  } else {
    paste0(href_package_ref(package), "/", rdname, ".html")
  }
}

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)
}

is_exported <- function(name, package) {
  name %in% getNamespaceExports(ns_env(package))
}

# If it's a re-exported function, we need to work a little harder to
# find out its source so that we can link to it.
href_topic_reexported <- function(topic, package) {
  ns <- ns_env(package)
  if (!env_has(ns, topic, inherit = TRUE)) {
    return(NA_character_)
  }

  obj <- env_get(ns, topic, inherit = TRUE)
  ex_package <- find_reexport_source(obj, ns, topic)
  # Give up if we're stuck in an infinite loop
  if (package == ex_package) {
    return(NA_character_)
  }

  href_topic_remote(topic, ex_package)
}

find_reexport_source <- function(obj, ns, topic) {
  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)]]
  }
}

# Articles ----------------------------------------------------------------

#' @export
#' @rdname href_topic
href_article <- function(article, package = NULL) {
  if (is_package_local(package)) {
    path <- find_article(NULL, article)
    if (!is.null(path)) {
      return(paste0(getOption("downlit.article_path"), path))
    }
  }

  if (is.null(package)) {
    package <- find_vignette_package(article)
    if (is.null(package)) {
      return(NA_character_)
    }
  }

  path <- find_article(package, article)
  if (is.null(path)) {
    return(NA_character_)
  }

  base_url <- remote_package_article_url(package)
  if (!is.null(base_url)) {
    paste0(base_url, "/", path)
  } else if (is_bioc_pkg(package)) {
    paste0("https://bioconductor.org/packages/release/bioc/vignettes/", package, "/inst/doc/", path)
  } else {
    paste0("https://cran.rstudio.com/web/packages/", package, "/vignettes/", path)
  }
}

# Returns NA if package is not installed.
# Returns TRUE if `package` is from Bioconductor, FALSE otherwise
is_bioc_pkg <- function(package) {
  if (!rlang::is_installed(package)) {
    return(FALSE)
  }
  biocviews <- utils::packageDescription(package, fields = "biocViews")
  !is.na(biocviews) && biocviews != ""
}


# Try to figure out package name from attached packages
find_vignette_package <- function(x) {
  for (pkg in getOption("downlit.attached")) {
    if (!is_installed(pkg)) {
      next
    }

    info <- tools::getVignetteInfo(pkg)

    if (x %in% info[, "Topic"]) {
      return(pkg)
    }
  }

  NULL
}

# Packages ----------------------------------------------------------------

#' @export
#' @rdname href_topic
href_package <- function(package) {
  urls <- package_urls(package)
  if (length(urls) == 0) {
    NA_character_
  } else {
    urls[[1]]
  }
}

href_package_ref <- function(package) {
  reference_url <- remote_package_reference_url(package)

  if (!is.null(reference_url)) {
    reference_url
  } else {
    # Fall back to rdrr.io
    if (is_base_package(package)) {
      paste0("https://rdrr.io/r/", package)
    } else {
      paste0("https://rdrr.io/pkg/", package, "/man")
    }
  }
}

is_base_package <- function(x) {
  x %in% c(
    "base", "compiler", "datasets", "graphics", "grDevices", "grid",
    "methods", "parallel", "splines", "stats", "stats4", "tcltk",
    "tools", "utils"
  )
}
r-lib/downlit documentation built on June 12, 2024, 1:55 p.m.