|
| 1 | +## Two-step knitting |
| 2 | +## ----------------- |
| 3 | +## The first step generatates (purls) the BloomR lib. |
| 4 | +## The second step, thanks to step1 functions, runs demos and generate docs, |
| 5 | +## |
| 6 | +## Note: Demos chunks need lib functions from step 1, |
| 7 | +## unless we put all demos at the end, after lib functions, |
| 8 | +## which we don't like. |
| 9 | +## |
| 10 | +## First Step |
| 11 | +## ---------- |
| 12 | +## 1. Knit chunks tagged with opts.label='brfuncs' and step2=F. This will: |
| 13 | +## a) make <lib>.R, whose functions are stored in <lib> environment (via store()), |
| 14 | +## b) load functions in tagged chunks to be used in step2 demos. |
| 15 | +## |
| 16 | +## 2. Generate <lib>.tmp.Rmd with auto-generated argument descriptions. |
| 17 | +## Descs are extracted from special formatted comments by parseargs(). |
| 18 | +## |
| 19 | +## Second Step |
| 20 | +## ----------- |
| 21 | +## 1. Knit chunks in <lib>.tmp.Rmd tagged with opts.label="demo*" and step2=T. |
| 22 | +## This runs the demo code and generate <lib>.md with code examples. |
| 23 | +## 2. Add the TOC to <lib>.md vai topics(). |
| 24 | +## 3. Render to html/PDF |
| 25 | +## |
| 26 | +## See "setup" chunk for definition of "demo*" option templates. |
| 27 | +## TODO (cf bloomr.Rmd) For beta code, source(".../bloomr.R") is usually required to execute step2. See "betaonly" chunk. |
| 28 | + |
| 29 | + |
| 30 | +## TO CHECK |
| 31 | +## To source in Rmds consider |
| 32 | +## source("your-script.R", local = knitr::knit_global()) |
| 33 | +# or sys.source("your-script.R", envir = knitr::knit_global()) |
| 34 | + |
| 35 | +FRAMES <- sys.frames() |
| 36 | +SCRIPTPATH <- NULL |
| 37 | +#THISLIB <- NULL |
| 38 | + |
| 39 | +knit.twice <- function(this.file) {# knit this Rmd |
| 40 | + |
| 41 | +# ## Polymode tends to start R synced with git root dir. |
| 42 | +# ## We check the Rmd path is absolute or as expected |
| 43 | +# GITPOS <- "/src/br-libs" |
| 44 | +# abspt <- function(pt) unlist(strsplit(pt, split="/"))[[1]] == "" || regexpr("^.:(/|\\\\)", pt) != -1L |
| 45 | +# is.rel <- !abspt(this.file) |
| 46 | +# bad.dir <- sub(paste0("^", dirname(dirname(getwd()))), "", getwd()) != GITPOS |
| 47 | +# |
| 48 | +# ## Try to fix a bad path, when not abolute |
| 49 | +# if(bad.dir && is.rel) |
| 50 | +# if(dir.exists(file.path(normalizePath("."), "src/br-libs"))) { |
| 51 | +# setwd("src/br-libs") # Polymode |
| 52 | +# } else { |
| 53 | +# stop("Rmd file expected to be in '", GITPOS, "'. The given absolute path is\n", |
| 54 | +# normalizePath(this.file), "\nUse an absolute path or check 'GITPOS' var in knit.twice()") |
| 55 | +# } |
| 56 | + |
| 57 | + SCRIPTPATH <- script.path() |
| 58 | + assign("THISLIB", get("THISLIB", parent.frame()), knitr::knit_global()) |
| 59 | + ## other possibiliteis could be (not testes) when you soruce this file |
| 60 | + ## source("br-common.R", local = knitr::knit_global()) |
| 61 | + |
| 62 | + ## Keep this.file arg as-is if absolute, or adjust to this script dir. |
| 63 | + ## Then cd script |
| 64 | + abspt <- function(pt) unlist(strsplit(pt, split="/"))[[1]] == "" || regexpr("^.:(/|\\\\)", pt) != -1L |
| 65 | + is.rel <- !abspt(this.file) |
| 66 | + if(is.rel) this.file <- file.path(dirname(SCRIPTPATH), basename(this.file)) |
| 67 | + if(!file.exists(this.file)) stop("Unable to find:\n", normalizePath(this.file)) |
| 68 | + newdir <- normalizePath(dirname(this.file)) |
| 69 | + olddir <- normalizePath(setwd(newdir)) |
| 70 | + this.file <- basename(this.file) |
| 71 | + |
| 72 | + |
| 73 | + ## In BloomR use the shipped pandoc/LaTeX |
| 74 | + is.bloomr <- Sys.getenv("bloomr_branch") != "" |
| 75 | + wp <- function(expr) # Execute expression with modified PATH |
| 76 | + if(is.bloomr) .br.pathexe(substitute(expr), quiet = FALSE) else expr |
| 77 | + |
| 78 | + ## Prepare to knit |
| 79 | + knitr::opts_chunk$set(tidy.opts=list(width.cutoff=60)) |
| 80 | + fse <- tools:::file_path_sans_ext(this.file) |
| 81 | + |
| 82 | + ## Step 1 |
| 83 | + step2 <- FALSE |
| 84 | + knitr::knit(this.file) |
| 85 | + parseargs(paste0(fse, '.Rmd'), paste0(fse, '.tmp.Rmd')) |
| 86 | + |
| 87 | + ## Step 2 |
| 88 | + step2 <- TRUE |
| 89 | + knitr::knit(paste0(fse, '.tmp.Rmd'), output=paste0(fse, '.md')) |
| 90 | + topics(paste0(fse, '.Rmd'), paste0(fse, '.md')) |
| 91 | + wp(rmarkdown::render(paste0(fse, '.md'), "html_document")) |
| 92 | + wp(rmarkdown::render(paste0(fse, '.md'), rmarkdown::pdf_document())) |
| 93 | + unlink(paste0(fse, '.tmp.Rmd')) |
| 94 | + file.rename(paste0(fse, '.tmp.R'), paste0(fse, '.R')) |
| 95 | + |
| 96 | + ## Fix issue with purl commenting everything |
| 97 | + txt <- readLines(paste0(fse, ".R")) |
| 98 | + txt <- sapply(txt, function(line) sub("^# ", "", line)) |
| 99 | + writeLines(txt, paste0(fse, ".R")) |
| 100 | + |
| 101 | + ## Inform on new wdir |
| 102 | + if(newdir != olddir) message("\nNOTE: Workdir was adjusted to\n", newdir, ".") |
| 103 | + |
| 104 | + |
| 105 | + ## if with markdown lib & pandoc (but topics would need a rewrite): |
| 106 | + ## shell("pandoc bloomr.md -o bloomr.pdf", shell=Sys.getenv("COMSPEC")) |
| 107 | + ## markdownToHTML("bloomr.md", "bloomr.html") |
| 108 | + ## markdownToHTML("README.md", "README.html") |
| 109 | + |
| 110 | +} |
| 111 | +## In first eval = F chunk: |
| 112 | +## THISLIB <- "bloomr.time" |
| 113 | +## source(<thisfile>) |
| 114 | +## knit.twice("bloomr-time.Rmd") |
| 115 | + |
| 116 | + |
| 117 | + |
| 118 | +doc.setup <- function() { quote({ |
| 119 | +### Chunk templates and defaults |
| 120 | + knitr::opts_chunk$set(echo=TRUE) |
| 121 | + OTS <- knitr::opts_template$set |
| 122 | + ## Step1 template: Load in memory and copy to <lib>.R |
| 123 | + OTS(brfuncs= list(purl=TRUE, eval=!step2, include=FALSE)) |
| 124 | + ## Step1 template: Load in memory and copy to <lib>.R |
| 125 | + OTS(demofull= list(purl=FALSE, eval=step2, include=TRUE)) # run with code + output |
| 126 | + OTS(demorun= list(purl=FALSE, eval=step2, include=TRUE, echo=FALSE)) # run with output, but no code |
| 127 | + OTS(demohid= list(purl=FALSE, eval=step2, include=FALSE)) # run only (without code or output) |
| 128 | + OTS(democode= list(purl=FALSE, eval=FALSE, include=TRUE)) # show code only, no run |
| 129 | + |
| 130 | + ## Purl hook (suggested in the past, but probably no necessary today) |
| 131 | + knitr::knit_hooks$set(purl = knitr::hook_purl) |
| 132 | +})} |
| 133 | + |
| 134 | + |
| 135 | + |
| 136 | +### Store br.* objects in dedicated namespace |
| 137 | +store.chunk <- list( |
| 138 | + ## knit.twice will |
| 139 | + asgnlib = expression( |
| 140 | + assign(THISLIB, new.env(parent=asNamespace("stats"))) |
| 141 | + ), |
| 142 | + |
| 143 | + ## func: store(func); var: store("var") |
| 144 | + store = function(sym, reg = FALSE) { |
| 145 | + if(is.function(sym)) { |
| 146 | + name <- deparse(substitute(sym)) |
| 147 | + val <- sym |
| 148 | + } else { |
| 149 | + name <- sym |
| 150 | + val <- get(sym) |
| 151 | + } |
| 152 | + |
| 153 | + assign(name, val, envir = get(THISLIB)) |
| 154 | + if(reg) { |
| 155 | + ## name <- "%+%.Date" |
| 156 | + mtcs <- regmatches(name, regexec("(.+)(\\.)(.+)", name))[[1]] |
| 157 | + genname <- mtcs[2] |
| 158 | + class <- mtcs[4] |
| 159 | + method <- mtcs[1] |
| 160 | + registerS3method(genname, class, method, get(THISLIB)) |
| 161 | + ## registerS3method("%+%", "Date", "%+%.Date", bloomr.time) |
| 162 | + } |
| 163 | + rm( list=name, envir=parent.frame()) |
| 164 | + } |
| 165 | +) |
| 166 | + |
| 167 | + |
| 168 | +store.parse <- function(libname) { # makes store.chunk list into a text to feed a chunk |
| 169 | +### USAGE ```{r store, code = store.parse(<libname>) ...} |
| 170 | + |
| 171 | + with(store.chunk, { |
| 172 | + c(paste("THISLIB", "<-", shQuote(libname)), |
| 173 | + as.character(asgnlib), |
| 174 | + "store <- ", |
| 175 | + deparse(dput(store))) |
| 176 | + }) |
| 177 | +} |
| 178 | + |
| 179 | +lib.attach <- function() {# Make visible br.* in bloomr env and base ns |
| 180 | +### To feed the chunk: ```{r attach, code = lib.attach() ...} |
| 181 | + |
| 182 | + expression({ |
| 183 | + attach(get(THISLIB), name = THISLIB) |
| 184 | + rm(store) |
| 185 | + rm(list=c(THISLIB, "THISLIB")) |
| 186 | + }) |> |
| 187 | + as.character() |
| 188 | +} |
| 189 | + |
| 190 | +### Generate topics index |
| 191 | +read.head <- function(rmdfile) {x=readLines(rmdfile); x[grep("^=+", x)-1]} |
| 192 | +topics <- function(rmdfile, mdfile){ |
| 193 | + x <- read.head(rmdfile) |
| 194 | + x=sub("\\{", "]\\(", x ) |
| 195 | + x=sub("\\}", "\\)", x ) |
| 196 | + idx=paste0("[", x, " ") |
| 197 | + idx=c("\nR topics documented:", "-----------", idx) |
| 198 | + txt <- readLines(mdfile) |
| 199 | + yaml.end=grep("^---", txt)[2] |
| 200 | + txt=c(txt[1:yaml.end], idx, txt[(yaml.end+1):length(txt)]) |
| 201 | + writeLines(txt, mdfile) |
| 202 | +} |
| 203 | + |
| 204 | +### Auto arguments |
| 205 | +parseargs <- function(rmdfile, tmpfile) { |
| 206 | +## Generate a new tmp Rmd with BloomR function arguments descriptions. |
| 207 | +## These are embedded in comments preceding (or in line with) the arguments and detected via a magic prefix tag |
| 208 | +## The magic tag is hash (#) followed by a quote ('), henceforth denoted as HQ. In fact, to avoid side effects, |
| 209 | +## I can't explicitly write the magic tag. |
| 210 | +## Description comments have the format "HQ comment...". They can be in line with or precede the related arguments. |
| 211 | +## Standalone desc comments (i.e. non-inline) should immediately precede their argument, i.e. no-blank lines. |
| 212 | +## To break long lines, you can stack standalone comments above the argument, again without blank lines in the middle. |
| 213 | +## Once extracted from comments, the actual markdown description is written when a special placeholder is met. |
| 214 | +## The placeholder is "HQ @args". You are expected to insert it in the markdown following the function and |
| 215 | +## it will be replaced with the descriptions extracted from the comments in the function definition |
| 216 | +## HQ tag is always followed by a space (not a tab). Pay attention to casual use to avoid side effects. |
| 217 | +## Here is an example (replace HQ tag with hash and quote): |
| 218 | +## |
| 219 | +## foo |
| 220 | +## === |
| 221 | +## An important function. |
| 222 | +## |
| 223 | +## Usage |
| 224 | +## ------ |
| 225 | +## foo(x,y) |
| 226 | +## |
| 227 | +## |
| 228 | +## Arguments |
| 229 | +## ---------- |
| 230 | +## |
| 231 | +## HQ @args |
| 232 | +## |
| 233 | +## ```{r foo OPTIONS} |
| 234 | +## foo <- function( |
| 235 | +## HQ This is an important argument, and |
| 236 | +## HQ it deserves special attention. |
| 237 | +## x, |
| 238 | +## y HQ That's important too. |
| 239 | +## ){ |
| 240 | +## |
| 241 | +## BODY |
| 242 | +## } |
| 243 | +## ``` |
| 244 | + |
| 245 | + |
| 246 | + txt=readLines(rmdfile) |
| 247 | + HQ=paste0("#", "'") |
| 248 | + |
| 249 | + ## Outer HQ comments |
| 250 | + ## ----------------- |
| 251 | + ocrex.loose=sprintf("^ *%s +", HQ) #-> " *HQ +" |
| 252 | + ocrex=sprintf("%s+[^@]", ocrex.loose) #-> " *HQ ++[^@]", ++ need perl |
| 253 | + |
| 254 | + ## Test standard outer comments in the middle of HQ comments |
| 255 | + x= grep("^ *#[^']", txt) |
| 256 | + if(length(x)) { |
| 257 | + if(x[1]==1) x=x[-1] |
| 258 | + if(x[length(x)]==length(txt)) x=x[-length(x)] |
| 259 | + xx=lapply(x, function(xx) { |
| 260 | + if(grepl(ocrex, txt[xx-1], , perl=TRUE) & grepl(ocrex, txt[xx+1], perl=TRUE)) { |
| 261 | + stop("Detected standard outer comments in the middle of HQ comments.\n", |
| 262 | + paste(txt[xx+-1:1], collapse="\n" ))} |
| 263 | + }) |
| 264 | + } |
| 265 | + |
| 266 | + ## Get outer arg comments |
| 267 | + ocomp= grep(ocrex, txt, perl=TRUE) |
| 268 | + ocom=txt[ocomp] |
| 269 | + ## debug |
| 270 | + ## ocom=letters[1:9] |
| 271 | + ## ocomp=c(1, 3:6, 10, 21:23) |
| 272 | + |
| 273 | + ## Identify contiguos outer comments |
| 274 | + ccom=c(TRUE, diff(ocomp)!=1) |
| 275 | + |
| 276 | + ## Stack all to first of contiguos comments |
| 277 | + ocomp2=NULL; ocom2=NULL |
| 278 | + for(i in seq_along(ocomp)) { |
| 279 | + if(ccom[i]) { |
| 280 | + pos=ocomp[i] |
| 281 | + ocomp2=c(ocomp2, pos) |
| 282 | + val=sub(ocrex.loose, '', ocom[i]) |
| 283 | + ocom2=c(ocom2, val) |
| 284 | + } else { |
| 285 | + val=paste(val, sub(ocrex.loose, '', ocom[i])) |
| 286 | + ocom2[length(ocom2)]=val |
| 287 | + } |
| 288 | + } |
| 289 | + ocomp=ocomp2; ocom=ocom2 |
| 290 | + |
| 291 | + ## Get args lines after comments |
| 292 | + findAgs=function(pos) { # find non comment line postion, given outer HQ comment position |
| 293 | + while(startsWith(trimws(txt[pos]), "#")) pos=pos+1 |
| 294 | + pos |
| 295 | + } |
| 296 | + ocomp.ags=unlist(sapply(ocomp, findAgs)) |
| 297 | + ocom.ags=txt[ocomp.ags] |
| 298 | + |
| 299 | + ## Test blanks after HQ comments |
| 300 | + ocom.ags=trimws(ocom.ags) |
| 301 | + if(any(sapply(ocom.ags, nchar) == 0)) |
| 302 | + stop("Some HQ outer comments are followed by blank lines") |
| 303 | + |
| 304 | + ## Parse args line |
| 305 | + end=regexpr("[=,)]|$", ocom.ags) |
| 306 | + ocom.ags=substr(ocom.ags, 1, end-1) |
| 307 | + |
| 308 | + if(is.null(ocom)) ocom <- ocomp <- character(0) |
| 309 | + |
| 310 | + ## Inner HQ comments |
| 311 | + ## ----------------- |
| 312 | + icrex.loose=sprintf("[^ ]+ *%s ", HQ) #-> "[^ ]+ *HQ " |
| 313 | + |
| 314 | + ## Get inner arg comments |
| 315 | + icomp= grep(icrex.loose, txt) |
| 316 | + icom=txt[icomp] |
| 317 | + |
| 318 | + ## Extract args |
| 319 | + end=regexpr("[=,#]", icom) |
| 320 | + icom.ags=substr(icom, 1, end-1) |
| 321 | + icom.ags=trimws(icom.ags) |
| 322 | + icom=sub(paste0(".+", HQ, " +"), '', icom) |
| 323 | + |
| 324 | + |
| 325 | + ## Match @args tag |
| 326 | + ## --------------- |
| 327 | + acrex=sprintf(" *%s +@args", HQ) #-> " *HQ +@args" |
| 328 | + |
| 329 | + ## Get @args positions |
| 330 | + acomp= grep(acrex, txt) |
| 331 | + |
| 332 | + ## Merge argument slots and HQ comments by position |
| 333 | + df <- data.frame |
| 334 | + |
| 335 | + if(length(ocom)) ocom <- paste0(ocom.ags, "\n: ", ocom) |
| 336 | + if(length(icom)) icom <- paste0(icom.ags, "\n: ", icom) |
| 337 | + m <- merge( |
| 338 | + df(pos=ocomp, com=ocom), |
| 339 | + df(pos=icomp, com=icom), |
| 340 | + suffixes=c('.out','.in'), |
| 341 | + by="pos", all=TRUE) |
| 342 | + |
| 343 | + apos <- if(length(acomp)) TRUE else acomp |
| 344 | + m <- merge( |
| 345 | + df(pos=acomp, apos=apos), |
| 346 | + m, |
| 347 | + by="pos", all=TRUE) |
| 348 | + |
| 349 | + m[2]=!is.na(m[2]) |
| 350 | + m[is.na(m)]="" |
| 351 | + m=df(m[1:2], com=apply(m[3:4], 1, paste, collapse="")) |
| 352 | + |
| 353 | + ## Split by comment by slots and cat |
| 354 | + spl=rep(0, nrow(m)) |
| 355 | + spl[m$apos] = m$pos[m$apos] |
| 356 | + spl=cumsum(spl) |
| 357 | + spl=split(m$com, spl) |
| 358 | + spl=sapply(spl, function(x) paste(x[-1], collapse="\n\n")) |
| 359 | + |
| 360 | + |
| 361 | + ## Replace commment slots with comments |
| 362 | + for(i in seq_along(acomp)){ |
| 363 | + txt[acomp[i]] = spl[i] |
| 364 | + } |
| 365 | + writeLines(txt, tmpfile) |
| 366 | + |
| 367 | +} |
| 368 | + |
| 369 | +## Adapted from bloomr.build.R |
| 370 | +script.path <- function() { # Identify the sourced script and its parent for G$me and G$prjdir |
| 371 | + |
| 372 | + ## The standard method is 'sys.frame(1)$ofile', but we deal with |
| 373 | + ## non-top-level source calls, e.g. a source() nested in function. |
| 374 | + maybe.script <- lapply(FRAMES, \(frame) frame$ofile) |> unlist() |
| 375 | + if(length(maybe.script) > 1 || void(maybe.script)) stop("I am unable to identify the sourced script") |
| 376 | + maybe.dir <- dirname(maybe.script) |
| 377 | + |
| 378 | + ## The method is reliable, but we check that the project dir is a git |
| 379 | + ## dir and has the main build script |
| 380 | + proofs <- c("br-common.R") %in% dir(maybe.dir, all.files = TRUE) |
| 381 | + if(!all(proofs)) |
| 382 | + stop("The file you sourced, identified as below, does not seem to come from the original BloomR project") |
| 383 | + maybe.script |
| 384 | +} |
| 385 | + |
| 386 | +## From bloomr.build.R |
| 387 | +void <- function(x) # Similar to !nzchar(var) but works if var is NA or NULL |
| 388 | + is.null(x) || is.na(x) || nchar(as.character(x)) == 0 |
| 389 | + |
| 390 | + |
| 391 | +## may be useful for debug |
| 392 | +return(SCRIPTPATH) |
0 commit comments