Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions DESCRIPTION
Original file line number Diff line number Diff line change
Expand Up @@ -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
154 changes: 154 additions & 0 deletions R/roxygen.R
Original file line number Diff line number Diff line change
Expand Up @@ -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()
}

Expand Down Expand Up @@ -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}."
)
}