The R Project SVN R

Rev

Rev 87255 | Blame | Compare with Previous | Last modification | View Log | Download | RSS feed

#  File src/library/utils/R/help.search.R
#  Part of the R package, https://www.R-project.org
#
#  Copyright (C) 1995-2024 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
#  https://www.R-project.org/Licenses/

.hsearch_db <-
local({
    hdb <- NULL
    function(new) {
    if(!missing(new))
        hdb <<- new
    else
        hdb
    }
})

merge_vignette_index <-
function(hDB, path, pkg)
{
    ## Vignettes in the hsearch index started in R 2.14.0
    ## Most packages don't have them, so the following should not be
    ## too inefficient
    if(file.exists(v_file <- file.path(path, "Meta", "vignette.rds"))
       && !is.null(vDB <- readRDS(v_file))
       && nrow(vDB)) {
    ## Make it look like an hDB base matrix and append it
    base <- matrix("", nrow = nrow(vDB), ncol = 8L)
    colnames(base) <- colnames(hDB[[1L]])
    base[, "Package"] <- pkg
    base[, "LibPath"] <- path
    id <- as.character(1:nrow(vDB) + NROW(hDB[[1L]]))
    base[, "ID"] <- id
    base[, "Name"] <- tools::file_path_sans_ext(basename(vDB$PDF))
        ## As spotted by Henrik Bengtsson <henrik.bengtsson@gmail.com>,
        ## using tools::file_path_sans_ext(basename(vDB$File) does not
        ## work as intended, as non-Sweave vignettes could have nested
        ## extensions (e.g., 'foo.tex.rsp' or 'foo.pdf.asis').
        ## The docs say that the 'name' is the "base of the vignette
        ## filename", which can be interpreted as above for the case of
        ## nested extensions (and in fact, tools:::httpd() does so).
    base[, "Topic"] <- base[, "Name"]
    base[, "Title"] <- vDB$Title
    base[, "Type"] <- "vignette"
    hDB[[1L]] <- rbind(hDB[[1L]], base)
    aliases <- matrix("", nrow = nrow(vDB), ncol = 3L)
    colnames(aliases) <- colnames(hDB[[2L]])
    aliases[, "Alias"] <- base[, "Name"]
    aliases[, "ID"] <- id
    aliases[, "Package"] <- pkg
    hDB[[2L]] <- rbind(hDB[[2L]], aliases)
    nkeywords <- sum(lengths(vDB$Keywords))
    if (nkeywords) {
        keywords <- matrix("", nrow = nkeywords, ncol = 3L)
        colnames(keywords) <- colnames(hDB[[4L]])
        keywords[,"Concept"] <- unlist(vDB$Keywords)
        keywords[,"ID"] <- unlist(lapply(1:nrow(vDB),
           function(i) rep.int(id[i], length(vDB$Keywords[[i]]))))
        keywords[,"Package"] <- pkg
        hDB[[4L]] <- rbind(hDB[[4L]], keywords)
    }
    }
    hDB
}

merge_demo_index <-
function(hDB, path, pkg)
{
    ## Demos in the hsearch index started in R 2.14.0
    if(file.exists(d_file <- file.path(path, "Meta", "demo.rds"))
       && !is.null(dDB <- readRDS(d_file))
       && nrow(dDB)) {
    ## Make it look like an hDB base matrix and append it
    base <- matrix("", nrow = nrow(dDB), ncol = 8L)
    colnames(base) <- colnames(hDB[[1]])
    base[, "Package"] <- pkg
    base[, "LibPath"] <- path
    id <- as.character(1:nrow(dDB) + NROW(hDB[[1L]]))
    base[, "ID"] <- id
    base[, "Name"] <- dDB[, 1L]
    base[, "Topic"] <- base[, "Name"]
    base[, "Title"] <- dDB[, 2L]
    base[, "Type"] <- "demo"
    hDB[[1L]] <- rbind(hDB[[1L]], base)
    aliases <- matrix("", nrow = nrow(dDB), ncol = 3L)
    colnames(aliases) <- colnames(hDB[[2L]])
    aliases[, "Alias"] <- base[, "Name"]
    aliases[, "ID"] <- id
    aliases[, "Package"] <- pkg
    hDB[[2L]] <- rbind(hDB[[2L]], aliases)
    }
    hDB
}

