The morphdown R package is being developed to provide a way to programmatically change the structure of R Markdown files. For example, to turn a ‘full text’ file into a summarized slide presentation.
The package turns raw markdown (.md
, .rmd
, .qmd
, etc.) files into a R list of sections and their blocks, which can then be targeted individually by editing functions.
The morphing becomes much more declarative and documented than manually adapting the files, increasing speed, but most important,becoming more robust to changes in the source material.
Check an example of the package in the last section of this page.
I created this tool to turn the chapters of my book RFCD into slide presentations for the trainings I administer.
Disclaimer: this package is in the early most stage of life. It hasn’t been thoroughly tested and can present several bugs. I don’t recommend using it for large-scale projects, yet.
Please report any problems as a GitHub issue. Other comments can be posted as a GitHub discussion, or sent in my email below. Thank you!
Author: Ricardo Semião e Castro (ricardo.semiao@outlook).
Installation
You can install the development version of morphdown like so:
# install.packages("devtools")
devtools::install_github("ricardo-semiao/morphdown")
Development Comments
This package currently has some drawbacks:
- It acts on a very unpredictable input, with many possible ways to write markdown, and each flavor having features that need to be accounted for.
- It is most useful after the source material’s structure is mostly fixed, and mainly small changes in the writing style will happen.
A more robust solution, that wouldn’t need major redoing after structural changes, would need a interactive editor, that could hold memory of the changes made. This could be achieved with Shiny, which I have experience on, but I don’t have time to work on it at the moment.
Because of these reasons this package is not my main focus at the moment. If you need support to your custom markdown writing style and/or markdown flavor features, I suggest forking the package and adapting it to your needs. But also contact me, so I learn about your demands.
Some of the more important features I plan to add in the future are:
- Add support for lists with empty lines between the items.
- Add an option to remove double empty lines in the final output.
- Add testing with thestthat 3.
- Add error handling with rlang/cli, and more informative messages.
Note that this package:
- Follows the tydiverse style guide.
- Uses testthat 3 for automate tests.
- Uses rlang frameworks for tidy eval and rlang errors.
Example
The basic workflow of the package is as below:
- First, one splits the source file into a more interpretable R list, using
split_sections()
. - Then, the user defines a plan, specifying, for each section and each block, how they are to be edited.
- Each section is comprised of blocks, which the user edits with one of the editing functions (
e
ordiv
). The user can also add new lines of markdown.
A mock representation of such workflow is presented below.
Consider the exemplary .Rmd file (as a string) below:
# Title
This is a markdown file.
## Section A
We have some text here. This will be recognized as a single-line text expression,
and split into clauses. Thus, it should be edited with the `e()` function. The
user can choose to select only some clauses with `keep = c(1, 3, 4)`. The user
can also add line breaks with `adds = '\\n'`.
- We also have a list.
- This list will be split as a single block.
- And the user can select which items (lines) to keep.
### Blocks
We can count lv3 headers as sections or not, by controlling the `sec_lv` argument.
Regardless, it is saved as a 'headx' block, which can be manipulated by
`add_subhead()`.
:::{.result}
The same is true for markdown blocks.
The user can choose to ignore this line, the third of the block, with `keep = -3`.
:::
``` r
Code blocks are also recognized as a single block.
Note the use of `breaks`.
```
and lastly tables
------------ ----------------
are also blocks
whose rows can be ignored
------------ ----------------
Assume that original
is the path to such file, or the file as a string. Then, we can split the sections:
library(morphdown)
sections <- split_sections(original, sec_lv = 2)
sections
## $s1
## $s1$head1
## [1] "# Title"
##
## $s1$empty1
## [1] ""
##
## $s1$b1
## [1] "This is a markdown file."
##
## $s1$empty2
## [1] ""
##
##
## $s2
## $s2$head1
## [1] "## Section A"
##
## $s2$empty1
## [1] ""
##
## $s2$b1
## [1] "We have some text here."
## [2] "This will be recognized as a single-line text expression, and split into clauses."
## [3] "Thus, it should be edited with the `e()` function."
## [4] "The user can choose to select only some clauses with `keep = c(1, 3, 4)`."
## [5] "The user can also add line breaks with `adds = '\\n'`."
##
## $s2$empty2
## [1] ""
##
## $s2$b2
## [1] "- We also have a list."
## [2] "- This list will be split as a single block."
## [3] "- And the user can select which items (lines) to keep."
##
## $s2$empty3
## [1] ""
##
## $s2$head2
## [1] "### Blocks"
##
## $s2$empty4
## [1] ""
##
## $s2$b3
## [1] "We can count lv3 headers as sections or not, by controlling the `sec_lv` argument."
## [2] "Regardless, it is saved as a 'headx' block, which can be manipulated by"
## [3] "`add_subhead()`."
##
## $s2$empty5
## [1] ""
##
## $s2$b4
## [1] ":::{.result}"
## [2] "The same is true for markdown blocks."
## [3] ""
## [4] "The user can choose to ignore this line, the third of the block, with `keep = -3`."
## [5] ":::"
##
## $s2$empty6
## [1] ""
##
## $s2$b5
## [1] "```{r}"
## [2] "Code blocks are also recognized as a single block."
## [3] ""
## [4] "Note the use of `breaks`."
## [5] "```"
##
## $s2$empty7
## [1] ""
##
## $s2$b6
## [1] "and lastly tables " "------------ ----------------"
## [3] "are also blocks " "whose rows can be ignored "
## [5] "------------ ----------------"
##
## $s2$empty8
## [1] ""
Now, we can use this organization of the document to create our morphing plan:
result <- morph_doc(
sections,
end = "lb", #set the default value of `end` for `e()` and `div()`
head_lv = 1, #default value of `head_lv` for `morph_sec` and `add_cur_head()`
s1 = morph_sec(
#no head1 argument, so header is leaved as is, with a level equals `head_lv`
b1 = e() #get the text expression unaltered
),
s2 = morph_sec(
head_lv = 1, #alter the level of the section header
end = "br", #set a different default value of `sep` only for this section
b1 = e(c(1, 3, 4), adds = "\n"),
b2 = div(2:3),
head2 = add_subhead(n = 3),
#no b3 argument, such that it is ignored. The same can be done with sections
b4 = div(-3),
b5 = div(breaks = 2, sep = "I can add things here"),
b6 = div(-3)
)
)
cat(result)
## # Title
##
## This is a markdown file.
##
##
## # Section A
##
## We have some text here.
## Thus, it should be edited with the `e()` function.
## The user can choose to select only some clauses with `keep = c(1, 3, 4)`.
##
## <br>
##
##
## - This list will be split as a single block.
## - And the user can select which items (lines) to keep.
##
## <br>
##
##
## ### Section A - Blocks
## :::{.result}
## The same is true for markdown blocks.
## The user can choose to ignore this line, the third of the block, with `keep = -3`.
## :::
##
## <br>
##
##
## ```{r}
## Code blocks are also recognized as a single block.
## I can add things here
##
## Note the use of `breaks`.
## ```
##
## <br>
##
##
## and lastly tables
## ------------ ----------------
## whose rows can be ignored
## ------------ ----------------
##
## <br>
One could save the result to a variable, and write it to any file with writeLines
.