├── .gitignore ├── example ├── .gitignore ├── src │ ├── common │ │ └── deps.typ │ ├── ex1 │ │ └── ex.typ │ └── ex2 │ │ └── ex.typ ├── tutor.toml ├── main.typ └── Taskfile.yml ├── imgs ├── example_mod.png └── example.typ ├── typst.toml ├── docs ├── manual.typ └── Taskfile.yml ├── LICENSE ├── Taskfile.yml ├── lib.typ └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | imgs/example.pdf 2 | build 3 | -------------------------------------------------------------------------------- /example/.gitignore: -------------------------------------------------------------------------------- 1 | build 2 | main_sol.typ 3 | -------------------------------------------------------------------------------- /example/src/common/deps.typ: -------------------------------------------------------------------------------- 1 | #import "@local/tutor:0.8.0" 2 | -------------------------------------------------------------------------------- /imgs/example_mod.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rangerjo/tutor/HEAD/imgs/example_mod.png -------------------------------------------------------------------------------- /example/tutor.toml: -------------------------------------------------------------------------------- 1 | level = 1 2 | sol = true 3 | test = true 4 | 5 | [utils] 6 | [utils.checkbox] 7 | sym_false = '☐' 8 | sym_question = '☐' 9 | sym_true = '☒' 10 | 11 | [utils.lines] 12 | spacing = '8mm' 13 | 14 | [utils.grid] 15 | spacing = '4mm' 16 | 17 | [utils.totalpoints] 18 | outline = true 19 | -------------------------------------------------------------------------------- /typst.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "tutor" 3 | version = "0.8.0" 4 | entrypoint = "lib.typ" 5 | authors = ["Jonas Amstutz"] 6 | license = "MIT" 7 | description = "Utilities to create exams." 8 | 9 | 10 | repository = "https://github.com/rangerjo/tutor" 11 | keywords = ["exam", "test", "quiz", "teacher"] 12 | categories = ["components"] 13 | disciplines = ["education"] 14 | compiler = "0.13.0" 15 | -------------------------------------------------------------------------------- /imgs/example.typ: -------------------------------------------------------------------------------- 1 | #set page( 2 | width: auto, 3 | height: auto, 4 | margin: (x: 0cm), 5 | ) 6 | #table( 7 | columns: 2, 8 | stroke: none, 9 | align: center, 10 | [ Question Mode ], [ Solution Mode ], 11 | [#rect(stroke: 2pt+blue, 12 | image("../example/build/example_question_mode.svg", width: 12cm) 13 | )], 14 | [#rect(stroke: 2pt+green, 15 | image("../example/build/example_solution_mode.svg", width: 12cm) 16 | )] 17 | ) 18 | -------------------------------------------------------------------------------- /example/src/ex1/ex.typ: -------------------------------------------------------------------------------- 1 | #import "../common/deps.typ": tutor 2 | #import tutor: points, checkbox 3 | 4 | #let exercise(cfg) = [ 5 | #heading( 6 | level: cfg.level, 7 | [Abbreviation FHIR (#points(1) point)], 8 | ) 9 | 10 | What does FHIR stand for? 11 | 12 | #set list(marker: none) 13 | - #checkbox(cfg, false) Finally He Is Real 14 | - #checkbox(cfg, true) Fast Health Interoperability Resources 15 | - #checkbox(cfg, false) First Health Inactivation Restriction 16 | 17 | #if cfg.sol { 18 | [ Further explanation: FHIR is the new standard developed by HL7. ] 19 | } 20 | ] 21 | -------------------------------------------------------------------------------- /example/src/ex2/ex.typ: -------------------------------------------------------------------------------- 1 | #import "../common/deps.typ": tutor 2 | #import tutor: points, lines 3 | 4 | #let exercise(cfg) = [ 5 | #heading( 6 | level: cfg.level, 7 | [FHIR vs HL7v2 (#points(4.5) points)], 8 | ) 9 | 10 | List two differences between HL7v2 and FHIR: 11 | 12 | + #if cfg.sol { [ HL7v2 uses a non-standard line format, where as FHIR uses XML or JSON] } else { [ #lines(cfg, 3) ] } 13 | + #if cfg.sol { 14 | [ FHIR specifies various resources that can be queried, where as HL7v2 has a number of fixed fields that are either filled in or not] 15 | } else { [ #lines(cfg, 3) ] } 16 | 17 | ] 18 | 19 | -------------------------------------------------------------------------------- /docs/manual.typ: -------------------------------------------------------------------------------- 1 | #import "@preview/tidy:0.2.0" 2 | #import "/lib.typ" 3 | 4 | #let docs = tidy.parse-module( 5 | read("/lib.typ"), 6 | name: "tutor", 7 | scope: (tutor: lib) 8 | ) 9 | #let VERSION = toml("/typst.toml").package.version 10 | 11 | #align(center)[ 12 | #text(size: 24pt)[tutor] \ 13 | #datetime.today().display() - Jonas Amstutz - Version #VERSION 14 | ] 15 | 16 | - question mode: No solutions 17 | - solution mode: Show solutions 18 | - exercise mode: No points, no comments on how points are distributed when correcting an exam 19 | - test mode: Show points and comments on how points are distributed when correcting an exam (in solution mode). 20 | 21 | #tidy.show-module(docs, style: tidy.styles.default) 22 | -------------------------------------------------------------------------------- /example/main.typ: -------------------------------------------------------------------------------- 1 | #import "@local/tutor:0.8.0": totalpoints, lines, default-config 2 | 3 | #import "src/ex1/ex.typ" as ex1 4 | #import "src/ex2/ex.typ" as ex2 5 | 6 | #let cfg = toml("tutor.toml") 7 | 8 | 9 | #if "tutor_sol" in sys.inputs { 10 | if sys.inputs.tutor_sol == "true" { 11 | (cfg.sol = true) 12 | } else if sys.inputs.tutor_sol == "false" { 13 | (cfg.sol = false) 14 | } 15 | } 16 | 17 | #set heading(numbering: "1.1") 18 | 19 | #text(16pt)[ 20 | Name: $underline(#h(15cm))$ 21 | #v(3mm) 22 | #grid( 23 | columns: (1fr, 1fr), 24 | rows: 10mm, 25 | gutter: 5mm, 26 | // align: left + horizon, 27 | [Points: $underline(#h(4cm))$ / #totalpoints(cfg)], 28 | [Grade: $underline(#h(6cm))$], 29 | ) 30 | ] 31 | 32 | #outline() 33 | 34 | #ex1.exercise(cfg) 35 | #ex2.exercise(cfg) 36 | -------------------------------------------------------------------------------- /docs/Taskfile.yml: -------------------------------------------------------------------------------- 1 | # https://taskfile.dev 2 | 3 | version: '3' 4 | 5 | vars: 6 | docname: manual.pdf 7 | file: manual 8 | builddir: build 9 | 10 | tasks: 11 | default: 12 | cmds: 13 | - go-task --list 14 | build: 15 | desc: Compile {{.file}}.typ to {{.builddir}}/{{.docname}} 16 | cmds: 17 | - typst compile --root ../ {{.file}}.typ {{.builddir}}/{{.docname}} 18 | deps: [mkdirs] 19 | mkdirs: 20 | desc: Create a temporary build dir. 21 | cmds: 22 | - mkdir -p {{.builddir}} 23 | - mkdir -p history 24 | clean: 25 | desc: Remove the temporary build dir. 26 | cmds: 27 | - rm -r {{.builddir}} 28 | watch: 29 | desc: Watch for file changes and compile {{.file}}.typ to {{.builddir}}/{{.docname}}. 30 | cmds: 31 | - typst watch --root ../ {{.file}}.typ {{.builddir}}/{{.docname}} 32 | save: 33 | desc: Save current document into the history directory. 34 | cmds: 35 | - cp {{.builddir}}/{{.docname}} history/ 36 | open: 37 | desc: Open the document. 38 | cmds: 39 | - xdg-open {{.builddir}}/{{.docname}} 40 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Jonas Amstutz 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Taskfile.yml: -------------------------------------------------------------------------------- 1 | # https://taskfile.dev 2 | 3 | version: '3' 4 | 5 | vars: 6 | package_name: 7 | sh: tq -f typst.toml .package.name 8 | package_version: 9 | sh: tq -f typst.toml .package.version 10 | local_repo_path: ~/.local/share/typst/packages/local 11 | preview_repo_path: /home/jonas/temp/packages/packages/preview 12 | 13 | tasks: 14 | default: 15 | cmds: 16 | - go-task --list 17 | sync: 18 | desc: Copy latest version to local package repository 19 | cmds: 20 | - mkdir -p {{.local_repo_path}}/{{.package_name}}/{{.package_version}} 21 | - cp -r ./* {{.local_repo_path}}/{{.package_name}}/{{.package_version}} 22 | silent: false 23 | img: 24 | desc: "Create images for README.md" 25 | cmds: 26 | - echo "Attention! This does not work (final pdf lacks lines for some mysterious reason...). Create example_mod.png by hand!" 27 | - typst compile imgs/example.typ imgs/example.pdf --root . 28 | - inkscape -o imgs/example.svg imgs/example.pdf 29 | publish: 30 | desc: "Copy package to official typst preview repo." 31 | cmds: 32 | - mkdir -p {{.preview_repo_path}}/{{.package_name}}/{{.package_version}} 33 | - cp ./typst.toml {{.preview_repo_path}}/{{.package_name}}/{{.package_version}}/ 34 | - cp ./lib.typ {{.preview_repo_path}}/{{.package_name}}/{{.package_version}}/ 35 | - cp ./README.md {{.preview_repo_path}}/{{.package_name}}/{{.package_version}}/ 36 | - cp ./LICENSE {{.preview_repo_path}}/{{.package_name}}/{{.package_version}}/ 37 | -------------------------------------------------------------------------------- /example/Taskfile.yml: -------------------------------------------------------------------------------- 1 | # https://taskfile.dev 2 | 3 | version: '3' 4 | 5 | vars: 6 | version: "1.0" 7 | docname: Exam 8 | file: main 9 | builddir: build 10 | 11 | tasks: 12 | default: 13 | cmds: 14 | - go-task --list 15 | all: 16 | desc: Compile document in question and solution mode. 17 | deps: [doc, docsol] 18 | mkdirs: 19 | cmds: 20 | - mkdir -p {{.builddir}} 21 | clean: 22 | desc: Remove all generated files. 23 | cmds: 24 | - rm -r {{.builddir}} 25 | doc: 26 | desc: Compile document in question mode. 27 | cmds: 28 | - typst compile --input tutor_sol=false {{.file}}.typ {{.builddir}}/{{.docname}}.{{.version}}.pdf 29 | deps: [mkdirs] 30 | docsol: 31 | desc: Compile document in solution mode. 32 | cmds: 33 | - typst compile --input tutor_sol=true {{.file}}.typ {{.builddir}}/{{.docname}}_solution.{{.version}}.pdf 34 | deps: [mkdirs] 35 | watch: 36 | desc: Watch document in question mode. 37 | cmds: 38 | - typst watch --input tutor_sol=false {{.file}}.typ {{.builddir}}/{{.docname}}.{{.version}}.pdf 39 | deps: [mkdirs] 40 | watchsol: 41 | desc: Watch document in solution mode. 42 | cmds: 43 | - typst watch --input tutor_sol=true {{.file}}.typ {{.builddir}}/{{.docname}}_solution.{{.version}}.pdf 44 | deps: [mkdirs] 45 | open: 46 | desc: Open document in question mode. 47 | cmds: 48 | - xdg-open {{.builddir}}/{{.docname}}.{{.version}}.pdf 49 | opensol: 50 | desc: Open document in solution mode. 51 | cmds: 52 | - xdg-open {{.builddir}}/{{.docname}}_solution.{{.version}}.pdf 53 | img: 54 | cmds: 55 | - inkscape -o {{.builddir}}/example_question_mode.svg {{.builddir}}/{{.docname}}.{{.version}}.pdf 56 | - inkscape -o {{.builddir}}/example_solution_mode.svg {{.builddir}}/{{.docname}}_solution.{{.version}}.pdf 57 | - cp {{.builddir}}/example_question_mode.svg ../imgs/ 58 | - cp {{.builddir}}/example_solution_mode.svg ../imgs/ 59 | -------------------------------------------------------------------------------- /lib.typ: -------------------------------------------------------------------------------- 1 | 2 | /// Load the default tutor config. 3 | /// 4 | /// The tutor configuration holds all settings for the individual utilities provided by tutor. 5 | /// 6 | /// #example(` 7 | /// let cfg = tutor.default-config() 8 | /// [#cfg] 9 | /// `) 10 | /// 11 | /// -> dictionary 12 | #let default-config() = { 13 | let cfg = ( 14 | sol: false, 15 | level: 1, 16 | test: true, 17 | utils: ( 18 | lines: (spacing: 10mm), 19 | grid: (spacing: 4mm), 20 | checkbox: ( 21 | sym_true: "☒", 22 | sym_false: "☐", 23 | sym_question: "☐", 24 | ), 25 | totalpoints: ( 26 | outline: false, 27 | ), 28 | ), 29 | ) 30 | return cfg 31 | } 32 | 33 | /// Show a checkbox. 34 | /// 35 | /// *Example in Question Mode:* 36 | /// 37 | /// #example(` 38 | /// let cfg = tutor.default-config() 39 | /// [What does FHIR stand for? 40 | /// - #tutor.checkbox(cfg, false) Finally He Is Real 41 | /// - #tutor.checkbox(cfg, true) Fast Health Interoperability Resources] 42 | /// `) 43 | /// 44 | /// *Example in Solution Mode:* 45 | /// 46 | /// #example(` 47 | /// let cfg = tutor.default-config() 48 | /// (cfg.sol = true) // enable solutions 49 | /// [What does FHIR stand for? 50 | /// - #tutor.checkbox(cfg, false) Finally He Is Real 51 | /// - #tutor.checkbox(cfg, true) Fast Health Interoperability Resources] 52 | /// `) 53 | /// 54 | /// - cfg (dictionary): Global Tutor configuration 55 | /// - answer (boolean): Wheter the checkbox should be filled in solution mode. 56 | /// -> content 57 | #let checkbox(cfg, answer) = { 58 | if cfg.sol { 59 | if answer { 60 | cfg.utils.checkbox.sym_true 61 | } else { 62 | cfg.utils.checkbox.sym_false 63 | } 64 | } else { 65 | cfg.utils.checkbox.sym_question 66 | } 67 | } 68 | 69 | 70 | 71 | 72 | /// Print a blank line with a solution text. 73 | /// 74 | /// *Example in Question Mode:* 75 | /// 76 | /// #example(` 77 | /// let cfg = tutor.default-config() 78 | /// [Word for top of mountain: #tutor.blankline(cfg, 2cm, [peak])] 79 | /// `) 80 | /// 81 | /// *Example in Solution Mode:* 82 | /// 83 | /// #example(` 84 | /// let cfg = tutor.default-config() 85 | /// (cfg.sol = true) // enable solutions 86 | /// [Word for top of mountain: #tutor.blankline(cfg, 2cm, [peak])] 87 | /// `) 88 | /// 89 | /// - cfg (dictionary): Global Tutor configuration 90 | /// - width (length): Line length. 91 | /// - answer (content): Answer to display in solution mode. 92 | /// -> content 93 | #let blankline(cfg, width, answer) = { 94 | if cfg.sol { 95 | box(width: width, baseline: 5pt, stroke: (bottom: black), text(baseline: -5pt)[#answer]) 96 | } else { 97 | box(width: width, baseline: 5pt, stroke: (bottom: black))[] 98 | } 99 | } 100 | 101 | /// Print lines for students to write answers. 102 | /// 103 | /// *Example:* 104 | /// 105 | /// #example(` 106 | /// let cfg = tutor.default-config() 107 | /// (cfg.utils.lines.spacing = 2mm) 108 | /// [Write answer here:] 109 | /// tutor.lines(cfg, 3)`) 110 | /// 111 | /// 112 | /// 113 | /// - cfg (dictionary): Global Tutor configuration 114 | /// - count (integer): Number of lines to display. 115 | /// -> content 116 | #let lines(cfg, count) = { 117 | let content = [] 118 | let spacing = cfg.utils.lines.spacing 119 | if type(spacing) == str { 120 | spacing = eval(spacing) 121 | } 122 | for n in range(count) { 123 | content += [#v(spacing) #line(length: 100%) ] 124 | } 125 | return content 126 | } 127 | 128 | /// Print a grid for students to write answers. 129 | /// 130 | /// *Example:* 131 | /// 132 | /// #example(` 133 | /// let cfg = tutor.default-config() 134 | /// [Write answer here:] 135 | /// tutor.grid(cfg, 4cm, 2cm)`) 136 | /// 137 | /// 138 | /// 139 | /// - cfg (dictionary): Global Tutor configuration 140 | /// - width (length): Width of grid box. 141 | /// - height (length): Length of grid box. 142 | /// -> content 143 | #let grid(cfg, width, height) = { 144 | let spacing = cfg.utils.grid.spacing 145 | if type(spacing) == str { 146 | spacing = eval(spacing) 147 | } 148 | 149 | let pat = tiling(size: (spacing, spacing))[ 150 | #place(line(start: (0%, 0%), end: (0%, 100%), stroke: 0.2pt)) 151 | #place(line(start: (0%, 0%), end: (100%, 0%), stroke: 0.2pt)) 152 | ] 153 | 154 | align(center, rect(fill: pat, width: width, height: height, stroke: 0.2pt)) 155 | } 156 | 157 | 158 | 159 | /// Maximum points that can be achieved for a question. Will be internally added up to the total points counter. 160 | /// 161 | /// *Example:* 162 | /// 163 | /// #example(`[In this question a maxiumum of #tutor.points(4.5) points can be achieved.]`) 164 | /// 165 | /// 166 | /// 167 | /// - num (integer, float): Number of points. 168 | /// -> content 169 | #let points(num) = { 170 | let c = state("points", 0.0) 171 | c.update(points => points + num) 172 | [#num] 173 | } 174 | 175 | 176 | /// Display the total points of this exam, typically in the exam header. 177 | /// 178 | /// *Example:* 179 | /// 180 | /// #example(` 181 | /// let cfg = tutor.default-config() 182 | /// [In this a exam a total of #tutor.totalpoints(cfg) points can be achieved.]`) 183 | /// 184 | /// 185 | /// - cfg (dictionary): Global Tutor configuration 186 | /// -> content 187 | #let totalpoints(cfg) = { 188 | context { 189 | let c = state("points", 0.0) 190 | let points = c.final() 191 | if cfg.utils.totalpoints.outline { 192 | points = points / 2 193 | } 194 | [ #points ] 195 | } 196 | } 197 | 198 | /// Display only in solution mode. 199 | /// 200 | /// - cfg (dictionary): Global Tutor configuration 201 | /// - sol (content): Content to be shown in solution mode 202 | /// -> content 203 | #let if-sol(cfg, sol) = { 204 | if cfg.sol { 205 | sol 206 | } 207 | } 208 | 209 | /// Display different content in solution and question mode. 210 | /// 211 | /// - cfg (dictionary): Global Tutor configuration 212 | /// - sol (content): Content to be shown in solution mode 213 | /// - question (content): Content to be shown in question mode 214 | /// -> content 215 | #let if-sol-else(cfg, sol, question) = { 216 | if cfg.sol { 217 | sol 218 | } else { 219 | question 220 | } 221 | } 222 | 223 | /// Display only in test mode. 224 | /// 225 | /// - cfg (dictionary): Global Tutor configuration 226 | /// - test (content): Content to be shown in test mode 227 | /// -> content 228 | #let if-test(cfg, test) = { 229 | if cfg.test { 230 | test 231 | } 232 | } 233 | 234 | /// Display different content in solution and question mode. 235 | /// 236 | /// - cfg (dictionary): Global Tutor configuration 237 | /// - test (content): Content to be shown in test mode 238 | /// - exercise (content): Content to be shown in exercise mode 239 | /// -> content 240 | #let if-test-else(cfg, test, exercise) = { 241 | if cfg.test { 242 | test 243 | } else { 244 | exercise 245 | } 246 | } 247 | 248 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # tutor 2 | 3 | Utilities to write exams and exercises with integrated solutions. Set the variable `#(cfg.sol = true)` to display the solutions of a document. 4 | 5 | Currently the following features are supported: 6 | 7 | * Automatic total point calculation through the `#points()` and `#totalpoints()` functions. 8 | * Checkboxes that are either blank or show the solution state using eg. `#checkbox(cfg, true)`. 9 | * Display blank lines allowing students to write their answer using eg. `#lines(cfg, 3)`. 10 | * A proposition for a project structure allowing self-contained exercises and a mechanism to show or hide the solutions of an exercise. 11 | 12 | ## Usage 13 | 14 | ### Minimal Example 15 | 16 | ```typst 17 | #import "@local/tutor:0.8.0": points, totalpoints, lines, checkbox, default-config 18 | 19 | #let cfg = default-config() 20 | // enable solution mode 21 | #(cfg.sol = true) 22 | 23 | // display 3 lines (for hand written answer) 24 | #lines(cfg, 3) 25 | // checkbox for multiple choice (indicates correct state) 26 | #checkbox(cfg, true) 27 | 28 | // show achievable points 29 | Max. points: #points(2) 30 | Max. points: #points(3) 31 | // show sum of all total achievable points (will show 5) 32 | Total points: #totalpoints(cfg) 33 | ``` 34 | 35 | ### Practical Example 36 | 37 | Check [example](https://github.com/rangerjo/tutor/tree/main/example) for a more practical example. 38 | 39 | `tutor` is best used with the following directory and file structure: 40 | 41 | ``` 42 | ├── main.typ 43 | ├── src 44 | │   ├── ex1 45 | │   │   └── ex.typ 46 | │   └── ex2 47 | │   └── ex.typ 48 | └── tutor.toml 49 | ``` 50 | 51 | Every directory in `src` holds one self-contained exercise. The exercises can be imported into `main.typ`: 52 | 53 | ```typst 54 | #import "@local/tutor:0.8.0": totalpoints, lines, default-config 55 | 56 | #import "src/ex1/ex.typ" as ex1 57 | #import "src/ex2/ex.typ" as ex2 58 | 59 | 60 | #let cfg = default-config() 61 | #ex1.exercise(cfg) 62 | #ex2.exercise(cfg) 63 | ``` 64 | Supporting self-contained exercises is one of `tutor`s primary design goals. Each exercise lives within a folder and can easily be copied or referenced in a new document. 65 | 66 | An exercise is a folder that contains an `ex.typ` file along with any other assets (images, source code aso). The following exercise shows a practical usage of the `#checkbox()` and `#points()` functions. 67 | 68 | `src/ex1/ex.typ` 69 | ```typst 70 | #import "@local/tutor:0.8.0": points, checkbox 71 | 72 | #let exercise(cfg) = [ 73 | #heading(level:cfg.lvl, [Abbreviation FHIR (#points(1) point)]) 74 | 75 | What does FHIR stand for? 76 | 77 | #set list(marker: none) 78 | - #checkbox(cfg, false) Finally He Is Real 79 | - #checkbox(cfg, true) Fast Health Interoperability Resources 80 | - #checkbox(cfg, false) First Health Inactivation Restriction 81 | 82 | #if cfg.sol { 83 | [ Further explanation: FHIR is the new standard developed by HL7. ] 84 | } 85 | ] 86 | ``` 87 | 88 | Finally this second example shows the `#lines()` function. 89 | `src/ex2/ex.typ` 90 | ```typst 91 | #import "@local/tutor:0.8.0": points, lines 92 | 93 | #let exercise(cfg) = [ 94 | #heading(level:cfg.lvl, [FHIR vs HL7v2 (#points(4.5) points)]) 95 | 96 | List two differences between HL7v2 and FHIR: 97 | 98 | + #if cfg.sol { [ HL7v2 uses a non-standard line format, where as FHIR uses XML or JSON] } else { [ #lines(cfg, 3) ] } 99 | + #if cfg.sol { [ FHIR specifies various resources that can be queried, where as HL7v2 has a number of fixed fields that are either filled in or not]} else { [ #lines(cfg, 3) ] } 100 | ] 101 | ``` 102 | 103 | This would then give the following output in question mode (`#(cfg.sol=false)`) 104 | and in solution mode (`#(cfg.sol=true)`): 105 | ![Example document in solution mode](https://raw.githubusercontent.com/rangerjo/tutor/main/imgs/example_mod.png) 106 | 107 | ## Utilities 108 | 109 | ### lines 110 | 111 | `#lines(cfg, count)` prints `count` lines for students to write their answer. 112 | 113 | Configuration: 114 | 115 | ```typst 116 | // Vertical line spacing between rows. 117 | #(cfg.utils.lines.spacing = 8mm) 118 | ``` 119 | 120 | ### grid 121 | 122 | `#grid(cfg, width, height)` prints a grid for students to write their answer. 123 | 124 | Configuration: 125 | 126 | ```typst 127 | // Grid spacing. 128 | #(cfg.utils.grid.spacing = 4mm) 129 | ``` 130 | 131 | ### checkbox 132 | 133 | `#checkbox(cfg, answer)` shows a checkbox. In solution mode, the checkbox is shown filled out. 134 | 135 | Configuration: 136 | 137 | ```typst 138 | // Symbol to show if answer is true 139 | #(cfg.utils.checkbox.sym_true = "☒") 140 | // Symbol to show if answer is false 141 | #(cfg.utils.checkbox.sym_false = "☐") 142 | // Symbol to show in question mode 143 | #(cfg.utils.checkbox.sym_question = "☐") 144 | ``` 145 | 146 | ### points 147 | 148 | `#points(cfg, num)` displays the given `num` while adding its value to the total points counter. 149 | 150 | Configuration: none 151 | 152 | ### totalpoints 153 | 154 | `#totalpoints(cfg)` shows the final value of the total points counter. 155 | 156 | 157 | Configuration: 158 | 159 | ```typst 160 | // If points() is used in the outline, totalpoints value becomes doubled. 161 | // By setting outline to true, totalpoints gets divided by half. 162 | #(cfg.utils.totalpoints.outline = false) 163 | ``` 164 | 165 | ## Modes 166 | 167 | `tutor` comes with a solution and a test mode. 168 | 169 | ### solution mode 170 | 171 | Solution mode controls wheter solutions are shown or not. This mode controls eg. the utility `#checkbox(cfg, answer)`. 172 | 173 | 1. `(cfg.sol = false)`: Solutions are hidden. This is used for the actual exam handed out to students. 174 | 2. `(cfg.sol = true)`: Solutions are shown. This is used to create the exam solutions. 175 | 176 | You can also use the following helper functions: 177 | 178 | * `if-sol(cfg,[Content only shown in solution mode.])` 179 | * `if-sol-else(cfg,[Content only shown in solution mode.], [Content only shown in exam mode.])` 180 | 181 | ### test mode 182 | 183 | Test mode can be used to show or hide additional information. In test mode, one might want 184 | 1. `(cfg.test = true)`: Test information are shown. Use this eg. to display `#points(4)`. This is used in case the document is used as an exam/test. 185 | 186 | 2. `(cfg.test = false)`: Test information are hidden. This is used in case the document is used as an excerise. 187 | 188 | The following would show the points only in test mode. 189 | ```typst 190 | #if cfg.test { 191 | #points(4) 192 | } 193 | ``` 194 | 195 | Or you can use the following helper functions: 196 | 197 | * `if-test(cfg,[Content only shown in test mode.])` 198 | * `if-test-else(cfg,[Content only shown in test mode.], [Content only shown in exercise mode.])` 199 | 200 | ## Configuration 201 | 202 | `tutor` is designed to create exams and solutions with one single document source. Furthermore, the individual utilities provided by `tutor` can be configured. This can be done in one of three ways: 203 | 204 | 1. Use the `#default-config()` function and patch your configuration. The following example would configure the solution mode and basic line spacings to 8 millimeters: 205 | 206 | ```typst 207 | #let cfg = default-config() 208 | #(cfg.sol = false) 209 | #(cfg.utils.lines.spacing = 8mm) 210 | ``` 211 | 212 | 2. Use an external file to hold the configurations in your prefered format. See [tutor.toml](https://github.com/rangerjo/tutor/blob/main/example/tutor.toml) for a configuration in TOML. Load the configuration into your main document using 213 | ```typst 214 | #let cfg = toml("tutor.toml") 215 | ``` 216 | 217 | 3. Use typst's input feature added with compiler version 0.11.0. Add the following snippet to load the configuration, then overwrite it from the CLI like this: `typst compile --input tutor_sol=true main.typ` 218 | 219 | ```typst 220 | #let cfg = toml("tutor.toml") 221 | 222 | #if sys.inputs.tutor_sol == "true" { 223 | (cfg.sol = true) 224 | } else if sys.inputs.tutor_sol == "false" { 225 | (cfg.sol = false) 226 | } 227 | ``` 228 | --------------------------------------------------------------------------------