hsearch_db_fields <-
    c("alias", "concept", "keyword", "name", "title")
hsearch_db_types <-
    c("help", "vignette", "demo")

## FIXME: use UTF-8, either always or optionally
## (Needs UTF-8-savvy & fast agrep, and PCRE regexps.)
help.search <-
function(pattern, fields = c("alias", "concept", "title"),
         apropos, keyword, whatis, ignore.case = TRUE,
         package = NULL, lib.loc = NULL,
         help.db = getOption("help.db"),
         verbose = getOption("verbose"),
         rebuild = FALSE, agrep = NULL, use_UTF8 = FALSE,
         types = getOption("help.search.types"))
{
    ### Argument handling.
    .wrong_arg <- function(arg)
    gettextf("'%s' must be a character string", arg)
    if(is.logical(verbose)) verbose <- 2 * as.integer(verbose)
    fuzzy <- agrep
    if(!missing(pattern)) {
    if(!is.character(pattern) || (length(pattern) > 1L))
        stop(.wrong_arg("pattern"), domain = NA)
    i <- pmatch(fields, hsearch_db_fields)
    if(anyNA(i))
        stop("incorrect field specification")
    else
        fields <- hsearch_db_fields[i]
    } else if(!missing(apropos)) {
    if(!is.character(apropos) || (length(apropos) > 1L))
        stop(.wrong_arg("apropos"), domain = NA)
    else {
        pattern <- apropos
        fields <- c("alias", "title")
    }
    } else if(!missing(keyword)) {
    if(!is.character(keyword) || (length(keyword) > 1L))
        stop(.wrong_arg("keyword"), domain = NA)
    else {
        pattern <- keyword
        fields <- "keyword"
        if(is.null(fuzzy)) fuzzy <- FALSE
    }
    } else if(!missing(whatis)) {
    if(!is.character(whatis) || (length(whatis) > 1))
        stop(.wrong_arg("whatis"), domain = NA)
    else {
        pattern <- whatis
        fields <- "alias"
    }
    } else {
    stop("do not know what to search")
    }

    if(!missing(help.db))
    warning("argument 'help.db' is deprecated")

    ## This duplicates expansion in hsearch_db(), but there is no simple
    ## way to avoid this.
    i <- pmatch(types, hsearch_db_types)
    if (anyNA(i))
    stop("incorrect type specification")
    else
    types <- hsearch_db_types[i]
    
    ### Set up the hsearch db.
    db <- hsearch_db(package, lib.loc, types, verbose, rebuild,
                     use_UTF8)
    ## Argument lib.loc was expanded when building the hsearch db, so
    ## get from there.
    lib.loc <- attr(db, "LibPaths")

    ## Subset to the requested help types if necessary.
    if(!identical(sort(types), sort(attr(db, "Types")))) {
        db$Base <- db$Base[!is.na(match(db$Base$Type, types)), ]
        db[-1L] <-
            lapply(db[-1L],
                   function(e) {
                       e[!is.na(match(e$ID, db$Base$ID)), ]
                   })
    }
        
    if(!is.null(package)) {
    ## Argument 'package' was given.  Need to check that all given
    ## packages exist in the db, and only search the given ones.
    pos_in_hsearch_db <-
        match(package, unique(db$Base[, "Package"]), nomatch = 0L)
        ## This should not happen for R >= 2.4.0
    if(any(pos_in_hsearch_db) == 0L)
        stop(gettextf("no information in the database for package %s: need 'rebuild = TRUE'?",
              sQuote(package[pos_in_hsearch_db == 0][1L])),
                 domain = NA)
    db[] <-
        lapply(db,
           function(e) {
               e[!is.na(match(e$Package, package)), ]
           })
    }

    ### Matching.
    if(verbose >= 2L) {
    message("Database of ",
                NROW(db$Base), " help objects (",
                NROW(db$Aliases), " aliases, ",
                NROW(db$Concepts), " concepts, ",
                NROW(db$Keywords), " keywords)",
                domain = NA)
        flush.console()
    }

    ## <FIXME>
    ## No need continuing if there are no objects in the data base.
    ## But shouldn't we return something of class "hsearch"?
    if(!length(db$Base)) return(invisible())
    ## </FIXME>

    ## If agrep is NULL (default), we want to use fuzzy matching iff
    ## 'pattern' contains no characters special to regular expressions.
    ## We use the following crude approximation: if pattern contains
    ## only alphanumeric characters or whitespace or a '-', it is taken
    ## 'as is', and fuzzy matching is used unless turned off explicitly,
    ## or pattern has very few (currently, less than 5) characters.
    if(is.null(fuzzy) || is.na(fuzzy))
    fuzzy <-
        (grepl("^([[:alnum:]]|[[:space:]]|-)+$", pattern)
         && (nchar(pattern, type="c") > 4L))
    if(is.logical(fuzzy)) {
    if(fuzzy)
        max.distance <- 0.1
    }
    else if(is.numeric(fuzzy) || is.list(fuzzy)) {
    max.distance <- fuzzy
    fuzzy <- TRUE
    }
    else
    stop("incorrect 'agrep' specification")

    dbBase <- db$Base
    search_fun <- if(fuzzy) {
        function(x) {
        agrep(pattern, x, ignore.case = ignore.case,
          max.distance = max.distance)
        }
    } else {
        function(x) {
            grep(pattern, x, ignore.case = ignore.case,
                 perl = use_UTF8)
        }
    }
    search_db_results <- function(p, f, e)
        list2DF(list(Position = p, Field = f, Entry = e))
    search_db_field <- function(field) {
    switch(field,
           alias = {
           aliases <- db$Aliases$Alias
                   matched <- search_fun(aliases)
                   search_db_results(match(db$Aliases$ID[matched],
                                           dbBase$ID),
                                     rep.int(field, length(matched)),
                                     aliases[matched])
           },
           concept = {
           concepts <- db$Concepts$Concept
                   matched <- search_fun(concepts)
                   search_db_results(match(db$Concepts$ID[matched],
                                           dbBase$ID),
                                     rep.int(field, length(matched)),
                                     concepts[matched])
           },
           keyword = {
           keywords <- db$Keywords$Keyword
                   matched <- search_fun(keywords)
                   search_db_results(match(db$Keywords$ID[matched],
                                           dbBase$ID),
                                     rep.int(field, length(matched)),
                                     keywords[matched])
           },
               ## Alternatively, generically use field mapped to title
               ## case.
               name = {
                   matched <- search_fun(dbBase$Name)
                   search_db_results(matched,
                                     rep.int("Name", length(matched)),
                                     dbBase$Name[matched])
               },
               title = {
                   matched <- search_fun(dbBase$Title)
                   search_db_results(matched,
                                     rep.int("Title", length(matched)),
                                     dbBase$Title[matched])
               }
               )
    }

    matches <- NULL
    for(f in fields)
        matches <- rbind(matches, search_db_field(f))
    matches <- matches[order(matches$Position), ]
    db <- cbind(dbBase[matches$Position,
                       c("Topic", "Title", "Name", "ID",
                         "Package", "LibPath", "Type"),
                       drop = FALSE],
                matches[c("Field", "Entry")])
    rownames(db) <- NULL
    if(verbose>= 2L) {
        n_of_objects_matched <- length(unique(db[, "ID"]))
        message(sprintf(ngettext(n_of_objects_matched,
                                 "matched %d object.",
                                 "matched %d objects."),
                        n_of_objects_matched),
                domain = NA)
        flush.console()
    }

    ## Retval.
    y <- list(pattern = pattern, fields = fields,
          type = if(fuzzy) "fuzzy" else "regexp",
          agrep = agrep,
          ignore.case = ignore.case, types = types,
          package = package, lib.loc = lib.loc,
          matches = db)
    class(y) <- "hsearch"
    y
}

