use std::path::Path;
use crate::ast::{Arg, AstNode, CallExpr, HasArgList};
use crate::parser::parse;
use crate::project::scope::package_root;
use crate::rindex::harvest::parse_dcf;
use crate::syntax::SyntaxKind;
pub fn roxygen_markdown_default(root: &Path) -> bool {
let desc = std::fs::read_to_string(root.join("DESCRIPTION"))
.ok()
.and_then(|text| roxygen_field(&text))
.and_then(|expr| markdown_from_r_text(&expr));
let meta = std::fs::read_to_string(root.join("man/roxygen/meta.R"))
.ok()
.and_then(|text| markdown_from_r_text(&text));
meta.or(desc).unwrap_or(false)
}
pub fn roxygen_markdown_default_for_file(path: &Path) -> bool {
package_root(path).is_some_and(|root| roxygen_markdown_default(&root))
}
pub fn roxygen_markdown_default_for_dir(dir: &Path) -> bool {
roxygen_markdown_default_for_file(&dir.join("_stdin_.R"))
}
#[derive(Debug, Default)]
pub struct MarkdownDefaultResolver {
by_dir: std::collections::HashMap<std::path::PathBuf, bool>,
}
impl MarkdownDefaultResolver {
pub fn new() -> Self {
Self::default()
}
pub fn resolve(&mut self, file: &Path) -> bool {
match file.parent() {
Some(dir) => *self
.by_dir
.entry(dir.to_path_buf())
.or_insert_with(|| roxygen_markdown_default_for_file(file)),
None => false,
}
}
}
fn roxygen_field(description: &str) -> Option<String> {
parse_dcf(description)
.into_iter()
.find(|(key, _)| key == "Roxygen")
.map(|(_, value)| value)
}
fn markdown_from_r_text(text: &str) -> Option<bool> {
let output = parse(text);
if !output.diagnostics.is_empty() {
return None;
}
let last = output
.cst
.children_with_tokens()
.filter(|el| {
!matches!(
el.kind(),
SyntaxKind::WHITESPACE
| SyntaxKind::NEWLINE
| SyntaxKind::COMMENT
| SyntaxKind::SEMICOLON
)
})
.last()?;
let last = CallExpr::cast(last.into_node()?)?;
if last.callee_name().as_deref() != Some("list") {
return None;
}
last.args()
.filter(|arg| arg.name().as_deref() == Some("markdown"))
.last()
.and_then(|arg| literal_logical(&arg))
}
fn literal_logical(arg: &Arg) -> Option<bool> {
let value = arg.value()?;
let token = value.into_token()?;
match token.text() {
"TRUE" => Some(true),
"FALSE" => Some(false),
_ => None,
}
}
#[cfg(test)]
mod tests {
use super::*;
fn package(description: &str) -> tempfile::TempDir {
let dir = tempfile::tempdir().expect("tempdir");
std::fs::create_dir(dir.path().join("R")).expect("R/");
std::fs::write(dir.path().join("R/a.R"), "NULL\n").expect("a.R");
std::fs::write(dir.path().join("DESCRIPTION"), description).expect("DESCRIPTION");
dir
}
fn write_meta(dir: &tempfile::TempDir, source: &str) {
let meta_dir = dir.path().join("man/roxygen");
std::fs::create_dir_all(&meta_dir).expect("man/roxygen/");
std::fs::write(meta_dir.join("meta.R"), source).expect("meta.R");
}
#[test]
fn markdown_true_from_roxygen_field() {
let dir = package("Package: p\nRoxygen: list(markdown = TRUE)\n");
assert!(roxygen_markdown_default(dir.path()));
}
#[test]
fn markdown_defaults_off_without_field() {
let dir = package("Package: p\n");
assert!(!roxygen_markdown_default(dir.path()));
}
#[test]
fn markdown_false_is_explicit_off() {
let dir = package("Package: p\nRoxygen: list(markdown = FALSE)\n");
assert!(!roxygen_markdown_default(dir.path()));
}
#[test]
fn field_with_more_options_and_continuation() {
let dir = package(
"Package: p\nRoxygen: list(load = \"installed\",\n markdown = TRUE)\nDepends: R\n",
);
assert!(roxygen_markdown_default(dir.path()));
}
#[test]
fn non_literal_markdown_value_is_unknown() {
let dir = package("Package: p\nRoxygen: list(markdown = flag)\n");
assert!(!roxygen_markdown_default(dir.path()));
}
#[test]
fn non_list_field_is_unknown() {
let dir = package("Package: p\nRoxygen: make_options()\n");
assert!(!roxygen_markdown_default(dir.path()));
}
#[test]
fn meta_overrides_description_on() {
let dir = package("Package: p\nRoxygen: list(markdown = FALSE)\n");
write_meta(&dir, "list(markdown = TRUE)\n");
assert!(roxygen_markdown_default(dir.path()));
}
#[test]
fn meta_overrides_description_off() {
let dir = package("Package: p\nRoxygen: list(markdown = TRUE)\n");
write_meta(&dir, "list(markdown = FALSE)\n");
assert!(!roxygen_markdown_default(dir.path()));
}
#[test]
fn meta_last_expression_wins_after_other_statements() {
let dir = package("Package: p\n");
write_meta(&dir, "x <- 1\nlist(markdown = TRUE)\n");
assert!(roxygen_markdown_default(dir.path()));
}
#[test]
fn meta_trailing_atom_is_not_the_list() {
let dir = package("Package: p\nRoxygen: list(markdown = FALSE)\n");
write_meta(&dir, "list(markdown = TRUE)\nx\n");
assert!(!roxygen_markdown_default(dir.path()));
}
#[test]
fn unresolvable_meta_defers_to_description() {
let dir = package("Package: p\nRoxygen: list(markdown = TRUE)\n");
write_meta(&dir, "build_meta()\n");
assert!(roxygen_markdown_default(dir.path()));
}
#[test]
fn meta_list_without_markdown_defers_to_description() {
let dir = package("Package: p\nRoxygen: list(markdown = TRUE)\n");
write_meta(&dir, "list(knitr_chunk_options = NULL)\n");
assert!(roxygen_markdown_default(dir.path()));
}
#[test]
fn for_file_resolves_via_package_root() {
let dir = package("Package: p\nRoxygen: list(markdown = TRUE)\n");
assert!(roxygen_markdown_default_for_file(&dir.path().join("R/a.R")));
}
#[test]
fn for_file_outside_a_package_is_off() {
let dir = tempfile::tempdir().expect("tempdir");
let loose = dir.path().join("loose.R");
std::fs::write(&loose, "NULL\n").expect("loose.R");
assert!(!roxygen_markdown_default_for_file(&loose));
}
}