Warning
This extension requires Quarto >= 1.4.538
It is a Quarto extension, providing additional formatting options for Quarto documents. It is designed to help educators create tutorials, practicals, and exams (coming soon 😉) with Quarto. With unilur
, you can render from one single .qmd
file:
- One file with only questions
- One file with questions and answers (and if used also comments), where answers appear in a coloured box (and comments in a different coloured box)
It supports both HTML and PDF outputs and allows toggling the visibility of solutions (and comments) using a simple YAML switch.
Note
This Quarto extension is a conversion/adaptation of the {unilur} R package for rmarkdown, which was developed by Eric Koncina.
Prerequisites: you need to have Quarto installed (see here).
To use this extension in your Quarto project, run this in the top level of your Quarto project:
quarto add ginolhac/unilur
This will install the extension under the _extensions/
sub-directory. If you're using version control, you will want to check in this directory. You can find more information in the Quarto documentation for managing
extensions.
Tip
For a detailed explanation on how to use it, please see the upcoming vignette.
To use the extension, you just need to add the following lines to your YAML header (you can use only HTML or PDF outputs):
format:
unilur-html: default
unilur-html+solution:
output-file: example-solution.html
unilur-pdf: default
unilur-pdf+solution: default
The above code will give you four different files. Let's say that you have a Quarto file called example.qmd
after running:
quarto render example.qmd
Then, you will get (you can click on each file to get an overview):
example.html
: file with only the exercises in HTML format (the name of that file is the name of your.qmd
file)example-solution.html
: file with solutions (and comments if used) in HTML format (the name of that file was defined in the YAML section above)example.pdf
: file with only the exercises in PDF format (the name of that file is the name of your.qmd
file)example+solution.pdf
: file with solutions (and comments if used) in PDF format (the name of that file is the name of your.qmd
file with+solution
added automatically to it)
Note
You have to specify a different output file for HTML, otherwise the solutions will be saved into the exercises file (overwrite themselves). Of course, it is possible to change the file name of all of the output files if wished.
Two boolean options for all chunks are recognized in the YAML Quarto header:
show-solution
:true
generates solution boxes (ie like formatunilur-html+solution
).false
generates instructions without solutions.
collapse-solution
:true
(the default) for HTML output, all solution boxes are collapsed, students need to click on them to unfolded.false
, all solutions are unfolded.
Tip
You have to specify per chunk the option unilur-collapse
which override the behavior of the global option collapse-solution
.
You can use the the default prefix #exr-
for writing your exercises.
The extension made a slight modification to it in order to allow numbering the exercises without headers.
By using it, you will have Exercise 1, Exercise 2, .... and not Exercise 1.1, Exercise 1.2, ... (default behavior with a header mandatory).
You have three different ways to show the solution to an exercise.
1️⃣ Code chunks | 2️⃣ Fenced div | 3️⃣ Code Blocks |
---|---|---|
Use unilur-solution as hashpipe to the code chunks. |
Fences divs can also be used, and they can contain code chunks too | Code Blocks are used for text |
![]() |
![]() |
![]() |
The comments have the same three options as above for solutions.
The only difference is to use unilur-comment
instead of unilur-solution
.
The comments can be used for any additional information for the instructors,
such as any general comments about a specific exercise or the points to be attributed for each answer (in the case of an exam for example).
If you do not need it, you can skip it. If it is used, it will be shown in the same document as the solutions.
Note
In HTML, the solution and comment boxes are collapsed by default, but can be shown with the chunk option unilur-collapse
.
Exercise only | Exercise with solution and comment |
---|---|
![]() |
![]() |
Note
The solutions coloured box is green. Thus, to avoid confusion, but also to homage of the original {unilur} R package, the tip callout was changed to the colour yellow.
unilur is probably best used within Quarto website. One additional feature not
included in the extension per se is template.
Those are Quarto docs where the solution chunks/divs are replaced with a chunk placeholder or removed for divs.
This allows students to have a starting doc to fill-up with questions, tips and useful header option such as
embed-resources: true
.
Templates are generated by a R script and template are displayed in table with a custom listing since files with .qmd
are unusal in the listing pages.
A demo website can be seen at https://ginolhac.github.io/unilur/ and source code in the docs
sub-folder.
People who developed and released extensions I got inspiration from:
- JJ Allaire for
filename
extension - Andrie de Vries for
reveal-auto-agenda
extension
Moreover:
- Christophe Dervieux for precious advises and his precious time
- Andrew Heiss for his great teaching websites, both content and tweaks are very useful
- Mickaël Canouil for maintaining the awesome Quarto
We welcome contributions! Please submit a pull request or open an issue.
- Add tests.
- Remove the
unilur-solution: true
option from chunks when displayed (visible inecho: fenced
).- Idea from Christophe Dervieux: treat
div
attribute - Get
el
content and iterate withwalk()
- Add a filter
CodeBlock
which remove theunilur
options
- Idea from Christophe Dervieux: treat
This helped me developing this extension.
Described here in the docs
format: native
Which returns something like this:
pandoc AST
Pandoc
Meta
{ unMeta =
fromList
[ ( "biblio-config" , MetaBool True )
, ( "labels"
, MetaMap
(fromList
[ ( "abstract" , MetaInlines [ Str "Abstract" ] )
, ( "affiliations"
, MetaInlines [ Str "Affiliations" ]
)
, ( "authors" , MetaInlines [ Str "Authors" ] )
, ( "description"
, MetaInlines [ Str "Description" ]
)
, ( "doi" , MetaInlines [ Str "Doi" ] )
, ( "modified" , MetaInlines [ Str "Modified" ] )
, ( "published" , MetaInlines [ Str "Published" ] )
])
)
, ( "solution" , MetaBool True )
, ( "title"
, MetaInlines [ Str "Unilur" , Space , Str "Example" ]
)
]
}
[ Header 2 ( "usage" , [] , [] ) [ Str "Usage" ]
, BulletList
[ [ Plain
[ Strong [ Str "Activate" ]
, Space
, Str "the"
, Space
, Str "extension"
, Space
, Str "by"
, Space
, Str "adding"
, Space
, Str "the"
, Space
, Str "following"
, Space
, Str "lines"
, Space
, Str "to"
, Space
, Str "your"
, Space
, Str "YAML"
, Space
, Str "header:"
]
]
]
, CodeBlock
( "" , [ "yaml" ] , [] ) "filters:\n - unilur\n"
, BulletList
[ [ Plain
[ Strong [ Str "Solution" ]
, Space
, Str "code"
, Space
, Str "blocks"
, Space
, Str "are"
, Space
[...]
, BulletList
[ [ Plain
[ Str "Solution"
, Space
, Str "with"
, Space
, Code ( "" , [] , [] ) "collapse: true"
]
]
]
, Div
( "" , [ "cell" ] , [ ( "solution" , "true" ) ] )
[ CodeBlock
( "" , [ "r" , "cell-code" ] , [] ) "1 + 2\n## [1] 3"
]
]
quarto render example.qmd -M keep-md:true
/usr/lib/rstudio/resources/app/bin/quarto/bin/tools/pandoc -f markdown -t json -o ast.json example.md
Then visualize the Abstract Syntax Tree (AST) using this code
pandoc AST
xfun:::tree(
jsonlite::fromJSON('ast.json', simplifyVector = FALSE)
)
List of 3
|-pandoc-api-version:List of 3
| |-: int 1
| |-: int 22
| |-: int 2
|-meta :List of 5
| |-execute:List of 2
| | |-t: chr "MetaMap"
| | |-c:List of 1
| | |-keep-md:List of 2
| | |-t: chr "MetaBool"
| | |-c: logi TRUE
| |-filters:List of 2
| | |-t: chr "MetaList"
| | |-c:List of 1
| | |-:List of 2
| | |-t: chr "MetaInlines"
| | |-c:List of 1
| | |-:List of 2
| | |-t: chr "Str"
| | |-c: chr "unilur"
| |-format :List of 2
| | |-t: chr "MetaMap"
| | |-c:List of 1
| | |-html:List of 2
| | |-t: chr "MetaMap"
| | |-c:List of 1
| | |-theme:List of 2
| | |-t: chr "MetaInlines"
| | |-c:List of 1
| | |-:List of 2
| | |-t: chr "Str"
| | |-c: chr "cosmo"
| |-title :List of 2
| | |-t: chr "MetaInlines"
| | |-c:List of 3
| | |-:List of 2
| | | |-t: chr "Str"
| | | |-c: chr "Unilur"
| | |-:List of 1
| | | |-t: chr "Space"
| | |-:List of 2
| | |-t: chr "Str"
| | |-c: chr "Example"
| |-unilur :List of 2
| |-t: chr "MetaMap"
| |-c:List of 1
| |-solution:List of 2
| |-t: chr "MetaInlines"
| |-c:List of 1
| |-:List of 2
| |-t: chr "Str"
| |-c: chr "true"
|-blocks :List of 5
|-:List of 2
| |-t: chr "Div"
| |-c:List of 2
| |-:List of 3
| | |-: chr ""
| | |-:List of 1
| | | |-: chr "cell"
| | |-:List of 1
| | |-:List of 2
| | |-: chr "solution"
| | |-: chr "true"
| |-:List of 2
| |-:List of 2
| | |-t: chr "CodeBlock"
| | |-c:List of 2
| | |-:List of 3
| | | |-: chr ""
| | | |-:List of 1
| | | | |-: chr "cell-code"
| | | |-: list()
| | |-: chr "```{r}\n#| solution: true\n\n1 + 1\n```"
| |-:List of 2
| |-t: chr "Div"
| |-c:List of 2
| |-:List of 3
| | |-: chr ""
| | |-:List of 2
| | | |-: chr "cell-output"
| | | |-: chr "cell-output-stdout"
| | |-: list()
| |-:List of 1
| |-:List of 2
| |-t: chr "CodeBlock"
| |-c:List of 2
| |-:List of 3
| | |-: chr ""
| | |-: list()
| | |-: list()
| |-: chr "[1] 2"
|-:List of 2
| |-t: chr "Para"
| |-c:List of 1
| |-:List of 2
| |-t: chr "Str"
| |-c: chr "Classic"
|-:List of 2
| |-t: chr "Div"
| |-c:List of 2
| |-:List of 3
| | |-: chr ""
| | |-:List of 1
| | | |-: chr "cell"
| | |-: list()
| |-:List of 2
| |-:List of 2
| | |-t: chr "CodeBlock"
| | |-c:List of 2
| | |-:List of 3
| | | |-: chr ""
| | | |-:List of 1
| | | | |-: chr "cell-code"
| | | |-: list()
| | |-: chr "```{r}\n1 + 1\n```"
| |-:List of 2
| |-t: chr "Div"
| |-c:List of 2
| |-:List of 3
| | |-: chr ""
| | |-:List of 2
| | | |-: chr "cell-output"
| | | |-: chr "cell-output-stdout"
| | |-: list()
| |-:List of 1
| |-:List of 2
| |-t: chr "CodeBlock"
| |-c:List of 2
| |-:List of 3
| | |-: chr ""
| | |-: list()
| | |-: list()
| |-: chr "[1] 2"
|-:List of 2
| |-t: chr "Para"
| |-c:List of 1
| |-:List of 2
| |-t: chr "Str"
| |-c: chr "Solution"
|-:List of 2
|-t: chr "Div"
|-c:List of 2
|-:List of 3
| |-: chr ""
| |-:List of 1
| | |-: chr "cell"
| |-:List of 1
| |-:List of 2
| |-: chr "solution"
| |-: chr "true"
|-:List of 2
|-:List of 2
| |-t: chr "CodeBlock"
| |-c:List of 2
| |-:List of 3
| | |-: chr ""
| | |-:List of 2
| | | |-: chr "r"
| | | |-: chr "cell-code"
| | |-: list()
| |-: chr "1 + 2"
|-:List of 2
|-t: chr "Div"
|-c:List of 2
|-:List of 3
| |-: chr ""
| |-:List of 2
| | |-: chr "cell-output"
| | |-: chr "cell-output-stdout"
| |-: list()
|-:List of 1
|-:List of 2
|-t: chr "CodeBlock"
|-c:List of 2
|-:List of 3
| |-: chr ""
| |-: list()
| |-: list()
|-: chr "[1] 3"
MIT License