hsearch_db <-
function(package = NULL, lib.loc = NULL,
         types = getOption("help.search.types"),
         verbose = getOption("verbose"),
         rebuild = FALSE, use_UTF8 = FALSE)
{
    WINDOWS <- .Platform$OS.type == "windows"
    if(is.logical(verbose)) verbose <- 2 * as.integer(verbose)
    if(is.null(lib.loc))
    lib.loc <- .libPaths()
    i <- pmatch(types, hsearch_db_types)
    if (anyNA(i))
    stop("incorrect type specification")
    else
    types <- hsearch_db_types[i]

    db <- eval(.hsearch_db())
    if(is.null(db))
    rebuild <- TRUE
    else if(!rebuild) {
    ## Need to find out whether this has the info we need.
    ## Note that when looking for packages in libraries we always
    ## use the first location found.  Hence if the library search
    ## path changes we might find different versions of a package.
    ## Thus we need to rebuild the hsearch db in case the specified
    ## library path is different from the one used when building the
    ## hsearch db (stored as its "LibPaths" attribute).
    if(!identical(lib.loc, attr(db, "LibPaths")) ||
       anyNA(match(types, attr(db, "Types"))) ||
       ## We also need to rebuild the hsearch db in case an existing
       ## dir in the library path was modified more recently than
       ## the db, as packages might have been installed or removed.
           any(attr(db, "mtime") < file.mtime(lib.loc[file.exists(lib.loc)])) ||
       ## Or if the user changed the locale character type ...
       !identical(attr(db, "ctype"), Sys.getlocale("LC_CTYPE"))
           )
        rebuild <- TRUE
        ## We also need to rebuild if 'packages' was used before and has
        ## changed.
        if(!is.null(package) &&
           any(is.na(match(package, db$Base[, "Package"]))))
            rebuild <- TRUE
    }
    if(rebuild) {
    if(verbose > 0L) {
            message("Rebuilding the help.search() database", " ", "...",
                    if(verbose > 1L) "...", domain = NA)
            flush.console()
        }

        want_type_help <- any(types == "help")
        want_type_demo <- any(types == "demo")
        want_type_vignette <- any(types == "vignette")

    if(!is.null(package)) {
        packages_in_hsearch_db <- package
            package_paths <- NULL
    } else {
            ## local version of .packages(all.available = TRUE),
            ## recording paths
            ans <- character(0L); paths <- character(0L)
            lib.loc <- lib.loc[file.exists(lib.loc)]
            valid_package_version_regexp <-
                .standard_regexps()$valid_package_version
            for (lib in lib.loc) {
                a <- list.files(lib, all.files = FALSE, full.names = FALSE)
                for (nam in a) {
                    pfile <- file.path(lib, nam, "Meta", "package.rds")
                    if (file.exists(pfile))
                        info <- readRDS(pfile)$DESCRIPTION[c("Package", "Version")]
                    else next
                    if ( (length(info) != 2L) || anyNA(info) ) next
                    if (!grepl(valid_package_version_regexp, info["Version"])) next
                    ans <- c(ans, nam)
                    paths <- c(paths, file.path(lib, nam))
                }
            }
            un <- !duplicated(ans)
        packages_in_hsearch_db <-  ans[un]
            package_paths <- paths[un]
            names(package_paths) <- ans[un]
        }

    ## Create the hsearch db.
    np <- 0L
    if(verbose >= 2L) {
        message("Packages {readRDS() sequentially}:", domain = NA)
            flush.console()
        }
        tot <- length(package_paths)
        incr <- 0L
        if(verbose && WINDOWS) {
            pb <- winProgressBar("R: creating the help.search() DB", max = tot)
            on.exit(close(pb))
        } else if(verbose == 1L) incr <- ifelse(tot > 500L, 100L, 10L)

    ## Starting with R 1.8.0, prebuilt hsearch indices are available
    ## in Meta/hsearch.rds, and the code to build this from the Rd
    ## contents (as obtained from both new and old style Rd indices)
    ## has been moved to tools:::.build_hsearch_index() which
    ## creates a per-package list of base, aliases and keywords
    ## information.  When building the global index, it seems (see
    ## e.g. also the code in tools:::Rdcontents()), most efficient to
    ## create a list *matrix* (dbMat below), stuff the individual
    ## indices into its rows, and finally create the base, alias,
    ## keyword, and concept information in rbind() calls on the
    ## columns.  This is *much* more efficient than building
    ## incrementally.
    dbMat <- vector("list", length(packages_in_hsearch_db) * 4L)
    dim(dbMat) <- c(length(packages_in_hsearch_db), 4L)

        ## Empty hsearch index:
        hDB0 <- tools:::.build_hsearch_index(NULL)

    for(p in packages_in_hsearch_db) {
            if(incr && np %% incr == 0L) {
                message(".", appendLF = FALSE, domain = NA)
                flush.console()
            }
        np <- np + 1L
            if(verbose && WINDOWS) setWinProgressBar(pb, np)
        if(verbose >= 2L) {
        message(" ", p, appendLF = ((np %% 5L) == 0L), domain=NA)
                flush.console()
            }
            path <- if(!is.null(package_paths)) package_paths[p]
        else find.package(p, lib.loc, quiet = TRUE)
        if(length(path) == 0L) {
                if(is.null(package)) next
        else stop(packageNotFoundError(p, lib.loc, sys.call()))
            }
        ## Hsearch 'Meta/hsearch.rds' indices were introduced in
        ## R 1.8.0.  If they are missing, we really cannot use
        ## the package (as library() will refuse to load it).
        ## We always load hsearch.rds to establish the format,
        ## sometimes vignette.rds.

            hDB <- NULL
            if(want_type_help) {
                if(file.exists(hs_file <-
                    file.path(path, "Meta", "hsearch.rds"))) {
                    hDB <- readRDS(hs_file)
                    if(!is.null(hDB)) {
                        ## Fill up possibly missing information.
                        if(is.na(match("Encoding", colnames(hDB[[1L]]))))
                            hDB[[1L]] <- cbind(hDB[[1L]], Encoding = "")
                        ## <FIXME>
                        ## Transition fro old-style to new-style colnames.
                        ## Remove eventually.
                        for(i in seq_along(hDB)) {
                            colnames(hDB[[i]]) <-
                                tools:::hsearch_index_colnames[[i]]
                        }
                        ## </FIXME>
                    } else if(verbose >= 2L) {
                        message(gettextf("package %s has empty hsearch data - strangely",
                                         sQuote(p)),
                                domain = NA)
                        flush.console()
                    }
                } else if(!is.null(package))
                      warning("no hsearch.rds meta data for package ", p,
                              domain = NA)
            }
            if(is.null(hDB))
                hDB <- hDB0
            nh <- NROW(hDB[[1L]])
            hDB[[1L]] <- cbind(hDB[[1L]], Type = rep.int("help", nh))
            if(nh)
                hDB[[1L]][, "LibPath"] <- path
            if(want_type_vignette)
                hDB <- merge_vignette_index(hDB, path, p)
            if(want_type_demo)
                hDB <- merge_demo_index(hDB, path, p)
            ## Put the hsearch index for the np-th package into the
            ## np-th row of the matrix used for aggregating.
            dbMat[np, seq_along(hDB)] <- hDB
    }

    if(verbose >= 2L)  {
        message(ifelse(np %% 5L == 0L, "\n", "\n\n"),
                    sprintf("Built dbMat[%d,%d]", nrow(dbMat), ncol(dbMat)),
                    domain = NA)
            flush.console()
            ## DEBUG save(dbMat, file="~/R/hsearch_dbMat.rda", compress=TRUE)
        }

    ## Create the global base, aliases, keywords and concepts tables
    ## via calls to rbind() on the columns of the matrix used for
    ## aggregating.
    db <- list(Base     = do.call(rbind, dbMat[, 1]),
           Aliases  = do.call(rbind, dbMat[, 2]),
           Keywords = do.call(rbind, dbMat[, 3]),
           Concepts = do.call(rbind, dbMat[, 4]))
        rownames(db$Base) <- NULL
        ## <FIXME>
        ## Remove eventually ...
    if(is.null(db$Concepts)) {
        db$Concepts <-
                matrix(character(), ncol = 3L,
                       dimnames =
                           list(NULL,
                                tools:::hsearch_index_colnames$Concepts))
        }
        ## </FIXME>

        ## Make the IDs globally unique by prefixing them with the
    ## number of the package in the global index.
    for(i in which(vapply(db, NROW, 0L) > 0L)) {
        db[[i]][, "ID"] <-
        paste(rep.int(seq_along(packages_in_hsearch_db),
                  vapply(dbMat[, i], NROW, 0L)),
              db[[i]][, "ID"],
              sep = "/")
    }
    ## And maybe re-encode ...
    if(!identical(Sys.getlocale("LC_CTYPE"), "C")) {
        if(verbose >= 2L) {
                message("reencoding ...", appendLF = FALSE, domain = NA)
                flush.console()
            }
        encoding <- db$Base[, "Encoding"]
            target <- ifelse(use_UTF8 && !l10n_info()$`UTF-8`, "UTF-8", "")
        ## As iconv is not vectorized in the 'from' argument, loop
        ## over groups of identical encodings.
        for(enc in unique(encoding)) {
                if(enc != target) next
        IDs <- db$Base[encoding == enc, "ID"]
        for(i in seq_along(db)) {
            ind <- db[[i]][, "ID"] %in% IDs
            db[[i]][ind, ] <- iconv(db[[i]][ind, ], enc, "")
        }
        }
        if(verbose >= 2L) {
                message(" ", "done", domain = NA)
                flush.console()
            }
    }
    bad_IDs <-
        unlist(lapply(db,
              function(u)
                              u[rowSums(is.na(nchar(u, "chars",
                                                    allowNA = TRUE,
                                                    keepNA = FALSE))) > 0,
                                "ID"]))
        ## FIXME: drop this fallback
    if(length(bad_IDs)) {           # try latin1
            for(i in seq_along(db)) {
                ind <- db[[i]][, "ID"] %in% bad_IDs
                db[[i]][ind, ] <- iconv(db[[i]][ind, ], "latin1", "")
            }
            bad_IDs <-
                unlist(lapply(db,
                              function(u)
                                  u[rowSums(is.na(nchar(u, "chars",
                                                        allowNA = TRUE,
                                                        keepNA = FALSE))) > 0,
                                    "ID"]))
        }
    ## If there are any invalid multi-byte character data
    ## left, we simple remove all Rd objects with at least one
    ## invalid entry, and warn.
        if(length(bad_IDs)) {
        warning("removing all entries with invalid multi-byte character data")
        for(i in seq_along(db)) {
        ind <- db[[i]][, "ID"] %in% bad_IDs
        db[[i]] <- db[[i]][!ind, , drop = FALSE]
        }
    }

        ## Drop entries without topic as these cannot be accessed.
        ## (These come from help pages without \alias.)
        bad_IDs <- db$Base[is.na(db$Base[, "Topic"]), "ID"]
        if(length(bad_IDs)) {
        for(i in seq_along(db)) {
        ind <- db[[i]][, "ID"] %in% bad_IDs
        db[[i]] <- db[[i]][!ind, , drop = FALSE]
        }
    }

        ## Remove keywords which are empty.
        ind <- nzchar(db$Keywords[, "Keyword"])
        db$Keywords <- db$Keywords[ind, , drop = FALSE]
        ## Remove concepts which are empty.
        ind <- nzchar(db$Concepts[, "Concept"])
        db$Concepts <- db$Concepts[ind, , drop = FALSE]

        ## Map non-standard keywords to concepts, and use the
        ## descriptions of the standard keywords as concepts, with the
        ## exception of keyword 'internal'.
        standard <- .get_standard_Rd_keywords_with_descriptions()
        keywords <- standard$Keywords
        concepts <- standard$Descriptions
        pos <- match(db$Keywords[, "Keyword"], keywords)
        ind <- !is.na(pos) & (keywords[pos] != "internal")
        db$Concepts <-
            rbind(db$Concepts,
                  db$Keywords[is.na(pos), , drop = FALSE],
                  cbind(concepts[pos[ind]],
                        db$Keywords[ind, -1L, drop = FALSE]))
        db$Keywords <- db$Keywords[!is.na(pos), , drop = FALSE]

        ## Doing this earlier will not work: in particular, re-encoding
        ## is written for character matrices.
        db <- lapply(db, as.data.frame,
                     stringsAsFactors = FALSE, row.names = NULL)

        if(verbose >= 2L) {
            message("saving the database ...", appendLF = FALSE, domain = NA)
            flush.console()
        }
        attr(db, "LibPaths") <- lib.loc
        attr(db, "mtime") <- Sys.time()
        attr(db, "ctype") <- Sys.getlocale("LC_CTYPE")
        attr(db, "Types") <- unique(c("help", types))
        class(db) <- "hsearch_db"
        .hsearch_db(db)
        if(verbose >= 2L) {
            message(" ", "done", domain = NA)
            flush.console()
        }
        if(verbose > 0L) {
            message("... database rebuilt", domain = NA)
            if(WINDOWS) {
                close(pb)
                on.exit()               # clear closing of progress bar
            }
            flush.console()
        }
    }

    db
}

