Developer’s Guide to qrtdown

Internal architecture, code flow, and maintenance guide for qrtdown contributors

Overview

qrtdown is a modular Quarto-based documentation system for R packages. This guide explains the internal architecture for developers who want to understand, maintain, or extend the package.

Total codebase: ~4,100 lines across 11 R source files.

File Architecture

R/
├── build.R           (745 lines)  # Main orchestration & CLI
├── config.R          (426 lines)  # Configuration system
├── utils.R           (207 lines)  # Core utilities
├── build-home.R      (244 lines)  # Homepage generation
├── build-reference.R (374 lines)  # Function reference pages
├── build-articles.R  (258 lines)  # Article processing
├── build-news.R      (111 lines)  # Changelog handling
├── build-citation.R   (94 lines)  # Citation page
├── build-llm.R       (323 lines)  # LLM-optimized docs
├── rd-to-qmd.R       (432 lines)  # Rd → Quarto conversion
└── usethis.R         (928 lines)  # Setup & maintenance functions

Core Build Pipeline

The main entry point is build_site(). Here’s the complete execution flow:

build_site()
    │
    ├──► find_package_root()    # Locate DESCRIPTION
    ├──► pkg_meta()             # Extract package metadata
    ├──► get_config()           # Merge user + default config
    │       │
    │       ├──► read_config()        # Read _qrtdown.yml
    │       └──► default_config()     # Generate defaults
    │               ├──► has_articles_dir()
    │               ├──► has_citation_file()
    │               ├──► get_main_article()
    │               └──► get_articles_menu()
    │
    ├──► init_site()            # Setup site structure
    │       ├──► write_quarto_yml()
    │       ├──► update_gitignore()
    │       └──► update_rbuildignore()
    │
    ├──► build_home()           # README → index.qmd (with include)
    ├──► build_reference()      # man/*.Rd → _qrtdown/reference/*.qmd
    ├──► build_articles()       # vignettes/articles/ → _qrtdown/articles.qmd
    ├──► build_news()           # NEWS.md handling
    ├──► build_citation()       # CITATION → _qrtdown/citation.qmd
    │
    └──► run_quarto(["render"]) # Execute Quarto
            │
            └──► [auto-retry on cache failure]

Module Dependencies

                    ┌─────────────┐
                    │  build.R    │ ← Main entry point
                    │ build_site  │
                    └──────┬──────┘
                           │
       ┌───────────────────┼───────────────────┐
       │                   │                   │
       ▼                   ▼                   ▼
┌─────────────┐    ┌─────────────┐    ┌─────────────┐
│  config.R   │    │   utils.R   │    │  usethis.R  │
│ get_config  │◄───│ pkg_meta    │───►│use_qrtdown  │
│ defaults    │    │ run_quarto  │    │deploy_site  │
└──────┬──────┘    └──────┬──────┘    └─────────────┘
       │                  │
       │    ┌─────────────┼─────────────┐
       │    │             │             │
       ▼    ▼             ▼             ▼
