Skip to content

Commit a99b002

Browse files
committed
br-libs dir
1 parent b74db8e commit a99b002

File tree

1 file changed

+392
-0
lines changed

1 file changed

+392
-0
lines changed

src/br-libs/br-common.R

Lines changed: 392 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,392 @@
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

Comments
 (0)