## Cf. tools:::.get_standard_Rd_keywords().
.get_standard_Rd_keywords_with_descriptions <-
function()
{
    lines <- readLines(file.path(R.home("doc"), "KEYWORDS.db"))
    ## Strip top-level entries.
    lines <- grep("^.*\\|([^:]*):.*", lines, value = TRUE)
    ## Strip comments.
    lines <- sub("[[:space:]]*#.*", "", lines)
    list(Keywords = sub("^.*\\|([^:]*):.*", "\\1", lines),
         Descriptions = sub(".*:[[:space:]]*", "", lines))
}

## This extra indirection allows the Mac GUI to replace this
## yet call the printhsearchInternal function.
print.hsearch <-
function(x, ...)
    printhsearchInternal(x, ...)

printhsearchInternal <-
function(x, ...)
{
    help_type <- getOption("help_type", default = "text")
    types <- x$types
    if (help_type == "html") {
        browser <- getOption("browser")
        port <- tools::startDynamicHelp(NA)
    if (port > 0L) {
            tools:::.httpd_objects(port, x)
            url <- sprintf("http://127.0.0.1:%d/doc/html/Search?objects=1&port=%d",
                           port, port)
            ## <NOTE>
            ## Older versions used the following, which invokes the
            ## dynamic HTML help system in a way that this calls
            ## help.search() to give the results to be displayed.
            ## This is now avoided by passing the (already available)
            ## results to the dynamic help system using the dynamic
            ## variable .httpd_objects().
        ## url <-
            ##     paste0("http://127.0.0.1:", port,
            ##            "/doc/html/Search?pattern=",
            ##            tools:::escapeAmpersand(x$pattern),
            ##            paste0("&fields.", x$fields, "=1",
            ##                   collapse = ""),
            ##            if (!is.null(x$agrep)) paste0("&agrep=", x$agrep),
            ##            if (!x$ignore.case) "&ignore.case=0",
            ##            if (!identical(types,
            ##                           getOption("help.search.types")))
            ##                paste0("&types.", types, "=1",
            ##                       collapse = ""),
            ##            if (!is.null(x$package))
            ##                paste0("&package=",
            ##                       paste(x$package, collapse=";")),
            ##            if (!identical(x$lib.loc, .libPaths()))
            ##                paste0("&lib.loc=",
            ##                       paste(x$lib.loc, collapse=";"))
            ##            )
            ## </NOTE>
            browseURL(url, browser)
            return(invisible(x))
        }
    }
    hfields <- paste(x$fields, collapse = " or ")
    vfieldnames <-
        c(alias = "name", concept = "keyword", keyword = NA,
          name = "name", title = "title")
    vfieldnames <- vfieldnames[x$fields]
    vfields <- paste(unique(vfieldnames[!is.na(vfieldnames)]),
                     collapse = " or ")
    dfieldnames <-
        c(alias = "name", concept = NA, keyword = NA,
          name = "name", title = "title")
    dfieldnames <- dfieldnames[x$fields]
    dfields <- paste(unique(dfieldnames[!is.na(dfieldnames)]),
                     collapse = " or ")
    fields_used <-
        list(help = hfields, vignette = vfields, demo = dfields)
    matchtype <- switch(x$type, fuzzy = "fuzzy", "regular expression")
    typenames <-
        c(vignette = "Vignettes", help = "Help files", demo = "Demos")
    fields_for_match_details <-
        list(help = c("alias", "concept", "keyword"),
             vignette = c("concept"),
             demo = character())
    field_names_for_details <-
        c(alias = "Aliases", concept = "Concepts", keyword = "Keywords")

    db <- x$matches
    if(NROW(db) == 0) {
        typenames <- paste(tolower(typenames[types]), collapse= " or ")
    writeLines(strwrap(paste("No", typenames,
                                 "found with", fields_used$help,
                 "matching", sQuote(x$pattern),
                 "using", matchtype,
                                 "matching.")))
        return(invisible(x))
    }

    outFile <- tempfile()
    outConn <- file(outFile, open = "w")
    typeinstruct <-
        c(vignette =
              paste("Type 'vignette(PKG::FOO)' to",
                    "inspect entries 'PKG::FOO'."),
          help =
              paste("Type '?PKG::FOO' to",
                    "inspect entries 'PKG::FOO',",
                    "or 'TYPE?PKG::FOO' for entries like",
                    "'PKG::FOO-TYPE'."),
          demo =
              paste("Type 'demo(PKG::FOO)' to",
                    "run demonstration 'PKG::FOO'."))

    for(type in types) {
    if(NROW(dbtemp <- db[db[, "Type"] == type, , drop = FALSE]) > 0) {
        writeLines(c(strwrap(paste(typenames[type], "with",
                                       fields_used[[type]], "matching",
                                       sQuote(x$pattern), "using",
                                       matchtype, "matching:")),
             "\n"),
               outConn)
            top <- dbtemp[ , "Topic"]
            ind <- (top != make.names(top))
            if(type == "help")
                ind <- ind & !grepl("-(class|method|package)$", top)
            top[ind] <- paste0("`", top[ind], "`")
            fields <- fields_for_match_details[[type]]
            chunks <- split.data.frame(dbtemp,
                                       paste0(dbtemp[, "Package"],
                                              "::",
                                              top))
            nms <- names(chunks)
            for(i in seq_along(nms)) {
                chunk <- chunks[[i]]
                writeLines(formatDL(nms[i], chunk[1L, "Title"]),
                           outConn)
                matches <- Filter(length,
                                  split(chunk[, "Entry"],
                                        chunk[, "Field"])[fields])
                if(length(matches)) {
                    tags <- field_names_for_details[names(matches)]
                    vals <- vapply(matches, paste, "", collapse = ", ")
                    writeLines(strwrap(paste0(tags, ": ", vals),
                                       indent = 2L, exdent = 4L),
                               outConn)
                }
            }
        writeLines(c("\n",
             strwrap(typeinstruct[type]),
             "\n\n"),
               outConn)
    }
    }
    close(outConn)
    file.show(outFile, delete.file = TRUE)
    invisible(x)
}