┌───────────────┐ ┌───────────────┐ ┌───────────────┐
│ build-home.R  │ │build-reference│ │build-articles │
│   README →    │ │   man/*.Rd   │ │  vignettes/   │
│   index.qmd   │ │      →       │ │   articles/   │
└───────────────┘ │ reference/   │ └───────────────┘
                  └───────┬───────┘
                          │
                          ▼
                  ┌───────────────┐
                  │  rd-to-qmd.R  │
                  │  Rd parsing   │
                  │  & conversion │
                  └───────────────┘

Key Subsystems

1. Configuration System (config.R)

The configuration system uses a merge strategy: user settings in _qrtdown.yml override intelligent defaults generated from package metadata.

User Config (_qrtdown.yml)
         │
         ▼
    ┌─────────┐
    │ merge() │ ◄── Default Config (from pkg_meta)
    └────┬────┘
         │
         ▼
   Merged Config
         │
         ├──► generate_quarto_yml() → _quarto.yml
         └──► Used by all build_*() functions

Critical functions:

Function Purpose
get_config() Main entry: returns merged config
default_config() Generates defaults from DESCRIPTION
generate_quarto_yml() Creates Quarto-compatible YAML
get_articles_menu() Dynamically builds Articles dropdown

What to watch for:

  • Navbar is dynamically populated based on detected content
  • Articles, Citation, and Changelog items are injected even if not in user config
  • YAML boolean handling: Quarto 1.2+ requires "true"/"false" strings

2. Rd Conversion System (rd-to-qmd.R)

Converts roxygen2 .Rd files to Quarto markdown. Uses two parsing strategies:

                    .Rd File
                        │
                        ▼
              ┌─────────────────┐
              │ rd_to_md_via_   │ ← Primary strategy
              │     html()      │
              └────────┬────────┘
                       │
        ┌──────────────┴──────────────┐
        │                             │
        ▼                             ▼
┌───────────────┐            ┌───────────────┐
│ tools::Rd2HTML│            │ Direct Rd     │
│       ↓       │            │ Parsing       │
│ HTML output   │            │ (fallback)    │
│       ↓       │            └───────────────┘
│ extract_html_ │
│   section()   │
│       ↓       │
│ html_to_      │
│  markdown()   │
└───────────────┘
        │
        ▼
  Quarto Markdown

Rd Markup Conversion Table:

Rd Markup Markdown Output
\code{x} `x`
\emph{x} *x*
\strong{x} **x**
\link[pkg]{fn} [fn]()
\url{...} <...>
\dontrun{...} # Not run:\n...
\donttest{...} (unwrapped)

What to watch for:

  • HTML parsing is more robust but may fail on unusual Rd
  • Direct Rd parsing is the fallback
  • HTML entity unescaping is critical (&lt;<)
  • Zero-width spaces must be removed

3. Reference Builder (build-reference.R)

Generates individual function pages and an index.

man/*.Rd files
      │
      ▼
┌─────────────────────────────────────────────────┐
│ FOR EACH .Rd file:                              │
│   ├──► parse_rd_file()                          │
│   │       └──► rd_to_md_via_html()              │
│   ├──► find_function_source() → source link    │
│   └──► build_function_page() → reference/fn.qmd│
└─────────────────────────────────────────────────┘
      │
      ▼
build_reference_index()
      │
      ├──► Custom grouping via match_pattern()
      │       ├── starts_with("prefix")
      │       ├── ends_with("suffix")
      │       ├── contains("text")
      │       ├── matches("regex")
      │       └── exact function name
      │
      └──► _qrtdown/reference.qmd (index page)

Output structure:

_qrtdown/
├── reference.qmd           # Index with all functions
└── reference/
    ├── build_site.qmd
    ├── use_qrtdown.qmd
    ├── deploy_site.qmd
    └── ... (one file per function)

Source link generation:

The find_function_source() function locates function definitions and generates repository-appropriate URLs:

  • GitHub: https://github.com/user/repo/blob/HEAD/R/file.R#L42
  • GitLab: https://gitlab.com/user/repo/-/blob/main/R/file.R#L42
  • Codeberg: https://codeberg.org/user/repo/src/branch/main/R/file.R#L42

4. Article System (build-articles.R)

Articles live in vignettes/articles/ (NOT vignettes/).

vignettes/articles/
├── getting-started.qmd   ← "Get started" tab (special)
├── tutorial.qmd
└── advanced.qmd

Main article detection priority:

  1. {packagename}.qmd
  2. getting-started.qmd
  3. index.qmd
  4. First article found

The main article appears as a “Get started” navbar tab; others go in the “Articles” dropdown.

What to watch for:

  • Users often put articles in vignettes/ instead of vignettes/articles/
  • The distinction prevents R CMD check warnings
  • Underscore-prefixed files (_draft.qmd) are excluded

5. usethis Functions (usethis.R)

User-facing setup and maintenance functions.

Version Bump Workflow (use_qrtdown_version()):

use_qrtdown_version("patch")
         │
         ├──► Update DESCRIPTION (via desc package)
         ├──► Add heading to NEWS.md
         ├──► Update navbar title in _qrtdown.yml
         ├──► Regenerate _quarto.yml
         └──► Clear .quarto cache  ← CRITICAL!

Why cache clearing matters: Quarto caches site chrome (navbar, footer). Without clearing, version changes don’t appear until a full rebuild.

Deployment Workflow (deploy_site()):

deploy_site()
     │
     ├──► git subtree push (preferred)
     │         │
     │         ▼
     │    pages branch updated
     │
     └──► [on failure] git subtree split + force push

Error Handling & Edge Cases

Quarto Cache Issues

Symptom: “No such file or directory” during rename operations

Cause: Quarto’s internal cache out of sync

Solution implemented in build_site():

# Auto-retry logic
if (render_fails && "rename" in error) {
  unlink(".quarto", recursive = TRUE)
  retry_render()
}

Articles Not Appearing

Symptom: Articles dropdown missing despite having .qmd files

Cause: Articles in vignettes/ instead of vignettes/articles/

Detection: qrtdown_sitrep() checks for this condition

Testing Strategy

tests/testthat/
├── test-build-home.R      # Home page generation
├── test-build-articles.R  # Article processing
├── test-config.R          # Configuration merging
├── test-quarto-yml.R      # YAML generation
├── test-utils.R           # Utility functions
├── test-usethis.R         # Setup functions
└── helper.R               # Test fixtures

Test helpers in helper.R:

Helper Purpose
create_test_package() Minimal package with DESCRIPTION
create_test_package_with_articles() Add articles dir
create_test_package_with_citation() Add CITATION file
cleanup_test_package() Teardown

Running tests:

devtools::test()

Adding New Features

Adding a New Builder

  1. Create R/build-{feature}.R
  2. Export main function: build_{feature}(pkg, quiet = FALSE)
  3. Add call to build_site() in build.R
  4. Update default navbar in config.R if needed
  5. Add tests in tests/testthat/test-build-{feature}.R

Adding Configuration Options

  1. Add default in default_config() (config.R)
  2. Document in _qrtdown.yml examples
  3. Update generate_quarto_yml() if Quarto-facing
  4. Test config merging behavior

Adding a usethis Function

  1. Add function in usethis.R
  2. Follow naming: use_qrtdown_{feature}()
  3. Use cli package for user messages
  4. Export in NAMESPACE via roxygen2
  5. Add template files to inst/templates/ if needed

Diagnostic Tools

qrtdown_sitrep()

Comprehensive diagnostic that checks:

  • Package installation and version mismatches
  • Quarto installation(s) and versions
  • Configuration files existence and validity
  • Package content (man files, articles, citations)
  • Navbar configuration (auto vs manual)
  • CLI installation status
  • Build settings (freeze, cache)
  • Deployment setup (CI workflows)

quarto_info()

Returns detailed Quarto installation info:

quarto_info()
#> $path
#> [1] "/usr/local/bin/quarto"
#> $version
#> [1] "1.4.550"

Package Dependencies

Critical imports:

Package Used For
cli User-facing messages
desc DESCRIPTION file handling
fs File system operations
yaml YAML read/write
tools Rd parsing (parse_Rd, Rd2HTML)
glue String interpolation
purrr Functional programming

Optional (Suggests):

Package Used For
quarto preview_site() R interface
docopt CLI argument parsing
usethis Some setup utilities

Common Maintenance Tasks

Updating Quarto Compatibility

If Quarto changes output format:

  1. Check rd_to_md_via_html() HTML extraction patterns
  2. Update generate_quarto_yml() for new YAML syntax
  3. Test with multiple Quarto versions

Adding New Rd Tag Support

  1. Add pattern in clean_rd_text() or html_to_markdown()
  2. Handle in extract_rd_section()
  3. Test with an Rd file using that tag

Updating CI Templates

Templates are in inst/templates/: - github-action.yml - GitHub Actions workflow - woodpecker.yml - Woodpecker CI workflow

Update the R/Quarto installation steps as needed.

Architectural Decisions

Decision Rationale
Generated files in _qrtdown/ Keeps package root clean, clear separation
index.qmd uses include directive README.qmd stays untouched, sidebar added
Articles in vignettes/articles/ Prevents R CMD check warnings
HTML intermediate for Rd parsing More robust than direct Rd parsing
Git subtree for deployment Simple, works with any Git host
Individual function pages Better SEO, cross-linking support
Config merging (not replacement) Sensible defaults with customization
Cache clearing on version bump Prevents stale navbar issues

Debugging Tips

  1. Build failures: Run qrtdown_sitrep() first
  2. Cache issues: clean_site() then rebuild
  3. Rd conversion: Test with rd_to_qmd(path, output) directly
  4. Config issues: Compare get_config() output with expectations
  5. Quarto issues: Check quarto_info() for path conflicts

Quick Reference: Function Locations

Function File Line (approx)
build_site() build.R 1-150
init_site() build.R 150-250
qrtdown_sitrep() build.R 400-700
get_config() config.R 200-250
default_config() config.R 50-150
generate_quarto_yml() config.R 250-400
build_home() build-home.R 1-100
build_reference() build-reference.R 1-150
rd_to_md_via_html() rd-to-qmd.R 200-350
use_qrtdown_version() usethis.R 600-700
deploy_site() usethis.R 750-900