diff --git a/DESCRIPTION b/DESCRIPTION index 26d57b9..079f603 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -15,8 +15,9 @@ Depends: LazyData: true URL: https://github.com/igraph/igraph.r2cdocs BugReports: https://github.com/igraph/igraph.r2cdocs/issues -Imports: +Imports: rlang, roxygen2, treesitter, - treesitter.r + treesitter.r, + yaml diff --git a/R/roxygen.R b/R/roxygen.R index 9999087..7471fce 100644 --- a/R/roxygen.R +++ b/R/roxygen.R @@ -17,6 +17,7 @@ roclet_process.roclet_docs_rd <- function(x, blocks, env, base_path) { roclet_output.roclet_docs_rd <- function(x, results, base_path, ...) { results <- lapply_with_names(results, add_cdocs_section, base_path) + update_pkgdown(results, base_path) NextMethod() } @@ -228,3 +229,156 @@ handle_dt <- function(dt) { url = url ) } + +update_pkgdown <- function(results, base_path) { + categories_path <- file.path( + base_path, + "src/vendor/cigraph/interfaces/functions-categories.yaml" + ) + + categories <- yaml::read_yaml(categories_path) + + # Build call graph once (memoized) + ts_graph <- treesitter_graph(base_path) + vertex_names <- igraph::V(ts_graph)$name + + # Map each Rd topic to its C category (or "other" if not mappable) + topic_categories <- list() + + for (rd_name in names(results)) { + topic <- results[[rd_name]] + + # Skip internal topics and package/data doc pages + keywords <- topic$get_value("keyword") + if ("internal" %in% keywords) next + doc_type <- topic$get_value("docType") + if (length(doc_type) > 0 && doc_type %in% c("package", "data")) next + + # Use the primary alias (\name{} in Rd) not the file name, since @rdname + # can cause the file to be named differently from the actual function. + r_name <- topic$get_value("alias")[[1]] + aliases <- topic$get_value("alias") + + # Use only direct neighbors (distance 1) to identify the primary C function, + # rather than the full transitive closure used for Rd link sections. + direct_impls <- purrr::map(aliases, function(alias) { + v <- which(vertex_names == alias) + if (length(v) == 0) return(character(0)) + nb <- names(igraph::neighbors(ts_graph, v, mode = "out")) + nb[endsWith(nb, "_impl")] + }) |> unlist() + + if (length(direct_impls) == 0) { + topic_categories[[r_name]] <- list(category = "other", subcategory = NA_character_) + next + } + + c_funcs <- sprintf("igraph_%s", sub("_impl$", "", direct_impls)) + + cats <- purrr::map_chr(c_funcs, function(f) { + entry <- categories[[f]] + if (!is.null(entry)) entry[["category"]] else NA_character_ + }) + cats <- unique(cats[!is.na(cats)]) + + if (length(cats) == 0) { + topic_categories[[r_name]] <- list(category = "other", subcategory = NA_character_) + next + } + + subcats <- purrr::map_chr(c_funcs, function(f) { + entry <- categories[[f]] + if (!is.null(entry)) entry[["subcategory"]] %||% NA_character_ else NA_character_ + }) + subcats <- unique(subcats[!is.na(subcats)]) + + topic_categories[[r_name]] <- list( + category = cats[[1]], + subcategory = if (length(subcats) > 0) subcats[[1]] else NA_character_ + ) + } + + if (length(topic_categories) == 0) { + cli::cli_warn( + "No R topics could be mapped to C categories; skipping {.path _pkgdown.yml} update." + ) + return(invisible(NULL)) + } + + # Build category → subcategory → [r_names] structure + by_cat <- list() + for (r_name in names(topic_categories)) { + info <- topic_categories[[r_name]] + cat <- info$category + sub <- info$subcategory %||% NA_character_ + if (is.null(by_cat[[cat]])) { + by_cat[[cat]] <- list() + } + sub_key <- if (is.na(sub)) ".nosub" else sub + by_cat[[cat]][[sub_key]] <- c(by_cat[[cat]][[sub_key]], r_name) + } + + # Generate YAML lines + start_marker <- "# BEGIN GENERATED BY igraph.r2cdocs - DO NOT EDIT" + end_marker <- "# END GENERATED BY igraph.r2cdocs" + + gen_lines <- c(start_marker, "reference:") + cat_names <- c(sort(setdiff(names(by_cat), "other")), intersect("other", names(by_cat))) + for (cat_name in cat_names) { + gen_lines <- c(gen_lines, sprintf("- title: \"%s\"", cat_name)) + subs <- by_cat[[cat_name]] + for (sub_name in sort(names(subs))) { + funcs <- sort(subs[[sub_name]]) + if (sub_name != ".nosub") { + gen_lines <- c(gen_lines, sprintf("- subtitle: \"%s\"", sub_name)) + } + gen_lines <- c(gen_lines, "- contents:", sprintf(" - %s", funcs)) + } + } + gen_lines <- c(gen_lines, end_marker) + + # Read and update _pkgdown.yml + pkgdown_path <- file.path(base_path, "_pkgdown.yml") + if (!file.exists(pkgdown_path)) { + cli::cli_warn( + "{.path _pkgdown.yml} not found at {.path {pkgdown_path}}, skipping." + ) + return(invisible(NULL)) + } + + lines <- readLines(pkgdown_path) + start_idx <- which(lines == start_marker) + end_idx <- which(lines == end_marker) + + if (length(start_idx) == 1 && length(end_idx) == 1) { + # Replace existing generated block + new_lines <- c( + lines[seq_len(start_idx - 1)], + gen_lines, + lines[seq(end_idx + 1, length(lines))] + ) + } else { + # First run: replace the existing `reference:` section. + # Find the `reference:` line and the next top-level key after it. + ref_idx <- which(lines == "reference:") + if (length(ref_idx) == 0) { + cli::cli_warn("No {.code reference:} key found in {.path _pkgdown.yml}, skipping.") + return(invisible(NULL)) + } + # The next top-level key is the first line after reference: that starts + # with a non-space character and is not a YAML list item. + after_ref <- seq(ref_idx[[1]] + 1L, length(lines)) + next_key_idx <- after_ref[grepl("^[a-zA-Z]", lines[after_ref])][[1]] + new_lines <- c( + lines[seq_len(ref_idx[[1]] - 1L)], + gen_lines, + "", + lines[seq(next_key_idx, length(lines))] + ) + } + + writeLines(new_lines, pkgdown_path) + cli::cli_inform( + "Updated {.path _pkgdown.yml}: {length(topic_categories)} R function{?s} across {length(by_cat)} categor{?y/ies}." + ) +}