hsearch_db_concepts <-
function(db = hsearch_db())
{
    ## <NOTE>
    ## This should perhaps get an ignore.case = TRUE argument.
    ## </NOTE>
    pos <- match(db$Concepts[, "ID"], db$Base[, "ID"])
    entries <- split(as.data.frame(db$Base[pos, ],
                                   stringsAsFactors = FALSE),
                     db$Concepts[, "Concept"])
    enums <- vapply(entries, NROW, 0L)
    pnums <- vapply(entries, function(e) length(unique(e$Package)), 0L)
    pos <- order(enums, pnums, decreasing = TRUE)
    list2DF(list(Concept = names(entries)[pos],
                 Frequency = enums[pos],
                 Packages = pnums[pos]))
}

hsearch_db_keywords <-
function(db = hsearch_db())
{
    pos <- match(db$Keywords[, "ID"], db$Base[, "ID"])
    entries <- split(as.data.frame(db$Base[pos, ],
                                   stringsAsFactors = FALSE),
                     db$Keywords[, "Keyword"])
    enums <- vapply(entries, NROW, 0L)
    pnums <- vapply(entries, function(e) length(unique(e$Package)), 0L)
    standard <- .get_standard_Rd_keywords_with_descriptions()
    concepts <- standard$Descriptions[match(names(entries),
                                            standard$Keywords)]
    pos <- order(enums, pnums, decreasing = TRUE)
    list2DF(list(Keyword = names(entries)[pos],
                 Concept = concepts[pos],
                 Frequency = enums[pos],
                 Packages = pnums[pos]))
}

print.hsearch_db <-
function(x, ...)
{
    writeLines(c("A help search database:",
                 sprintf("Objects: %d, Aliases: %d, Keywords: %d, Concepts: %d",
                         NROW(x$Base),
                         NROW(x$Aliases),
                         NROW(x$Keywords),
                         NROW(x$Concepts))))
    invisible(x)
}