Macros turn an interactive session into a replayable YAML recipe. This vignette covers the full macro lifecycle: save, load, replay, parameterise, batch-process, diff, and snapshot-test.
Every step you run on a session is silently recorded. Call
save_macro() when you are done:
library(macrox)
sess <- mx_session("report_2024.pdf")
sess |> select_table("monthly", page = 8, method = "bbox")
sess |> rename_columns("monthly", c(Month = "month", Total = "total"))
sess |> cast_types("monthly", c(total = "integer"))
sess |> filter_rows("monthly", exclude_where = "month == 'Total'")
sess |> save_macro("my_report")
# Writes my_report.yml to the working directoryNext year — one call replays every step:
tables <- mx_replay("report_2025.pdf", macro = "my_report")
tables$monthly # clean data frame, ready to usemy_report.yml looks like this:
macro:
name: my_report
created: '2024-11-01 14:22'
source: report_2024.pdf
n_steps: 4
steps:
- step: select_table
label: monthly
page: 8
method: bbox
- step: rename_columns
label: monthly
mapping:
Month: month
Total: total
- step: cast_types
label: monthly
cols:
total: integer
- step: filter_rows
label: monthly
exclude_where: month == 'Total'The YAML is plain text — commit it to version control, review diffs, and edit it by hand when needed.
When the same macro needs to adapt to a value that changes each year
— a page number, a year filter, a sheet name — use
$parameter placeholders instead of hard-coding the
value.
sess |> select_table("annual",
page = 8,
method = "bbox",
filter_value = "$year" # placeholder
)
sess |> filter_rows("annual", exclude_where = "year_col != '$year'")
sess |> save_macro("annual_report",
params = c(year = "integer") # declare with optional type annotation
# or just: params = "year" # bare name, no type
)The macro YAML gains a params: header:
Supply values at replay time — macrox substitutes every
$year occurrence before dispatching any steps:
Multiple parameters work the same way:
$ in step
fields.$year_end is substituted before $year so there
are no prefix collisions.$name strings and a warning is issued at replay.Process a folder of reports in one call:
files <- list.files("reports/", pattern = "\\.pdf$", full.names = TRUE)
results <- mx_replay_batch(files, macro = "my_report")
# results is a named list: one table list per fileShow a progress bar and run in parallel (requires
mirai):
results <- mx_replay_batch(files,
macro = "my_report",
.progress = TRUE,
.parallel = TRUE # uses mirai for parallel execution
)Parameterised macros work in batch too — pass a named list:
diff_replay() replays the same macro against two files
and returns a structured diff:
d <- diff_replay(
file1 = "report_2024.pdf",
file2 = "report_2025.pdf",
macro = "my_report"
)
print(d)
# ~ monthly [changed] (rows: 12→13, 4 cell changes)
# total Δ mean=123.4 max=210.0 sum=1234.5 (4 rows)Drill into a specific table’s cell-level changes:
test_macro() is designed for use inside
testthat blocks. On the first call it writes a YAML
snapshot; subsequent calls compare against it and fail descriptively if
output drifts.
test_macro() locally with your reference PDF. The
snapshot is written to
tests/testthat/macro-snapshots/.tables. test_macro() compares and fails on any
drift.# tests/testthat/test-my-report.R
test_that("my_report extracts correctly", {
test_macro("report_2024.pdf", macro = "my_report")
})The snapshot file
tests/testthat/macro-snapshots/my_report.snap.yml records
row counts, column names, and the first 5 rows of each table.
Useful in CI when you don’t want to keep a PDF in the repository:
test_that("annual_report with year=2024 matches snapshot", {
test_macro("report_2024.pdf",
macro = "annual_report",
params = list(year = 2024L),
name = "annual_report_2024"
)
})Use a unique name per parameter set so snapshots don’t
overwrite each other.
When the report genuinely changes and you want to update the snapshot:
The snapshot YAML records per table:
| Field | Description |
|---|---|
nrow |
Total row count |
ncol |
Column count |
cols |
Column names |
head |
First n_head rows (default 5), stored as character
strings |
In .github/workflows/R-CMD-check.yaml, no extra steps
are needed — test_macro() reads snapshots committed to the
repo. If you use a local PDF that shouldn’t be checked in, supply
pre-replayed tables instead.
To regenerate snapshots from CI, add a step that sets
TESTTHAT_SNAPSHOT_UPDATE=true and commits the result, or
use testthat::snapshot_accept() locally after reviewing the
diff.
validate_macro() checks that a macro YAML is well-formed
and that all referenced step types are known before attempting a
replay:
validate_macro("my_report")
# v my_report.yml: 4 steps, valid
# x quarterly_report.yml: unknown step 'slect_table' at position 1Use this in pre-commit hooks or CI to catch typos in hand-edited macros before they reach production.