toolero 0.5.0
Breaking changes
create_qmd(): both bundled templates now setformat: html: embed-resources: truerather than stating the Quarto default offalse. A non-self-contained HTML depends on the_files/sidecar directory rendered beside it, and nothing that moves these documents around knows about sidecars:submitr::htc_gen_executable()archives a folder, an emailed report is one file, and a rendered document committed next to its analysis quietly depends on a directory nobody thinks to copy. The cost oftrueis a larger file; the cost offalseis an artifact that works only on the machine that made it. Set it back in your own header if you would rather have the sidecar.create_qmd(use_purl = TRUE): the generated document’s header now declares aparamsblock with aninput_fileentry, added asparams: input_file: "data-raw/data.csv"when the template does not already declare one. The input-resolution pattern this family of packages recommends – seedetect_execution_context()andsubmitr::htc_gen_submit()– readsparams$input_filein itsquartobranch, which requires the document to declareparams:. The example template does; the skeleton deliberately does not, sinceinclude_examples = FALSEasks for a bare document.use_purl = TRUEis the user saying this document is destined to become a script, and a script is exactly the artifact that runs underRscripton an execute node, so that is where the block is earned. Aninput_filethe template oryaml_datasupplies is never overwritten, and the placeholder is meant to be edited.-
init_project(): usesrenv::scaffold()instead ofrenv::init()whenuse_renv = TRUE, no longer writes a.renvignorecontaining*.qmd, and no longer takes a snapshot at creation time.renv::init()loads the newly created project into the calling R session, repointing.libPaths()at a library that is empty apart fromrenvitself. Every package the caller had available disappears until they restart R, which surfaces later as confusingthere is no package called ...errors having nothing to do with the project just created. Therestartargument suppresses the restart, not the activation, andbare = TRUEskips dependency discovery but still loads.renv::scaffold()creates the same infrastructure –renv/library,renv/activate.R,renv/.gitignore, an.Rprofilethat activates the project in future sessions, and an initialrenv.lock– and leaves the caller’s session untouched.The
.renvignoreexcluded Quarto documents fromrenv’s dependency discovery, so a project whoselibrary()calls live in its.qmdsource – the arrangement this package recommends – could snapshot a lockfile with none of the analysis packages in it, andcontainr::generate_dockerfile()would then build an image that could not run the analysis. The file was written at a point ininit_project()where the project contained no.qmdfiles at all, so it never affected the snapshot taken at creation time; its only effect was on every snapshot the user took afterwards. Take a snapshot yourself once the project has code in it, and before containerizing. Projects created by earlier versions still carry the file and should have it removed by hand. write_by_group(): the job manifest now has one schema regardless of how many grouping columns were supplied. One column per grouping variable holding the raw value, thengroup_value,n_rows,file_path. Grouping on a single column previously produced only the last three; it now also carries the grouping column, whose value repeatsgroup_valueexactly. That redundancy is deliberate – one schema with a varying column count is easier to read, validate and rely on than two schemas selected by how many columns you happened to group on.run_by_group()andsubmitr::htc_gen_submit()readgroup_valueandfile_pathand are unaffected.write_by_group(): groups are now written, and manifest rows recorded, in order of first appearance in the data rather than in sort order of the sanitized key. Numeric groups no longer come out10, 11, 9. This is more than cosmetic:submitrwritessubdatasets.csvin manifest order, HTCondor assignsProcIdin that order, and log filenames are reconstructed from position, so manifest row order is the mapping from a job number back to a group. The caveat about iteration order has been removed from the documentation, since there is no longer anything to warn about.write_by_group(): withdrop_na = FALSE, a grouping column holding both missing values and the literal string"NA"is now an error naming the column, rather than silently merging two different groups into one file. Missing values are coerced to"NA"so they form their own group, so a column containing North America, Not Applicable, or a country code would otherwise have collapsed the two.drop_na = TRUEis unaffected: the missing rows are gone before the coercion, so no collision is possible.check_project(): the standard folder set now comes from.default_folders()and therefore includesR/. A project without anR/folder gains a"warn"row it did not have before.init_project():R/is now part of the standard folder set. The derived.Rscript belongs there, whether it comes fromqmd_to_r()or from the post-render hookcreate_qmd(use_purl = TRUE)scaffolds, andR/purl.Rwas already being created there bycreate_qmd()without the folder ever being declared.scripts/remains in the set and is now documented as the home for hand-written scripts.init_project(): every precondition is now checked before anything is created. A call that would previously create the project directory, its folders, and its branding assets before aborting on an existing README now aborts first and leaves nothing behind.init_project(): README detection at the destination is now case-insensitive and extension-agnostic, matchingcheck_project(). A project already containingreadme.txt,README, orReadme.pdfnow aborts rather than quietly acquiring a secondREADME.mdbeside it. Both functions now share one.find_readme()helper (issue #11).init_project(): when the folder set comes fromconfig, acustom_foldersremoval is honored literally and the parent folder is no longer added back.custom_folders = "-output/figures"against the built-in default set still leavesoutput/behind, since that set is a convention; against a config it does not, since a config is an explicit and complete statement of the intended structure.init_project(): aborts rather than overwriting an existing_toolero.ymlor existing files inassets/.generate_project_config(): the generated file now carriesschema_versionand aconventions:block in addition tofolders:, and is written from the same template and writer as the_toolero.ymlthatinit_project()records. Files produced by earlier versions, which carried onlyfolders:, continue to be read without change.init_project(): theuw_brandingargument is deprecated in favor of the newbrandingargument.uw_branding = TRUEmaps tobranding = "uw-madison"(notbranding = TRUE, which now means generic placeholder assets);uw_branding = FALSEmaps tobranding = "none". Alifecycle::deprecate_warn()fires whenuw_brandingis supplied. Removal planned for v0.6.0 alongsidecheck_project(error).create_qmd(): theuse_styleargument now detects branding files by standardized name (styles.css,header.html,footer.html) rather than scanning for any.cssor.htmlfile and erroring on ambiguity. Custom directories are the user’s responsibility to populate under these exact names. The old “error when multiple.cssor.htmlfiles found” behavior is removed.create_qmd():footer.htmlis now separately wired asinclude-after-body:in the generated YAML, a new Quarto YAML key not present in previous versions. Projects usinguse_style = TRUEwill now have a footer included ifassets/footer.htmlexists.init_project(): now creates aREADME.mdfile at the project root by default. The newuse_readmeargument defaults toTRUE; calls that previously created no README – which was all of them, since the argument did not exist – will now produce one. Passuse_readme = FALSEto opt out and preserve the old behavior.
New features
write_by_group(): newprefixargument, a namespace prepended to every output filename.prefix = "data"turnsa.csvintodata-a.csv, anda--female.csvintodata-a--female.csv. It is sanitized the same way group values are and joined with a single-, not the--that separates grouping columns:--is there to keep the group tuple and the filename in one-to-one correspondence, and a prefix is constant across every file in a call, so it cannot create a collision. Without a prefix, splitting on a short column produces short filenames such asa.csv, and sincesubmitr::htc_gen_submit()reduces the manifest tobasename()those land in one flat directory on the access point where two datasets split on the same column would overwrite each other.prefixis last in the signature, so adding it shifts no existing positional argument. Defaults toNULL, which leaves filenames exactly as before.check_project(): reads the project’s own_toolero.ymlwhen noconfigargument is supplied, so a customized project no longer has to be handed the same config on every call. Precedence is explicitconfig, then the project’s manifest, then the built-in standard set. A folder declared in either of the first two and missing from disk is reported as"fail"rather than"warn": a declaration that is not met is a conformance failure, whereas the standard set is a suggestion nobody signed up for. A_toolero.ymlthat exists but cannot be parsed is reported as a failing check and the audit continues against the standard set, since aborting the audit is less useful than reporting the problem (issue #12).check_project(): new_toolero.ymlcheck, reporting whether the project carries a manifest. Absence is a"warn"– a project predating v0.5.0 legitimately has none – and the message says how to create one.check_project(): reports naming conventions only when they differ from the defaults, as a single"info"row naming what changed. Silence when they match, so a conventions row always means something in this project resolves differently from every other one.check_project(): newrenv.lock packagescheck, reporting a lockfile that records no packages other thanrenvitself when the project also has.Ror.qmdsource files. The pairing is what makes it worth printing: a freshly scaffolded project legitimately has an empty lockfile, sincerenv::scaffold()does no dependency discovery, but a project with code in it and nothing in its lockfile is the state that produces a container image which builds cleanly and then cannot run. The source scan covers the project root and the declared folders rather than recursing through everything, sorenv/libraryis neither walked nor mistaken for the project’s own code.check_project(): new.renvignorecheck, reporting an entry that excludes.qmdfiles. The advice is to remove it. Purling to.Rlater is not a substitute, since the snapshot you containerize from may be taken before the purl and the.qmdis the file being maintained either way. Versions ofinit_project()before v0.5.0 wrote such a file, so projects created by those versions still carry one.init_project(): writes a project manifest,_toolero.yml, to the project root, recording the folder set it resolved and the naming conventions in force. The file records the resolved structure, never the inputs that produced it, so a project built from aconfig, one built withcustom_folders, and one built from the defaults all produce the same shape of file. It exists because the structure is configurable:check_project()can audit a customized project without being handed the same config again, andcontainrandsubmitrcan resolve where code, data, and outputs live rather than assuming. Commit the file – it describes the project, not the machine it was created on. The format is experimental and may gain keys before it settles;schema_versionexists so a reader can tell whether it understands what it is holding (issue #12).init_project()andgenerate_project_config()now share one schema, one template (inst/templates/_toolero.yml), and one writer. A config authored by hand and a manifest a project carries are the same kind of document; the only difference is who wrote it.Project configuration files may now declare a
conventions:block alongsidefolders:. Three keys are recognized:output_dir(where the analysis writes artifacts, read bysave_output()andgenerate_manifest()),script_dir(where the derived.Rscript lives), andsplit_dir(wherewrite_by_group()writes per-group subsets). Any key a file does not supply falls back to the package default, and unrecognized keys are ignored with a warning. These are the names the toolero family resolves rather than hardcodes.init_project(): writes a zero-byte.gitkeepinto each folder it creates that is still empty when the call finishes. git tracks files rather than directories, so without this a scaffolded structure survives nothing – the opening commit contains the files at the project root and none of the layout, and a collaborator cloning the repository gets a project with no folders in it. It is also the most common waycheck_project()would report a folder as failing on a project where nothing is actually wrong. The placeholders are written whether or notuse_git = TRUE, since a project can be git-initialized at any point afterwards. Folders that already have content are left alone, soassets/gets none.init_project(): whenbrandingis enabled,assets/now joins the project’s folder set and is recorded in the manifest alongside every other folder, so downstream packages can find the branding files without being told about them separately.init_project(): newbrandingargument replacinguw_branding. AcceptsTRUE(generic placeholder assets),"uw-madison"(UW-Madison RCI branding), or"none"/FALSE(no assets folder). All modes produce the same five standardized filenames inassets/:logo.png,favicon.png,header.html,footer.html,styles.css– so downstream consumers (create_qmd(),containr::generate_dockerfile()) can reference those names regardless of which branding mode was used.init_project(): newuse_readmeargument controlling whether a README is created at the project root.TRUE(default) createsREADME.md;"plain"createsREADME.txtwith identical content – only the extension differs;FALSEcreates no README. Both formats copy the same file,inst/templates/readme-template.md: a generalist guide that explains what a README is and why it matters, lays out a recommended section structure covering material shared by all research artifacts as well as software-specific and data-specific sections, and defers detailed guidance to the Cornell Data Services README guides rather than reproducing them. If a README already exists at the destination,init_project()aborts with an informative message instead of overwriting it.create_qmd():use_style = TRUEnow wires all three styling files present inassets/–styles.cssascss:,header.htmlasinclude-before-body:, andfooter.htmlasinclude-after-body:– rather than onlycss:and one HTML include. Any subset may be present; only files that exist are injected.create_qmd():assets/logo.pngis now exempt fromoverwrite. An existing logo (e.g. placed byinit_project(branding = )) is always left in place even whenoverwrite = TRUE, since a generic placeholder silently replacing institutional branding would be surprising. All other files (sample.csv,_quarto.yml,purl.R, the.qmditself) continue to respectoverwrite.
Bug fixes
create_qmd(): the YAML header was matched with a regular expression whose match included both---fences, and that whole string, trailing fence and all, was handed toyaml::yaml.load(), where the closing fence reads as the start of a second, empty YAML document. The parser tolerated it, so nothing visibly broke; the fences are now stripped before anything parses the header.create_qmd(use_purl = TRUE): the warning issued when automatic post-render wiring is skipped for awebsite,book, ormanuscriptproject asserted thatR/purl.R“was still created”. It may not have been: the script is scaffolded subject tooverwrite, so an existing copy is left in place. The warning now reports which of the two happened, since whoever reads it is about to point a post-render hook at that script by hand and needs to know whether it is the copy this version ships. The manual instructions in the same warning are now phrased inline rather than as a two-line YAML snippet, whichclicollapsed onto one line anyway.init_project(use_git = TRUE)initialized the git repository, staged files and made the opening commit in the caller’s project rather than the project it had just created.usethis::create_project()sets the activeusethisproject only for its own duration – it usesusethis::local_project()internally and restores the caller’s project when it returns withopen = FALSE– andinit_project()then calledusethis::use_git()without setting the project again. Runninginit_project()from inside another package or project therefore added entries to that project’s.gitignoreand offered to commit its uncommitted files under the message"initial commit", which is easy to accept because the prompt looks entirely plausible.init_project()now callsusethis::local_project(path, force = TRUE, setwd = FALSE)after creating the project, so every later step resolves against the new project, and the caller’s project is restored when the function returns. Every test in the suite passeduse_git = FALSE, which is why this went unnoticed; the git path is now covered.
Internal changes
New
R/utils-yaml.Rholds the line-oriented header helpers:.split_yaml_header(),.join_yaml_header(),.set_yaml_key()and.set_yaml_keys(), with.yaml_indent(),.yaml_line_key(),.find_yaml_key(),.yaml_entry_extent(),.first_child_indent(),.splice()and.render_yaml_entry()beneath them. Only the value being written passes throughyaml::as.yaml()..inject_style_yaml(),.inject_purl_yaml()and.substitute_yaml()were three near-identical copies of the same parse-mutate-serialize block, differing only in the mutation between the two. They are now three thin callers of.set_yaml_keys(), and.stamp_params_yaml()joins them as a fourth.create_qmd()resolvessample.csv, the.qmdtemplates,purl.Rand_quarto.ymlthrough.package_template()rather than callingsystem.file(mustWork = TRUE)directly, so a missing or misnamed template reports which file it wanted instead ofpkgload’s bareCan't find package file..standard_folder_message()and.cli_escape()moved fromR/check-project.RtoR/utils-project.R. The folder set and the advice for each member of it are one fact, and keeping them in separate files is how they drift;.standard_folder_message()gained entries forR/andassets/in the move.check_project()’s inline config parsing and README detection were replaced by calls to the shared.read_config_file()and.find_readme().write_by_group(): documentation now uses the term job manifest consistently for themanifest.csvit writes, distinguishing it from the project manifestgenerate_manifest()produces. The first lists inputs to a computation that has not happened; the second records outputs from one that has.sanitize_filename()gained roxygen explaining the invariant the filename scheme depends on: because a run of non-alphanumeric characters collapses to exactly one dash, a sanitized value can contain a single-but never two consecutive ones, which is what leaves--free to mark a column boundary.Added
.renv_lock_is_bare(),.project_has_sources()and.renvignore_excludes_qmd()toR/check-project.R, backing the two new renv checks.Added
R/utils-project.R, holding the facts about a toolero project that more than one function needs:.default_folders(),.default_conventions(),.project_yml_name(),.project_yml_schema_version(),.write_project_yml(),.read_project_yml(),.read_config_file(),.substitute_block(), and.find_readme(). The standard folder set was previously spelled out in four places –init_project(),generate_project_config(),check_project(), and.standard_folder_message()– with nothing keeping them in step. Adding a folder to the standard set is now a one-line change in.default_folders().Added
inst/templates/_toolero.yml, the annotated template bothinit_project()andgenerate_project_config()render. The folder list and conventions block are placeholders filled at write time from.default_folders()and.default_conventions()rather than literal text, so the template cannot drift from the package defaults. Substitution is line-based rather than a YAML round trip, so the template’s explanatory comments survive into the written file..resolve_custom_folders()gains apreserve_parentsargument.init_project()passesFALSEwhen the base folder set came from aconfig.Added
.branding_asset_names(), replacing the inline vector of five standardized asset filenames.init_project(): the new project is now made the activeusethisproject explicitly, viausethis::local_project(), for the duration of the call. The surroundingwithr::with_dir(getwd(), ...)block, which set the working directory to the working directory and therefore did nothing, has been removed. See the bug fix below for why the explicit call is needed.inst/assets/now contains ten files under auw-*/generic-*prefix convention:uw-logo.png,uw-favicon.png,uw-header.html,uw-footer.html,uw-styles.css, and fivegeneric-*counterparts. The copy step ininit_project()strips the prefix and writes standardized names into the project’sassets/directory. The old three-file UW set (rci-banner.png,header.html,styles.css) ininst/extdata/has been removed;inst/extdata/now contains onlydata-provenance.md.inst/templates/logo.pngremoved. The generic placeholder logo is nowinst/assets/generic-logo.png, whichcreate_qmd(include_examples = TRUE)copies intoassets/logo.pngwhen no logo already exists..inject_style_yaml()gainsheader_fileandfooter_filearguments (replacing the old singlehtml_fileargument), maps them toinclude-before-body:andinclude-after-body:respectively, and drops thefavicon_fileargument (favicon wiring belongs in_quarto.ymlas a website-project option, not in the per-document YAML).style_diris absolutized viafs::path_abs()increate_qmd()before style detection, ensuringfs::path_rel()comparisons are valid when a relativeuse_stylepath is combined with an absolutepathargument.Added
inst/templates/readme-template.md, the generalist README template copied byinit_project()’s newuse_readmeargument.
New features (continued)
- Added
save_output()for writing an object to disk via a user-supplied function and recording the write in a project-level accumulator atoutput_dir/accumulator.csv. The accumulator is append-only and carries one row per call, recordingfile_path,r_class,timestamp,function_used,status,error_message, andnote. The call to the write function is wrapped in a narrowly-scopedtryCatch()– only that call, not the rest ofsave_output()’s body – so a failed write is recorded withstatus = "failure"and the caught error message before the original condition is rethrown unmodified, preserving condition class and traceback. Missing destination directories are created automatically and reported. - Added
generate_manifest()for reading the project accumulator, collapsing it to one row per output file (keeping the latest write per path, since the accumulator may contain superseded rows from reruns within a session), and writingproject-manifest.json. The manifest recordsexecution_contextandgenerated_atonce at the top level, followed by anartifactsarray with one entry per deduplicated output, ordered chronologically. Field names are toolero-native rather than RO-Crate vocabulary – that translation belongs inencapsulr::describe()as a thin mapping layer. Checksums are deliberately excluded:rocrateR::bag_rocrate()computesmanifest-sha512.txtat bagging time, and duplicating that here would create a second record to keep in sync. A missing accumulator is an error; an accumulator with no rows produces an empty manifest with a warning.
Improvements
create_qmd(): every edit to a document’s YAML header is now made line by line rather than by parsing the header and writing it back out. Keys the edit does not touch keep the template’s own quoting, indentation, comments and ordering, so the document a reader opens is the template we shipped plus the keys they asked for. Previously a header was parsed and re-serialized once per edit – up to three times in a single call withuse_style,use_purlandyaml_datatogether – and each pass stripped quoting from scalars, moved sequence indentation, and would have deleted any comment the header carried. Both templates now carry a YAML comment explainingembed-resources, which the old implementation could not have preserved.create_qmd(yaml_data = ): a key the supplied file does not mention is now left exactly as the template wrote it. Top-level keys the file does mention are replaced wholesale, as before.check_project(): README detection is now case-insensitive and extension-agnostic. Any file whose stem matchesreadme(in any capitalization) is recognized, regardless of extension or the absence of one. Previously onlyREADME.md,README.Rmd, andREADME.qmdwere checked, all case-sensitively, missing common variants likereadme.mdor a plainREADMEon Linux (issue #11).check_project(): the standard folder set now matchesinit_project()–data-raw/,data/,scripts/,output/figures/,output/tables/, andreports/. The previous hardcoded set (data-raw/,data/,docs/) was stale relative to the v0.4.0 breaking change toinit_project().check_project(): newconfigargument accepts a path to a YAML file produced bygenerate_project_config(). When supplied, thefolders:list in the file replaces the standard toolero folder set for the folder checks. Non-folder hygiene checks (.Rproj,renv.lock, git,.gitignore, README, and hidden files) always run regardless of the config. Folders declared in the config but missing from the project are reported as"fail"rather than"warn"– the user declared them explicitly, so their absence is a conformance failure rather than an advisory (issue #12).
Deprecated features
-
check_project(error): theerrorargument is deprecated and will be removed in v0.6.0. The cli report now always prints and the tibble is always returned invisibly. To access results programmatically, assign the output directly:out <- check_project(). Passingerror = FALSEcontinues to work but triggers a deprecation warning.
toolero 0.4.0
CRAN release: 2026-07-16
New features
- Added
run_by_group(), the apply half of the split-apply workflow. Accepts either a manifest CSV produced bywrite_by_group(manifest = TRUE)or a named list of data frames. Applies a user-supplied function to each group subset and collects the results into a flat tibble (when the function returns a data frame) or a nested tibble with a list-column (when it returns anything else). Supports parallel execution viafurrrandfuturethrough theworkersargument, with a ceiling atmax(1L, parallelly::availableCores() - 1L)to reserve one core for the main session. Aseedargument enables reproducible parallel execution for analyses involving randomness. - Added
read_clean_csv()for reading CSV files into a tibble withjanitor::clean_names()applied automatically. Supports explicit missing-value codes viana, selective row dropping viadrop_na(acceptsTRUEor a character vector of column names), an optional ingest summary viasummary, and pass-through arguments toreadr::read_csv()via.... - Added
write_clean_csv()for writing data frames to CSV with clean column names. Appliesjanitor::clean_names()if column names are not already clean and reports affected columns via cli feedback. - Added
check_project()for auditing a project directory against toolero conventions. Checks for expected folders, an.Rprojfile,renv.lock, a git repository, a README, a.gitignore, and hidden files such as.RDataor.Rhistory. Operates in two modes: a cli report (default) or a tibble return for programmatic use (error = FALSE). - Added
qmd_to_r()for extracting R code chunks from any.qmdfile into a standalone.Rscript viaknitr::purl(). The output path defaults to the same directory as the input with the extension replaced. Thedocumentationargument controls how much context is preserved in the extracted script. - Added
generate_project_config()for writing a skeleton YAML project configuration file pre-filled with the standard toolero folder structure. Intended to be edited by the user and passed toinit_project()via the newconfigargument.filenameis required and explicit;pathdefaults to".". Anoverwriteargument (defaultFALSE) guards against accidental replacement of an existing config. The file extension is normalized to.ymlregardless of what is supplied. - Added Palmer Penguins attribution (Horst, Hill & Gorman, 2020) to the template
.qmd, thecreate_qmd()roxygen@detailssection, and a provenance note ininst/extdata/.
Breaking changes
-
init_project(): the standard folder structure has been revised to better reflect research workflow conventions established by The Carpentries and UW-Madison Libraries. The new standard set isdata-raw/,data/,scripts/,output/figures/,output/tables/, andreports/. The previous set (data/,data-raw/,images/,plots/,results/,scripts/,docs/,R/) is no longer created by default. -
init_project():extra_foldershas been renamed tocustom_folders. The argument now supports a dplyr-select-like syntax: bare names add folders (e.g."models"), names prefixed with"-"suppress creation of that folder from the resolved set (e.g."-output/figures"). Suppression removes only the named leaf – parent directories are preserved. Duplicate additions emit an informational message and are skipped; references to non-existent folders via"-"emit a warning. -
create_qmd(): no longer copiesstyles.cssandheader.htmlfrom the package into the project. Custom styling is now controlled exclusively by the newuse_styleargument. Projects that relied oncreate_qmd()copying UW-branded assets should useinit_project(branding = "uw-madison")to scaffold those files, then passuse_style = TRUEtocreate_qmd()to wire them into the YAML. -
create_qmd(): sample data is now copied intodata-raw/instead ofdata/, consistent withinit_project()’s folder structure.
New features (continued from above)
-
init_project(): addedconfigargument. When supplied, the folder list in the YAML file replaces the built-in standard structure entirely.custom_foldersis still applied on top of the config-derived set. Configs are produced bygenerate_project_config()and can be stored in the user home directory for reuse across project types. -
create_qmd(): addedinclude_examplesargument (defaultTRUE). WhenTRUE, copies a sample dataset (sample.csv) intodata-raw/, a placeholder logo (logo.png) intoassets/, and uses a worked example template with aparamsblock referencing the sample data. WhenFALSE, creates a blank skeleton.qmdwith only the YAML header and a setup chunk – no sample data, no logo, no example analysis block. -
create_qmd(): addeduse_styleargument (defaultFALSE). AcceptsFALSE(no custom styling),TRUE(scansassets/for standardized branding files by name), or a directory path (scans that directory instead).styles.cssis added ascss:,header.htmlasinclude-before-body:, andfooter.htmlasinclude-after-body:. Only files that exist are wired into the YAML. - Added
inst/templates/skeleton.qmd– a minimal Quarto template used wheninclude_examples = FALSE. Contains the YAML header, a setup chunk withlibrary(toolero), and a single placeholder heading. -
write_by_group():group_colnow accepts a character vector of column names, enabling grouping by more than one column at once. Sanitized filenames join multiple columns with--(e.g.group_col = c("species", "sex")on an Adelie male producesadelie--male.csv); only combinations actually present in the data produce files, not the full cross-product of possible values. Whenmanifest = TRUE, the manifest gains one column per grouping variable (holding the raw, unsanitized value) in addition to a compositegroup_valuecolumn joining the raw values with" | ". Single-column calls are unaffected – filenames, manifest schema, and behavior are unchanged from previous versions. -
write_by_group(): addeddrop_naargument (defaultTRUE). Rows with a missing value in any grouping column are dropped before splitting, with a cli message reporting how many rows were dropped and from which column(s) – this was previously silent, undocumented behavior inherited fromsplit(). Setdrop_na = FALSEto instead treat missing values as their own group rather than dropping them.
Bug fixes
-
create_qmd():use_style = TRUEnow correctly copiesrci-banner.pngfrominst/assets/into the projectassets/directory. Previously the banner was only copied inside theinclude_examplesblock and was silently omitted whenuse_style = TRUEwas combined withinclude_examples = FALSE. -
create_qmd():filenameargument now normalizes the file extension to.qmdviafs::path_ext_set(). Passing"my-document"and"my-document.qmd"both producemy-document.qmd; a double extension is never added. -
init_project(): path construction now usesfs::path()throughout rather thanglue::glue("{path}/{folder}"), ensuring correct behavior on all platforms. -
init_project(): branding files are now copied frominst/assets/rather thaninst/extdata/, consistent with the rest of the package.
toolero 0.3.0
CRAN release: 2026-04-27
Breaking changes
-
create_qmd():filenameis now the first argument and has no default – it must be supplied explicitly.pathis now the second argument and defaults to".", allowing natural calls likecreate_qmd("analysis.qmd"). -
write_by_group(): sanitized output filenames now use-(dash) as the separator instead of_(underscore), consistent with the package convention that file names use dashes. Existing workflows that reference output paths by name will need to update accordingly. -
init_project(): thefile_pathargument has been renamed topathfor consistency withcreate_qmd()and the broader package API. Calls usingfile_path =by name will error; positional calls are unaffected.
New features
- Added
generate_kb_xml()to produce UW-Madison KB-importable XML files from rendered Quarto documents. Extracts metadata from the.qmdYAML header and re-renders with embedded resources for self-contained import. -
create_qmd(): addeduse_purlargument (defaultTRUE) that scaffolds a_quarto.ymlpost-render hook and apurl.Rscript for extracting R code from rendered documents intoR/.
Bug fixes
-
init_project(): now runsrenv::snapshot()and creates.renvignoreafterrenv::init(), ensuring the lockfile is populated and.qmdfiles are excluded from dependency scanning at project creation time. -
create_qmd():_quarto.ymlis now copied frominst/templates/rather than written from a hardcoded string, so changes to the template are reflected automatically. -
create_qmd():purl.Ris now correctly placed inR/instead of the project root, consistent with_quarto.ymlcallingRscript R/purl.R. -
create_qmd(): fixed YAML boolean serialization whenyaml_datais supplied.yaml::as.yaml()was convertingtrue/falsetoyes/no, which Quarto does not recognize. A custom handler now forces unquotedtrue/falseoutput. -
inst/templates/purl.R: replacedQUARTO_DOCUMENT_PATHenvironment variable approach withfs::dir_ls()glob scan, which works reliably regardless of how Quarto invokes the post-render script.
toolero 0.2.0
CRAN release: 2026-04-24
Breaking changes
-
create_qmd():pathis now a required argument with no default. PassingNULLor omitting it raises an error. Usetempdir()for temporary output. -
write_by_group():output_diris now a required argument with no default. PassingNULLor omitting it raises an error. Usetempdir()for temporary output. -
init_project():opennow defaults toFALSEinstead ofTRUEto avoid disrupting the current RStudio session in non-interactive contexts.
New features
- Added
detect_execution_context()to identify whether code is running in an interactive R session, aquarto rendercall, or a plainRscriptinvocation. Returns one of"interactive","quarto", or"rscript". - Added
create_qmd()to scaffold a new Quarto document from a reproducible template, including a sample dataset, UW-Madison branded assets, and three-context input resolution viadetect_execution_context(). Optionally pre-populates the YAML header from a user-supplied YAML config file. - Added
write_by_group()to split a data frame by a single grouping column and write each group to a separate CSV file. Filenames are derived from sanitized group values. Optionally writes amanifest.csvlisting output files, group values, and row counts.
toolero 0.1.1
New features
- Added
uw_brandingargument toinit_project(). WhenTRUE, creates anassets/folder in the new project and populates it with UW-Madison RCI branding files (styles.css,header.html,rci-banner.png).
